Skip to content

Commit 2cf5f3f

Browse files
kriskowalcaridy
andcommitted
Divide into low-level chapters
Co-authored-by: Caridy Patiño <[email protected]>
1 parent 747ad88 commit 2cf5f3f

File tree

6 files changed

+1830
-738
lines changed

6 files changed

+1830
-738
lines changed

0.md

+322
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
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

Comments
 (0)