import-maps
import-maps copied to clipboard
How do we install import maps in worker/worklet contexts?
It's unclear how to apply import maps to a worker. There are essentially three categories of approach I can think of:
- The worker-creator specifies the import map, e.g. with
new Worker(url, { type: "module", importMap: ... }) - The worker itself specifies the import map, e.g. with
self.setImportMap(...)orimport "map.json" assert { type: "importmap" }. - The HTTP headers on the worker script specify the import map, e.g.
Import-Map: file.jsonor maybe evenImport-Map: { ... a bunch of JSON inlined into the header ... }.
The worker-creator specified import map is a bit strange:
- It's unlike the Window-context mechanism in that the creator of the realm controls the realm's module resolution, instead of the realm itself
- It's like the Window-context mechanism in that the script that runs in the realm does not control the realm's module resolution; the control is external to the script itself.
Basically, there is no clear translation of <script type="importmap"> into a worker setting.
Also, as pointed out below, anything where the creator controls the import map works poorly for service worker updates.
The worker itself specifying seems basically unworkable, for reasons discussed below.
And the header-based mechanism is hard to develop against and deploy.
Original post, for posterity, including references to the old "package name map" name
We have a sketch of an idea for how to supply a package name map to a worker:
new Worker(someURL, { type: "module", packageMap: ... });
This is interesting to contrast with the mechanism for window contexts proposed currently (and discussed in #1):
- It's unlike the Window-context mechanism in that the creator of the realm controls the realm's module resolution, instead of the realm itself
- It's like the Window-context mechanism in that the script that runs in the realm does not control the realm's module resolution; the control is external to the script itself.
Basically, there is no clear translation of <script type="packagemap"> into a worker setting.
If we went with this, presumably we'd do the same for SharedWorker. Service workers would probably use an option to navigator.serviceWorker.register(), and have an impact similar to the other options?
For worklets, I guess you'd do something like CSS.paintWorklet.setModuleMap({ ... }). Only callable once, of course.
In all cases, it would be nice to make it easy to inherit a package name map. With the current tentative proposal you could do
new Worker(url, {
type: "module",
packageMap: JSON.parse(document.querySelector('script[type="packagemap"]').textContent)
});
but this is fairly verbose and a bit wasteful (since the browser has already done all the parsing and processing of the map, and you're making it do that over again). At the same time, inheriting by default seems conceptually weird, since workers are a separate realm.
Could the packagemap script have a property on it, packageMap or something so you could use: document.querySelector('#mymap').packageMap to get it? Not sure if there is precedent for properties on elements only when it's a certain type like this.
@matthewp I'd certainly like to see a .module property for <script type=module> that's a Promise resolving to the module object. If this ends up being a general feature, maybe we just need a .content or .value property. edit: those are bad names, because they don't imply them being the post-parse/instantiate object
Yeah, that seems pretty reasonable. I think I like the idea of a general property, with some name or another.
Could we by default just have workers inherit the package map from the parent window?
That seems to cover the 99% scenario.
Isolation doesn't seem to align with the worker boundary, as much as it does concepts of Realms.
I understand the issues for worklets and service workers but I'm' not sure they should be dominating use cases here.
Hmm, I don't understand. Workers don't share the parent realm so if you're saying isolation is aligned with realms then it is also aligned with the worker boundary.
I was referring to the proposal to create isolated realms, but it's a somewhat misleading tangent.
The question really is what is the use case for having a different package map for the worker, that couldn't be shared with the page?
Note if we first shipped with the default of having the worker share the package map, then a future proposal could always provide package map isolation. The isolation picture and use cases will be clearer then than they are now to spec something good as necessary, and realms are possibly part of that picture too.
In the end I am very uncomfortable with sharing module-related features cross-realm. In general cross-realm sharing of state of this sort is largely unprecedented, and I'd be especially unhappy about doing it for modules, which right now are purely tied to realms.
I am pretty certain that v0 will not have such a strange new feature as cross-realm sharing, even if we eventually want to introduce an easy way to do so.
@domenic then we must guarantee that when it comes to implementations of the Realms proposal, that they do not share the package map with the main page by default.
Also, note that what I'm suggesting here is a default behaviour - there is nothing to say this cannot be overridden.
As someone who has worked on build tools in JS for a while, assuming all workers are instantiated with a packagemap argument will be a tooling nightmare - the third-party worker story is really being neglected from all directions currently which is incredibly worrying already.
I see this is already catered to in the Realms spec (although interesting the Realms spec then effectively hinges on a full blown module API for modules support - https://github.com/tc39/proposal-realms/issues/103).
Passing the package map can work - we'd likely have build workflows that inject it into the new Worker instantiations, while npm packages leave it out. It's just annoying to have non-portable ties on this boundary effectively requiring a build step for third-party libraries with worker instantiations.
Yes, in general the realms proposal has a large variety of blocking issues on how it interacts with how browsers work with realms.
I understand that breaking the current model for how realms work can be convenient, especially for tools or similar. I don't think that means we should do so, especially by default.
Then make it a boolean option - packageMap: 'parent' and at least provide a portable-non build workflow for the web that can deal with workers. It's the complete lack of concern for portable worker workflows that gets me here. Us build tool authors then have to fix the problems you create here.
That's an idea. I'd prefer we have a more holistic story for how workers relate to other realms, instead of creating a one-off feature in package name maps. For example it's not clear why a package name map would be easily inherited but not a module map, or URL, or content security policy, or...
Related: https://github.com/w3ctag/design-principles/issues/111
Note that any kind of inheritance would also be "racy" for shared and service workers and therefore not an acceptable solution for them.
This feature doesn't seem needed for worklets as they can't fetch anything.
My understanding is that worklets can use static import statements. (And per spec dynamic ones, but IIUC the sole worklet implementer so far removed support for that JS language feature, but hasn't updated the spec.)
I see, I doubt the fetching model for that is well-tested. All those module fetches would come with Origin: null per how things are written today which I doubt is how it's implemented...
If worklets should be able to access built-in modules, we may want to work through how import maps could work, so that these modules can be polyfilled/virtualized.
For example it's not clear why a package name map would be easily inherited but not a module map, or URL, or content security policy
As per CSP3 dedicated workers and worklets now inherit their page's CSP so this indicates some precedence.
Dedicated workers now always inherit their creator’s policy.
As a web developers, it's trying experiencing inconsistent old outdated capabilities in workers. Article after article highlights the importance of this feature set for keeping a responsive web app & doing computation, but support for Workers has been awful & this ticket is another intimidating document implying that working with workers is going to remain really hard for the forseeable future, & not receiving the benefit of the es2015 modules progress that the happier paths have gotten.
We still don't have module 1.0 support in workers on Chrome. Now, I'm finding that when we do get support, it's going to be for modules that won't be able to use import-maps? It's frustrating that,
- modules shipped a 1.0 that wasn't modular (cannot reuse scripts across sites because of hardcoded specifiers),
- that we still don't have platform support for them (in workers),
- that import-maps was required to make them modular,
- and now 18 months into import-maps discussion, there's no indicator how import-maps may someday be able to help workers get useful modules.
Modules feels like it was shipped prematurely, and I'd like to avoid going 1.0 with import-maps in a similar fashion, before there's a plan for how it can be used in practice across the platform.
Thanks for the call to action @rektide.
FWIW es-module-shims supports import maps in web workers in Chrome today using importMap as an input, which seems very sensible to me. See https://github.com/guybedford/es-module-shims/#module-workers for more info.
In terms of spec work, I think it's just the constraints of prioritization with a serial process with limited people working on this. And I'm sure a spec PR for a worker approach wouldn't be ignored.
I have to say, it definitely discourages me from working on module stuff, if making incremental progress gets that kind of negative reception. If the demand is that every piece of the puzzle be perfect and full before any specs are accepted or any browsers ship modules, then I don't want to work on modules.
Fortunately I think we have a lot of web developers who gain value from what we have today, and appreciate it, instead of saying that it's "premature" to ship anything but a big-bang modules + dynamic import() + import.meta + modulepreload + workers + import maps + import maps in workers + more all at once. So I plan to ignore such sentiments.
With service workers, the import map should be updatable between versions of the service worker. There isn't a JS call directly linked to service worker updates, so something like a header, or something inline would work better.
Also consider an API:
// First line inside worker or service worker type=module
self.importmap = await fetch('importmap-a18de65f.json').json();
?
How about a new property in import.meta?
That way the import map could be taken from the current script rather than the document, making it more portable.
If parsing becomes an issue, the property could be an interface or id rather than an object with json data. So that the import map doesn't have to be parsed and processed again.
This would give something like:
new Worker(url, {
type: "module",
packageMap: import.meta.importMap
});
Not sure about what the default behaviour should be. But this doesn't seem too verbose, so having no import map as default seems fine.
Has there been any further thought to this feature? With Firefox making implementation progress on workers, it may be worth starting to consider that path forward. es-module-shims can implement this feature as well and it would be great to start building on top of these kinds of universal workers workflows.
How about importing the import map? Like
import "map.json" assert { type: "importmap" }
The import map would be the first static import and then its rules would apply to the subsequent imports. It could then be applied to the entire worker context or maybe scoped to the current module.
I've updated the original post with an updated description of the problem and solution space. The main part of this comment is to reply to a few recent suggestions for ways that the worker itself could specify the import map.
However, before we get into that, let me say that it's pretty hard to find resources at Chrome, and probably at other browsers, to work on import maps. I'm trying to get a sense of how many people care about these extensions, and how much they care about different features relative to others. So, if you want import maps in workers, please add your thumbs-up to the original post here to give us a better signal.
Anyway, onto the technical questions.
Basically, worker-itself specified import maps are generally unworkable, because we need to assign the import map before module resolution ever happens. But module resolution happens extremely early, during parsing of the worker script. So e.g.
import "map.json" assert { type: "importmap" };
import "package";
would fail because at parse time, we haven't fetched map.json. We would have to do something ridiculously complicated like doing a separate parsing pass looking for assert { type: "importmap" } import statements, fetching those using a totally separate fetching path than the usual module importing path (e.g. bypassing all of the JS spec/JS engine's machinery), and then once it's ready and installed, re-parsing and re-fetching the original worker module using the normal path.
(And this isn't even getting into what you might expect to happen in a situation where those two import statements are reversed.)
Similarly, something like
self.setImportMap(...);
import "package";
doesn't work, because we need to resolve and then fetch "package" before we can start executing any code in the module worker---including the code that calls self.setImportMap(...).
So I don't really know how we could possibly make any of these inline variants work for workers.
How about a new property in import.meta?
This seems like a reasonable manifestation of the programmatic API extension, but is pretty separable from the harder question of how to install import maps in workers.
So that means the worker itself specifying the import map is off the table then? And with the worker-creator specifying the import map not being a viable option for service workers I think the only thing that's left is a header?
I'm not sure how I feel about a header because it's not always easy (and sometimes impossible) to configure them server side.
Would a combination of both a header and an argument for new Worker be an option? So service workers could use a header and other workers could use an argument, or maybe both.