diff --git a/doc/api/errors.md b/doc/api/errors.md
index e2ce2c913eabb2..64c697df1c6dcb 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -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.
+
+
+### `ERR_MODULE_LINK_MISMATCH`
+
+A module can not be linked because the same module requests in it are not
+resolved to the same module.
+
### `ERR_MODULE_NOT_FOUND`
diff --git a/doc/api/vm.md b/doc/api/vm.md
index db4dad5b8cbe89..bf50aacaac43da 100644
--- a/doc/api/vm.md
+++ b/doc/api/vm.md
@@ -417,9 +417,7 @@ class that closely mirrors [Module Record][]s as defined in the ECMAScript
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
@@ -447,7 +445,7 @@ const contextifiedObject = vm.createContext({
// 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);
@@ -457,39 +455,48 @@ const bar = new vm.SourceTextModule(`
//
// "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
//
@@ -497,7 +504,7 @@ await bar.link(linker);
// resolve after the module has finished evaluating.
// Prints 42.
-await bar.evaluate();
+await rootModule.evaluate();
```
```cjs
@@ -519,7 +526,7 @@ const contextifiedObject = vm.createContext({
// 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);
@@ -529,39 +536,48 @@ const contextifiedObject = vm.createContext({
//
// "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
//
@@ -569,7 +585,7 @@ const contextifiedObject = vm.createContext({
// resolve after the module has finished evaluating.
// Prints 42.
- await bar.evaluate();
+ await rootModule.evaluate();
})();
```
@@ -658,6 +674,10 @@ changes:
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:
@@ -803,8 +823,9 @@ const module = new vm.SourceTextModule(
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.
@@ -830,8 +851,9 @@ const contextifiedObject = vm.createContext({ secret: 42 });
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.
//
@@ -896,6 +918,69 @@ to disallow any changes to it.
Corresponds to the `[[RequestedModules]]` field of [Cyclic Module Record][]s in
the ECMAScript specification.
+### `sourceTextModule.instantiate()`
+
+
+
+* 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)`
+
+
+
+* `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:
+
+
+
+```mjs
+import foo from 'foo';
+import source Foo from 'foo';
+```
+
+
+
+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`
* `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';
@@ -1021,7 +1109,6 @@ const m = new vm.SyntheticModule(['x'], () => {
m.setExport('x', 1);
});
-await m.link(() => {});
await m.evaluate();
assert.strictEqual(m.namespace.x, 1);
@@ -1033,7 +1120,6 @@ const vm = require('node:vm');
const m = new vm.SyntheticModule(['x'], () => {
m.setExport('x', 1);
});
- await m.link(() => {});
await m.evaluate();
assert.strictEqual(m.namespace.x, 1);
})();
@@ -2083,7 +2169,9 @@ const { Script, SyntheticModule } = require('node:vm');
[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
@@ -2095,13 +2183,14 @@ const { Script, SyntheticModule } = require('node:vm');
[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
diff --git a/lib/internal/bootstrap/realm.js b/lib/internal/bootstrap/realm.js
index c11f70dd6bf329..af283265d37dc4 100644
--- a/lib/internal/bootstrap/realm.js
+++ b/lib/internal/bootstrap/realm.js
@@ -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;
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index a9655974e673c3..e47f5d50918806 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -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}`);
diff --git a/lib/internal/vm/module.js b/lib/internal/vm/module.js
index 266e019be9c3da..e6e5c9d40749cc 100644
--- a/lib/internal/vm/module.js
+++ b/lib/internal/vm/module.js
@@ -38,6 +38,7 @@ const {
ERR_VM_MODULE_DIFFERENT_CONTEXT,
ERR_VM_MODULE_CANNOT_CREATE_CACHED_DATA,
ERR_VM_MODULE_LINK_FAILURE,
+ ERR_MODULE_LINK_MISMATCH,
ERR_VM_MODULE_NOT_MODULE,
ERR_VM_MODULE_STATUS,
} = require('internal/errors').codes;
@@ -50,6 +51,7 @@ const {
validateUint32,
validateString,
validateThisInternalField,
+ validateArray,
} = require('internal/validators');
const binding = internalBinding('module_wrap');
@@ -354,6 +356,37 @@ class SourceTextModule extends Module {
}
}
+ linkRequests(modules) {
+ validateThisInternalField(this, kWrap, 'SourceTextModule');
+ if (this.status !== 'unlinked') {
+ throw new ERR_VM_MODULE_STATUS('must be unlinked');
+ }
+ validateArray(modules, 'modules');
+ if (modules.length !== this.#moduleRequests.length) {
+ throw new ERR_MODULE_LINK_MISMATCH(
+ `Expected ${this.#moduleRequests.length} modules, got ${modules.length}`,
+ );
+ }
+ const moduleWraps = ArrayPrototypeMap(modules, (module) => {
+ if (!isModule(module)) {
+ throw new ERR_VM_MODULE_NOT_MODULE();
+ }
+ if (module.context !== this.context) {
+ throw new ERR_VM_MODULE_DIFFERENT_CONTEXT();
+ }
+ return module[kWrap];
+ });
+ this[kWrap].link(moduleWraps);
+ }
+
+ instantiate() {
+ validateThisInternalField(this, kWrap, 'SourceTextModule');
+ if (this.status !== 'unlinked') {
+ throw new ERR_VM_MODULE_STATUS('must be unlinked');
+ }
+ this[kWrap].instantiate();
+ }
+
get dependencySpecifiers() {
this.#dependencySpecifiers ??= ObjectFreeze(
ArrayPrototypeMap(this.#moduleRequests, (request) => request.specifier));
@@ -420,10 +453,15 @@ class SyntheticModule extends Module {
context,
identifier,
});
+ // A synthetic module does not have dependencies.
+ this[kWrap].link([]);
+ this[kWrap].instantiate();
}
- [kLink]() {
- /** nothing to do for synthetic modules */
+ link() {
+ validateThisInternalField(this, kWrap, 'SyntheticModule');
+ // No-op for synthetic modules
+ // Do not invoke super.link() as it will throw an error.
}
setExport(name, value) {
diff --git a/src/module_wrap.cc b/src/module_wrap.cc
index 1f73ee09de51bf..1ff4971d6fedf6 100644
--- a/src/module_wrap.cc
+++ b/src/module_wrap.cc
@@ -67,6 +67,25 @@ void ModuleCacheKey::MemoryInfo(MemoryTracker* tracker) const {
tracker->TrackField("import_attributes", import_attributes);
}
+std::string ModuleCacheKey::ToString() const {
+ std::string result = "ModuleCacheKey(\"" + specifier + "\"";
+ if (!import_attributes.empty()) {
+ result += ", {";
+ bool first = true;
+ for (const auto& attr : import_attributes) {
+ if (first) {
+ first = false;
+ } else {
+ result += ", ";
+ }
+ result += attr.first + ": " + attr.second;
+ }
+ result += "}";
+ }
+ result += ")";
+ return result;
+}
+
template
ModuleCacheKey ModuleCacheKey::From(Local context,
Local specifier,
@@ -123,12 +142,23 @@ ModuleWrap::ModuleWrap(Realm* realm,
object->SetInternalField(kSyntheticEvaluationStepsSlot,
synthetic_evaluation_step);
object->SetInternalField(kContextObjectSlot, context_object);
+ object->SetInternalField(kLinkedRequestsSlot,
+ v8::Undefined(realm->isolate()));
if (!synthetic_evaluation_step->IsUndefined()) {
synthetic_ = true;
}
MakeWeak();
module_.SetWeak();
+
+ HandleScope scope(realm->isolate());
+ Local context = realm->context();
+ Local requests = module->GetModuleRequests();
+ for (int i = 0; i < requests->Length(); i++) {
+ ModuleCacheKey module_cache_key = ModuleCacheKey::From(
+ context, requests->Get(context, i).As());
+ resolve_cache_[module_cache_key] = i;
+ }
}
ModuleWrap::~ModuleWrap() {
@@ -149,6 +179,30 @@ Local ModuleWrap::context() const {
return obj.As