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 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, scopes 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 "depcache
", 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, 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) 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.
-
-
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.
-
-
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.
-
-
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.
-
-
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.
-
-
If !IsArray(dependencies), then:
-
Report a warning to the console that the value of the depcache for module module must be a JSON array.
-
-
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.
-
-
-
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
6.1. Threat models
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.
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.
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.
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.