Skip to content

Commit 2262e98

Browse files
committed
vm: sync-ify SourceTextModule linkage
Split `module.link(linker)` into two synchronous step `sourceTextModule.linkRequestedModules()` 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 4de0197 commit 2262e98

13 files changed

+505
-78
lines changed

doc/api/errors.md

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

2269+
<a id="ERR_MODULE_LINK_MISMATCH"></a>
2270+
2271+
### `ERR_MODULE_LINK_MISMATCH`
2272+
2273+
A module can not be linked due to a mismatch of the requested modules, and the
2274+
list of given dependency modules.
2275+
22692276
<a id="ERR_MODULE_NOT_FOUND"></a>
22702277

22712278
### `ERR_MODULE_NOT_FOUND`

doc/api/vm.md

Lines changed: 132 additions & 63 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.linkRequestedModules(modules)` can be
465+
// empty, however.
476466
//
477-
// Note: This is a contrived example in that the linker function creates a new
467+
// Note: This is a contrived example in that the linker creates a new
478468
// "foo" module every time it is called. In a full-fledged module system, a
479469
// cache would probably be used to avoid duplicated modules.
480470

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 });
471+
const moduleMap = new Map([
472+
['root', rootModule],
473+
]);
488474

489-
// Using `contextifiedObject` instead of `referencingModule.context`
490-
// here would work as well.
491-
}
492-
throw new Error(`Unable to resolve dependency: ${specifier}`);
475+
function linker(module) {
476+
const requestedModules = module.moduleRequests.map((request) => {
477+
// In a full-fledged module system, the linker would resolve the
478+
// 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+
linker(requestedModule);
492+
}
493+
494+
return requestedModule;
495+
});
496+
497+
module.linkRequestedModules(requestedModules);
493498
}
494-
await bar.link(linker);
499+
500+
linker(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,55 @@ 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.linkRequestedModules(modules)` can be
546+
// empty, however.
548547
//
549-
// Note: This is a contrived example in that the linker function creates a new
548+
// Note: This is a contrived example in that the linker creates a new
550549
// "foo" module every time it is called. In a full-fledged module system, a
551550
// cache would probably be used to avoid duplicated modules.
552551

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 });
552+
const moduleMap = new Map([
553+
['root', rootModule],
554+
]);
555+
556+
function linker(module) {
557+
const requestedModules = module.moduleRequests.map((request) => {
558+
// In a full-fledged module system, the linker would resolve the
559+
// 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+
linker(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.linkRequestedModules(requestedModules);
565579
}
566-
await bar.link(linker);
580+
581+
linker(rootModule);
567582

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

573588
// Prints 42.
574-
await bar.evaluate();
589+
await rootModule.evaluate();
575590
})();
576591
```
577592

@@ -635,6 +650,9 @@ changes:
635650
former name is still provided for backward compatibility.
636651
-->
637652

653+
> Stability: 0 - Deprecated: Use [`sourceTextModule.linkRequestedModules(modules)`][] and
654+
> [`sourceTextModule.instantiate()`][] instead.
655+
638656
* `linker` {Function}
639657
* `specifier` {string} The specifier of the requested module:
640658
```mjs
@@ -898,6 +916,53 @@ to disallow any changes to it.
898916
Corresponds to the `[[RequestedModules]]` field of [Cyclic Module Record][]s in
899917
the ECMAScript specification.
900918
919+
### `sourceTextModule.instantiate()`
920+
921+
<!-- YAML
922+
added: REPLACEME
923+
-->
924+
925+
* Returns: {void}
926+
927+
Instantiate the module with the linked requested modules.
928+
929+
This resolves the imported bindings of the module, including re-exported
930+
binding names.
931+
932+
If the requested modules include cyclic dependencies, the
933+
[`sourceTextModule.linkRequestedModules(modules)`][] method must be called on all
934+
modules in the cycle before calling this method.
935+
936+
### `sourceTextModule.linkRequestedModules(modules)`
937+
938+
<!-- YAML
939+
added: REPLACEME
940+
-->
941+
942+
* `modules` {vm.Module\[]} Array of `vm.Module` objects that this module depends on.
943+
The order of the modules in the array is the order of
944+
[`sourceTextModule.moduleRequests`][].
945+
* Returns: {void}
946+
947+
Link module dependencies. This method must be called before evaluation, and
948+
can only be called once per module.
949+
950+
The order of the `modules` array should respect the order of
951+
[`sourceTextModule.moduleRequests`][].
952+
953+
If the module has no dependencies, the `modules` array can be empty, or skip this
954+
method call.
955+
956+
Composing `sourceTextModule.moduleRequests` and `sourceTextModule.link()`,
957+
this acts similar to [HostLoadImportedModule][] and [FinishLoadingImportedModule][]
958+
abstract operations in the ECMAScript specification, respectively.
959+
960+
It's up to the creator of the `SourceTextModule` to determine if the resolution
961+
of the dependencies is synchronous or asynchronous.
962+
963+
After each module in the `modules` array is linked, call
964+
[`sourceTextModule.instantiate()`][].
965+
901966
### `sourceTextModule.moduleRequests`
902967

903968
<!-- YAML
@@ -1017,14 +1082,17 @@ the module to access information outside the specified `context`. Use
10171082
added:
10181083
- v13.0.0
10191084
- v12.16.0
1085+
changes:
1086+
- version: REPLACEME
1087+
pr-url: https://github.com/nodejs/node/pull/XXXXX
1088+
description: No longer need to call `syntheticModule.link()` before
1089+
calling this method.
10201090
-->
10211091

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

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.
1095+
This method sets the module export binding slots with the given value.
10281096

10291097
```mjs
10301098
import vm from 'node:vm';
@@ -1033,7 +1101,6 @@ const m = new vm.SyntheticModule(['x'], () => {
10331101
m.setExport('x', 1);
10341102
});
10351103
1036-
await m.link(() => {});
10371104
await m.evaluate();
10381105
10391106
assert.strictEqual(m.namespace.x, 1);
@@ -1045,7 +1112,6 @@ const vm = require('node:vm');
10451112
const m = new vm.SyntheticModule(['x'], () => {
10461113
m.setExport('x', 1);
10471114
});
1048-
await m.link(() => {});
10491115
await m.evaluate();
10501116
assert.strictEqual(m.namespace.x, 1);
10511117
})();
@@ -2037,7 +2103,9 @@ const { Script, SyntheticModule } = require('node:vm');
20372103
[Cyclic Module Record]: https://tc39.es/ecma262/#sec-cyclic-module-records
20382104
[ECMAScript Module Loader]: esm.md#modules-ecmascript-modules
20392105
[Evaluate() concrete method]: https://tc39.es/ecma262/#sec-moduleevaluation
2106+
[FinishLoadingImportedModule]: https://tc39.es/ecma262/#sec-FinishLoadingImportedModule
20402107
[GetModuleNamespace]: https://tc39.es/ecma262/#sec-getmodulenamespace
2108+
[HostLoadImportedModule]: https://tc39.es/ecma262/#sec-HostLoadImportedModule
20412109
[HostResolveImportedModule]: https://tc39.es/ecma262/#sec-hostresolveimportedmodule
20422110
[ImportDeclaration]: https://tc39.es/ecma262/#prod-ImportDeclaration
20432111
[Link() concrete method]: https://tc39.es/ecma262/#sec-moduledeclarationlinking
@@ -2049,13 +2117,14 @@ const { Script, SyntheticModule } = require('node:vm');
20492117
[WithClause]: https://tc39.es/ecma262/#prod-WithClause
20502118
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG`]: errors.md#err_vm_dynamic_import_callback_missing_flag
20512119
[`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
20532120
[`Error`]: errors.md#class-error
20542121
[`URL`]: url.md#class-url
20552122
[`eval()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval
20562123
[`optionsExpression`]: https://tc39.es/proposal-import-attributes/#sec-evaluate-import-call
20572124
[`script.runInContext()`]: #scriptrunincontextcontextifiedobject-options
20582125
[`script.runInThisContext()`]: #scriptruninthiscontextoptions
2126+
[`sourceTextModule.instantiate()`]: #sourcetextmoduleinstantiate
2127+
[`sourceTextModule.linkRequestedModules(modules)`]: #sourcetextmodulelinkrequestedmodulesmodules
20592128
[`sourceTextModule.moduleRequests`]: #sourcetextmodulemodulerequests
20602129
[`url.origin`]: url.md#urlorigin
20612130
[`vm.compileFunction()`]: #vmcompilefunctioncode-params-options

lib/internal/errors.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1597,6 +1597,9 @@ E('ERR_MISSING_ARGS',
15971597
return `${msg} must be specified`;
15981598
}, TypeError);
15991599
E('ERR_MISSING_OPTION', '%s is required', TypeError);
1600+
E('ERR_MODULE_LINK_MISMATCH', function(message) {
1601+
return message;
1602+
}, TypeError);
16001603
E('ERR_MODULE_NOT_FOUND', function(path, base, exactUrl) {
16011604
if (exactUrl) {
16021605
lazyInternalUtil().setOwnProperty(this, 'url', `${exactUrl}`);

0 commit comments

Comments
 (0)