WICG / import-maps

How to control the behavior of JavaScript imports
https://html.spec.whatwg.org/multipage/webappapis.html#import-maps
Other
2.65k stars 69 forks source link

Multiple import maps and the concept of a "package" #252

Closed Jamesernator closed 2 years ago

Jamesernator commented 2 years ago

Currently we can only load a single import map, for code that is structured as packages this requires tooling to generate import maps, this is especially complicated for CDNs that may wish to collapse modules into single files without breaking people's import maps.

This problem is already known and has some suggestions but I would like to propose another potential solution that allows composition of import maps and importantly allows packages to be more independent.

In particular what I propose is the ability to slightly extend import maps to themselves be targets of imports, acting as a package descriptor of sorts. This idea is inspired by package.json in Node.js, but rather than searching up the "file system" (or parent urls) for a package.json, inside the import map we would directly point to the "import map".

The general idea (however some bikeshedding would be needed on the details) is that we would extend the import map to have some kind of "exports" field, consumers of the package could then put a reference to the imported package into their own import map.

As an example of how this would work consider the following example:

// import-map.json
{
    "imports": {
        // defer everything after a bare lib specifier to whatever the imported
        // map's exports point to
        "lib": "package:https://cdn.tld/lib/import-map.json",
        // This could even work with non-bare specifiers in exactly the same way
        // (although they musn't end with a /)
        "/some/absolute/path": "package:https://cdn.tld/other-lib/import-map.json"
    }
}
// https://cdn.tld/lib/import-map.json
{
    "exports": {
        // Only relative urls may be present here
        ".": "./main.js",
        "./other": "./lib/other.js"
    },
    // any modules loaded via resolving inside "exports" (and their dependencies) would use
    // these import mappings instead of ours
    "imports": {
        "dep": "package:https://cdn.tld/dep/import-map.json",
        "local-dep": "package:./local-folder/import-map.json"
    }
}
// main.js

// If this file were loaded under our import-map.json (not the CDN one), then
// upon seeing "lib" the import map defined by "package:https://cdn.tld/lib/import-map.json"
// would be fetched, by seeing that the bare specifier is used as is we would look at the
// "exports" field and see that there is an export for ".", the value there would be taken
// relative to the to that import map, i.e. because the package's import-map is at
//     https://cdn.tld/lib/import-map.json
// we will see the export "./main.js" and resolve it to:
//     https://cdn.tld/lib/main.js
// Hence this export is resolved as https://cdn.tld/lib/main.js, although note that this is not
// semantically equivalent to loading https://cdn.tld/lib/main.js directly, as if we to replace
// this import with import lib from "https://cdn.tld/lib/main.js" any specifiers there would
// be loaded from our import map
import lib from "lib";

// Importing subpaths works the same way, we look them up in the "exports", so this becomes
// roughly similar to loading https://cdn.tld/lib/lib/other.js
import other from "lib/other";

// We could also provide a way to load more directly things more directly, in fact this could
// even be the canonical "url" disambiguating it from if we to load the file directly
// Effectively these urls would say "Load the second url as a module using the first url as its import map"
import lib from "package:https://cdn.tld/lib/import-map.json|https://cdn.tld/lib/main.js";
Jamesernator commented 2 years ago

Thinking about this, I think a processing model would work something like this:

The actual "exports" idea would itself be layered on top of this to provide a better way for authoring multiple import maps (as combined urls would be rather unpleasant to author).

domenic commented 2 years ago

I believe this violates the design principle at https://github.com/WICG/import-maps#scope

Jamesernator commented 2 years ago

It's not clear to me how the design of:

They are not meant to be composed, but instead produced by a human or tool with a holistic view of your web application

Is compatible with any multiple import map support, and if there's only one I don't see how

produced by a human

Is in any way viable without delegation to other import maps. Even with a small number of modules it quickly becomes unviable to maintain by hand.

Note that the user under my suggestion user still has the choice to use their own import map to load the remote package if they want. It's just that it gives the user the freedom to delegate to packages (which may even be author controlled, as is the case with mine).

domenic commented 2 years ago

Is compatible with any multiple import map support, and if there's only one I don't see how

The case we've been told of is when multiple tools are producing the import maps (e.g. JS bundler + CSS bundler; or, a JS bundler for packages and a separate hash-rewriter), or when a tool is producing half the import map (e.g. the node_modules portion) and the developer is hand-editing the other half (e.g. for nicer aliases for common app-wide utilities).

Is in any way viable without delegation to other import maps. Even with a small number of modules it quickly becomes unviable to maintain by hand.

In general I don't think maintaining package mappings is viable for humans; that is best done by tools. Instead it's bespoke per-app mappings that might be hand-maintained.