WICG/import-maps

Allow for multiple package name maps.

bmeck opened this issue · 9 comments

bmeck commented

This is an issue is to discuss the potential to have multiple package name maps within a single document. As stated in #1 (comment) it is important to ensure that the application developer has full control over all package name maps.

In particular, there seems to be some contention about if the package name map must be a single resource/file. I would like to discuss having a single package name map per resource, but not a single package name map per document.

There are a few use cases that investigating would be good when talking about this:

Progressive loading of maps

As a document loads it may have a desire to inline a package name map, just like how documents inline <script> content for the initial render as well. If replacement of standard module as shown in the readme is desirable, this may be to simply replace the standard modules with replacements such as polyfills separate from the application modules not needed for first render.

Compose "scope" of packages in map rather than define entirety

The "scope" mechanism is setup as a means to allow control of imports in a nested manner based upon pathname boundaries. This requires management always be done as a whole rather than on a per package basis. This is problematic when using 3rd party scripts. Allowing the ability to reference a separate module map would alleviate this management burden. However, care needs to be taken that subresource integrity is preserved across all loads in order to prevent the 3rd party from being able to change the separate module map contents.

Cache subsection scopes

In a large module map we can imagine having several hundred different entries in a package name map. Being able to replace smaller parts of the package name map rather than the whole seems appealing. Doing so requires having separated caching for subsections, similar to how ESM has separate caching per module.

Contained package name map expectations

Allowing resources to control their package name map allows for them to declare their expected resolution. It would prevent any dependency hell that could occur from the global package name map from going out of sync with the scope's package name map which could be managed and generated separately.

I just had a discussion with @cdata about multiple package name maps as well. His use case was having basically two: one that's tool-generated, and one that's author-edited. That makes sense to me, as does the progressive rendering. (The scope composition I don't believe makes sense, but is a larger discussion.)

I think it would be easy to add multiple package name maps if the merging rules were very simple and restrictive. For example:

  • Only the first can have "path_prefix"; if any beyond the first have one, that entire package name map is ignored
  • All top-level "packages" maps get concatenated into one map. If this causes duplicate keys, the entirety of the second package name map is ignored.
  • All top-level "scopes" maps get concatenated into one map. If this causes duplicate keys, the entirety of the second package name map is ignored.

With these rules we can basically just merge the maps at the top level and treat the result as if it were a single map JSON object.

Now, these rules might be too restrictive to be useful. We could probably loosen some of them, e.g. perhaps duplicate keys override earlier versions, and perhaps path_prefix gets normalized into the path of each package/scope, so you can have path_prefix be per-package map. But that adds complexity. I'd want to see how #6 goes, and then try to build on it to see what's natural and low-complexity.

What I want to avoid is some kind of recursive normalized merging, or any series of cascading fallbacks.

It's also worth noting that this whole discussion is pretty additive, and we could add multiple package name maps post-MVP.

Given duplicate keys, an alternative choice is to take the last one - is there a rationale for one over the other? (given objects and json take the last)

Objects throw. You're not writing sloppy mode modules...... are you? 😉

@Kovensky obj.a = 2; obj.a = 3; means that obj.a is 3. Object literal syntax throws, but objects don't.

bmeck commented

@domenic

  • Only the first can have "path_prefix"; if any beyond the first have one, that entire package name map is ignored

You mean outside of scopes right? Since I would expect being able to add scopes would also mean being able to add "path_prefix" to the scope.

  • All top-level "packages" maps get concatenated into one map. If this causes duplicate keys, the entirety of the second package name map is ignored.
  • All top-level "scopes" maps get concatenated into one map. If this causes duplicate keys, the entirety of the second package name map is ignored.

Seems good to me.

One idea we had early on was to allow a scope to point to a URL, to import the definition from another file. This seemed useful for pulling in a package and its dependencies from a CDN.

Example:

  "packages": {
    "package1": { "path": "packages/package1" },
    "jquery": {
      "path": "https://unpkg.com/jquery",
      "main": "jquery.js"
    }
  },
  "scopes": {
    "packages/package1": { /* overrides in the local package1 scope */ },
    // let's say everything on unpkg pulls dependencies from unpkg
    "https://unpkg.com/": "https://unpkg.com/packagemap.json"
  }
}

This would set up a map where jquery and any of its dependencies would be read from unpkg.com, as defined by the package map at unpkg.com/packagemap.json. It may be a bit unrealistic to have a single package map for a CDN, but you can imagine a number of different ways the CDN itself, or an application, or 3rd party, could generate package maps for subsets of the CDN.

Given that cross-origin scopes only make sense as top level, I'm pretty sure that this use case would be mostly covered by @domenic's idea to just merge the top-level packages and scopes objects.

Referencing external package maps is a little more powerful though, since you can compose at any level of the scope tree, so that might cover some non-CDN use cases. It still avoids recursive merging due to a scope only being defined in one map, not partially in multiple maps, if that's the main problem to be solved.

cdata commented

Referencing external package name maps this way would indeed be nice, and would certainly unlock some interesting distribution strategies. However, the CDN case is actually quite a complicated one Consider that NPM packages express their dependencies as ranges, so a package name map for a given package on a CDN may change depending on whether or not a relative in the package graph shares a dependency. Also, maybe this is an unintended consequence of simplification in your example, but wouldn't https://unpkg.com/packagemap.json be a gigantic, uncacheable name map?

The incrementally additive change of allowing more than one package name map in a document (+/- some rudimentary strategy for merging all the name maps together) greatly simplifies some important practical use cases, and as you say, supports certain CDN use cases as well.

I think the original proposal makes sense, if I understood it well. You don't always have the luxury of compiling all of the package maps into a single package map, server-side.

Having support for distributed package maps enables better cache reuse and scales better.

Unlike RequireJS, SystemJS enables distributed package definitions, which are loaded incrementally (see https://github.com/systemjs/systemjs/blob/master/docs/config-api.md#packageconfigpaths). In RequireJS, there is a single, usually gigantic, package map.

The proposal has been overhauled. In the new version, multiple maps are now supported. Yay!