Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -2301,6 +2301,13 @@ The V8 platform used by this instance of Node.js does not support creating
Workers. This is caused by lack of embedder support for Workers. In particular,
this error will not occur with standard builds of Node.js.

<a id="ERR_MODULE_LINK_MISMATCH"></a>

### `ERR_MODULE_LINK_MISMATCH`

A module can not be linked because the same module requests in it are not
resolved to the same module.

<a id="ERR_MODULE_NOT_FOUND"></a>

### `ERR_MODULE_NOT_FOUND`
Expand Down
237 changes: 163 additions & 74 deletions doc/api/vm.md
Original file line number Diff line number Diff line change
Expand Up @@ -417,9 +417,7 @@
specification.

Unlike `vm.Script` however, every `vm.Module` object is bound to a context from
its creation. Operations on `vm.Module` objects are intrinsically asynchronous,
in contrast with the synchronous nature of `vm.Script` objects. The use of
'async' functions can help with manipulating `vm.Module` objects.
its creation.

Using a `vm.Module` object requires three distinct steps: creation/parsing,
linking, and evaluation. These three steps are illustrated in the following
Expand Down Expand Up @@ -447,7 +445,7 @@
// Here, we attempt to obtain the default export from the module "foo", and
// put it into local binding "secret".

const bar = new vm.SourceTextModule(`
const rootModule = new vm.SourceTextModule(`
import s from 'foo';
s;
print(s);
Expand All @@ -457,47 +455,56 @@
//
// "Link" the imported dependencies of this Module to it.
//
// The provided linking callback (the "linker") accepts two arguments: the
// parent module (`bar` in this case) and the string that is the specifier of
// the imported module. The callback is expected to return a Module that
// corresponds to the provided specifier, with certain requirements documented
// in `module.link()`.
//
// If linking has not started for the returned Module, the same linker
// callback will be called on the returned Module.
// Obtain the requested dependencies of a SourceTextModule by
// `sourceTextModule.moduleRequests` and resolve them.
//
// Even top-level Modules without dependencies must be explicitly linked. The
// callback provided would never be called, however.
//
// The link() method returns a Promise that will be resolved when all the
// Promises returned by the linker resolve.
// array passed to `sourceTextModule.linkRequests(modules)` can be
// empty, however.
//
// Note: This is a contrived example in that the linker function creates a new
// "foo" module every time it is called. In a full-fledged module system, a
// cache would probably be used to avoid duplicated modules.

async function linker(specifier, referencingModule) {
if (specifier === 'foo') {
return new vm.SourceTextModule(`
// The "secret" variable refers to the global variable we added to
// "contextifiedObject" when creating the context.
export default secret;
`, { context: referencingModule.context });

// Using `contextifiedObject` instead of `referencingModule.context`
// here would work as well.
}
throw new Error(`Unable to resolve dependency: ${specifier}`);
// Note: This is a contrived example in that the resolveAndLinkDependencies
// creates a new "foo" module every time it is called. In a full-fledged
// module system, a cache would probably be used to avoid duplicated modules.

const moduleMap = new Map([
['root', rootModule],
]);

function resolveAndLinkDependencies(module) {
const requestedModules = module.moduleRequests.map((request) => {
// In a full-fledged module system, the resolveAndLinkDependencies would
// resolve the module with the module cache key `[specifier, attributes]`.
// In this example, we just use the specifier as the key.
const specifier = request.specifier;

let requestedModule = moduleMap.get(specifier);
if (requestedModule === undefined) {
requestedModule = new vm.SourceTextModule(`
// The "secret" variable refers to the global variable we added to
// "contextifiedObject" when creating the context.
export default secret;
`, { context: referencingModule.context });
moduleMap.set(specifier, linkedModule);
// Resolve the dependencies of the new module as well.
resolveAndLinkDependencies(requestedModule);
}

return requestedModule;
});

module.linkRequests(requestedModules);
}
await bar.link(linker);

resolveAndLinkDependencies(rootModule);
rootModule.instantiate();

// Step 3
//
// Evaluate the Module. The evaluate() method returns a promise which will
// resolve after the module has finished evaluating.

// Prints 42.
await bar.evaluate();
await rootModule.evaluate();
```

```cjs
Expand All @@ -519,7 +526,7 @@
// Here, we attempt to obtain the default export from the module "foo", and
// put it into local binding "secret".

const bar = new vm.SourceTextModule(`
const rootModule = new vm.SourceTextModule(`
import s from 'foo';
s;
print(s);
Expand All @@ -529,47 +536,56 @@
//
// "Link" the imported dependencies of this Module to it.
//
// The provided linking callback (the "linker") accepts two arguments: the
// parent module (`bar` in this case) and the string that is the specifier of
// the imported module. The callback is expected to return a Module that
// corresponds to the provided specifier, with certain requirements documented
// in `module.link()`.
//
// If linking has not started for the returned Module, the same linker
// callback will be called on the returned Module.
// Obtain the requested dependencies of a SourceTextModule by
// `sourceTextModule.moduleRequests` and resolve them.
//
// Even top-level Modules without dependencies must be explicitly linked. The
// callback provided would never be called, however.
//
// The link() method returns a Promise that will be resolved when all the
// Promises returned by the linker resolve.
// array passed to `sourceTextModule.linkRequests(modules)` can be
// empty, however.
//
// Note: This is a contrived example in that the linker function creates a new
// "foo" module every time it is called. In a full-fledged module system, a
// cache would probably be used to avoid duplicated modules.

async function linker(specifier, referencingModule) {
if (specifier === 'foo') {
return new vm.SourceTextModule(`
// The "secret" variable refers to the global variable we added to
// "contextifiedObject" when creating the context.
export default secret;
`, { context: referencingModule.context });
// Note: This is a contrived example in that the resolveAndLinkDependencies
// creates a new "foo" module every time it is called. In a full-fledged
// module system, a cache would probably be used to avoid duplicated modules.

const moduleMap = new Map([
['root', rootModule],
]);

function resolveAndLinkDependencies(module) {
const requestedModules = module.moduleRequests.map((request) => {
// In a full-fledged module system, the resolveAndLinkDependencies would
// resolve the module with the module cache key `[specifier, attributes]`.
// In this example, we just use the specifier as the key.
const specifier = request.specifier;

let requestedModule = moduleMap.get(specifier);
if (requestedModule === undefined) {
requestedModule = new vm.SourceTextModule(`
// The "secret" variable refers to the global variable we added to
// "contextifiedObject" when creating the context.
export default secret;
`, { context: referencingModule.context });
moduleMap.set(specifier, linkedModule);
// Resolve the dependencies of the new module as well.
resolveAndLinkDependencies(requestedModule);
}

return requestedModule;
});

// Using `contextifiedObject` instead of `referencingModule.context`
// here would work as well.
}
throw new Error(`Unable to resolve dependency: ${specifier}`);
module.linkRequests(requestedModules);
}
await bar.link(linker);

resolveAndLinkDependencies(rootModule);
rootModule.instantiate();

// Step 3
//
// Evaluate the Module. The evaluate() method returns a promise which will
// resolve after the module has finished evaluating.

// Prints 42.
await bar.evaluate();
await rootModule.evaluate();
})();
```

Expand Down Expand Up @@ -658,6 +674,10 @@
Link module dependencies. This method must be called before evaluation, and
can only be called once per module.

Use [`sourceTextModule.linkRequests(modules)`][] and
[`sourceTextModule.instantiate()`][] to link modules either synchronously or
asynchronously.

The function is expected to return a `Module` object or a `Promise` that
eventually resolves to a `Module` object. The returned `Module` must satisfy the
following two invariants:
Expand Down Expand Up @@ -803,8 +823,9 @@
meta.prop = {};
},
});
// Since module has no dependencies, the linker function will never be called.
await module.link(() => {});
// The module has an empty `moduleRequests` array.
module.linkRequests([]);
module.instantiate();
await module.evaluate();

// Now, Object.prototype.secret will be equal to 42.
Expand All @@ -830,8 +851,9 @@
meta.prop = {};
},
});
// Since module has no dependencies, the linker function will never be called.
await module.link(() => {});
// The module has an empty `moduleRequests` array.
module.linkRequests([]);
module.instantiate();
await module.evaluate();
// Now, Object.prototype.secret will be equal to 42.
//
Expand Down Expand Up @@ -896,6 +918,69 @@
Corresponds to the `[[RequestedModules]]` field of [Cyclic Module Record][]s in
the ECMAScript specification.

### `sourceTextModule.instantiate()`

<!-- YAML
added: REPLACEME
-->

* Returns: {undefined}

Instantiate the module with the linked requested modules.

This resolves the imported bindings of the module, including re-exported
binding names. When there are any bindings that cannot be resolved,
an error would be thrown synchronously.

If the requested modules include cyclic dependencies, the
[`sourceTextModule.linkRequests(modules)`][] method must be called on all
modules in the cycle before calling this method.

### `sourceTextModule.linkRequests(modules)`

<!-- YAML
added: REPLACEME
-->

* `modules` {vm.Module\[]} Array of `vm.Module` objects that this module depends on.
The order of the modules in the array is the order of
[`sourceTextModule.moduleRequests`][].
* Returns: {undefined}

Link module dependencies. This method must be called before evaluation, and
can only be called once per module.

The order of the module instances in the `modules` array should correspond to the order of
[`sourceTextModule.moduleRequests`][] being resolved. If two module requests have the same
specifier and import attributes, they must be resolved with the same module instance or an
`ERR_MODULE_LINK_MISMATCH` would be thrown. For example, when linking requests for this
module:

<!-- eslint-disable no-duplicate-imports -->

```mjs
import foo from 'foo';
import source Foo from 'foo';
```

<!-- eslint-enable no-duplicate-imports -->

The `modules` array must contain two references to the same instance, because the two
module requests are identical but in two phases.

If the module has no dependencies, the `modules` array can be empty.

Users can use `sourceTextModule.moduleRequests` to implement the host-defined
[HostLoadImportedModule][] abstract operation in the ECMAScript specification,
and using `sourceTextModule.linkRequests()` to invoke specification defined
[FinishLoadingImportedModule][], on the module with all dependencies in a batch.

It's up to the creator of the `SourceTextModule` to determine if the resolution
of the dependencies is synchronous or asynchronous.

After each module in the `modules` array is linked, call
[`sourceTextModule.instantiate()`][].

### `sourceTextModule.moduleRequests`

<!-- YAML
Expand Down Expand Up @@ -1005,14 +1090,17 @@
added:
- v13.0.0
- v12.16.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/59000

Check warning on line 1095 in doc/api/vm.md

View workflow job for this annotation

GitHub Actions / lint-pr-url

pr-url doesn't match the URL of the current PR.
description: No longer need to call `syntheticModule.link()` before
calling this method.
-->

* `name` {string} Name of the export to set.
* `value` {any} The value to set the export to.

This method is used after the module is linked to set the values of exports. If
it is called before the module is linked, an [`ERR_VM_MODULE_STATUS`][] error
will be thrown.
This method sets the module export binding slots with the given value.

```mjs
import vm from 'node:vm';
Expand All @@ -1021,7 +1109,6 @@
m.setExport('x', 1);
});

await m.link(() => {});
await m.evaluate();

assert.strictEqual(m.namespace.x, 1);
Expand All @@ -1033,7 +1120,6 @@
const m = new vm.SyntheticModule(['x'], () => {
m.setExport('x', 1);
});
await m.link(() => {});
await m.evaluate();
assert.strictEqual(m.namespace.x, 1);
})();
Expand Down Expand Up @@ -2083,7 +2169,9 @@
[Cyclic Module Record]: https://tc39.es/ecma262/#sec-cyclic-module-records
[ECMAScript Module Loader]: esm.md#modules-ecmascript-modules
[Evaluate() concrete method]: https://tc39.es/ecma262/#sec-moduleevaluation
[FinishLoadingImportedModule]: https://tc39.es/ecma262/#sec-FinishLoadingImportedModule
[GetModuleNamespace]: https://tc39.es/ecma262/#sec-getmodulenamespace
[HostLoadImportedModule]: https://tc39.es/ecma262/#sec-HostLoadImportedModule
[HostResolveImportedModule]: https://tc39.es/ecma262/#sec-hostresolveimportedmodule
[ImportDeclaration]: https://tc39.es/ecma262/#prod-ImportDeclaration
[Link() concrete method]: https://tc39.es/ecma262/#sec-moduledeclarationlinking
Expand All @@ -2095,13 +2183,14 @@
[WithClause]: https://tc39.es/ecma262/#prod-WithClause
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG`]: errors.md#err_vm_dynamic_import_callback_missing_flag
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`]: errors.md#err_vm_dynamic_import_callback_missing
[`ERR_VM_MODULE_STATUS`]: errors.md#err_vm_module_status
[`Error`]: errors.md#class-error
[`URL`]: url.md#class-url
[`eval()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval
[`optionsExpression`]: https://tc39.es/proposal-import-attributes/#sec-evaluate-import-call
[`script.runInContext()`]: #scriptrunincontextcontextifiedobject-options
[`script.runInThisContext()`]: #scriptruninthiscontextoptions
[`sourceTextModule.instantiate()`]: #sourcetextmoduleinstantiate
[`sourceTextModule.linkRequests(modules)`]: #sourcetextmodulelinkrequestsmodules
[`sourceTextModule.moduleRequests`]: #sourcetextmodulemodulerequests
[`url.origin`]: url.md#urlorigin
[`vm.compileFunction()`]: #vmcompilefunctioncode-params-options
Expand Down
1 change: 1 addition & 0 deletions lib/internal/bootstrap/realm.js
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,7 @@ class BuiltinModule {
this.setExport('default', builtin.exports);
});
// Ensure immediate sync execution to capture exports now
this.module.link([]);
this.module.instantiate();
this.module.evaluate(-1, false);
return this.module;
Expand Down
1 change: 1 addition & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1592,6 +1592,7 @@ E('ERR_MISSING_ARGS',
return `${msg} must be specified`;
}, TypeError);
E('ERR_MISSING_OPTION', '%s is required', TypeError);
E('ERR_MODULE_LINK_MISMATCH', '%s', TypeError);
E('ERR_MODULE_NOT_FOUND', function(path, base, exactUrl) {
if (exactUrl) {
lazyInternalUtil().setOwnProperty(this, 'url', `${exactUrl}`);
Expand Down
Loading
Loading