|
| 1 | +# First-class Module and ModuleSource |
| 2 | + |
| 3 | +## Synopsis |
| 4 | + |
| 5 | +Provide first-class `Module` and `ModuleSource` constructors and extend dynamic |
| 6 | +import to operate on `Module` instances. |
| 7 | + |
| 8 | +A `ModuleSource` represents the result of compiling EcmaScript module source |
| 9 | +text. |
| 10 | +A `Module` instance represents the lifecycle of a EcmaScript module and allows |
| 11 | +virtual import behavior. |
| 12 | +Multiple `Module` instances can share a common `ModuleSource`. |
| 13 | + |
| 14 | +## Interfaces |
| 15 | + |
| 16 | +### ModuleSource |
| 17 | + |
| 18 | +```ts |
| 19 | +interface ModuleSource { |
| 20 | + constructor(source: string); |
| 21 | +} |
| 22 | +``` |
| 23 | + |
| 24 | +Semantics: A `ModuleSource` instance gives the holder no powers. |
| 25 | +It represents a compiled EcmaScript module and does not capture any information |
| 26 | +beyond what can be inferred from a module's source text. |
| 27 | +Import specifiers in a module's source text cannot be interpreted without |
| 28 | +further information. |
| 29 | + |
| 30 | +Note 1: `ModuleSource` does for modules what `eval` already does for scripts. |
| 31 | +We expect content security policies to treat module sources similarly. |
| 32 | +A `ModuleSource` instance constructed from text would not have an associated |
| 33 | +origin. |
| 34 | +A `ModuleSource` instance can be constructed from vetted text and host-defined |
| 35 | +import hooks may reveal module sources that were vetted behind the scenes. |
| 36 | + |
| 37 | +Note 2: Multiple `Module` instances can be constructed from a single `ModuleSource`, |
| 38 | +producing one exports namespaces for each imported `Module` instance. |
| 39 | + |
| 40 | +Note 3: The internal record of a `ModuleSource` instance is immutable and |
| 41 | +serializable. |
| 42 | +This data can be shared without cost between realms of an agent or even agents |
| 43 | +of an agent cluster. |
| 44 | + |
| 45 | +### Module instances |
| 46 | + |
| 47 | +```ts |
| 48 | +type ImportSpecifier = string; |
| 49 | + |
| 50 | +type ImportHook = (specifier: ImportSpecifier, importMeta: object) => |
| 51 | + Promise<Module>; |
| 52 | + |
| 53 | +interface Module { |
| 54 | + constructor( |
| 55 | + source: ModuleSource, |
| 56 | + options: { |
| 57 | + importHook?: ImportHook, |
| 58 | + importMeta?: object, |
| 59 | + }, |
| 60 | + ); |
| 61 | + |
| 62 | + readonly source: ModuleSource, |
| 63 | +} |
| 64 | +``` |
| 65 | + |
| 66 | +Semantics: A `Module` has a 1-1-1-1 relationship with a ***Module Environment |
| 67 | +Record***, a ***Module Record*** and a ***module namespace exotic object***. |
| 68 | + |
| 69 | +The module has a lifecycle and fresh instances have not been linked, |
| 70 | +initialized, or executed. |
| 71 | + |
| 72 | +Invoking dynamic import on a `Module` instances advances it and its transitive |
| 73 | +dependencies to their end state. |
| 74 | +Consistent with dynamic import for a stringly-named module, |
| 75 | +dynamic import on a `Module` instance produces a promise for the corresponding |
| 76 | +***Module Namespace Object*** |
| 77 | + |
| 78 | +Dynamic import induces calls to `importHook` for each unsatisfied dependency of |
| 79 | +each module instance in separate events, before any dependency advances to the |
| 80 | +link phase of its lifecycle. |
| 81 | + |
| 82 | +Dynamic import within the evaluation of a `Module` also invokes the |
| 83 | +`importHook`. |
| 84 | + |
| 85 | +`Module` instances memoize the result of their `importHook` keyed on the given |
| 86 | +Import Specifier. |
| 87 | + |
| 88 | +`Module` constructors like `Function` constructors are bound to a realm |
| 89 | +and evaluate modules in their particular realm. |
| 90 | + |
| 91 | +## Examples |
| 92 | + |
| 93 | +### Import Kicker |
| 94 | + |
| 95 | +Any dynamic import function is suitable for initializing, linking, and |
| 96 | +evaluating a module instance and all of its transitive dependencies. |
| 97 | + |
| 98 | +```js |
| 99 | +const source = new ModuleSource(``); |
| 100 | +const instance = new Module(source, importHook, import.meta); |
| 101 | +const namespace = await import(instance); |
| 102 | +``` |
| 103 | +
|
| 104 | +### Module Idempotency |
| 105 | +
|
| 106 | +Since the Module has a bound module namespace exotic object, importing the same |
| 107 | +instance should yield the same result: |
| 108 | +
|
| 109 | +```js |
| 110 | +const source = new ModuleSource(``); |
| 111 | +const instance = new Module(source, importHook, import.meta); |
| 112 | +const namespace1 = await import(instance); |
| 113 | +const namespace2 = await import(instance); |
| 114 | +namespace1 === namespace2; // true |
| 115 | +``` |
| 116 | +
|
| 117 | +### Reusing ModuleSource |
| 118 | +
|
| 119 | +Any dynamic import function is suitable for initializing a module instance and |
| 120 | +any of its transitive dependencies that have not yet been initialized. |
| 121 | +
|
| 122 | +```js |
| 123 | +const source = new ModuleSource(``); |
| 124 | +const instance1 = new Module(source, importHook1, import.meta); |
| 125 | +const instance2 = new Module(source, importHook2, import.meta); |
| 126 | +instance1 === instance2; // false |
| 127 | +const namespace1 = await import(instance1); |
| 128 | +const namespace2 = await import(instance2); |
| 129 | +namespace1 === namespace2; // false |
| 130 | +``` |
| 131 | +
|
| 132 | +### Intersection Semantics with Module Blocks |
| 133 | +
|
| 134 | +Proposal: https://github.com/tc39/proposal-js-module-blocks |
| 135 | +
|
| 136 | +In relation to module blocks, we can extend the proposal to accommodate both, |
| 137 | +the concept of a module block instance and module block source: |
| 138 | +
|
| 139 | +```js |
| 140 | +const instance = module {}; |
| 141 | +instance instanceof Module; |
| 142 | +instance.source instanceof ModuleSource; |
| 143 | +const namespace = await import(instance); |
| 144 | +``` |
| 145 | +
|
| 146 | +To avoid needing a throw-away module-instance in order to get a module source, |
| 147 | +we can extend the syntax: |
| 148 | +
|
| 149 | +```js |
| 150 | +const source = static module {}; |
| 151 | +source instanceof ModuleSource; |
| 152 | +const instance = new Module(source, importHook, import.meta); |
| 153 | +const namespace = await import(instance); |
| 154 | +``` |
| 155 | +
|
| 156 | +### Intersection Semantics with deferred execution |
| 157 | +
|
| 158 | +The possibility to load the source, and create the instance with the default |
| 159 | +`importHook` and the `import.meta` of the importer, that can be imported at any |
| 160 | +given time, is sufficient: |
| 161 | +
|
| 162 | +```js |
| 163 | +import instance from 'module.js' deferred execution syntax; |
| 164 | +instance instanceof Module; |
| 165 | +instance.source instanceof ModuleSource; |
| 166 | +const namespace = await import(instance); |
| 167 | +``` |
| 168 | +
|
| 169 | +If the goal is to also control the `importHook` and the `importMeta` of the |
| 170 | +importer, then a new syntax can be provided to only get the `ModuleSource`: |
| 171 | +
|
| 172 | +```ts |
| 173 | +import source from 'module.js' static source syntax; |
| 174 | +source instanceof ModuleSource; |
| 175 | +const instance = new Module(source, importHook, import.meta); |
| 176 | +const namespace = await import(instance); |
| 177 | +``` |
| 178 | +
|
| 179 | +This is important, because it is analogous to block modules, but instead of |
| 180 | +inline source, it is a source that must be fetched. |
| 181 | +
|
| 182 | +### Intersection Semantics with import.meta.resolve() |
| 183 | +
|
| 184 | +Proposal: https://github.com/whatwg/html/pull/5572 |
| 185 | +
|
| 186 | +```ts |
| 187 | +const importHook = async (specifier, importMeta) => { |
| 188 | + const url = importMeta.resolve(specifier); |
| 189 | + const response = await fetch(url); |
| 190 | + const sourceText = await.response.text(); |
| 191 | + return new Module(sourceText, importHook, createCustomImportMeta(url)); |
| 192 | +} |
| 193 | + |
| 194 | +const source = new ModuleSource(`export foo from './foo.js'`); |
| 195 | +const instance = new Module(source, importHook, import.meta); |
| 196 | +const namespace = await import(instance); |
| 197 | +``` |
| 198 | +
|
| 199 | +In the example above, we re-use the `ImportHook` declaration for two instances, |
| 200 | +the `source`, and the corresponding dependency for specifier `./foo.js`. When |
| 201 | +the kicker `import(instance)` is executed, the `importHook` will be invoked |
| 202 | +once with the `specifier` argument as `./foo.js`, and the `meta` argument with |
| 203 | +the value of the `import.meta` associated to the kicker itself. As a result, |
| 204 | +the `specifier` can be resolved based on the provided `meta` to calculate the |
| 205 | +`url`, fetch the source, and create a new `Module` for the new source. This new |
| 206 | +instance opts to reuse the same `importHook` function while constructing the |
| 207 | +`meta` object. It is important to notice that the `meta` object has to |
| 208 | +purposes, to be referenced by syntax in the source text (via `import.meta`) and |
| 209 | +to be passed to the `importHook` for any dependencies of `./foo.js` itself. |
| 210 | +
|
| 211 | +## Design |
| 212 | +
|
| 213 | +A ***Module Source Record*** is an abstract class for immutable representations |
| 214 | +of the dependencies, bindings, initialization, and execution behavior of a |
| 215 | +module. |
| 216 | +
|
| 217 | +Host-defined import-hooks may specialize module source records with annotations |
| 218 | +such that the host can enforce content-security-policies. |
| 219 | +
|
| 220 | +A ***EcmaScript Module Source Record*** is a concrete ***Module Source |
| 221 | +Record*** for EcmaScript modules. |
| 222 | +
|
| 223 | +`ModuleSource` is a constructor that accepts EcmaScript module source text and |
| 224 | +produces an object with a [[ModuleSource]] slot referring to an ***EcmaScript |
| 225 | +Module Source Record***. |
| 226 | +
|
| 227 | +`ModuleSource` instances are handles on the result of compiling a EcmaScript |
| 228 | +module's source text. |
| 229 | +A module source has a [[ModuleSource]] internal slot that refers to a |
| 230 | +***Module Source Record***. |
| 231 | +Multiple `Module` instances can share a common module source. |
| 232 | +
|
| 233 | +Module source records only capture information that can be inferred from static |
| 234 | +analysis of the module's source text. |
| 235 | +
|
| 236 | +Multiple `ModuleSource` instances can share a common ***Module Source Record*** |
| 237 | +since these are immutable and so hosts have the option of sharing them between |
| 238 | +realms of an agent and even agents of an agent cluster. |
| 239 | +
|
| 240 | +The `Module` constructor accepts a source and |
| 241 | +A `Module` has a 1-1-1-1 relationship with a ***Module Environment Record***, |
| 242 | +a ***Module Source Record***, and a ***Module Exports Namespace Exotic Object***. |
| 243 | +
|
| 244 | +## Design Rationales |
| 245 | +
|
| 246 | +### Should `importHook` be synchronous or asynchronous? |
| 247 | +
|
| 248 | +When a source module imports from a module specifier, you might not have the |
| 249 | +source at hand to create the corresponding `Module` to be returned. If |
| 250 | +`importHook` is synchronous, then you must have the source ready when the |
| 251 | +`importHook` is invoked for each dependency. |
| 252 | +
|
| 253 | +Since the `importHook` is only triggered via the kicker (`import(instance)`), |
| 254 | +going async there has no implications whatsoever. |
| 255 | +In prior iterations of this, the user was responsible for loop thru the |
| 256 | +dependencies, and prepare the instance before kicking the next phase, that's |
| 257 | +not longer the case here, where the level of control on the different phases is |
| 258 | +limited to the invocation of the `importHook`. |
| 259 | +
|
| 260 | +### Can cycles be represented? |
| 261 | +
|
| 262 | +Yes, `importHook` can return a `Module` that was either `import()` already or |
| 263 | +was returned by an `importHook` already. |
| 264 | +
|
| 265 | +### Idempotency of dynamic imports in ModuleSource |
| 266 | +
|
| 267 | +Any `import()` statement inside a module source will result of a possible |
| 268 | +`importHook` invocation on the `Module`, and the decision on whether or not to |
| 269 | +call the `importHook` depends on whether or not the `Module` has already |
| 270 | +invoked it for the `specifier` in question. So, a `Module` |
| 271 | +most keep a map for every `specifier` and its corresponding `Module` to |
| 272 | +guarantee the idempotency of those static and dynamic import statements. |
| 273 | +
|
| 274 | +User-defined and host-defined import-hooks will likely enforce stronger |
| 275 | +consistency between import behavior across module instances, but module |
| 276 | +instances enforce local consistency and some consistency in aggregate by |
| 277 | +induction of other modules. |
| 278 | +
|
| 279 | +### toString |
| 280 | +
|
| 281 | +`ModuleSource` instances do not necessarily retain the source text itself so |
| 282 | +hosts are free to reveal module source texts for programs that were compiled |
| 283 | +out of band, for which only bytecode remains. |
| 284 | +Consequently, we do not specify that `toString` returns the original source. |
| 285 | +The [module block][module-blocks] proposal may necessitate the retention |
| 286 | +of text so that hosts can transmit use sources in their serial representation |
| 287 | +of a module, as could be an extension to `structuredClone`. |
| 288 | +
|
| 289 | +### Factoring ECMA-262 |
| 290 | +
|
| 291 | +This proposal decouples a new ***Module Source Record*** and ***EcmaScript |
| 292 | +Module Source Record*** from the existing ***Module Record*** class hierarchy |
| 293 | +and introduces a concrete ***Virtual Module Record***. |
| 294 | +The hope if not expectation is that this refactoring makes evident that |
| 295 | +***Virtual Module Record***, ***Cyclic Module Record***, and the abstract |
| 296 | +base class ***Module Record*** could be refactored into a single concrete |
| 297 | +record (***Module Record***) since all meaningful variation can be expressed |
| 298 | +with implementations of the abstract ***Module Source Record***. |
| 299 | +But, this proposal does not go so far as to make that refactoring normative. |
| 300 | +
|
| 301 | +This proposal does not impose on host-defined import behavior. |
| 302 | +
|
| 303 | +### Referrer Specifier |
| 304 | +
|
| 305 | +This proposal expressly avoids specifying a module referrer. |
| 306 | +We are convinced that the `importMeta` object passed to the `Module` |
| 307 | +constructor is sufficient to denote (have a host-specific referrer property |
| 308 | +like `url` or a method like `resolve`) or conote (serve as a key in a `WeakMap` |
| 309 | +side table) since the improt behavior carries that exact object to the |
| 310 | +`importHook`, regardless of whether `import.meta` is identical to `importMeta`. |
| 311 | +This allows us virtual modules to emulate even hosts that provide empty |
| 312 | +`import.meta` objects. |
| 313 | +
|
| 314 | +## Design Variables |
| 315 | +
|
| 316 | +A functionally equivalent proposal would add an `import` method to the `Module` |
| 317 | +prototype to get a promise for the module's exports namespace instead of |
| 318 | +overloading dynamic `import`. |
| 319 | +Using dynamic import is consistent with an interpretation of the module blocks |
| 320 | +proposal where module blocks evaluate to `Module` instances. |
| 321 | +
|
| 322 | +[module-blocks]: https://github.com/tc39/proposal-js-module-blocks |
0 commit comments