Import Maps

Draft Community Group Report,

This version:
https://wicg.github.io/import-maps/
Editor:
Domenic Denicola (Google)
Participate:
GitHub WICG/import-maps (new issue, open issues)
Commits:
GitHub spec.bs commits
Not Ready For Implementation

This spec is not yet ready for implementation. It exists in this repository to record the ideas and promote discussion.

Before attempting to implement this spec, please contact the editors.


Abstract

Import maps allow web pages to control the behavior of JavaScript imports, and introduce a new import: URL scheme to allow using this mapping in other URL-accepting contexts

Status of this document

This specification was published by the Web Platform Incubator Community Group. It is not a W3C Standard nor is it on the W3C Standards Track. Please note that under the W3C Community Contributor License Agreement (CLA) there is a limited opt-out and other conditions apply. Learn more about W3C Community and Business Groups.

1. Definitions

A specifier map is an ordered map from strings to lists of URLs.

A import map is a struct with two items:

An empty import map is an import map with its imports and scopes both being empty maps.

To update an import map import map with a second import map new import map:
  1. Assert: import map is not null.

  2. Assert: new import map is not null.

  3. 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:

2.2. Prepare a script

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.

To fetch an import map given url, settings object, and options, run the following steps. This algorithm asynchronously returns a string or a failure.

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.

  1. 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 the script-src-elem CSP directive applies.

  2. Set up the module script request given request and options.

  3. 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.

  4. If any of the following conditions are met, asynchronously complete this algorithm with a failure, and abort these steps:

  5. Asynchronously complete this algorithm with the result of UTF-8 decoding response’s body.

2.3. Wait for import maps

To wait for import maps given settings object:
  1. Set settings object’s acquiring import maps to false.

  2. Spin the event loop until settings object’s pending import maps count is 0.

  3. Asynchronously complete this algorithm.

Insert a call to wait for import maps at the beginning of the following HTML spec concepts.

In this draft of the spec, which inserts itself into these HTML concepts, the settings object used here is the module map settings object, not fetch client settings object, because resolve a module specifier uses the import map of module map settings object. In a potential future version of the import maps infrastructure, which interjects itself at the layer of the Fetch spec in order to support 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.

Depending on the exact location of wait for import maps, 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

To register an import map given a string or a null source text, an environment settings object settings object, a URL base URL, and an HTMLScriptElement element:
  1. If source text is null, then queue a task to fire an event named error at element, and return.

  2. 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 fire error events, then we should change this step accordingly.

  3. Let import map be the result of parsing an import map string, given source text and base URL.

  4. If import map is null, then queue a task to fire an event named error at element, and return.

  5. Otherwise, update element’s node document's import map with import map.

3. Parsing import maps

To parse an import map string, given a string input and a URL baseURL:
  1. Let parsed be the result of parsing JSON into Infra values given input.

  2. If parsed is not a map, then throw a TypeError indicating that the top-level value must be a JSON object.

  3. Let sortedAndNormalizedImports be an empty map.

  4. If parsed["imports"] exists, then:

    1. If parsed["imports"] is not a map, then throw a TypeError indicating that the "imports" top-level key must be a JSON object.

    2. Set sortedAndNormalizedImports to the result of sorting and normalizing a specifier map given parsed["imports"] and baseURL.

  5. Let sortedAndNormalizedScopes be an empty map.

  6. If parsed["scopes"] exists, then:

    1. If parsed["scopes"] is not a map, then throw a TypeError indicating that the "scopes" top-level key must be a JSON object.

    2. Set sortedAndNormalizedScopes to the result of sorting and normalizing scopes given parsed["scopes"] and baseURL.

  7. 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.

  8. Return the import map whose imports are sortedAndNormalizedImports and whose scopes scopes are sortedAndNormalizedScopes.

The import map is a highly normalized structure. For example, given a base URL of <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.

To sort and normalize a specifier map, given a map originalMap and a URL baseURL:
  1. Let normalized be an empty map.

  2. First, normalize all entries so that their values are lists. For each specifierKeyvalue of originalMap,

    1. Let normalizedSpecifierKey be the result of normalizing a specifier key given specifierKey and baseURL.

    2. If normalizedSpecifierKey is null, then continue.

    3. If value is a string, then set normalized[normalizedSpecifierKey] to «value».

    4. Otherwise, if value is null, then set normalized[normalizedSpecifierKey] to a new empty list.

    5. Otherwise, if value is a list, then set normalized[normalizedSpecifierKey] to value.

    6. Otherwise, report a warning to the console that addresses must be strings, arrays, or null.

  3. Next, normalize and validate each potential address in the value lists. For each specifierKeypotentialAddresses of normalized,

    1. Assert: potentialAddresses is a list, because of the previous normalization pass.

    2. Let validNormalizedAddresses be an empty list.

    3. For each potentialAddress of potentialAddresses,

      1. If potentialAddress is not a string, then:

        1. Report a warning to the console that the contents of address arrays must be strings.

        2. Continue.

      2. Let addressURL be the result of parsing a URL-like import specifier given potentialAddress and baseURL.

      3. If addressURL is null, then:

        1. Report a warning to the console that the address was invalid.

        2. Continue.

      4. If specifierKey ends with U+002F (/), and the serialization of addressURL does not end with U+002F (/), then:

        1. 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.

        2. Continue.

      5. If specifierKey’s scheme is "std" and the serialization of addressURL contains U+002F (/), then:

        1. Report a warning to the console that built-in module URLs must not contain slashes.

        2. Continue.

      6. Append addressURL to validNormalizedAddresses.

    4. Set normalized[specifierKey] to validNormalizedAddresses.

  4. 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.

To sort and normalize scopes, given a map originalMap and a URL baseURL:
  1. Let normalized be an empty map.

  2. For each scopePrefixpotentialSpecifierMap of originalMap,

    1. 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.

    2. Let scopePrefixURL be the result of parsing scopePrefix with baseURL as the base URL.

    3. If scopePrefixURL is failure, then:

      1. Report a warning to the console that the scope prefix URL was not parseable.

      2. Continue.

    4. If scopePrefixURL’s scheme is not a fetch scheme, then:

      1. Report a warning to the console that scope prefix URLs must have a fetch scheme.

      2. Continue.

    5. Let normalizedScopePrefix be the serialization of scopePrefixURL.

    6. Set normalized[normalizedScopePrefix] to the result of sorting and normalizing a specifier map given potentialSpecifierMap and baseURL.

  3. 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.

To normalize a specifier key, given a string specifierKey and a URL baseURL:
  1. If specifierKey is the empty string, then:

    1. Report a warning to the console that specifier keys cannot be the empty string.

    2. Return null.

  2. Let url be the result of parsing a URL-like import specifier, given specifierKey and baseURL.

  3. If url is not null, then:

    1. Let urlString be the serialization of url.

    2. If url’s scheme is "std" and urlString contains U+002F (/), then:

      1. Report a warning to the console that built-in module specifiers must not contain slashes.

      2. Return null.

    3. Return urlString.

  4. Return specifierKey.

To parse a URL-like import specifier, given a string specifier and a URL baseURL:
  1. If specifier starts with "/", "./", or "../", then:

    1. Let url be the result of parsing specifier with baseURL as the base URL.

    2. If url is failure, then return null.

      One way this could happen is if specifier is "../foo" and baseURL is a data: URL.

    3. Return url.

  2. Let url be the result of parsing specifier (with no base URL).

  3. If url is failure, then return null.

  4. If url’s scheme is either a fetch scheme or "std", then return url.

  5. Return null.

A string a is longer or code unit less than b if a’s length is greater than b’s length, or if a is code unit less than b.

4. Resolving module specifiers

HTML already has a resolve a module specifier algorithm. We replace it with the following resolve a module specifier algorithm, given a script referringScript and a JavaScript string specifier:
  1. Let importMap be referringScript’s settings object's import map.

  2. Let moduleMap be referringScript’s settings object's module map.

  3. Let scriptURL be referringScript’s base URL.

  4. Let scriptURLString be scriptURL, serialized.

  5. Let asURL be the result of parsing a URL-like import specifier given specifier and scriptURL.

  6. Let normalizedSpecifier be the serialization of asURL, if asURL is non-null; otherwise, specifier.

  7. For each scopePrefixscopeImports of importMap’s scopes,

    1. If scopePrefix is scriptURLString, or if scopePrefix ends with U+002F (/) and scriptURLString starts with scopePrefix, then:

      1. Let scopeImportsMatch be the result of resolving an imports match given normalizedSpecifier, scopeImports, and moduleMap.

      2. If scopeImportsMatch is not null, then return scopeImportsMatch.

  8. Let topLevelImportsMatch be the reuslt of resolving an imports match given normalizedSpecifier, importMap’s imports, and moduleMap.

  9. If topLevelImportsMatch is not null, then return topLevelImportsMatch.

  10. 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:
    1. If asURL’s scheme is "std", and moduleMap[asURL] does not exist, then throw a TypeError indicating that the requested built-in module is not implemented.

    2. Return asURL.

  11. Throw a TypeError indicating 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 TypeErrors manually, so this is straightforward.)

To resolve an imports match, given a string normalizedSpecifier, a specifier map specifierMap, and a module map moduleMap:
  1. For each specifierKeyaddresses of specifierMap,

    1. If specifierKey is normalizedSpecifier, then:

      1. If addresses’s size is 0, then throw a TypeError indicating that normalizedSpecifier was mapped to no addresses.

      2. If addresses’s size is 1, then:

        1. Let singleAddress be addresses[0].

        2. If singleAddress’s scheme is "std", and moduleMap[singleAddress] does not exist, then throw a TypeError indicating that the requested built-in module is not implemented.

        3. Return singleAddress.

      3. If addresses’s size is 2, and addresses[0]'s scheme is "std", and addresses[1]'s scheme is not "std", then:

        1. Return addresses[0], if moduleMap[addresses[0]] exists; otherwise, return addresses[1].

      4. Otherwise, we have no specification for more complicated fallbacks yet; throw a TypeError indicating this is not yet supported.

    2. If specifierKey ends with U+002F (/) and normalizedSpecifier starts with specifierKey, then:

      1. If addresses’s size is 0, then throw a TypeError indicating that normalizedSpecifier was mapped to no addresses.

      2. If addresses’s size is 1, then:

        1. Let afterPrefix be the portion of normalizedSpecifier after the initial specifierKey prefix.

        2. Let url be the result of parsing afterPrefix relative to addresses[0].

        3. If url is failure, throw a TypeError, implicating normalizedSpecifier (and in particular the afterPrefix portion).

        4. Return url.

      3. Otherwise, we have no specification for more complicated fallbacks yet; throw a TypeError indicating this is not yet supported.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[CONSOLE]
Dominic Farolino; Terin Stock; Robert Kowalski. Console Standard. Living Standard. URL: https://console.spec.whatwg.org/
[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[ENCODING]
Anne van Kesteren. Encoding Standard. Living Standard. URL: https://encoding.spec.whatwg.org/
[FETCH]
Anne van Kesteren. Fetch Standard. Living Standard. URL: https://fetch.spec.whatwg.org/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[URL]
Anne van Kesteren. URL Standard. Living Standard. URL: https://url.spec.whatwg.org/
[WebIDL]
Boris Zbarsky. Web IDL. URL: https://heycam.github.io/webidl/

Issues Index

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.
CSP is applied to import maps just like JavaScript scripts. Is this sufficient? #105.