import-maps icon indicating copy to clipboard operation
import-maps copied to clipboard

Dynamic import maps support

Open guybedford opened this issue 6 years ago • 45 comments

It seems like injecting import maps into the page after the first load is explicitly prohibited here:

Similarly, attempting to add a new

This seems very restrictive to me. If the composition rule of import maps was to have them throw on conflicts, then the nondeterministic effects can be reduced here (at least for bare package imports).

This is something users will definitely request as soon as they start using this API. Sandboxes entirely rely on being able to do this sort of thing.

guybedford avatar Jan 11 '19 16:01 guybedford

I'm not sure what you're asking for here. Once modules have begun fetching, we can't change mid-flight how module resolution works, so this constraint is very necessary. At the same time, it seems clearly documented. What is the action item?

domenic avatar Jan 11 '19 16:01 domenic

Once modules have begun fetching, we can't change mid-flight how module resolution works

It could be possible to move from a state where import('asdf') throws for having no map, to a state where import('asdf') works out due to a map being dynamically injected. This can be a well-defined transition.

I know you have reservations on the determinism here but I would argue that this is an important feature to have, if not initially, that it will be needed eventually.

The action item would be to lay the groundwork for the above determinism by throwing when a package map name collides during composition.

The full behaviour for a deterministic dynamic injection would then be:

  • It does not delay the resolver, only those package maps initial on page load delay the resolver
  • Once loaded, it composes into the package map, throwing on conflicts with existing packages
  • If there are any URL mappings, these throw as well as they are not supported for the dynamic case.
  • On its load event, an import() to any name in the new map can then be guaranteed to work.

guybedford avatar Jan 11 '19 16:01 guybedford

That's a strange and crippled subset of the functionality that import maps provide; I don't think it makes sense to integrate it into this proposal. Rather, folks should investigate different workflows, e.g. server-generating their import maps. Import maps really aren't designed around dynamism, and it is a non-goal to support that; they have a hard enough job already solving all their existing goals without also taking on the role of a dynamic resolver system.

domenic avatar Jan 11 '19 16:01 domenic

Import maps really aren't designed around dynamism, and it is a non-goal to support that; they have a hard enough job already solving all their existing goals.

This sounds more like an opinion than an argument. But yes, it does come down to discussing goals. I can tell from experience that this will be the top requested feature of import maps as soon as users use them.

guybedford avatar Jan 11 '19 16:01 guybedford

No, it's more that if we had a different set of goals, we would have designed a different solution, e.g. one based on JavaScript functions instead of declarative JSON structures.

I suggest those interested in adding dynamic mapping to the platform work on doing that in a different manner, and not trying to extend this proposal to encompass it.

domenic avatar Jan 11 '19 16:01 domenic

@domenic JS module loaders like RequireJS and SystemJS have provided dynamic mappings through a JSON mapping structure for years, so I don't think the design argument holds on that point here.

guybedford avatar Jan 11 '19 16:01 guybedford

For whatever it's worth, I agree that dynamically adding an import map in the browser is a very worthwhile feature. Even after initial module loading has begun. I think it only makes sense for modules that are loaded after the initial page load (either via import() or via injecting a <script type="module"> after initial page load), but I find those use cases compelling.

I'm not sure if I understand all of the nuances of the argument about determinism, but it seems to me that if the user does import('my-lib') before they have defined my-lib in an import map, that that's quite reasonable as a user that my import doesn't work. And also that I'd like to import some modules with an initial import map before adding a new import map that has a few more modules.

joeldenning avatar Jan 11 '19 18:01 joeldenning

Agreed with @joeldenning. If the browser has never evaluated import('some-lib'), then why would it be bad to interpret a new package map that defines some-lib even after other modules have been loaded?

blittle avatar Jan 11 '19 20:01 blittle

To explain the reasoning here, in the draft import maps specification, HTML doesn't really keep track of whether it's done import("./some-lib.mjs") yet--what it keeps track of is what `"./some-lib.mjs" resolved to via the import map, and whether that was requested yet. I think it'd be a bit surprising if you had an API that observably changed what a particular import statement or dynamic import with the same argument does over time.

At the same time, it's true that the dynamism in this proposal is very limited. Although it's nice that you can use scripts to modify it, any use of <link rel="modulepreload"> will block further import maps from taking effect. It'd take a pretty opinionated page to be able to actually dynamically load more import maps.

I'm wondering what problems people have with this static design. My intuition is, if these things can be pre-computed, it could lead to faster page load time, so I like the design of the import maps proposal. What wouldn't fit in well to https://github.com/WICG/import-maps/issues/92#issuecomment-453579568 ?

littledan avatar Mar 06 '19 09:03 littledan

I think being able to load import-maps dynamically would be beneficial specially for SPAs.

We are currently using some sort of map for long term caching of static assets with RequireJS and the whole file would be huge if it was not split and loaded on demand. Other concern I have is the performance penalty from fetching multiple maps at the start of the application if different parts of it were deployed in different servers or CDNs. Finally some existing pages with complex setups heavily rely on adding some mappings from withing the application (i.e. environment/user dependent configuration) and being able to have a single module system is good for managing complexity.

Regarding:

I think it'd be a bit surprising if you had an API that observably changed what a particular import statement or dynamic import with the same argument does over time.

Maybe merging dynamically loaded maps could behave in a way in which the current one take precedence when coming across duplicated keys.

isidrok avatar Mar 06 '19 10:03 isidrok

We are currently using some sort of map for long term cache of static assets with RequireJS and the whole file would be huge if it was not spitted and loaded on demand.

For me, that's the most compelling argument. In general, I can't imagine that you want to alter the way how imports are resolved. But you certainly want to extend it.

jhnns avatar Mar 06 '19 10:03 jhnns

I'm a little unclear on the setup. How do you want to use import maps here?

littledan avatar Mar 06 '19 12:03 littledan

I'm a little unclear on the setup. How do you want to use import maps here?

Imagine an app with some common modules to all routes and a big module foo which is loaded when navigating to /foo. Module foo has lots of sub-routes and requires lots of static assets so the import-map needed for it to work is quite big. I would like to be able to load foo import-map just before loading the foo module. If having a big SPA the amount of modules like foo could be quite big, thus resulting in quite a big import-map specially if we include information for using import: URLs.

Another use case would be for example for A/B testing or feature toggles: I may want to offer a given set of users a new version of some module while others keep using the old one, this is definitely doable without dynamic import-maps:

// foo.js
import new from './foo.new.js';
import old from './foo.old.js'

const foo = useNew ? new : old;
export default foo;

But may be nicer using them.

In my current setup (nothing serious, just a PoC) I use import-maps for:

  • Decoupling in general.
  • Resolving the different app-modules though bare imports which allows for partial builds/updates only by updating the import-map.
  • Implementing long term caching aliasing bare specifiers to hashed files. I.e. '@x/foo' : 'foo.asd123.js'

I don't have the need for dynamic import-maps here since I use rollup to bundle each module or sub-app and it takes care of dynamic imports and assets, but if I didn't use a bundler then a good number of entries would be written into the map which in most cases would be just fine but who knows how much could it grow.

I could definitely pass without the dynamic behavior of import-maps but I'm used to it while using different module loaders and I think it can be convenient.

isidrok avatar Mar 06 '19 12:03 isidrok

@isidrok I'm missing something--why would the import map be dynamic, as opposed to what you dynamically choose to import with import()?

littledan avatar Mar 06 '19 13:03 littledan

I'll try to be more specific:

  1. Reduce initial load time in cases where the import map has a significant weight.
  2. Allow to specify mappings at run-time conditionally without changing existing code. It's true that one could use dynamic imports and choose what to import but if that was not planned when the module was created then a static synchronous module would be turned into an asynchronous one requiring changes in its consumers.

All in all, I don't think its a needed feature but a convenient one.

isidrok avatar Mar 06 '19 15:03 isidrok

Would it be accurate to sum up the issue like this?

If there are many import map entries potentially accessed by dynamic imports, this proposal is suboptimal because all entries are downloaded up-front, even those that are not used on initial load.

littledan avatar Mar 07 '19 02:03 littledan

@littledan I would add:

  • When different import-maps the application needs cannot be merged in advance (i.e. third party modules) initial load time could be penalized.
  • Some module loaders allow dynamic configuration and people find it convenient sometimes.

isidrok avatar Mar 09 '19 19:03 isidrok

Based on some related experiments, there's an alternative approach which may help achieve some of the goals in this thread (and solve other problems too e.g. for workers or Node) - without making it dynamic:

It might be better to spec a local map for each module in it’s metadata. The overall characteristics that arise are much better than a global map: for example, two modules could refer to different versions of a module, naturally avoiding dependency hell - without the additional scope mechanism, which tries to inverse the process and add module-local information on top of a global map.

The metadata could be populated by HTTP (link?) headers, co-locating the meaning of a specifier with the delivery of the module that uses it means you can have pay-for-what-you-use incremental loading, as opposed to having to download and parse what will eventually be an entire package-lock.json before you can run anything else.

In hindsight, this could still work, by taking the current import map as just one way to populate that canonical information that lives in the module metadata, enabling servers to also populate it via headers. I saw this was already discussed elsewhere (#1), but thought it was worth raising again as the trade-off's which pulled this strongly towards an application-level only approach may have changed and be better suited to extend in this manner.

pemrouz avatar Mar 14 '19 23:03 pemrouz

See https://github.com/WICG/import-maps#scope; maps are intentionally global.

domenic avatar Mar 15 '19 01:03 domenic

@domenic Do you think there might be a place for package-level maps? Maybe as source material for generating the global-level one? It feels like it would be useful for packages to bundle their own internal import maps for their dependencies (and maybe also their exports), and for that map to be part of what gets published to the npm registry.

GeoffreyBooth avatar Mar 15 '19 01:03 GeoffreyBooth

Based on what we've seen so far in the ecosystem, I do not. E.g. you have a single app-level package-lock.json/yarn.lock. But, anything's possible in tooling land. I just haven't seen it.

domenic avatar Mar 15 '19 01:03 domenic

Based on what we’ve seen so far in the ecosystem, I do not. E.g. you have a single app-level package-lock.json/yarn.lock. But, anything’s possible in tooling land. I just haven’t seen it.

Yes but each package has its own package.json. I think it would be useful as part of generating the global import map to know what the public paths are within each package. Like in your example for "/node_modules/socksjs-client/querystringify/index.js", what if that file is moved within that package or renamed? How would the global import map be updated to reflect that change, if not a user fixing it manually after investigating the new package structure? Whereas if socksjs-client could specify somehow what the path is to its querystringify export, then the export’s file could be renamed or moved without the package author worrying about breaking users’ import maps.

For Node we’ve been discussing this here, and maybe it’s something that Node just implements on its own, but it would be nice to coordinate the two since they’re obviously related if not interconnected.

GeoffreyBooth avatar Mar 15 '19 01:03 GeoffreyBooth

In the end it's the application which decides what files are in node_modules, and on the web, it makes sense to me for the application to decide what paths are in the import map. I just don't see the value of the extra complexity you are proposing.

if not a user fixing it manually after investigating the new package structure

Precisely, a user (or more likely a tool the user is using, like npm or yarn) would become aware of the app's new package structure and generate a new import map. After all, most user's change their app's package structure via such tools.

domenic avatar Mar 15 '19 01:03 domenic

See https://github.com/WICG/import-maps#scope; maps are intentionally global.

I’m not sure I fully grok, correct me if I’m wrong, but is your main concern essentially some XSS-esque scenario where a CDN changes the meaning of a specifier to be something malicious, and that wouldn’t happen with this application-level config?

In that case, I think you may be conflating goals and handicapping both. If you include a script in the page from somewhere else, that server can serve pretty much anything. The only way to lock that down would be SRI. Specifier-to-URL or URL-to-URL translation doesn’t change anything - even if it’s the application author doing it. If they map “jquery” to load from somewhere else, they could still get anything. This is probably more misleading to users that they are secure.

A better approach might be to separate out an application-level map for defining the hashes for the modules the page is willing to accept. That would be an actual package-lock for the web, and a way for app authors to lock things down. This isn’t a package-lock.

This way a CDN could actually legitimately fallback and serve that transitive jquery file from some other server. But the application doesn’t care, because it knows it’s getting the same file because of the hash.

pemrouz avatar Mar 15 '19 05:03 pemrouz

@pemrouz no, the main concern is not XSS. The main concern is that application authors are the appropriate party to be in control here, not spread throughout their dependency graph.

domenic avatar Mar 15 '19 16:03 domenic

Precisely, a user (or more likely a tool the user is using, like npm or yarn) would become aware of the app’s new package structure and generate a new import map. After all, most user’s change their app’s package structure via such tools.

I guess what I’m suggesting is that there be something for such tools to use to know how to generate the import map, for example to know what the paths within a package should be or point to. If there isn’t something defined in the spec for how this data should be stored within packages, each tool will come up with its own solution, and then packages might have a JSON file for npm and another for yarn and so on. I think we would be better off if it were standardized.

GeoffreyBooth avatar Mar 15 '19 17:03 GeoffreyBooth

I don’t see how this spec, which is unaware of package.json files or even the concept of packages, would be able to address that. It might be a more reliable approach to get all the popular tools on board with the same joint standard, at which point (like broserify’s browser field) it becomes a defacto standard that everyone follows.

ljharb avatar Mar 15 '19 17:03 ljharb

I welcome folks to work on inter-tool standardization efforts for how to generate import maps, and even use this repository as a way to coordinate while getting started. (See for example #108 or #60.) But this specification is indeed about a web platform feature, so the specification work here will be about import maps.

domenic avatar Mar 15 '19 17:03 domenic

@pemrouz no, the main concern is not XSS. The main concern is that application authors are the appropriate party to be in control here, not spread throughout their dependency graph.

That was the other conflation I think, between "library" and "module".

For example, it would not make sense for a library to include a import map

My suggestion is not to have each "library" published with their own map.

It's that the meaning of the bare imports used by each module should be canonically stored in it's own metadata.

Even in the latter case, the resolution is done from the application-level perspective. It's not controlled by dependencies. It just means it can be delivered more progressively.


Those were the only two possible reasons I could understand from what you wrote. If you agree XSS is now not a concern, and app authors can still be in control, then you probably have additional valid context that leads you to a very different conclusion on the goals as @guybedford mentioned i.e. considering any progressive loading a non-goal, import maps a security mechanism, etc.

If other implementors share some of these concerns, then it would be more beneficial to discuss offline sometime, otherwise I don't have a strong opinion/desire to push this (/cc @littledan @annevk @MylesBorins). Small outline of what the counter proposal would consist of:

  • Speccing a place in module metadata to store their own maps (this would also mean Node or workers can easily use the same)
  • Keep the current format as a way to populate those maps
  • Allow additional ways to populate those maps, such as via <link>/Link
  • Spec a different out-of-band format for actually locking modules down using SRI. Agree and communicate that security is not a goal of specifier-to-URL or URL-to-URL rewriting and they should use that "module-lock.json" file.

pemrouz avatar Mar 15 '19 22:03 pemrouz

I imagine we all share the goal that import maps not cause a lot of additional front-loaded fetching, and there are different thoughts on how big the map would be. Is this an accurate summary? How might we investigate further how big the maps would need to be in practice?

littledan avatar Mar 16 '19 06:03 littledan