1. Definitions
A specifier map is an ordered map from strings to lists of URLs .
A import map is a struct with two items :
-
imports , a specifier map , and
-
scopes , an ordered map of URLs to specifier maps .
An empty import map is an import map with its imports and scopes both being empty maps.
-
Assert: import map is not null.
-
Assert: new import map is not null.
-
TODO: Implement merging. This merges new import map into import map and thus updates import map in-place.
2. Acquiring import maps
2.1. New members of environment settings object
Each
environment
settings
object
will
get
an
import
map
algorithm,
which
returns
an
import
map
created
by
parsing
and
merging
all
<script
type="importmap">
elements
that
are
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.
Each environment settings object has a pending import maps count , which is an integer. It is initially 0.
Each environment settings object has an acquiring import maps boolean. It is initially true.
These two pieces of state are used to achieve the following behavior:
-
Import
maps
are
accepted
if
and
only
if
they
are
added
(i.e.,
their
corresponding
scriptelements are added) before the first module load is started, even if the loading of the import map files don’t finish before the first module load is started. - Module loading waits for any import maps that have already started loading, if any.
2.2. Prepare a script
-
the script’s type should be:
-
which is either "classic", "module", or "
importmap".Although we add the new script type, we don’t add the new subclass of script . Import maps are processed outside the existing paths for scripts and execute a script block , because the mechanism controlling the orders/dependencies between import map registration and script evaluation is quite separeted and different from the mechanism for controlling script evaluation order.
-
-
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 settings object ’s acquiring import maps is false, then queue a task to fire an event namederrorat the element, and return.Alternative considered: We can proceed to import map loading if the pending import maps count isn’t 0, even when acquiring import maps is false, because at that time subsequent module loading is blocked and new import map loads could be still added. This would allow a few more opportinities for adding import maps, but this would highly depend on the timing of network loading. For example, if the preceding import map load finishes earlier than expected, then subsequent import maps depending on this behavior might fail. To avoid this kind of nondeterminism, we didn’t choose this option, at least for now.
-
-
Insert the following before prepare a script step 24.6:
-
If the script’s type is "
importmap", then:-
Increment settings object ’s pending import maps count .
-
Fetch an import map given url , settings object , and options .
-
When the previous step asynchronously completes with result :
-
Decrement settings object ’s pending import maps count .
If this decreases the pending import maps count to 0, it will (asynchronously) unblock any wait for import maps algorithm instances.
-
Register an import map given result , settings object , base URL , and the element.
-
-
Return.
-
-
-
Insert the following before prepare a script step 25.2:
-
If the script’s type is "
importmap", then:-
Register an import map given source text , settings object , base URL , and the element.
-
Return.
-
-
CSP is applied to import maps just like JavaScript scripts. Is this sufficient? #105 .
For import maps, the script never becomes ready and the script’s script remains null.
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-elemCSP 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 a failure, 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"For more contexts about MIME type checking, see #105 and #119 .
-
-
Asynchronously complete this algorithm with the result of UTF-8 decoding response’s body .
2.3. Wait for import maps
-
Set settings object ’s acquiring import maps to false.
-
Spin the event loop until settings object ’s pending import maps count is 0.
-
Asynchronously complete this algorithm.
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.4. Registering an import map
HTMLScriptElement
element
:
-
If source text is null, then queue a task to fire an event named
errorat element , and return. -
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
errorevents in this case. If we change the decision at whatwg/html#2673 to fireerrorevents, then we should change this step accordingly. -
Let import map be the result of parsing an import map string , given source text and base URL .
-
If import map is null, then queue a task to fire an event named
errorat element , and return. -
Otherwise, update element ’s node document 's import map with import map .
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
TypeErrorindicating 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 aTypeErrorindicating 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 aTypeErrorindicating 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 ’s keys contains any items besides "
imports" or "scopes", report a warning to the console that an invalid top-level key was present in the import map.This 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 .
<https://example.com/base/page.html>
,
the
input
{ "imports" : { "/app/helper" : "node_modules/helper/index.mjs" , "std:kv-storage" : [ "std:kv-storage" , "node_modules/kv-storage-polyfill/index.mjs" , ] } }
will generate an import map with imports of
«[
"https://example.com/app/helper" → «
<https://example.com/base/node_modules/helper/index.mjs>
»,
"std:kv-storage" → «
<std:kv-storage>,
<https://example.com/base/node_modules/kv-storage-polyfill/index.mjs>
»
]»
and (despite nothing being present in the input) an empty map for its scopes .
-
Let normalized be an empty map .
-
First, normalize all entries so that their values are lists . 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 a string , then set normalized [ normalizedSpecifierKey ] to « value ».
-
Otherwise, if value is null, then set normalized [ normalizedSpecifierKey ] to a new empty list.
-
Otherwise, if value is a list , then set normalized [ normalizedSpecifierKey ] to value .
-
Otherwise, report a warning to the console that addresses must be strings, arrays, or null.
-
-
Next, normalize and validate each potential address in the value lists . For each specifierKey → potentialAddresses of normalized ,
-
Assert: potentialAddresses is a list , because of the previous normalization pass.
-
Let validNormalizedAddresses be an empty list .
-
For each potentialAddress of potentialAddresses ,
-
If potentialAddress is not a string ,
thenthen:Report a warning to the console that the contents of address arrays must be strings.
-
Let addressURL be the result of parsing a URL-like import specifier given potentialAddress and baseURL .
-
If addressURL is null,
thenthen:Report a warning to the console that the address was invalid.
-
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
targetaddress was given for the specifier key specifierKey ; since specifierKey ended in a slash, so must the address. -
Continue .
-
-
If specifierKey ’s scheme is "
std" and the serialization of addressURL contains U+002F (/), then:-
Report a warning to the console that built-in module URLs must not contain slashes.
-
Continue .
-
-
Append addressURL to validNormalizedAddresses .
-
-
Set normalized [ specifierKey ] to validNormalizedAddresses .
-
-
Return the result of sorting normalized , with an entry a being less than an entry b if a ’s key is longer or code unit less than b ’s key .
-
Let normalized be an empty map .
-
For each scopePrefix → potentialSpecifierMap of originalMap ,
-
If potentialSpecifierMap is not a map , then throw a
TypeErrorindicating 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,
thenthen:Report a warning to the console that the scope prefix URL was not parseable.
-
If scopePrefixURL ’s scheme is not a fetch scheme , then:
-
Report a warning to the console that scope prefix URLs must have a fetch scheme.
-
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 a ’s key is longer or code unit less than b ’s key .
-
If specifierKey is the empty string,
then returnthen: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:
-
Let urlString be the serialization of url .
-
If url ’s scheme is "
std" and urlString contains U+002F (/), then:-
Report a warning to the console that built-in module specifiers must not contain slashes.
-
Return null.
-
-
Return urlString .
-
-
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.
-
If url ’s scheme is either a fetch scheme or "
std", then return url . -
Return null.
4. Resolving module specifiers
-
Let importMap be referringScript ’s settings object 's import map .
-
Let moduleMap be referringScript ’s settings object 's module map .
-
Let scriptURL be referringScript ’s base URL .
-
Let scriptURLString be scriptURL , serialized .
-
Let asURL be the result of parsing a URL-like import specifier given specifier and scriptURL .
-
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 scriptURLString , or if scopePrefix ends with U+002F (/) and scriptURLString starts with scopePrefix , then:
-
Let scopeImportsMatch be the result of resolving an imports match given normalizedSpecifier , scopeImports , and moduleMap .
-
If scopeImportsMatch is not null, then return scopeImportsMatch .
-
-
-
Let topLevelImportsMatch be the reuslt of resolving an imports match given normalizedSpecifier , importMap ’s imports , and moduleMap .
-
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: -
Throw a
TypeErrorindicating that specifier was a bare specifier, but was not remapped to anything by importMap .
It seems possible that the return type could end up being a list of URLs , not just a single URL, to support HTTPS → HTTPS fallback. But, we haven’t gotten that far yet; for now let’s assume it stays a single URL.
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 .
They
will
also
need
to
be
updated
to
account
for
it
now
throwing
exceptions,
instead
of
returning
failure.
(Previously
most
call
sites
just
turned
failures
into
TypeError
s
manually,
so
this
is
straightforward.)
-
For each specifierKey → addresses of specifierMap ,
-
If specifierKey is normalizedSpecifier , then:
-
If addresses ’s size is 0, then throw a
TypeErrorindicating that normalizedSpecifier was mapped to no addresses. -
If addresses ’s size is 1, then:
-
If addresses ’s size is 2, and addresses [0]'s scheme is "
std", and addresses [1]'s scheme is not "std", then:-
Return addresses [0], if moduleMap [ addresses [0]] exists ; otherwise, return addresses [1].
-
-
Otherwise, we have no specification for more complicated fallbacks yet; throw a
TypeErrorindicating this is not yet supported .
-
-
If specifierKey ends with U+002F (/) and normalizedSpecifier starts with specifierKey , then:
-