Skip to content

Commit 2086f33

Browse files
legendecastargos
authored andcommitted
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. PR-URL: #59000 Refs: #37648 Reviewed-By: Joyee Cheung <joyeec9h3@gmail.com>
1 parent c8d6b60 commit 2086f33

16 files changed

+602
-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 because the same module requests in it are not
2293+
resolved to the same module.
2294+
22882295
<a id="ERR_MODULE_NOT_FOUND"></a>
22892296

22902297
### `ERR_MODULE_NOT_FOUND`

doc/api/vm.md

Lines changed: 163 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,69 @@ 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. If two module requests have the same
957+
specifier and import attributes, they must be resolved with the same module instance or an
958+
`ERR_MODULE_LINK_MISMATCH` would be thrown. For example, when linking requests for this
959+
module:
960+
961+
<!-- eslint-disable no-duplicate-imports -->
962+
963+
```mjs
964+
import foo from 'foo';
965+
import source Foo from 'foo';
966+
```
967+
968+
<!-- eslint-enable no-duplicate-imports -->
969+
970+
The `modules` array must contain two references to the same instance, because the two
971+
module requests are identical but in two phases.
972+
973+
If the module has no dependencies, the `modules` array can be empty.
974+
975+
Users can use `sourceTextModule.moduleRequests` to implement the host-defined
976+
[HostLoadImportedModule][] abstract operation in the ECMAScript specification,
977+
and using `sourceTextModule.linkRequests()` to invoke specification defined
978+
[FinishLoadingImportedModule][], on the module with all dependencies in a batch.
979+
980+
It's up to the creator of the `SourceTextModule` to determine if the resolution
981+
of the dependencies is synchronous or asynchronous.
982+
983+
After each module in the `modules` array is linked, call
984+
[`sourceTextModule.instantiate()`][].
985+
901986
### `sourceTextModule.moduleRequests`
902987

903988
<!-- YAML
@@ -1017,14 +1102,17 @@ the module to access information outside the specified `context`. Use
10171102
added:
10181103
- v13.0.0
10191104
- v12.16.0
1105+
changes:
1106+
- version: REPLACEME
1107+
pr-url: https://github.com/nodejs/node/pull/59000
1108+
description: No longer need to call `syntheticModule.link()` before
1109+
calling this method.
10201110
-->
10211111

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

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

10291117
```mjs
10301118
import vm from 'node:vm';
@@ -1033,7 +1121,6 @@ const m = new vm.SyntheticModule(['x'], () => {
10331121
m.setExport('x', 1);
10341122
});
10351123
1036-
await m.link(() => {});
10371124
await m.evaluate();
10381125
10391126
assert.strictEqual(m.namespace.x, 1);
@@ -1045,7 +1132,6 @@ const vm = require('node:vm');
10451132
const m = new vm.SyntheticModule(['x'], () => {
10461133
m.setExport('x', 1);
10471134
});
1048-
await m.link(() => {});
10491135
await m.evaluate();
10501136
assert.strictEqual(m.namespace.x, 1);
10511137
})();
@@ -2037,7 +2123,9 @@ const { Script, SyntheticModule } = require('node:vm');
20372123
[Cyclic Module Record]: https://tc39.es/ecma262/#sec-cyclic-module-records
20382124
[ECMAScript Module Loader]: esm.md#modules-ecmascript-modules
20392125
[Evaluate() concrete method]: https://tc39.es/ecma262/#sec-moduleevaluation
2126+
[FinishLoadingImportedModule]: https://tc39.es/ecma262/#sec-FinishLoadingImportedModule
20402127
[GetModuleNamespace]: https://tc39.es/ecma262/#sec-getmodulenamespace
2128+
[HostLoadImportedModule]: https://tc39.es/ecma262/#sec-HostLoadImportedModule
20412129
[HostResolveImportedModule]: https://tc39.es/ecma262/#sec-hostresolveimportedmodule
20422130
[ImportDeclaration]: https://tc39.es/ecma262/#prod-ImportDeclaration
20432131
[Link() concrete method]: https://tc39.es/ecma262/#sec-moduledeclarationlinking
@@ -2049,13 +2137,14 @@ const { Script, SyntheticModule } = require('node:vm');
20492137
[WithClause]: https://tc39.es/ecma262/#prod-WithClause
20502138
[`ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG`]: errors.md#err_vm_dynamic_import_callback_missing_flag
20512139
[`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
20532140
[`Error`]: errors.md#class-error
20542141
[`URL`]: url.md#class-url
20552142
[`eval()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval
20562143
[`optionsExpression`]: https://tc39.es/proposal-import-attributes/#sec-evaluate-import-call
20572144
[`script.runInContext()`]: #scriptrunincontextcontextifiedobject-options
20582145
[`script.runInThisContext()`]: #scriptruninthiscontextoptions
2146+
[`sourceTextModule.instantiate()`]: #sourcetextmoduleinstantiate
2147+
[`sourceTextModule.linkRequests(modules)`]: #sourcetextmodulelinkrequestsmodules
20592148
[`sourceTextModule.moduleRequests`]: #sourcetextmodulemodulerequests
20602149
[`url.origin`]: url.md#urlorigin
20612150
[`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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1589,6 +1589,7 @@ E('ERR_MISSING_ARGS',
15891589
return `${msg} must be specified`;
15901590
}, TypeError);
15911591
E('ERR_MISSING_OPTION', '%s is required', TypeError);
1592+
E('ERR_MODULE_LINK_MISMATCH', '%s', TypeError);
15921593
E('ERR_MODULE_NOT_FOUND', function(path, base, exactUrl) {
15931594
if (exactUrl) {
15941595
lazyInternalUtil().setOwnProperty(this, 'url', `${exactUrl}`);

0 commit comments

Comments
 (0)