Skip to content

Latest commit

 

History

History
164 lines (107 loc) · 8.49 KB

spec.md

File metadata and controls

164 lines (107 loc) · 8.49 KB

Import maps proto-spec

Installation

When import maps can be encountered

Each realm (environment settings object) has a boolean, acquiring import maps. It is initially true.

The internal module script graph fetching procedure flips the boolean to false. Practically speaking, this happens when:

  • Any <script type="module"> element is connected to the document
  • Any import() call is made
  • Any worker module is created, e.g. via new Worker(url, { type: "module" })

Additionally, fetching any import: URLs will flip this boolean to false. (See below.)

If a <script type="importmap"> is encountered when acquiring import maps is false, then the developer has made an error. We will signal this by firing an error event on the <script type="importmap"> element, and implementations should also display the error in the developer console.

Acquiring import maps

Encountering a <script type="importmap"> while acquiring import maps is true will kick off a procedure roughly like this:

  1. If it's an external script (i.e. has a src=""), fetch it, using the usual "good defaults" of <script type="module">. (E.g., always UTF-8, "cors" mode, MIME type must match, ...)
  • What should be the MIME type? Let's say application/json+importmap? Maybe accept any JSON MIME type.
  1. Parse the result as JSON into a spec-level struct (see below). (Will need to solve whatwg/infra#159 as part of this.)
  2. Merge the resulting struct into our realm's merged import map (see below).

Any ongoing fetches of import maps are noted, while ongoing, so that import: URL fetching can block on them (see below).

Import map spec structure

For now, see the reference implementation to understand how an arbitrary string gets turned into a normalized "import map" structure. This will soon be ported to formal specification text, after a bit more validation that it works correctly.

Merging import maps

We're looking to do the minimal thing that could work here. As such, I propose the following:

Given two import maps A and B, the merged import map is a new import map whose imports are the result of merging A's imports and B's imports, and whose scopes are the result of merging A's scopes and B's scopes. Here, merging two maps means appending their entries to each other, with any conflicting keys from the first map removed.

Example:

{
  "imports": { "a": "1", "b": "2" }
}
{
  "imports": { "a": "3" }
}

=

{
  "imports": { "b": "2", "a": "3" }
}

Note that we do not introspect the scopes. If there's two conflicting definitions of how things behave inside a scope, then the last one wins.

Resolution

Modifications to the URL parser for import: URLs

Wait, what?

The URL parser is a foundational piece of the ecosystem, meant to be shared across not only the web platform, but the whole internet. So changing it should not be done lightly.

However, we have a useful precedent to follow: blob: URLs. blob: URLs are a type of browser-specific URL; they impact only the URL parser used by web browsers, not the basic URL parser used by the rest of the internet. import: URLs would be similar. Also similar to for blob: URLs, for import: URLs, we need to do some parse-time association of data, in web browsers only, for better handling by the fetch algorithm later.

Proposed modification

Add a new field to URL records, call it module base URL. (Alternately, repurpose the object field used by blob: URLs to be something more general.)

Add some new steps to the URL parser, around blob: URL steps (3-5):

  1. If url's scheme is "import", set url's module base URL to the active script's base URL, if the active script is not null; otherwise set it to the current settings object's API base URL.

Now, the fetch algorithm (below) can consult the module base URL.

What does this give us?

By giving us maximum context (the active script) when resolving import: URLs, we get the same context-sensitivity benefits of import statements and import() expressions everywhere on the platform that import: URLs show up.

For example, this gives us a nice tidy way of doing URL resolution relative to the module: instead of

const response = await fetch(new URL('../hamsters.jpg', import.meta.url).href);

we can just do

const response = await fetch('import:../hamsters.jpg');

since the URL parser will resolve import:../hamsters.jpg relative to the active script, and thus fetch will receive the appropriately-resolved absolute URL.

Without these URL resolution modifications, the fetch algorithm would just receive the raw URL import:../hamsters.jpg. It would need to either fail the fetch, or assume that it's meant to be relative to the current settings object's API base URL, i.e. treat it as just ../hamsters.jpg.

This is also crucial for making import maps' scopes feature work as expected with import: URLs. Consider the following import map:

{
  "imports": {
    "lodash": "/node_modules/lodash-es/lodash.js"
  },
  "scopes": {
    "/js/different-lodash-here/": {
      "lodash": "/node_modules/underscore/underscore.js"
    }
  }
}

Then, inside /js/different-lodash-here/code.mjs, we have the following:

import _ from "lodash";                                     // (1)
const source = await (await fetch("import:lodash")).text(); // (2)

As we know, the _ imported in (1) will be from /node_modules/underscore/underscore.js, per the scope configuration. But what will be the result of (2)?

With these URL resolution modifications, it will also be from /node_modules/underscore/underscore.js, since we have enough context to know that we're inside the /js/different-lodash-here/ scope. Without them, though, the fetch algorithm will only receive the raw data import:lodash, without any knowledge of scopes. In that case it will probably use the import map's non-scoped mapping, and retrieve /node_modules/lodash-es/lodash.js.

Modifications to Fetch for import: URLs

We treat import: URLs like blob: URLs, in that they get a special entry in scheme fetch. Roughly it would do this:

  1. Wait for any ongoing fetches of import maps.
  2. Let url be request's current URL.
  3. Let baseURL be url's module base URL.
  4. Let specifier be url's path.
  5. Let underlyingURL be null.
  6. If specifier starts with /, ./, or ../, then set underlyingURL to the result of URL-parsing specifier with baseURL as the base URL.
    • This is recursive. Should we just use the basic URL parser? Need to explore the consequences.
  7. Otherwise, set underlyingURL to the the result of consulting the current realm's merged import map, given specifier and baseURL. (baseURL is used to determine what scope we're in.)
    • This should return nothing for built-in modules by default, i.e., unless they have been remapped, you can't fetch their source code.
  8. "Do a request to underlyingURL". Details unclear; things to consider:
    • Should we just change request's current URL?
    • Should we make this behave like a redirect of some sort? (Probably a bad idea.)
    • Should we mandate all import:-based requests use CORS/etc.? In that case we'd probably recreate a request from scratch.
    • Should we restrict to GETs like blob: does?
    • Should baseURL be the referrer, or should we use the referrer we're given in request?

TODO: this needs to loop for fallbacks

Consulting the import map

Given a import map M, a specifier specifier, and a base URL baseURL:

... TODO ...

Trickier parts will be scopes and trailing-slash packages, as both are somewhat unprecedented in the web's URL handling.

Resolve a module specifier

I think with all the above in place, the current spec's "resolve a module specifier" can be reduced to just prepending import:? Hmm, but the "active script" isn't exactly the same as the base URL passed to that... TODO...