generated from tc39/template-for-proposals
-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: Caridy Patiño <caridy@gmail.com>
- Loading branch information
Showing
6 changed files
with
1,831 additions
and
738 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,322 @@ | ||
# First-class Module and ModuleSource | ||
|
||
## Synopsis | ||
|
||
Provide first-class `Module` and `ModuleSource` constructors and extend dynamic | ||
import to operate on `Module` instances. | ||
|
||
A `ModuleSource` represents the result of compiling EcmaScript module source | ||
text. | ||
A `Module` instance represents the lifecycle of a EcmaScript module and allows | ||
virtual import behavior. | ||
Multiple `Module` instances can share a common `ModuleSource`. | ||
|
||
## Interfaces | ||
|
||
### ModuleSource | ||
|
||
```ts | ||
interface ModuleSource { | ||
constructor(source: string); | ||
} | ||
``` | ||
|
||
Semantics: A `ModuleSource` instance gives the holder no powers. | ||
It represents a compiled EcmaScript module and does not capture any information | ||
beyond what can be inferred from a module's source text. | ||
Import specifiers in a module's source text cannot be interpreted without | ||
further information. | ||
|
||
Note 1: `ModuleSource` does for modules what `eval` already does for scripts. | ||
We expect content security policies to treat module sources similarly. | ||
A `ModuleSource` instance constructed from text would not have an associated | ||
origin. | ||
A `ModuleSource` instance can be constructed from vetted text and host-defined | ||
import hooks may reveal module sources that were vetted behind the scenes. | ||
|
||
Note 2: Multiple `Module` instances can be constructed from a single `ModuleSource`, | ||
producing one exports namespaces for each imported `Module` instance. | ||
|
||
Note 3: The internal record of a `ModuleSource` instance is immutable and | ||
serializable. | ||
This data can be shared without cost between realms of an agent or even agents | ||
of an agent cluster. | ||
|
||
### Module instances | ||
|
||
```ts | ||
type ImportSpecifier = string; | ||
|
||
type ImportHook = (specifier: ImportSpecifier, importMeta: object) => | ||
Promise<Module>; | ||
|
||
interface Module { | ||
constructor( | ||
source: ModuleSource, | ||
options: { | ||
importHook?: ImportHook, | ||
importMeta?: object, | ||
}, | ||
); | ||
|
||
readonly source: ModuleSource, | ||
} | ||
``` | ||
|
||
Semantics: A `Module` has a 1-1-1-1 relationship with a ***Module Environment | ||
Record***, a ***Module Record*** and a ***module namespace exotic object***. | ||
|
||
The module has a lifecycle and fresh instances have not been linked, | ||
initialized, or executed. | ||
|
||
Invoking dynamic import on a `Module` instances advances it and its transitive | ||
dependencies to their end state. | ||
Consistent with dynamic import for a stringly-named module, | ||
dynamic import on a `Module` instance produces a promise for the corresponding | ||
***Module Namespace Object*** | ||
|
||
Dynamic import induces calls to `importHook` for each unsatisfied dependency of | ||
each module instance in separate events, before any dependency advances to the | ||
link phase of its lifecycle. | ||
|
||
Dynamic import within the evaluation of a `Module` also invokes the | ||
`importHook`. | ||
|
||
`Module` instances memoize the result of their `importHook` keyed on the given | ||
Import Specifier. | ||
|
||
`Module` constructors like `Function` constructors are bound to a realm | ||
and evaluate modules in their particular realm. | ||
|
||
## Examples | ||
|
||
### Import Kicker | ||
|
||
Any dynamic import function is suitable for initializing, linking, and | ||
evaluating a module instance and all of its transitive dependencies. | ||
|
||
```js | ||
const source = new ModuleSource(``); | ||
const instance = new Module(source, importHook, import.meta); | ||
const namespace = await import(instance); | ||
``` | ||
### Module Idempotency | ||
Since the Module has a bound module namespace exotic object, importing the same | ||
instance should yield the same result: | ||
```js | ||
const source = new ModuleSource(``); | ||
const instance = new Module(source, importHook, import.meta); | ||
const namespace1 = await import(instance); | ||
const namespace2 = await import(instance); | ||
namespace1 === namespace2; // true | ||
``` | ||
### Reusing ModuleSource | ||
Any dynamic import function is suitable for initializing a module instance and | ||
any of its transitive dependencies that have not yet been initialized. | ||
```js | ||
const source = new ModuleSource(``); | ||
const instance1 = new Module(source, importHook1, import.meta); | ||
const instance2 = new Module(source, importHook2, import.meta); | ||
instance1 === instance2; // false | ||
const namespace1 = await import(instance1); | ||
const namespace2 = await import(instance2); | ||
namespace1 === namespace2; // false | ||
``` | ||
### Intersection Semantics with Module Blocks | ||
Proposal: https://github.com/tc39/proposal-js-module-blocks | ||
In relation to module blocks, we can extend the proposal to accommodate both, | ||
the concept of a module block instance and module block source: | ||
```js | ||
const instance = module {}; | ||
instance instanceof Module; | ||
instance.source instanceof ModuleSource; | ||
const namespace = await import(instance); | ||
``` | ||
To avoid needing a throw-away module-instance in order to get a module source, | ||
we can extend the syntax: | ||
```js | ||
const source = static module {}; | ||
source instanceof ModuleSource; | ||
const instance = new Module(source, importHook, import.meta); | ||
const namespace = await import(instance); | ||
``` | ||
### Intersection Semantics with deferred execution | ||
The possibility to load the source, and create the instance with the default | ||
`importHook` and the `import.meta` of the importer, that can be imported at any | ||
given time, is sufficient: | ||
```js | ||
import instance from 'module.js' deferred execution syntax; | ||
instance instanceof Module; | ||
instance.source instanceof ModuleSource; | ||
const namespace = await import(instance); | ||
``` | ||
If the goal is to also control the `importHook` and the `importMeta` of the | ||
importer, then a new syntax can be provided to only get the `ModuleSource`: | ||
```ts | ||
import source from 'module.js' static source syntax; | ||
source instanceof ModuleSource; | ||
const instance = new Module(source, importHook, import.meta); | ||
const namespace = await import(instance); | ||
``` | ||
This is important, because it is analogous to block modules, but instead of | ||
inline source, it is a source that must be fetched. | ||
### Intersection Semantics with import.meta.resolve() | ||
Proposal: https://github.com/whatwg/html/pull/5572 | ||
```ts | ||
const importHook = async (specifier, importMeta) => { | ||
const url = importMeta.resolve(specifier); | ||
const response = await fetch(url); | ||
const sourceText = await.response.text(); | ||
return new Module(sourceText, importHook, createCustomImportMeta(url)); | ||
} | ||
|
||
const source = new ModuleSource(`export foo from './foo.js'`); | ||
const instance = new Module(source, importHook, import.meta); | ||
const namespace = await import(instance); | ||
``` | ||
In the example above, we re-use the `ImportHook` declaration for two instances, | ||
the `source`, and the corresponding dependency for specifier `./foo.js`. When | ||
the kicker `import(instance)` is executed, the `importHook` will be invoked | ||
once with the `specifier` argument as `./foo.js`, and the `meta` argument with | ||
the value of the `import.meta` associated to the kicker itself. As a result, | ||
the `specifier` can be resolved based on the provided `meta` to calculate the | ||
`url`, fetch the source, and create a new `Module` for the new source. This new | ||
instance opts to reuse the same `importHook` function while constructing the | ||
`meta` object. It is important to notice that the `meta` object has to | ||
purposes, to be referenced by syntax in the source text (via `import.meta`) and | ||
to be passed to the `importHook` for any dependencies of `./foo.js` itself. | ||
## Design | ||
A ***Module Source Record*** is an abstract class for immutable representations | ||
of the dependencies, bindings, initialization, and execution behavior of a | ||
module. | ||
Host-defined import-hooks may specialize module source records with annotations | ||
such that the host can enforce content-security-policies. | ||
A ***EcmaScript Module Source Record*** is a concrete ***Module Source | ||
Record*** for EcmaScript modules. | ||
`ModuleSource` is a constructor that accepts EcmaScript module source text and | ||
produces an object with a [[ModuleSource]] slot referring to an ***EcmaScript | ||
Module Source Record***. | ||
`ModuleSource` instances are handles on the result of compiling a EcmaScript | ||
module's source text. | ||
A module source has a [[ModuleSource]] internal slot that refers to a | ||
***Module Source Record***. | ||
Multiple `Module` instances can share a common module source. | ||
Module source records only capture information that can be inferred from static | ||
analysis of the module's source text. | ||
Multiple `ModuleSource` instances can share a common ***Module Source Record*** | ||
since these are immutable and so hosts have the option of sharing them between | ||
realms of an agent and even agents of an agent cluster. | ||
The `Module` constructor accepts a source and | ||
A `Module` has a 1-1-1-1 relationship with a ***Module Environment Record***, | ||
a ***Module Source Record***, and a ***Module Exports Namespace Exotic Object***. | ||
## Design Rationales | ||
### Should `importHook` be synchronous or asynchronous? | ||
When a source module imports from a module specifier, you might not have the | ||
source at hand to create the corresponding `Module` to be returned. If | ||
`importHook` is synchronous, then you must have the source ready when the | ||
`importHook` is invoked for each dependency. | ||
Since the `importHook` is only triggered via the kicker (`import(instance)`), | ||
going async there has no implications whatsoever. | ||
In prior iterations of this, the user was responsible for loop thru the | ||
dependencies, and prepare the instance before kicking the next phase, that's | ||
not longer the case here, where the level of control on the different phases is | ||
limited to the invocation of the `importHook`. | ||
### Can cycles be represented? | ||
Yes, `importHook` can return a `Module` that was either `import()` already or | ||
was returned by an `importHook` already. | ||
### Idempotency of dynamic imports in ModuleSource | ||
Any `import()` statement inside a module source will result of a possible | ||
`importHook` invocation on the `Module`, and the decision on whether or not to | ||
call the `importHook` depends on whether or not the `Module` has already | ||
invoked it for the `specifier` in question. So, a `Module` | ||
most keep a map for every `specifier` and its corresponding `Module` to | ||
guarantee the idempotency of those static and dynamic import statements. | ||
User-defined and host-defined import-hooks will likely enforce stronger | ||
consistency between import behavior across module instances, but module | ||
instances enforce local consistency and some consistency in aggregate by | ||
induction of other modules. | ||
### toString | ||
`ModuleSource` instances do not necessarily retain the source text itself so | ||
hosts are free to reveal module source texts for programs that were compiled | ||
out of band, for which only bytecode remains. | ||
Consequently, we do not specify that `toString` returns the original source. | ||
The [module block][module-blocks] proposal may necessitate the retention | ||
of text so that hosts can transmit use sources in their serial representation | ||
of a module, as could be an extension to `structuredClone`. | ||
### Factoring ECMA-262 | ||
This proposal decouples a new ***Module Source Record*** and ***EcmaScript | ||
Module Source Record*** from the existing ***Module Record*** class hierarchy | ||
and introduces a concrete ***Virtual Module Record***. | ||
The hope if not expectation is that this refactoring makes evident that | ||
***Virtual Module Record***, ***Cyclic Module Record***, and the abstract | ||
base class ***Module Record*** could be refactored into a single concrete | ||
record (***Module Record***) since all meaningful variation can be expressed | ||
with implementations of the abstract ***Module Source Record***. | ||
But, this proposal does not go so far as to make that refactoring normative. | ||
This proposal does not impose on host-defined import behavior. | ||
### Referrer Specifier | ||
This proposal expressly avoids specifying a module referrer. | ||
We are convinced that the `importMeta` object passed to the `Module` | ||
constructor is sufficient to denote (have a host-specific referrer property | ||
like `url` or a method like `resolve`) or conote (serve as a key in a `WeakMap` | ||
side table) since the improt behavior carries that exact object to the | ||
`importHook`, regardless of whether `import.meta` is identical to `importMeta`. | ||
This allows us virtual modules to emulate even hosts that provide empty | ||
`import.meta` objects. | ||
## Design Variables | ||
A functionally equivalent proposal would add an `import` method to the `Module` | ||
prototype to get a promise for the module's exports namespace instead of | ||
overloading dynamic `import`. | ||
Using dynamic import is consistent with an interpretation of the module blocks | ||
proposal where module blocks evaluate to `Module` instances. | ||
[module-blocks]: https://github.com/tc39/proposal-js-module-blocks |
Oops, something went wrong.