Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 79b2387

Browse filesBrowse files
joyeecheungaduh95
authored andcommitted
vm: make vm.Module.evaluate() conditionally synchronous
- Make sure that the vm.Module.evaluate() method is conditionally synchronous based on the specification. Previously, the returned promise is unconditionally pending (even for synthetic modules and source text modules without top-level await) instead of immediately fulfilled, making it harder to debug as it deviates from the specified behavior. - Clarify the synchronicity of this method in the documentation - Add more tests for the synchronicity of this method. PR-URL: #60205 Refs: #59656 Refs: #37648 Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
1 parent e600711 commit 79b2387
Copy full SHA for 79b2387

File tree

Expand file treeCollapse file tree

6 files changed

+390
-30
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

6 files changed

+390
-30
lines changed
Open diff view settings
Collapse file

‎doc/api/vm.md‎

Copy file name to clipboardExpand all lines: doc/api/vm.md
+39-10Lines changed: 39 additions & 10 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -618,19 +618,47 @@ in the ECMAScript specification.
618618
work after that. **Default:** `false`.
619619
* Returns: {Promise} Fulfills with `undefined` upon success.
620620

621-
Evaluate the module.
621+
Evaluate the module and its depenendencies. Corresponds to the [Evaluate() concrete method][] field of
622+
[Cyclic Module Record][]s in the ECMAScript specification.
622623

623-
This must be called after the module has been linked; otherwise it will reject.
624-
It could be called also when the module has already been evaluated, in which
625-
case it will either do nothing if the initial evaluation ended in success
626-
(`module.status` is `'evaluated'`) or it will re-throw the exception that the
627-
initial evaluation resulted in (`module.status` is `'errored'`).
624+
If the module is a `vm.SourceTextModule`, `evaluate()` must be called after the module has been instantiated;
625+
otherwise `evaluate()` will return a rejected promise.
628626

629-
This method cannot be called while the module is being evaluated
630-
(`module.status` is `'evaluating'`).
627+
For a `vm.SourceTextModule`, the promise returned by `evaluate()` may be fulfilled either
628+
synchronously or asynchronously:
631629

632-
Corresponds to the [Evaluate() concrete method][] field of [Cyclic Module
633-
Record][]s in the ECMAScript specification.
630+
1. If the `vm.SourceTextModule` has no top-level `await` in itself or any of its dependencies, the promise will be
631+
fulfilled _synchronously_ after the module and all its dependencies have been evaluated.
632+
1. If the evaluation succeeds, the promise will be _synchronously_ resolved to `undefined`.
633+
2. If the evaluation results in an exception, the promise will be _synchronously_ rejected with the exception
634+
that causes the evaluation to fail, which is the same as `module.error`.
635+
2. If the `vm.SourceTextModule` has top-level `await` in itself or any of its dependencies, the promise will be
636+
fulfilled _asynchronously_ after the module and all its dependencies have been evaluated.
637+
1. If the evaluation succeeds, the promise will be _asynchronously_ resolved to `undefined`.
638+
2. If the evaluation results in an exception, the promise will be _asynchronously_ rejected with the exception
639+
that causes the evaluation to fail.
640+
641+
If the module is a `vm.SyntheticModule`, `evaluate()` always returns a promise that fulfills synchronously, see
642+
the specification of [Evaluate() of a Synthetic Module Record][]:
643+
644+
1. If the `evaluateCallback` passed to its constructor throws an exception synchronously, `evaluate()` returns
645+
a promise that will be synchronously rejected with that exception.
646+
2. If the `evaluateCallback` does not throw an exception, `evaluate()` returns a promise that will be
647+
synchronously resolved to `undefined`.
648+
649+
The `evaluateCallback` of a `vm.SyntheticModule` is executed synchronously within the `evaluate()` call, and its
650+
return value is discarded. This means if `evaluateCallback` is an asynchronous function, the promise returned by
651+
`evaluate()` will not reflect its asynchronous behavior, and any rejections from an asynchronous
652+
`evaluateCallback` will be lost.
653+
654+
`evaluate()` could also be called again after the module has already been evaluated, in which case:
655+
656+
1. If the initial evaluation ended in success (`module.status` is `'evaluated'`), it will do nothing
657+
and return a promise that resolves to `undefined`.
658+
2. If the initial evaluation resulted in an exception (`module.status` is `'errored'`), it will re-reject
659+
the exception that the initial evaluation resulted in.
660+
661+
This method cannot be called while the module is being evaluated (`module.status` is `'evaluating'`).
634662

635663
### `module.identifier`
636664

@@ -2221,6 +2249,7 @@ const { Script, SyntheticModule } = require('node:vm');
22212249
[Cyclic Module Record]: https://tc39.es/ecma262/#sec-cyclic-module-records
22222250
[ECMAScript Module Loader]: esm.md#modules-ecmascript-modules
22232251
[Evaluate() concrete method]: https://tc39.es/ecma262/#sec-moduleevaluation
2252+
[Evaluate() of a Synthetic Module Record]: https://tc39.es/ecma262/#sec-smr-Evaluate
22242253
[FinishLoadingImportedModule]: https://tc39.es/ecma262/#sec-FinishLoadingImportedModule
22252254
[GetModuleNamespace]: https://tc39.es/ecma262/#sec-getmodulenamespace
22262255
[HostLoadImportedModule]: https://tc39.es/ecma262/#sec-HostLoadImportedModule
Collapse file

‎lib/internal/vm/module.js‎

Copy file name to clipboardExpand all lines: lib/internal/vm/module.js
+25-20Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const {
1313
ObjectPrototypeHasOwnProperty,
1414
ObjectSetPrototypeOf,
1515
PromisePrototypeThen,
16+
PromiseReject,
1617
PromiseResolve,
1718
ReflectApply,
1819
SafePromiseAllReturnArrayLike,
@@ -208,27 +209,31 @@ class Module {
208209
this[kWrap].instantiate();
209210
}
210211

211-
async evaluate(options = kEmptyObject) {
212-
validateThisInternalField(this, kWrap, 'Module');
213-
validateObject(options, 'options');
214-
215-
let timeout = options.timeout;
216-
if (timeout === undefined) {
217-
timeout = -1;
218-
} else {
219-
validateUint32(timeout, 'options.timeout', true);
220-
}
221-
const { breakOnSigint = false } = options;
222-
validateBoolean(breakOnSigint, 'options.breakOnSigint');
223-
const status = this[kWrap].getStatus();
224-
if (status !== kInstantiated &&
225-
status !== kEvaluated &&
226-
status !== kErrored) {
227-
throw new ERR_VM_MODULE_STATUS(
228-
'must be one of linked, evaluated, or errored',
229-
);
212+
evaluate(options = kEmptyObject) {
213+
try {
214+
validateThisInternalField(this, kWrap, 'Module');
215+
validateObject(options, 'options');
216+
217+
let timeout = options.timeout;
218+
if (timeout === undefined) {
219+
timeout = -1;
220+
} else {
221+
validateUint32(timeout, 'options.timeout', true);
222+
}
223+
const { breakOnSigint = false } = options;
224+
validateBoolean(breakOnSigint, 'options.breakOnSigint');
225+
const status = this[kWrap].getStatus();
226+
if (status !== kInstantiated &&
227+
status !== kEvaluated &&
228+
status !== kErrored) {
229+
throw new ERR_VM_MODULE_STATUS(
230+
'must be one of linked, evaluated, or errored',
231+
);
232+
}
233+
return this[kWrap].evaluate(timeout, breakOnSigint);
234+
} catch (e) {
235+
return PromiseReject(e);
230236
}
231-
await this[kWrap].evaluate(timeout, breakOnSigint);
232237
}
233238

234239
[customInspectSymbol](depth, options) {
Collapse file
+167Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// Flags: --experimental-vm-modules
2+
'use strict';
3+
4+
// This tests the result of evaluating a vm.SourceTextModule.
5+
const common = require('../common');
6+
7+
const assert = require('assert');
8+
// To make testing easier we just use the public inspect API. If the output format
9+
// changes, update this test accordingly.
10+
const { inspect } = require('util');
11+
const vm = require('vm');
12+
13+
globalThis.callCount = {};
14+
common.allowGlobals(globalThis.callCount);
15+
16+
// Synchronous error during evaluation results in a synchronously rejected promise.
17+
{
18+
globalThis.callCount.syncError = 0;
19+
const mod = new vm.SourceTextModule(`
20+
globalThis.callCount.syncError++;
21+
throw new Error("synchronous source text module");
22+
export const a = 1;
23+
`);
24+
mod.linkRequests([]);
25+
mod.instantiate();
26+
const promise = mod.evaluate();
27+
assert.strictEqual(globalThis.callCount.syncError, 1);
28+
assert.match(inspect(promise), /rejected/);
29+
assert(mod.error, 'Expected mod.error to be set');
30+
assert.strictEqual(mod.error.message, 'synchronous source text module');
31+
32+
promise.catch(common.mustCall((err) => {
33+
assert.strictEqual(err, mod.error);
34+
// Calling evaluate() again results in the same rejection synchronously.
35+
const promise2 = mod.evaluate();
36+
assert.match(inspect(promise2), /rejected/);
37+
promise2.catch(common.mustCall((err2) => {
38+
assert.strictEqual(err, err2);
39+
// The module is only evaluated once.
40+
assert.strictEqual(globalThis.callCount.syncError, 1);
41+
}));
42+
}));
43+
}
44+
45+
// Successful evaluation of a module without top-level await results in a
46+
// promise synchronously resolved to undefined.
47+
{
48+
globalThis.callCount.syncNamedExports = 0;
49+
const mod = new vm.SourceTextModule(`
50+
globalThis.callCount.syncNamedExports++;
51+
export const a = 1, b = 2;
52+
`);
53+
mod.linkRequests([]);
54+
mod.instantiate();
55+
const promise = mod.evaluate();
56+
assert.match(inspect(promise), /Promise { undefined }/);
57+
assert.strictEqual(mod.namespace.a, 1);
58+
assert.strictEqual(mod.namespace.b, 2);
59+
assert.strictEqual(globalThis.callCount.syncNamedExports, 1);
60+
promise.then(common.mustCall((value) => {
61+
assert.strictEqual(value, undefined);
62+
63+
// Calling evaluate() again results in the same resolved promise synchronously.
64+
const promise2 = mod.evaluate();
65+
assert.match(inspect(promise2), /Promise { undefined }/);
66+
assert.strictEqual(mod.namespace.a, 1);
67+
assert.strictEqual(mod.namespace.b, 2);
68+
promise2.then(common.mustCall((value) => {
69+
assert.strictEqual(value, undefined);
70+
// The module is only evaluated once.
71+
assert.strictEqual(globalThis.callCount.syncNamedExports, 1);
72+
}));
73+
}));
74+
}
75+
76+
{
77+
globalThis.callCount.syncDefaultExports = 0;
78+
// Modules with either named and default exports have the same behaviors.
79+
const mod = new vm.SourceTextModule(`
80+
globalThis.callCount.syncDefaultExports++;
81+
export default 42;
82+
`);
83+
mod.linkRequests([]);
84+
mod.instantiate();
85+
const promise = mod.evaluate();
86+
assert.match(inspect(promise), /Promise { undefined }/);
87+
assert.strictEqual(mod.namespace.default, 42);
88+
assert.strictEqual(globalThis.callCount.syncDefaultExports, 1);
89+
90+
promise.then(common.mustCall((value) => {
91+
assert.strictEqual(value, undefined);
92+
93+
// Calling evaluate() again results in the same resolved promise synchronously.
94+
const promise2 = mod.evaluate();
95+
assert.match(inspect(promise2), /Promise { undefined }/);
96+
assert.strictEqual(mod.namespace.default, 42);
97+
promise2.then(common.mustCall((value) => {
98+
assert.strictEqual(value, undefined);
99+
// The module is only evaluated once.
100+
assert.strictEqual(globalThis.callCount.syncDefaultExports, 1);
101+
}));
102+
}));
103+
}
104+
105+
// Successful evaluation of a module with top-level await results in a promise
106+
// that is fulfilled asynchronously with undefined.
107+
{
108+
globalThis.callCount.asyncEvaluation = 0;
109+
const mod = new vm.SourceTextModule(`
110+
globalThis.callCount.asyncEvaluation++;
111+
await Promise.resolve();
112+
export const a = 1;
113+
`);
114+
mod.linkRequests([]);
115+
mod.instantiate();
116+
const promise = mod.evaluate();
117+
assert.match(inspect(promise), /<pending>/);
118+
// Accessing the namespace before the promise is fulfilled throws ReferenceError.
119+
assert.throws(() => mod.namespace.a, { name: 'ReferenceError' });
120+
assert.strictEqual(globalThis.callCount.asyncEvaluation, 1);
121+
promise.then(common.mustCall((value) => {
122+
assert.strictEqual(value, undefined);
123+
assert.strictEqual(globalThis.callCount.asyncEvaluation, 1);
124+
125+
// Calling evaluate() again results in a promise synchronously resolved to undefined.
126+
const promise2 = mod.evaluate();
127+
assert.match(inspect(promise2), /Promise { undefined }/);
128+
assert.strictEqual(mod.namespace.a, 1);
129+
promise2.then(common.mustCall((value) => {
130+
assert.strictEqual(value, undefined);
131+
// The module is only evaluated once.
132+
assert.strictEqual(globalThis.callCount.asyncEvaluation, 1);
133+
}));
134+
}));
135+
}
136+
137+
// Rejection of a top-level await promise results in a promise that is
138+
// rejected asynchronously with the same reason.
139+
{
140+
globalThis.callCount.asyncRejection = 0;
141+
const mod = new vm.SourceTextModule(`
142+
globalThis.callCount.asyncRejection++;
143+
await Promise.reject(new Error("asynchronous source text module"));
144+
export const a = 1;
145+
`);
146+
mod.linkRequests([]);
147+
mod.instantiate();
148+
const promise = mod.evaluate();
149+
assert.match(inspect(promise), /<pending>/);
150+
// Accessing the namespace before the promise is fulfilled throws ReferenceError.
151+
assert.throws(() => mod.namespace.a, { name: 'ReferenceError' });
152+
promise.catch(common.mustCall((err) => {
153+
assert.strictEqual(err, mod.error);
154+
assert.strictEqual(err.message, 'asynchronous source text module');
155+
assert.strictEqual(globalThis.callCount.asyncRejection, 1);
156+
157+
// Calling evaluate() again results in a promise synchronously rejected
158+
// with the same reason.
159+
const promise2 = mod.evaluate();
160+
assert.match(inspect(promise2), /rejected/);
161+
promise2.catch(common.mustCall((err2) => {
162+
assert.strictEqual(err, err2);
163+
// The module is only evaluated once.
164+
assert.strictEqual(globalThis.callCount.asyncRejection, 1);
165+
}));
166+
}));
167+
}
Collapse file
+42Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Flags: --experimental-vm-modules
2+
'use strict';
3+
4+
// This tests the result of evaluating a vm.SyntheticModule with an async rejection
5+
// in the evaluation step.
6+
7+
const common = require('../common');
8+
9+
const assert = require('assert');
10+
// To make testing easier we just use the public inspect API. If the output format
11+
// changes, update this test accordingly.
12+
const { inspect } = require('util');
13+
const vm = require('vm');
14+
15+
// The promise _synchronously_ resolves to undefined, because for a synthethic module,
16+
// the evaluation operation can only either resolve or reject immediately.
17+
// In this case, the asynchronously rejected promise can't be handled from the outside,
18+
// so we'll catch it with the isolate-level unhandledRejection handler.
19+
// See https://tc39.es/ecma262/#sec-smr-Evaluate
20+
process.on('unhandledRejection', common.mustCall((err) => {
21+
assert.strictEqual(err.message, 'asynchronous source text module');
22+
}));
23+
24+
const mod = new vm.SyntheticModule(['a'], common.mustCall(async () => {
25+
throw new Error('asynchronous source text module');
26+
}));
27+
28+
const promise = mod.evaluate();
29+
assert.match(inspect(promise), /Promise { undefined }/);
30+
// Accessing the uninitialized export of a synthetic module returns undefined.
31+
assert.strictEqual(mod.namespace.a, undefined);
32+
33+
promise.then(common.mustCall((value) => {
34+
assert.strictEqual(value, undefined);
35+
}));
36+
37+
// Calling evaluate() again results in a promise _synchronously_ resolved to undefined again.
38+
const promise2 = mod.evaluate();
39+
assert.match(inspect(promise2), /Promise { undefined }/);
40+
promise2.then(common.mustCall((value) => {
41+
assert.strictEqual(value, undefined);
42+
}));

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.