18:18 | <kriskowal> | At plenary bakkot expressed dispreference for ModuleSource carrying a non-transferrable importHook. We pretty fully explored and I’ve shelved a design wit ha separate Module constructor, which I’m open to reexamining, but have come to appreciate primacy of ModuleSource as the target of dynamic import, avoiding a need for an It occurs to me that we’ve already stepped away from having a per-module-instance module-map, just a module-map on |
18:21 | <kriskowal> | I don’t think I’ve fully realized how far we’ve strayed from Caridy’s model, with per-module-instance module-map just with the concessions for ESM source phase imports. Agoric’s and the Endo project’s objectives are not hindered by this at all, but I need to adjust my mental model for the ramifications of transferrable ModuleSource identity for purposes of global module map keys. |
18:21 | <bakkot> | I didn't really get into this, but another way to address this is to make the import hook or other remapping mechanism something which is provided at import time, rather than being ambient data carried by the ModuleSource itself |
18:32 | <bakkot> | though also, don't these have to be per-module-graph anyway? because of:
import maps are global for this reason. they can be updated but updates which would conflict with things observed by previous modules are forbidden. |
18:38 | <kriskowal> | Threading the referrer through dynamic import is interesting and I’ll have to think through that more, in particular for the case of importing a ModuleSource instance. I will have to think through the ramifications of that and whether it covers all the cases. The importHook would be obligated to always return something obtained from import or import.source , and that might be funny for import.source(new ModuleSource(), { type, referrer/url/whatever }) . |
18:39 | <kriskowal> | But yes, my intuition is that this might cover all the motivating cases. |
18:41 | <kriskowal> | And the confusion you mention above, I believe, is adequately addressed by guybedford’s work in the ESM source phase proposal, where importing a ModuleSource uses a different keyspace than importing by specifier. At the first approximation, these are keyed by the identity of the instance, but that identity is transferrable through postMessage and structuredClone, so at a second approximation, they’re keyed by a transmissible unique identifier among agents of an agent cluster. |
18:41 | <kriskowal> | An importHook is thereby in a position to say “for this module specifier, this is the corresponding module source identity” as a sort of alias. |
18:44 | <kriskowal> | Consider:
|
19:29 | <bakkot> | Hm, I'm still not understanding. Does the import hook govern only the imports from the module itself, or also its transitive imports? I was assuming the latter but if it's the former then I understand how it could work. |
19:37 | <kriskowal> | I believe the former. For the thought experiment, I’m assuming that there is a per-Global importHook that gets called to populate the module map for every import for a fully-qualified specifier string (importing by ModuleSource instances bypass the importHook and get injected by virtue of their own identity.) |
20:29 | <bakkot> | Gotcha, ok. The utility of an import hook which doesn't handle transitive imports is not obvious to me - indeed it seems somewhat suspicious, since then an otherwise transparent refactoring where you move things out of the entrypoint into another file which gets imported from the entrypoint is suddenly a breaking change. |
20:32 | <kriskowal> | I don’t follow. The importHook gets consulted for transitive imports of any module imported by its specifier string. It’s only not consulted when given a ModuleSource. |
20:33 | <kriskowal> | So, it’s this particular virtue that makes it possible to employ your recommendation of binding the referrer specifier to a ModuleSource when invoking dynamic import. |
20:35 | <kriskowal> | With the importHook I proposed above, that gets configured by new Global({ importHook }) such that newGlobal.import(specifier) would bounce once for each of the transitive dependencies of specifier . |
20:37 | <kriskowal> | Each individual response from the importHook is necessarily the result of dynamic import or import.source . Returning a namespace adopts that instance into the new global’s module map, and so exits the new global’s module graph to the host’s module graph. Returning a module source binds the specifier to a new module instance in the new global’s module map, and induces the importHook for all its shallow imports. |
21:19 | <bakkot> | OK, I think I was missing the bit about returning namespaces vs returning imports. Though it seems fraught to return a namespace without first having analyzed its transitive dependencies. A concrete problem: suppose I have a module graph where I wish to mock Is it possible to accomplish this without manually constructing my own complete view of the module graph in userland? |
22:43 | <kriskowal> | You have definitely identified a shortcoming, though I don’t think it’s reducible. There is a potential to configure an importHook such that it admits a module instance from the host but does not graft the host module’s transitive dependencies, such that the host and guest have discontinuous identities. But, it’s also entirely possible to configure an importHook without that defect, and though it’s a footgun on one hand, it’s a feature on the other. In some cases, we will want to avoid sharing mutable instances between the host and guest. |
22:48 | <bakkot> | I think this comes up as a consequence of trying to have the full dynamism of importHook , instead of matching the existing web platform mechanism of an import map. With an import map, the host can compute a mapping from module -> { set of resolved imports }, such that if you import a module on two occasions (potentially with different import maps) the host can determine whether that (module, resolved imports for that module) pair has already been imported and provide it. |
22:49 | <kriskowal> | That, I think is a shortcoming of importmaps that we will want to avoid. |
22:50 | <kriskowal> | I grok that it’s a footgun on the one hand. But, being able to deliberately isolate instances is an intentional consequence of this design that we would have to shore up for importmap, and then also deal with our desire to, for example, load from a zip file, a database, or just not places keyed by URL. |
22:50 | <bakkot> | Hm. In what way is that a shortcoming? Generally speaking, users expect that importing the same module twice should result in only a single evaluation. |
22:51 | <bakkot> | If you want to hook some dependencies so they differ, that's fine, but it's extremely weird for that to affect unrelated things. |
22:51 | <bakkot> | To be clear, I'm imagining an extension of import maps which allow not just specifier -> URL but also specifier -> ModuleSource (or some other representation) |
22:52 | <bakkot> | so you'd still be able to load from places not keyed by URL just fine |
22:52 | <kriskowal> | We very intentionally wish to be able to intercept all imports so that we can prevent a guest from reaching for capabilities of the host. |
22:53 | <bakkot> | I see, that's fair. |
22:53 | <kriskowal> | That’s fair too. I’ll try to internalize this idea. |
22:54 | <bakkot> | Probably people who have worked on node's import hooks will have more thoughts on this question. |
22:54 | <kriskowal> | If we could rely on transitive dependencies to be pure and have no mutable state, I think it would make sense to adopt the transitive dependencies, even if we prevented edges from linking across the host guest gap. |
22:55 | <kriskowal> | Yes, I think it’s on me to convene a parade of conversations and frequent updates. Your engagement with the topic is much appreciated. |
22:57 | <kriskowal> | I think that the key here is that you’re not merely suggesting we lift the serialized form of importmap, but adopt its schema for a living object representation. |
22:57 | <kriskowal> | Importmaps do a bunch of muxing that is effectively shorthand for what can be expressed in an importHook otherwise. |
22:58 | <kriskowal> | And perhaps it might limit a degree of freedom intentionally somehow. I don’t know. |
22:59 | <kriskowal> | But I can say that importmap was a muse for me in implementing a compartment mapper, which is a generalization on the idea, but for multiple globals and isolated but cross-linked namespaces. |
23:15 | <bakkot> | Ok actually this is false when there isn't a single global import map, because dynamic imports mean that mapping cannot be computed up front - you don't know what a module might choose to import later. |