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.
-
For each specifier → addresses of newImportMap’s imports, set importMap’s imports[specifier] to addresses.
-
For each url → specifierMap of newImportMap’s scopes, set importMap’s scopes[url] to specifierMap.
-
Set importMap’s imports to the result of sorting importMap’s imports, with an entry a being less than an entry b if a’s key is longer or code unit less than b’s key.
-
Set importMap’s scopes to the result of sorting importMap’s scopes, with an entry a being less than an entry b if a’s key is longer or code unit less than b’s key.
imports" and "scopes" keys. For example,
< script type = "importmap" > { "imports" : { "a" : "/a-1.mjs" , "b" : "/b-1.mjs" , "std:kv-storage" : [ "std:kv-storage" , "/kvs-1.mjs" ] }, "scopes" : { "/scope1/" : { "a" : "/a-2.mjs" } } } </ script > < script type = "importmap" > { "imports" : { "b" : null , "std:kv-storage" : "kvs-2.mjs" }, "scopes" : { "/scope1/" : { "b" : "/b-2.mjs" } } } </ script >
is equivalent to
< script type = "importmap" > { "imports" : { "a" : "/a-1.mjs" , "b" : null , "std:kv-storage" : "kvs-2.mjs" }, "scopes" : { "/scope1/" : { "b" : "/b-2.mjs" } } } </ script >
Notice how the definition for "/scope1/" was completely overridden, so there is no longer a redirection for the "a" module specifier within that scope.
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 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.
A Document has a list of pending import map scripts, which is a list of HTMLScriptElements, initially empty.
HTMLScriptElements are added to this list by § 2.3 Prepare a script.
Each Document has an acquiring import maps boolean. It is initially true.
- 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. 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 the element’s node document’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 unless the script is ready for all
HTMLScriptElementin list of pending import map scripts, 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 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" -
Append the element to the element’s node document’s list of pending import map scripts.
When the script is ready, run the following steps:
-
Repeat while the list of pending import map scripts is not empty and the first entry’s the script is ready:
-
Register an import map given the first element of list of pending import map scripts.
-
Remove the first element of list of pending import map scripts.
If this makes the list of pending import map scripts empty, it will (asynchronously) unblock any wait for import maps algorithm instances.
-
-
- If the script’s type is "
CSP is applied to import maps just like JavaScript scripts. Is this sufficient? #105.
This is specified similar to the list of scripts that will execute in order as soon as possible, to register import maps and fire error events in order (list of scripts that will execute in order as soon as possible is rarely used in the wild though).
There can be other alternatives, e.g. executing a similar loop inside wait for import maps.
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 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
Windowobject:-
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 list of pending import map scripts is empty.
-
-
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 WorkerGlobalScopes 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
errorat 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
errorevents in this case. If we change the decision at whatwg/html#2673 to fireerrorevents, 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.
-
-
Update element’s node document's import map with import map parse result’s import map.
-
If element is from an external file, then fire an event named
loadat 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
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.
-
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" , "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, then:
-
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, then:
-
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 address was given for the specifier key specifierKey; since specifierKey ended in a slash, so must the address.
-
-
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.
-
-
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, then:
-
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.
-
-
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:
-
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
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.
-
Let moduleMap be settingsObject’s module map.
-
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, scopeImports, and moduleMap.
-
If scopeImportsMatch is not null, then:
-
Validate the module script URL given scopeImportsMatch, settingsObject, and baseURL.
-
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:
-
Validate the module script URL given topLevelImportsMatch, settingsObject, and baseURL.
-
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:-
Validate the module script URL given asURL, settingsObject, and baseURL.
-
Return asURL.
-
-
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.
-
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:
-
-
Return null.
-
If url’s scheme is "
std", then:-
If the serialization of url contains U+002F (/), then throw a
TypeErrorindicating that url is a malformed built-in URL. -
Let moduleMap be settings object’s module map.
-
If moduleMap[url] does not exist, then throw a
TypeErrorindicating that the requested built-in module is not implemented.This condition is added to ensure that moduleMap[url] does not exist for unimplemented built-ins. Without this condition, fetch a single module script might be called and moduleMap[url] can be set to null, which might complicates the spec around built-ins.
-
Return.
-
-
If url’s scheme is not a fetch scheme, then throw a
TypeErrorindicating that url is not a fetch scheme.
This introduces a type of internal built-in module that is only accessible to other internal built-in modules. Similar steps could be used to, for example, change how extension scripts access modules.
Since validate a module script URL is called before any module script fetches, such checks are reliable and can be used as a security mechanism.
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 TypeErrors manually, so this is straightforward.)
In addition to the call sites for validate a module script URL explicitly added within this spec, insert the following at the beginning of fetch a single module script:
-
Validate the module script URL given url, module map settings object, and module map settings object’s API base URL. If this throws an error, then asynchronously complete this algorithm with null, and abort these steps.
Alternatively, we can add the snippet at the beginning of the following HTML spec concepts (after wait for import maps), so that the validation is not done twice:
-
fetch an external module script graph (using settings object)
-
fetch a modulepreload module script graph (using settings object)
Validate a module script URL is applied to all module URLs before they start loading, even in paths where resolve a module specifier and import maps are not applied (e.g. <script src="..." type="module">).
Appendix A: MIME type registration
This appendix provides the provisional registration of the MIME type application/importmap+json application/importmap+json in accordance with [RFC6838].
- Type name:
-
application
- Subtype name:
-
importmap+json
- Required parameters:
-
N/A
- Optional parameters:
-
N/A
- Encoding considerations:
-
8bit (always UTF-8)
- Security considerations:
-
Since Import Maps have the ability to direct which module resolutions are to be provided to all module imports of a given JavaScript environment, control of the Import Map should be considered to be execution-level application access. Integration with existing policy systems, including for example CORS and CSP, can be used to mitigate and restrict unwanted target URLs from being executed. In addition, the specification states that only those Import Maps served to browsers with the
application/importmap+jsonMIME type will be executed. [FETCH] [CSP] - Interoperability considerations:
-
Backwards compatibility will be a necessity for any new features added to the format, and handling for this has been incorporated into the design of the specification.
- Published specification:
-
https://wicg.github.io/import-maps/
- Applications that use this media type:
-
This is a browser-specific MIME type but may also apply to other JavaScript environments.
- Fragment identifier considerations:
-
N/A
- Additional information:
-
- Deprecated alias names for this type:
-
N/A
- Magic number(s)
-
N/A
- File extension(s):
-
"importmap"
- Macintosh file type code:
-
Same as for
application/json[JSON]
- Person & email address to contact for further information:
-
Guy Bedford <guybedford@gmail.com>.
- Intended usage:
-
Common
- Restrictions on usage:
-
No restrictions apply.
- Change controller:
-
WHATWG