1. Definitions
A resolution result is either a URL or null.
A specifier map is an ordered map from strings to resolution results .
A dependency cache list is a optimization cache list of the dependency strings of a module.
A
import
map
is
a
struct
with
two
three
items
:
-
imports , a specifier map , and
-
scopes , an ordered map of URLs to specifier maps .
-
depcache , an ordered map of URLs to dependency cache lists .
An
empty
import
map
is
an
import
map
with
its
imports
and
,
scopes
both
and
depcache
all
being
empty
maps.
2. Acquiring import maps
2.1. New members of environment settings objects
Each
environment
settings
object
will
get
an
import
map
algorithm,
which
returns
an
import
map
created
by
the
first
<script
type="importmap">
element
that
is
encountered
(before
the
cutoff).
A
Document
has
an
import
map
import
map
.
It
is
initially
a
new
empty
import
map
.
In
set
up
a
window
environment
settings
object
,
settings
object
’s
import
map
returns
the
import
map
of
window
’s
associated
Document
.
A
WorkerGlobalScope
has
an
import
map
import
map
.
It
is
initially
a
new
empty
import
map
.
Specify
a
way
to
set
WorkerGlobalScope
's
import
map
.
We
might
want
to
inherit
parent
context’s
import
maps,
or
provide
APIs
on
WorkerGlobalScope
,
but
we
are
not
sure.
Currently
it
is
always
an
empty
import
map
.
See
#2
.
In set up a worker environment settings object , settings object ’s import map returns worker global scope ’s import map .
This infrastructure is very similar to the existing specification for module maps.
A
Document
has
a
pending
import
map
script
,
which
is
a
HTMLScriptElement
or
null,
initially
null.
This is modified by § 2.3 Prepare a script .
Each
Document
has
an
acquiring
import
maps
boolean.
It
is
initially
true.
-
An
import
map
is
accepted
if
and
only
if
it
is
added
(i.e.,
its
corresponding
script
element is added) before the first module load is started, even if the loading of the import map file doesn’t finish before the first module load is started. - Module loading waits for any import map that has already started loading.
2.2. Script type
To process import maps in the prepare a script algorithm consistently with existing script types (i.e. classic or module), we make the following changes:
-
Introduce import map parse result , which is a struct with three items :
-
a settings object , an environment settings object ;
-
an import map , an import map ; and
-
an error to rethrow , a JavaScript value representing a parse error when non-null.
-
-
the script’s type should be either "
classic
", "module
", or "importmap
". -
Rename the script’s script to the script’s result , which can be either a script or an import map parse result .
The following algorithms are updated accordingly:
-
execute a script block Step 4: add the following case.
-
"
importmap
" -
-
Assert: Never reached.
Import maps are processed by register an import map instead of execute a script block .
-
-
"
Because we don’t make import map parse result the new subclass of script , other script execution-related specs are left unaffected.
2.3. Prepare a script
Inside the prepare a script algorithm, we make the following changes:
-
Insert the following step to prepare a script step 7, under "Determine the script’s type as follows:":
-
If the script block’s type string is an ASCII case-insensitive match for the string "
importmap
", the script’s type is "importmap
".
-
-
Insert the following step before prepare a script step 24:
-
If the script’s type is "
importmap
", and either the element’s node document 's acquiring import maps is false or the element’s node document 's pending import map script is non-null, then queue a task to fire an event namederror
at the element, and return.In the future we could losen the constrain of erroring when the pending import map script is non-null, to allow multiple import maps.
-
-
Insert the following case to prepare a script step 24.6:
-
"
importmap
" - Fetch an import map given url , settings object , and options .
-
"
-
Insert the following case to prepare a script step 25.2:
-
"
importmap
" -
-
Let import map parse result be the result of create an import map parse result , given source text , base URL and settings object .
-
Set the script’s result to import map parse result .
-
-
"
-
Insert the following case to prepare a script step 26:
-
If
the
script’s
type
is
"
importmap
" -
Set
the
element’s
node
document
's
pending
import
map
script
to
the
element.
When
the
script
is
ready
,
run
the
following
steps:
-
Register an import map given the pending import map script .
-
Set the pending import map script to null.
This will (asynchronously) unblock any wait for import maps algorithm instances.
-
-
If
the
script’s
type
is
"
This is specified similar to the list of scripts that will execute in order as soon as possible .
CSPs are applied to inline import maps at Step 13 of prepare a script , and to external import maps in fetch an import map , just like applied to classic/module scripts.
This algorithm is specified consistently with fetch a single module script steps 5, 7, 8, 9, 10, and 12.1. Particularly, we enforce CORS to avoid leaking the import map contents that shouldn’t be accessed.
-
Let request be a new request whose url is url , destination is "
script
", mode is "cors
", referrer is "client
", and client is settings object .Here we use "
script
" as the destination , which means thescript-src-elem
CSP directive applies. -
Set up the module script request given request and options .
-
Fetch request . Return from this algorithm, and run the remaining steps as part of the fetch’s process response for the response response .
response is always CORS-same-origin .
-
If any of the following conditions are met, asynchronously complete this algorithm with null, and abort these steps:
-
response ’s type is "
error
" -
The result of extracting a MIME type from response ’s header list is not
"application/importmap+json"
-
-
Let source text be the result of UTF-8 decoding response’s body .
-
Asynchronously complete this algorithm with the result of create an import map parse result , given source text , response ’s url , and settings object .
2.4. Wait for import maps
-
If settings object ’s global object is a
Window
object:-
Let document be settings object ’s global object 's associated
Document
. -
Set document ’s acquiring import maps to false.
-
Spin the event loop until document ’s pending import map script is null.
-
-
Asynchronously complete this algorithm.
No
actions
are
specified
for
WorkerGlobalScope
because
for
now
there
are
no
mechanisms
for
adding
import
maps
to
WorkerGlobalScope
.
Insert a call to wait for import maps at the beginning of the following HTML spec concepts.
-
fetch a module worker script graph (using module map settings object )
import:
URLs,
we
would
instead
use
fetch
client
settings
object
.
This
only
affects
fetch
a
module
worker
script
graph
,
where
these
two
settings
objects
are
different.
And,
given
that
the
import
maps
for
WorkerGlobalScope
s
are
currently
always
empty,
the
only
fetch
that
could
be
impacted
is
that
of
the
initial
module.
But
even
that
would
not
be
impacted,
because
that
fetch
is
done
using
URLs,
not
specifiers.
So
this
is
not
a
future
compatibility
hazard,
just
something
to
keep
in
mind
as
we
develop
import
maps
in
module
workers.
import(unresolvableSpecifier)
might
behave
differently
between
a
HTML-spec-
and
Fetch-spec-based
import
maps.
In
particular,
in
the
current
draft,
acquiring
import
maps
is
set
to
false
after
an
import()
-initiated
failure
to
resolve
a
module
specifier
,
thus
causing
any
later-encountered
import
maps
to
cause
an
error
event
instead
of
being
processed.
Whereas,
if
wait
for
import
maps
was
called
as
part
of
the
Fetch
spec,
it’s
possible
it
would
be
natural
to
specify
things
such
that
acquiring
import
maps
remains
true
(as
it
does
for
cases
like
<script
type="module"
src="http://:invalidurl">
).
This should not be much of a compatibility hazard, as it only makes esoteric error cases into successes. And we can always preserve the behavior as specced here if necessary, with some potential additional complexity.
2.5. Registering an import map
HTMLScriptElement
element
:
-
If element ’s the script’s result is null, then fire an event named
error
at element , and return. -
Let import map parse result be element ’s the script’s result .
-
Assert: element ’s the script’s type is "
importmap
". -
Assert: import map parse result is an import map parse result .
-
Let settings object be import map parse result ’s settings object .
-
If element ’s node document ’s relevant settings object is not equal to settings object , then return.
This is spec’ed consistently with whatwg/html#2673 .
Currently we don’t fire
error
events in this case. If we change the decision at whatwg/html#2673 to fireerror
events, then we should change this step accordingly. -
If import map parse result ’s error to rethrow is not null, then:
-
Report the exception given import map parse result ’s error to rethrow .
There are no relevant script , because import map parse result isn’t a script . This needs to wait for whatwg/html#958 before it is fixable.
-
Return.
-
-
Set element ’s node document 's import map to import map parse result ’s import map .
-
If element is from an external file , then fire an event named
load
at element .
The
timing
of
register
an
import
map
is
observable
by
possible
error
and
load
events,
or
by
the
fact
that
after
register
an
import
map
an
import
map
script
can
be
moved
to
another
Document
.
On
the
other
hand,
the
updated
import
map
is
not
observable
until
wait
for
import
maps
completes.
3. Parsing import maps
-
Let parsed be the result of parsing JSON into Infra values given input .
-
If parsed is not a map , then throw a
TypeError
indicating that the top-level value must be a JSON object. -
Let sortedAndNormalizedImports be an empty map .
-
If parsed ["
imports
"] exists , then:-
If parsed ["
imports
"] is not a map , then throw aTypeError
indicating that the "imports
" top-level key must be a JSON object. -
Set sortedAndNormalizedImports to the result of sorting and normalizing a specifier map given parsed ["
imports
"] and baseURL .
-
-
Let sortedAndNormalizedScopes be an empty map .
-
If parsed ["
scopes
"] exists , then:-
If parsed ["
scopes
"] is not a map , then throw aTypeError
indicating that the "scopes
" top-level key must be a JSON object. -
Set sortedAndNormalizedScopes to the result of sorting and normalizing scopes given parsed ["
scopes
"] and baseURL .
-
-
If parsed ["
depcache
"] exists , then:If parsed ["
depcache
"] is not a map , then throw aTypeError
indicating that the "depcache
" top-level key must be a JSON object.Set normalizedDepcache to the result of normalizing depcache given parsed ["
depcache
"] and baseURL .
If parsed ’s keys contains any items besides "
imports
", "scopes
" or "scopesdepcacheThis can help detect typos. It is not an error, because that would prevent any future extensions from being added backward-compatibly.
-
Return the import map whose imports are sortedAndNormalizedImports
and whose, scopes scopes are sortedAndNormalizedScopes and depcache depcache are normalizedDepcache .
-
Let import map be the result of parse an import map string given input and baseURL . If this throws an exception, let error to rethrow be the exception. Otherwise, let error to rethrow be null.
-
Return an import map parse result with settings object is settings object , import map is import map , and error to rethrow is error to rethrow .
<https://example.com/base/page.html>
,
the
input
{ "imports" : { "/app/helper" : "node_modules/helper/index.mjs" , "lodash" : "/node_modules/lodash-es/lodash.js" } }
will generate an import map with imports of
«[ "https://example.com/app/helper" → <https://example.com/base/node_modules/helper/index.mjs> "lodash" → <https://example.com/node_modules/lodash-es/lodash.js> ]»
and
(despite
nothing
being
present
in
the
input)
an
empty
map
entries
for
its
scopes
and
depcache
.
-
Let normalized be an empty map .
-
For each specifierKey → value of originalMap ,
-
Let normalizedSpecifierKey be the result of normalizing a specifier key given specifierKey and baseURL .
-
If normalizedSpecifierKey is null, then continue .
-
If value is not a string , then:
-
Report a warning to the console that addresses must be strings.
-
Set normalized [ specifierKey ] to null.
-
Continue .
-
-
Let addressURL be the result of parsing a URL-like import specifier given value and baseURL .
-
If addressURL is null, then:
-
Report a warning to the console that the address was invalid.
-
Set normalized [ specifierKey ] to null.
-
Continue .
-
-
If specifierKey ends with U+002F (/), and the serialization of addressURL does not end with U+002F (/), then:
-
Report a warning to the console that an invalid address was given for the specifier key specifierKey ; since specifierKey ended in a slash, so must the address.
-
Set normalized [ specifierKey ] to null.
-
Continue .
-
-
Set normalized [ specifierKey ] to addressURL .
-
-
Return the result of sorting normalized , with an entry a being less than an entry b if b ’s key is code unit less than a ’s key .
-
Let normalized be an empty map .
-
For each scopePrefix → potentialSpecifierMap of originalMap ,
-
If potentialSpecifierMap is not a map , then throw a
TypeError
indicating that the value of the scope with prefix scopePrefix must be a JSON object. -
Let scopePrefixURL be the result of parsing scopePrefix with baseURL as the base URL.
-
If scopePrefixURL is failure, then:
-
Report a warning to the console that the scope prefix URL was not parseable.
-
Continue .
-
-
Let normalizedScopePrefix be the serialization of scopePrefixURL .
-
Set normalized [ normalizedScopePrefix ] to the result of sorting and normalizing a specifier map given potentialSpecifierMap and baseURL .
-
-
Return the result of sorting normalized , with an entry a being less than an entry b if b ’s key is code unit less than a ’s key .
Let normalized be an empty map .
For each module → dependencies of originalMap ,
Let moduleURL be the result of parsing module with baseURL as the base URL.
If moduleURL is failure, then:
Report a warning to the console that the depcache URL was not parseable.
Continue .
If ! IsArray (dependencies), then:
Report a warning to the console that the value of the depcache for module module must be a JSON array.
Continue .
Let validDependencies be true.
For each dependency of dependencies ,
If dependency is not a Javascript string , then:
Report a warning to the console that the depcache list for moduleURL is invalid.
Set _validDependencies_ to false.
Break .
If dependencies is not empty and validDependencies is true, then:
Let normalizedModule be the serialization of moduleURL .
Set normalized [ normalizedModule ] to dependencies .
Return normalized .
We
sort
keys/scopes
in
reverse
order,
to
put
"foo/bar/"
before
"foo/"
so
that
"foo/bar/"
has
a
higher
priority
than
"foo/"
.
-
If specifierKey is the empty string, then:
-
Report a warning to the console that specifier keys cannot be the empty string.
-
Return null.
-
-
Let url be the result of parsing a URL-like import specifier , given specifierKey and baseURL .
-
If url is not null, then return the serialization of url .
-
Return specifierKey .
-
If specifier starts with "
/
", "./
", or "../
", then:-
Let url be the result of parsing specifier with baseURL as the base URL.
-
If url is failure, then return null.
One way this could happen is if specifier is "
../foo
" and baseURL is adata:
URL. -
Return url .
-
-
Let url be the result of parsing specifier (with no base URL).
-
If url is failure, then return null.
-
Return url .
4. Resolving module specifiers
imports
"),
and
from
most-specific
to
least-specific
prefixes.
For
each
candidate,
the
result
is
one
of
the
following:
-
Successfully resolves a specifier to a URL . This makes the resolve a module specifier algorithm immediately return that URL .
-
Throws an error. This makes the resolve a module specifier algorithm rethrow the error, without any further fallbacks.
-
Fails to resolve, without an error. In this case the algorithm moves on to the next candidate.
4.1. New "resolve a module specifier"
-
Let settingsObject be the current settings object .
-
Let baseURL be settingsObject ’s API base URL .
-
If referringScript is not null, then:
-
Set settingsObject to referringScript ’s settings object .
-
Set baseURL to referringScript ’s base URL .
-
-
Let importMap be settingsObject ’s import map .
-
Return the result of resolve an import map given specifier , importMap and baseURL .
Let baseURLString be baseURL , serialized .
-
Let asURL be the result of parsing a URL-like import specifier given specifier and baseURL .
-
Let normalizedSpecifier be the serialization of asURL , if asURL is non-null; otherwise, specifier .
-
For each scopePrefix → scopeImports of importMap ’s scopes ,
-
If scopePrefix is baseURLString , or if scopePrefix ends with U+002F (/) and baseURLString starts with scopePrefix , then:
-
Let scopeImportsMatch be the result of resolving an imports match given normalizedSpecifier and scopeImports .
-
If scopeImportsMatch is not null, then return scopeImportsMatch .
-
-
-
Let topLevelImportsMatch be the result of resolving an imports match given normalizedSpecifier and importMap ’s imports .
-
If topLevelImportsMatch is not null, then return topLevelImportsMatch .
-
At this point, the specifier was able to be turned in to a URL, but it wasn’t remapped to anything by importMap .
If asURL is not null, then return asURL . -
Throw a
TypeError
indicating that specifier was a bare specifier, but was not remapped to anything by importMap .
-
For each specifierKey → resolutionResult of specifierMap ,
-
If specifierKey is normalizedSpecifier , then:
-
If resolutionResult is null, then throw a
TypeError
indicating that resolution of specifierKey was blocked by a null entry.This will terminate the entire resolve a module specifier algorithm, without any further fallbacks.
-
Assert: resolutionResult is a URL .
-
Return resolutionResult .
-
-
If specifierKey ends with U+002F (/) and normalizedSpecifier starts with specifierKey , then:
-
If resolutionResult is null, then throw a
TypeError
indicating that resolution of specifierKey was blocked by a null entry.This will terminate the entire resolve a module specifier algorithm, without any further fallbacks.
-
Assert: resolutionResult is a URL .
-
Let afterPrefix be the portion of normalizedSpecifier after the initial specifierKey prefix.
-
Assert: resolutionResult , serialized , ends with "
/
", as enforced during parsing . -
Let url be the result of parsing afterPrefix relative to the base URL resolutionResult .
-
If url is failure, then throw a
TypeError
indicating that resolution of specifierKey was blocked due to a URL parse failure.This will terminate the entire resolve a module specifier algorithm, without any further fallbacks.
-
Assert: url is a URL .
-
Return url .
-
-
-
Return null.
The resolve a module specifier algorithm will fallback to a less specific scope or to "
imports
", if possible.
4.2. Updates to other algorithms
All call sites of HTML’s existing resolve a module specifier will need to be updated to pass the appropriate script , not just its base URL . Some particular interesting cases:
-
HostResolveImportedModule and HostImportModuleDynamically no longer need to compute the base URL themselves, as resolve a module specifier now handles that.
-
Fetch an import() module script graph will also need to take a script instead of a base URL.
Call
sites
will
also
need
to
be
updated
to
account
for
resolve
a
module
specifier
now
throwing
exceptions,
instead
of
returning
failure.
(Previously
most
call
sites
just
turned
failures
into
TypeError
s
manually,
so
this
is
straightforward.)
5. Parallelizing module graphs
To parallelize the loading of modules in the import map, a module graph dependency cache list can be used to provide upfront a cache of module dependencies so they can be loaded in parallel when lazily loaded, without unnecessary requests or extra round trips.
When this list of dependency hints is provided for a module URL the algorithm will, on resolution for that module URL , immediately trigger resolution and preloading for each dependency hint provided.
The behavior of the dependency cache preloader is distinct from the fetch a modulepreload module script graph preloader in that it immediately iterates all dependency preloads, and does not trigger instantiation since the top-level module instantiation would already be queued.
5.1. Preloading dependency graphs
The preload depcache steps below should be included in fetch a single module script , right after step 4 setting moduleMap[url] to "fetching" combining to form an optimizally-terminating graph fetch parallelizing operation, with minimum complexity. Alternatively these steps can be inlined into fetch a single module script directly. The rationale of this ordering being that dependencies should be triggered for fetching before their parents, even when fetching in parallel.
Let urlString be url , serialized .
Let moduleMap be module map settings object ’s module map .
If moduleMap [ urlString ] is not undefined, return null.
null entries in the moduleMap for errored modules will also return here.
Let importMap be module map settings object ’s import map .
Let depcache be importMap ’s depcache ,
If depcache contains an entry for urlString , then:
Let dependencies be the list depcache [ urlString ].
For each dependency of dependencies ,
Let resolvedDependencyURL be the result of resolve an import map called with dependency , importMap and url .
If resolvedDependencyURL is null, then:
Throw a
TypeError
indicating that dependency was a specifier preloaded by the depcache for urlString , but was not resolved by importMap .
Perfom the steps of HTML’s [=fetch a single module script] with resolvedDependencyURL , fetch client settings object , destination , options , module map settings object , url , and with the top-level module fetch flag unset, without waiting for asynchronous completion.
This triggers the recursive fetch preload operations in turn, since this algorithm is in turn called by fetch a single module script .
Only dependency preload resolution errors are thrown, not dependency preload instantiation errors. These can be ignored as they will be rethrown appropriately by the top-level module load operation.
6. Security and Privacy
5.1.
6.1.
Threat
models
5.1.1.
6.1.1.
Comparison
with
first-party
scripts
Import maps are explicitly designed to be installed by page authors, i.e. those who have the ability to run first-party scripts. (See the explainer’s "Scope" section .)
Although it may seem that the ability to change how resources are imported from JavaScript and the capability of rewriting rules are powerful, there is no extra power really granted here, compared with first-party scripts. That is, they only change things which the page author could change already, by manually editing their code to use different URLs.
We do still need to apply the traditional protections against first-party malicious actors, for example:
-
CSP to protect against injection vulnerabilities. (See #105 for further discussion.)
-
CORS and strict MIME type checking (with a new MIME type, "
application/importmap+json
") for external import maps.
But there is no fundamentally new capability introduced here, that needs new consideration.
5.1.2.
6.1.2.
Comparison
with
Service
Workers
On one hand, the ability of import maps to change how resources are imported looks similar to the ability of Service Workers to intercept and rewrite fetch requests.
On
the
other
hand,
import
maps
have
a
much
more
restricted
scope
than
Service
Workers.
Import
maps
are
not
persistent,
and
an
import
map
only
affects
the
document
that
installs
the
import
map
via
<script
type="importmap">
.
Therefore, the security restrictions applied to Service Workers (beyond those applied to first-party scripts), e.g. the same-origin/secure contexts requirements, are not applied to import maps.
5.1.3.
6.1.3.
Time/memory
complexity
To avoid denial of service attacks, explosive memory usage, and the like, import maps are designed to have reasonably bounded time and memory complexity in the worst cases, and to not be Turing complete.
5.2.
6.2.
A
note
on
import
specifiers
The
import
specifiers
that
appear
in
import
statements
and
import()
expressions
are
not
URLs
,
and
should
not
be
thought
of
as
such.
To
date,
there
has
been
a
default
mechanism
for
translating
those
strings
into
URLs.
And
indeed,
some
of
the
strings,
such
as
"https://example.com/foo.mjs"
,
or
"./bar.mjs"
,
might
look
URL-like;
for
those,
the
default
translation
does
what
you
would
expect.
But
overall,
one
should
not
think
of
import(x)
as
corresponding
to
fetch(x)
.
Instead,
the
correspondence
is
to
fetch(translate(x))
,
where
the
translation
algorithm
produces
the
actual
URL
to
be
fetched.
In
this
framing,
the
way
to
think
about
import
maps
is
as
providing
a
mechanism
for
overriding
the
default
mechanism,
i.e.
customizing
the
translate()
function.
This
brings
some
clarity
to
some
common
security
questions.
For
example:
given
an
import
map
which
maps
the
specifier
"https://1.example.com/foo.mjs"
to
the
URL
<https://2.example.com/bar.mjs>
,
should
we
apply
CSP
checks
to
<https://1.example.com/foo.mjs>
or
to
<https://2.example.com/bar.mjs>
?
With
this
framing
we
can
see
that
we
should
apply
the
checks
to
the
post-translation
URL
<https://2.example.com/bar.mjs>
which
is
actually
fetched,
and
not
to
the
pre-translation
"https://1.example.com/foo.mjs"
module
specifier.