JavaScript Import maps, Part 2: In-Depth Exploration


We recently shipped import maps in Firefox 108. you might be wondering what this new feature is and what problems it solves. In the previous post we introduced what import maps are and how they work, in this article we are going to explore the feature in depth.

Explanation in-depth

Let’s explain the terms first. The string literal "app.mjs" in the above examples is called a Module Specifier in ECMAScript, and the map which maps "app.mjs" to a URL is called a Module Specifier Map.

An import map is an object with two optional items:

  • imports, which is a module specifier map.
  • scopes, which is a map of URLs to module specifier maps.

So an import map could be thought of as

  • A top-level module specifier map called “imports”.
  • A map of module specifier maps called “scopes”, which can override the top-level module specifier map according to the location of the referrer.

If we put it into a graph

Module Specifier Map:
  +------------------+-----------------+
  | Module Specifier |      URL        |
  +------------------+-----------------+
  |  ......          | ......          |
  +------------------+-----------------+
  |  foo             | https://foo.com |
  +------------------+-----------------+

Import Map:
  imports: Top-level Module Specifier Map
    +------------+------------------------+
    | URL        | Module Specifier Map   |
    +------------+------------------------+
    | /          | ...                    |
    +------------+------------------------+

  scopes: Sub-directories Module Specifier Map
    +------------+------------------------+
    | URL        | Module Specifier Map   |
    +------------+------------------------+
    | /subdir1/  | ...                    |
    +------------+------------------------+
    | /subdir2/  | ...                    |
    +------------+------------------------+

Validation of entries when parsing the import map

The format of the import map text has some requirements:

  • A valid JSON string.
  • The parsed JSON string must be a JSON object.
  • The imports and scopes must be JSON objects as well.
  • The values in scopes must be JSON objects since they should be the type of Module Specifier Maps.

Failing to meet any one of the above requirements will result in a failure to parse the import map, and a SyntaxError/TypeError will be thrown.1

<!-- In the HTML document -->
<script>
window.onerror = (e) => {
  // SyntaxError will be thrown.
};
</script>
<script type="importmap">
NOT_A_JSON_STRING
</script>
<!-- In another HTML document -->
<script>
window.onerror = (e) => {
  // TypeError will be thrown.
};
</script>
<script type="importmap">
{
  "imports": "NOT_AN_OBJECT"
}
</script>

After the validation of JSON is done, parsing the import map will check whether the values (URLs) in the Module specifier maps are valid.

If the map contains an invalid URL, the value of the entry in the module specifier map will be marked as invalid. Later when the browser is trying to resolve the module specifier, if the resolution result is the invalid value, the resolution will fail and throw a TypeError.

<!-- In the HTML document -->
<script type="importmap">
{
  "imports": {
    "foo": "INVALID URL"
  }
}
</script>

<script>
// Notice that TypeError is thrown when trying to resolve the specifier
// with an invalid URL, not when parsing the import map.
import("foo").catch((err) => {
  // TypeError will be thrown.
});
</script>

Resolution precedence

When the browser is trying to resolve the module specifier, it will find out the most specific Module Specifier Map to use, depending on the URL of the referrer.

The precedence order of the Module Specifier Maps from high to low is

  1. scopes
  2. imports

After the most specific Module Specifier Map is determined, then the resolving will iterate the parsed module specifier map to find out the best match of the module specifier:

  1. The entry whose key equals the module specifier.
  2. The entry whose key has the longest common prefix with the module specifier provided the key ends with a trailing slash ‘/’.
<!-- In the HTML document -->
<script type="importmap">
{
  "imports": {
    "a/": "/js/test/a/",
    "a/b/": "/js/dir/b/"
  }
}
</script>
// In a module script.
import foo from "a/b/c.js"; // will import "/js/dir/b/c.js"

Notice that although the first entry "a/" in the import map could be used to resolve "a/b/c.js", there is a better match "a/b/" below since it has a longer common prefix of the module specifier. So "a/b/c.js" will be resolved to "js/dir/b/c.js", instead of "/js/test/a/b/c.js".

Details can be found in Resolve A Module Specifier Specification.

Limitations of import maps

Currently, import maps have some limitations that may be lifted in the future:

  • Only one import map is supported
    • Processing the first import map script tag will disallow the following import maps from being processed. Those import map script tags won’t be parsed and the onError handlers will be called. Even if the first import map fails to parse, later import maps won’t be processed.
  • External import maps are not supported. See issue 235.
  • Import maps won’t be processed if module loading has already started. The module loading includes the following module loads:
    • Inline/External module load.
    • Static import/Dynamic import of Javascript modules.
    • Preload the module script in .
  • Not supported for workers/worklets. See issue 2.

Common problems when using import maps

There are some common problems when you use import maps incorrectly:

  • Invalid JSON format
  • The module specifier cannot be resolved, but the import map seems correct: This is one of the most common problems when using import maps. The import map tag needs to be loaded before any module load happens, check the Limitations of import maps section above.
  • Unexpected resolution
    • See the Resolution precedence part above, and check if there is another specifier key that takes higher precedence than the specifier key you thought.

The specification can be found in import maps.


Acknowledgments

Many thanks to Jon Coppeard, Yulia Startsev, and Tooru Fujisawa for their contributions to the modules implementations and code reviews on the import maps implementation in Spidermonkey. In addition, great thanks to Domenic Denicola for clarifying and explaining the specifications, and to Steven De Tar for coordinating this project.

Finally, thanks to Yulia Startsev, Steve Fink, Jon Coppeard, and Will Medina for reading a draft version of this post and giving their valuable feedback.

Notes

  1. If it isn’t a valid JSON string, a SyntaxError will be thrown. Otherwise, if the parsed strings are not of type JSON objects, a TypeError will be thrown.