Skip to content

Commit

Permalink
Divide into low-level chapters
Browse files Browse the repository at this point in the history
Co-authored-by: Caridy Patiño <caridy@gmail.com>
  • Loading branch information
kriskowal and caridy committed Jul 16, 2022
1 parent 747ad88 commit 2cf5f3f
Show file tree
Hide file tree
Showing 6 changed files with 1,830 additions and 738 deletions.
322 changes: 322 additions & 0 deletions 0.md
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
Loading

0 comments on commit 2cf5f3f

Please sign in to comment.