Skip to content

Commit b3e61f0

Browse files
committed
vm: sync-ify SourceTextModule linkage
Split `module.link(linker)` into two synchronous step `sourceTextModule.linkRequests()` and `sourceTextModule.instantiate()`. This allows creating vm modules and resolving the dependencies in a complete synchronous procedure. This also makes `syntheticModule.link()` redundant. The link step for a SyntheticModule is no-op and is already taken care in the constructor by initializing the binding slots with the given export names.
1 parent 886e4b3 commit b3e61f0

14 files changed

+569
-101
lines changed

doc/api/errors.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2285,6 +2285,13 @@ The V8 platform used by this instance of Node.js does not support creating
22852285
Workers. This is caused by lack of embedder support for Workers. In particular,
22862286
this error will not occur with standard builds of Node.js.
22872287

2288+
<a id="ERR_MODULE_LINK_MISMATCH"></a>
2289+
2290+
### `ERR_MODULE_LINK_MISMATCH`
2291+
2292+
A module can not be linked due to a mismatch of the requested modules, and the
2293+
list of given dependency modules.
2294+
22882295
<a id="ERR_MODULE_NOT_FOUND"></a>
22892296

22902297
### `ERR_MODULE_NOT_FOUND`

doc/api/vm.md

Lines changed: 148 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -419,9 +419,7 @@ class that closely mirrors [Module Record][]s as defined in the ECMAScript
419419
specification.
420420

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

426424
Using a `vm.Module` object requires three distinct steps: creation/parsing,
427425
linking, and evaluation. These three steps are illustrated in the following
@@ -449,7 +447,7 @@ const contextifiedObject = vm.createContext({
449447
// Here, we attempt to obtain the default export from the module "foo", and
450448
// put it into local binding "secret".
451449

452-
const bar = new vm.SourceTextModule(`
450+
const rootModule = new vm.SourceTextModule(`
453451
import s from 'foo';
454452
s;
455453
print(s);
@@ -459,47 +457,56 @@ const bar = new vm.SourceTextModule(`
459457
//
460458
// "Link" the imported dependencies of this Module to it.
461459
//
462-
// The provided linking callback (the "linker") accepts two arguments: the
463-
// parent module (`bar` in this case) and the string that is the specifier of
464-
// the imported module. The callback is expected to return a Module that
465-
// corresponds to the provided specifier, with certain requirements documented
466-
// in `module.link()`.
467-
//
468-
// If linking has not started for the returned Module, the same linker
469-
// callback will be called on the returned Module.
460+
// Obtain the requested dependencies of a SourceTextModule by
461+
// `sourceTextModule.moduleRequests` and resolve them.
470462
//
471463
// Even top-level Modules without dependencies must be explicitly linked. The
472-
// callback provided would never be called, however.
473-
//
474-
// The link() method returns a Promise that will be resolved when all the
475-
// Promises returned by the linker resolve.
464+
// array passed to `sourceTextModule.linkRequests(modules)` can be
465+
// empty, however.
476466
//
477-
// Note: This is a contrived example in that the linker function creates a new
478-
// "foo" module every time it is called. In a full-fledged module system, a
479-
// cache would probably be used to avoid duplicated modules.
480-
481-
async function linker(specifier, referencingModule) {
482-
if (specifier === 'foo') {
483-
return new vm.SourceTextModule(`
484-
// The "secret" variable refers to the global variable we added to
485-
// "contextifiedObject" when creating the context.
486-
export default secret;
487-
`, { context: referencingModule.context });
488-
489-
// Using `contextifiedObject` instead of `referencingModule.context`
490-
// here would work as well.
491-
}
492-
throw new Error(`Unable to resolve dependency: ${specifier}`);
467+
// Note: This is a contrived example in that the resolveAndLinkDependencies
468+
// creates a new "foo" module every time it is called. In a full-fledged
469+
// module system, a cache would probably be used to avoid duplicated modules.
470+
471+
const moduleMap = new Map([
472+
['root', rootModule],
473+
]);
474+
475+
function resolveAndLinkDependencies(module) {
476+
const requestedModules = module.moduleRequests.map((request) => {
477+
// In a full-fledged module system, the resolveAndLinkDependencies would
478+
// resolve the module with the module cache key `[specifier, attributes]`.
479+
// In this example, we just use the specifier as the key.
480+
const specifier = request.specifier;
481+
482+
let requestedModule = moduleMap.get(specifier);
483+
if (requestedModule === undefined) {
484+
requestedModule = new vm.SourceTextModule(`
485+
// The "secret" variable refers to the global variable we added to
486+
// "contextifiedObject" when creating the context.
487+
export default secret;
488+
`, { context: referencingModule.context });
489+
moduleMap.set(specifier, linkedModule);
490+
// Resolve the dependencies of the new module as well.
491+
resolveAndLinkDependencies(requestedModule);
492+
}
493+
494+
return requestedModule;
495+
});
496+
497+
module.linkRequests(requestedModules);
493498
}
494-
await bar.link(linker);
499+
500+
resolveAndLinkDependencies(rootModule);
501+
rootModule.instantiate();
495502

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

501508
// Prints 42.
502-
await bar.evaluate();
509+
await rootModule.evaluate();
503510
```
504511

505512
```cjs
@@ -521,7 +528,7 @@ const contextifiedObject = vm.createContext({
521528
// Here, we attempt to obtain the default export from the module "foo", and
522529
// put it into local binding "secret".
523530

524-
const bar = new vm.SourceTextModule(`
531+
const rootModule = new vm.SourceTextModule(`
525532
import s from 'foo';
526533
s;
527534
print(s);
@@ -531,47 +538,56 @@ const contextifiedObject = vm.createContext({
531538
//
532539
// "Link" the imported dependencies of this Module to it.
533540
//
534-
// The provided linking callback (the "linker") accepts two arguments: the
535-
// parent module (`bar` in this case) and the string that is the specifier of
536-
// the imported module. The callback is expected to return a Module that
537-
// corresponds to the provided specifier, with certain requirements documented
538-
// in `module.link()`.
539-
//
540-
// If linking has not started for the returned Module, the same linker
541-
// callback will be called on the returned Module.
541+
// Obtain the requested dependencies of a SourceTextModule by
542+
// `sourceTextModule.moduleRequests` and resolve them.
542543
//
543544
// Even top-level Modules without dependencies must be explicitly linked. The
544-
// callback provided would never be called, however.
545-
//
546-
// The link() method returns a Promise that will be resolved when all the
547-
// Promises returned by the linker resolve.
545+
// array passed to `sourceTextModule.linkRequests(modules)` can be
546+
// empty, however.
548547
//
549-
// Note: This is a contrived example in that the linker function creates a new
550-
// "foo" module every time it is called. In a full-fledged module system, a
551-
// cache would probably be used to avoid duplicated modules.
552-
553-
async function linker(specifier, referencingModule) {
554-
if (specifier === 'foo') {
555-
return new vm.SourceTextModule(`
556-
// The "secret" variable refers to the global variable we added to
557-
// "contextifiedObject" when creating the context.
558-
export default secret;
559-
`, { context: referencingModule.context });
548+
// Note: This is a contrived example in that the resolveAndLinkDependencies
549+
// creates a new "foo" module every time it is called. In a full-fledged
550+
// module system, a cache would probably be used to avoid duplicated modules.
551+
552+
const moduleMap = new Map([
553+
['root', rootModule],
554+
]);
555+
556+
function resolveAndLinkDependencies(module) {
557+
const requestedModules = module.moduleRequests.map((request) => {
558+
// In a full-fledged module system, the resolveAndLinkDependencies would
559+
// resolve the module with the module cache key `[specifier, attributes]`.
560+
// In this example, we just use the specifier as the key.
561+
const specifier = request.specifier;
562+
563+
let requestedModule = moduleMap.get(specifier);
564+
if (requestedModule === undefined) {
565+
requestedModule = new vm.SourceTextModule(`
566+
// The "secret" variable refers to the global variable we added to
567+
// "contextifiedObject" when creating the context.
568+
export default secret;
569+
`, { context: referencingModule.context });
570+
moduleMap.set(specifier, linkedModule);
571+
// Resolve the dependencies of the new module as well.
572+
resolveAndLinkDependencies(requestedModule);
573+
}
574+
575+
return requestedModule;
576+
});
560577

561-
// Using `contextifiedObject` instead of `referencingModule.context`
562-
// here would work as well.
563-
}
564-
throw new Error(`Unable to resolve dependency: ${specifier}`);
578+
module.linkRequests(requestedModules);
565579
}
566-
await bar.link(linker);
580+
581+
resolveAndLinkDependencies(rootModule);
582+
rootModule.instantiate();
567583

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

573589
// Prints 42.
574-
await bar.evaluate();
590+
await rootModule.evaluate();
575591
})();
576592
```
577593

@@ -660,6 +676,10 @@ changes:
660676
Link module dependencies. This method must be called before evaluation, and
661677
can only be called once per module.
662678

679+
Use [`sourceTextModule.linkRequests(modules)`][] and
680+
[`sourceTextModule.instantiate()`][] to link modules either synchronously or
681+
asynchronously.
682+
663683
The function is expected to return a `Module` object or a `Promise` that
664684
eventually resolves to a `Module` object. The returned `Module` must satisfy the
665685
following two invariants:
@@ -805,8 +825,9 @@ const module = new vm.SourceTextModule(
805825
meta.prop = {};
806826
},
807827
});
808-
// Since module has no dependencies, the linker function will never be called.
809-
await module.link(() => {});
828+
// The module has an empty `moduleRequests` array.
829+
module.linkRequests([]);
830+
module.instantiate();
810831
await module.evaluate();
811832
812833
// Now, Object.prototype.secret will be equal to 42.
@@ -832,8 +853,9 @@ const contextifiedObject = vm.createContext({ secret: 42 });
832853
meta.prop = {};
833854
},
834855
});
835-
// Since module has no dependencies, the linker function will never be called.
836-
await module.link(() => {});
856+
// The module has an empty `moduleRequests` array.
857+
module.linkRequests([]);
858+
module.instantiate();
837859
await module.evaluate();
838860
// Now, Object.prototype.secret will be equal to 42.
839861
//
@@ -898,6 +920,54 @@ to disallow any changes to it.
898920
Corresponds to the `[[RequestedModules]]` field of [Cyclic Module Record][]s in
899921
the ECMAScript specification.
900922
923+
### `sourceTextModule.instantiate()`
924+
925+
<!-- YAML
926+
added: REPLACEME
927+
-->
928+
929+
* Returns: {undefined}
930+
931+
Instantiate the module with the linked requested modules.
932+
933+
This resolves the imported bindings of the module, including re-exported
934+
binding names. When there are any bindings that cannot be resolved,
935+
an error would be thrown synchronously.
936+
937+
If the requested modules include cyclic dependencies, the
938+
[`sourceTextModule.linkRequests(modules)`][] method must be called on all
939+
modules in the cycle before calling this method.
940+
941+
### `sourceTextModule.linkRequests(modules)`
942+
943+
<!-- YAML
944+
added: REPLACEME
945+
-->
946+
947+
* `modules` {vm.Module\[]} Array of `vm.Module` objects that this module depends on.
948+
The order of the modules in the array is the order of
949+
[`sourceTextModule.moduleRequests`][].
950+
* Returns: {undefined}
951+
952+
Link module dependencies. This method must be called before evaluation, and
953+
can only be called once per module.
954+
955+
The order of the module instances in the `modules` array should correspond to the order of
956+
[`sourceTextModule.moduleRequests`][] being resolved.
957+
958+
If the module has no dependencies, the `modules` array can be empty.
959+
960+
Users can use `sourceTextModule.moduleRequests` to implement the host-defined
961+
[HostLoadImportedModule][] abstract operation in the ECMAScript specification,
962+
and using `sourceTextModule.linkRequests()` to invoke specification defined
963+
[FinishLoadingImportedModule][], on the module with all dependencies in a batch.
964+
965+
It's up to the creator of the `SourceTextModule` to determine if the resolution
966+
of the dependencies is synchronous or asynchronous.
967+
968+
After each module in the `modules` array is linked, call
969+
[`sourceTextModule.instantiate()`][].
970+
901971
### `sourceTextModule.moduleRequests`
902972

903973
<!-- YAML
@@ -1017,14 +1087,17 @@ the module to access information outside the specified `context`. Use
10171087
added:
10181088
- v13.0.0
10191089
- v12.16.0
1090+
changes:
1091+
- version: REPLACEME
1092+
pr-url: https://github.com/nodejs/node/pull/59000
1093+
description: No longer need to call `syntheticModule.link()` before
1094+
calling this method.
10201095
-->
10211096

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

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

10291102
```mjs
10301103
import vm from 'node:vm';
@@ -1033,7 +1106,6 @@ const m = new vm.SyntheticModule(['x'], () => {
10331106
m.setExport('x', 1);
10341107
});
10351108
1036-
await m.link(() => {});
10371109
await m.evaluate();
10381110
10391111
assert.strictEqual(m.namespace.x, 1);
@@ -1045,7 +1117,6 @@ const vm = require('node:vm');
10451117
const m = new vm.SyntheticModule(['x'], () => {
10461118
m.setExport('x', 1);
10471119
});
1048-
await m.link(() => {});
10491120
await m.evaluate();
10501121
assert.strictEqual(m.namespace.x, 1);
10511122
})();
@@ -2037,7 +2108,9 @@ const { Script, SyntheticModule } = require('node:vm');
20372108
[Cyclic Module Record]: https://tc39.es/ecma262/#sec-cyclic-module-records
20382109
[ECMAScript Module Loader]: esm.md#modules-ecmascript-modules
20392110
[Evaluate() concrete method]: https://tc39.es/ecma262/#sec-moduleevaluation
2111+
[FinishLoadingImportedModule]: https://tc39.es/ecma262/#sec-FinishLoadingImportedModule
20402112
[GetModuleNamespace]: https://tc39.es/ecma262/#sec-getmodulenamespace
2113+
[HostLoadImportedModule]: https://tc39.es/ecma262/#sec-HostLoadImportedModule
20412114
[HostResolveImportedModule]: https://tc39.es/ecma262/#sec-hostresolveimportedmodule
20422115
[ImportDeclaration]: https://tc39.es/ecma262/#prod-ImportDeclaration
20432116
[Link() concrete method]: https://tc39.es/ecma262/#sec-moduledeclarationlinking
@@ -2049,13 +2122,14 @@ const { Script, SyntheticModule } = require('node:vm');
20492122
[WithClause]: https://tc39.es/ecma262/#prod-WithClause
20502123
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG`]: errors.md#err_vm_dynamic_import_callback_missing_flag
20512124
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`]: errors.md#err_vm_dynamic_import_callback_missing
2052-
[`ERR_VM_MODULE_STATUS`]: errors.md#err_vm_module_status
20532125
[`Error`]: errors.md#class-error
20542126
[`URL`]: url.md#class-url
20552127
[`eval()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval
20562128
[`optionsExpression`]: https://tc39.es/proposal-import-attributes/#sec-evaluate-import-call
20572129
[`script.runInContext()`]: #scriptrunincontextcontextifiedobject-options
20582130
[`script.runInThisContext()`]: #scriptruninthiscontextoptions
2131+
[`sourceTextModule.instantiate()`]: #sourcetextmoduleinstantiate
2132+
[`sourceTextModule.linkRequests(modules)`]: #sourcetextmodulelinkrequestsmodules
20592133
[`sourceTextModule.moduleRequests`]: #sourcetextmodulemodulerequests
20602134
[`url.origin`]: url.md#urlorigin
20612135
[`vm.compileFunction()`]: #vmcompilefunctioncode-params-options

lib/internal/bootstrap/realm.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,7 @@ class BuiltinModule {
359359
this.setExport('default', builtin.exports);
360360
});
361361
// Ensure immediate sync execution to capture exports now
362+
this.module.link([]);
362363
this.module.instantiate();
363364
this.module.evaluate(-1, false);
364365
return this.module;

lib/internal/errors.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1602,6 +1602,9 @@ E('ERR_MISSING_ARGS',
16021602
return `${msg} must be specified`;
16031603
}, TypeError);
16041604
E('ERR_MISSING_OPTION', '%s is required', TypeError);
1605+
E('ERR_MODULE_LINK_MISMATCH', function(message) {
1606+
return message;
1607+
}, TypeError);
16051608
E('ERR_MODULE_NOT_FOUND', function(path, base, exactUrl) {
16061609
if (exactUrl) {
16071610
lazyInternalUtil().setOwnProperty(this, 'url', `${exactUrl}`);

0 commit comments

Comments
 (0)