Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
986b4df
src: add an option to make compile cache portable
Aditi-1400 Sep 12, 2025
1be854f
build, src: fix include paths for vtune files
rahulchaphalkar Oct 7, 2025
3ef0750
lib: remove redundant destroyHook checks
gurgunday Oct 8, 2025
4821ff8
test: prepare junit file attribute normalization
Han5991 Aug 18, 2025
25c33fd
test_runner: add junit file attribute support
Han5991 Aug 18, 2025
ef4026f
tools: use cooldown property correctly
RafaelGSS Oct 8, 2025
5009021
http2: do not crash on mismatched ping buffer length
Renegade334 Oct 8, 2025
3ddd7b4
test_runner: fix suite timeout
MoLow Oct 9, 2025
c88ea78
test: deflake test-fs-promises-watch-iterator
lpinca Oct 9, 2025
31a1c95
module: fix directory option in the enableCompileCache() API
joyeecheung Oct 9, 2025
f0f0291
test,doc: skip --max-old-space-size-percentage on 32-bit platforms
Asaf-Federman Oct 9, 2025
d80ec0d
test: skip tests that cause timeouts on IBM i
sravani1510 Oct 9, 2025
caafda2
lib: fix typo in QuicSessionStats
SeokhunEom Oct 10, 2025
3176940
doc: disambiguate top-level `worker_threads` module exports
Renegade334 Oct 10, 2025
cfc45d1
typings: add buffer internalBinding typing
JinhyeokFang Oct 10, 2025
2fc1abf
benchmark: use non-deprecated WriteUtf8V2 method
targos Oct 11, 2025
16e5db3
doc: update teams in collaborator-guide.md and add links
louwers Oct 11, 2025
4b7d836
src: update locks to use DictionaryTemplate
jasnell Sep 28, 2025
f335910
src: make additional cleanups in node locks impl
jasnell Sep 28, 2025
879e4c0
v8: add cpu profile
theanarkh Oct 11, 2025
6643c82
doc: use markdown when branch-diff major release
RafaelGSS Oct 11, 2025
40c984d
deps: upgrade npm to 11.6.2
npm-cli-bot Oct 11, 2025
987a023
doc: `createSQLTagStore` -> `createTagStore`
avivkeller Oct 11, 2025
9446832
doc: improve code snippet alternative of url.parse() using WHATWG URL
styfle Oct 13, 2025
bc3ed49
Revert "test: ensure message event fires in worker message port test"
lpinca Oct 13, 2025
cc2d9fd
http2: rename variable to additionalPseudoHeaders
tniessen Oct 13, 2025
0c130d0
lib: fix constructor in _errnoException stack tree
SeokhunEom Oct 13, 2025
157e1f1
http: fix http client leaky with double response
theanarkh Oct 13, 2025
ebe3bef
deps: update googletest to 279f847
nodejs-github-bot Oct 14, 2025
472e11d
doc: fix not working code example in vm docs
arturgawlik Oct 14, 2025
0007212
inspector: support handshake response for websocket inspection
islandryu Oct 14, 2025
619f6bb
http: add optimizeEmptyRequests server option
RafaelGSS Oct 14, 2025
d6a982b
module: handle null source from async loader hooks in sync hooks
joyeecheung Oct 14, 2025
93c5776
inspector: add network payload buffer size limits
legendecas Oct 15, 2025
2448de6
benchmark: add vm.SourceTextModule benchmark
joyeecheung Oct 10, 2025
73fe9d2
src: add a default branch for module phase
legendecas Oct 15, 2025
f419ba7
tools: add inspector_protocol updater
legendecas Oct 15, 2025
51fb64d
doc: fix `blob.bytes()` heading level
xty Oct 16, 2025
51adaaa
doc: add --heap-snapshot-on-oom to useful v8 flag
jakecastelli Oct 17, 2025
c95d974
src: use `Utf8Value` and `TwoByteValue` instead of V8 helpers
addaleax Oct 13, 2025
ec53d9f
src: initial enablement of IsolateGroups
jasnell Oct 17, 2025
00866b4
test: move sea tests into test/sea
joyeecheung Oct 13, 2025
ee2512f
test: skip sea tests on x64 macOS
joyeecheung Oct 13, 2025
5262707
doc: document wildcard supported by tools/test.py
joyeecheung Oct 17, 2025
8c497a6
test: parallelize test-without-async-context-frame correctly
joyeecheung Oct 17, 2025
83bd17c
typings: delete undefined property in ConfigBinding
whsung0330 Oct 14, 2025
58385da
typings: add missing properties in HTTPParser
whsung0330 Oct 14, 2025
33d97b3
typings: add missing properties and method in Worker
whsung0330 Oct 14, 2025
41cc57e
src: replace Environment::GetCurrent with args.GetIsolate
KimSH39 Oct 18, 2025
7bdfee8
tools: optimize wildcard execution in tools/test.py
joyeecheung Oct 18, 2025
5e374e9
benchmark: add benchmark for leaf source text modules
joyeecheung Oct 12, 2025
742eebc
vm: make vm.Module.evaluate() conditionally synchronous
joyeecheung Oct 10, 2025
931b80f
test: fix incorrect calculation in test-perf-hooks.js
joyeecheung Oct 18, 2025
c220da0
util: mark special properties when inspecting them
BridgeAR Oct 18, 2025
5f83d4d
tools: fix inspector_protocol updater
legendecas Oct 18, 2025
3a64383
doc: update decorator documentation to reflect actual policy
salman-aziz-4425 Oct 19, 2025
4390889
src: use cached primordials_string
KimSH39 Oct 19, 2025
77189c1
build: remove V8_COMPRESS_POINTERS_IN_ISOLATE_CAGE defs
joyeecheung Oct 19, 2025
f372303
test: split test-runner-watch-mode-kill-signal
joyeecheung Oct 19, 2025
b66f0f7
meta: loop userland-migrations in deprecations
legendecas Oct 19, 2025
1769597
build: build v8 with -fvisibility=hidden -fvisibility-inlines-hidden
joyeecheung Oct 19, 2025
fbc8b7b
doc: use `any` for `worker_threads.Worker` 'error' event argument `err`
jonasgeiler Oct 19, 2025
b9277a0
src: add watch config namespace
marco-ippolito Oct 9, 2025
63d5242
util: use more defensive code when inspecting error objects
aduh95 Oct 20, 2025
49046c5
meta: move one or more collaborators to emeritus
nodejs-github-bot Oct 20, 2025
f615116
deps: update inspector_protocol to af7f5a8173fdbc29f0835ec94395932e328b
nodejs-github-bot Oct 21, 2025
ce265a2
deps: update corepack to 0.34.1
nodejs-github-bot Oct 21, 2025
0087d9a
build: use call command when calling python configure
ZenMasterJacob20011 Oct 21, 2025
abdeb18
tools: limit inspector protocol PR title length
legendecas Oct 21, 2025
88a7df7
msi: fix WiX warnings
StefanStojanovic Oct 22, 2025
b674be4
test_runner: use module.registerHooks in module mocks
joyeecheung Oct 22, 2025
efbfc6c
doc, module: change async customization hooks to experimental
Flarna Oct 22, 2025
1790f50
test: fix small compile warning in test_network_requests_buffer.cc
xiaocainiao633 Oct 22, 2025
e25758c
tools: update gyp-next to 0.20.5
nodejs-github-bot Oct 22, 2025
83a7252
doc: add missing CAA type to dns.resolveAny() & dnsPromises.resolveAny()
hkleungai Oct 22, 2025
67bfe86
module: refactor and clarify async loader hook customizations
joyeecheung Oct 23, 2025
13e1929
build: ibmi follow aix visibility
sravani1510 Oct 24, 2025
388a163
test: put helper in test-runner-output into common
joyeecheung Oct 24, 2025
3ce93dd
test: increase debugger waitFor timeout on macOS
legendecas Oct 24, 2025
b15badb
test,crypto: sha3 algorithms aren't supported with BoringSSL
codebytere Oct 25, 2025
6454012
crypto: update root certificates to NSS 3.116
nodejs-github-bot Oct 25, 2025
0f89192
win: upgrade Visual Studio workload from 2019 to 2022
gengjiawen Oct 25, 2025
15a406d
test,crypto: fix conditional SHA3-* skip on BoringSSL
panva Oct 25, 2025
534f249
src: conditionally disable source phase imports by default
codebytere Oct 26, 2025
6181e9d
doc: mention more codemods in `deprecations.md`
AugustinMauroy Oct 27, 2025
b1f74c7
http: lazy allocate cookies array
ronag Oct 27, 2025
a330b57
benchmark: improve cpu.sh for safety and usability
meteorqz6 Oct 27, 2025
1af45bf
deps: update simdjson to 4.0.7
nodejs-github-bot Oct 27, 2025
365aed4
lib: fix typo in createBlobReaderStream
SeokhunEom Oct 27, 2025
b8fd0df
src: add COUNT_GENERIC_USAGE utility for tests
joyeecheung Oct 27, 2025
076115d
src: fix timing of snapshot serialize callback
joyeecheung Oct 27, 2025
639c0a5
deps: V8: backport e5dbbbadcbff
RaisinTen Nov 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
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 <[email protected]>
  • Loading branch information
joyeecheung authored and aduh95 committed Oct 31, 2025
commit 742eebc83d63706f1299201cb24e9426e32b4c41
49 changes: 39 additions & 10 deletions doc/api/vm.md
Original file line number Diff line number Diff line change
Expand Up @@ -618,19 +618,47 @@ in the ECMAScript specification.
work after that. **Default:** `false`.
* Returns: {Promise} Fulfills with `undefined` upon success.

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

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

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

Corresponds to the [Evaluate() concrete method][] field of [Cyclic Module
Record][]s in the ECMAScript specification.
1. If the `vm.SourceTextModule` has no top-level `await` in itself or any of its dependencies, the promise will be
fulfilled _synchronously_ after the module and all its dependencies have been evaluated.
1. If the evaluation succeeds, the promise will be _synchronously_ resolved to `undefined`.
2. If the evaluation results in an exception, the promise will be _synchronously_ rejected with the exception
that causes the evaluation to fail, which is the same as `module.error`.
2. If the `vm.SourceTextModule` has top-level `await` in itself or any of its dependencies, the promise will be
fulfilled _asynchronously_ after the module and all its dependencies have been evaluated.
1. If the evaluation succeeds, the promise will be _asynchronously_ resolved to `undefined`.
2. If the evaluation results in an exception, the promise will be _asynchronously_ rejected with the exception
that causes the evaluation to fail.

If the module is a `vm.SyntheticModule`, `evaluate()` always returns a promise that fulfills synchronously, see
the specification of [Evaluate() of a Synthetic Module Record][]:

1. If the `evaluateCallback` passed to its constructor throws an exception synchronously, `evaluate()` returns
a promise that will be synchronously rejected with that exception.
2. If the `evaluateCallback` does not throw an exception, `evaluate()` returns a promise that will be
synchronously resolved to `undefined`.

The `evaluateCallback` of a `vm.SyntheticModule` is executed synchronously within the `evaluate()` call, and its
return value is discarded. This means if `evaluateCallback` is an asynchronous function, the promise returned by
`evaluate()` will not reflect its asynchronous behavior, and any rejections from an asynchronous
`evaluateCallback` will be lost.

`evaluate()` could also be called again after the module has already been evaluated, in which case:

1. If the initial evaluation ended in success (`module.status` is `'evaluated'`), it will do nothing
and return a promise that resolves to `undefined`.
2. If the initial evaluation resulted in an exception (`module.status` is `'errored'`), it will re-reject
the exception that the initial evaluation resulted in.

This method cannot be called while the module is being evaluated (`module.status` is `'evaluating'`).

### `module.identifier`

Expand Down Expand Up @@ -2215,6 +2243,7 @@ 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
[Evaluate() of a Synthetic Module Record]: https://tc39.es/ecma262/#sec-smr-Evaluate
[FinishLoadingImportedModule]: https://tc39.es/ecma262/#sec-FinishLoadingImportedModule
[GetModuleNamespace]: https://tc39.es/ecma262/#sec-getmodulenamespace
[HostLoadImportedModule]: https://tc39.es/ecma262/#sec-HostLoadImportedModule
Expand Down
45 changes: 25 additions & 20 deletions lib/internal/vm/module.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const {
ObjectPrototypeHasOwnProperty,
ObjectSetPrototypeOf,
PromisePrototypeThen,
PromiseReject,
PromiseResolve,
ReflectApply,
SafePromiseAllReturnArrayLike,
Expand Down Expand Up @@ -208,27 +209,31 @@ class Module {
this[kWrap].instantiate();
}

async evaluate(options = kEmptyObject) {
validateThisInternalField(this, kWrap, 'Module');
validateObject(options, 'options');

let timeout = options.timeout;
if (timeout === undefined) {
timeout = -1;
} else {
validateUint32(timeout, 'options.timeout', true);
}
const { breakOnSigint = false } = options;
validateBoolean(breakOnSigint, 'options.breakOnSigint');
const status = this[kWrap].getStatus();
if (status !== kInstantiated &&
status !== kEvaluated &&
status !== kErrored) {
throw new ERR_VM_MODULE_STATUS(
'must be one of linked, evaluated, or errored',
);
evaluate(options = kEmptyObject) {
try {
validateThisInternalField(this, kWrap, 'Module');
validateObject(options, 'options');

let timeout = options.timeout;
if (timeout === undefined) {
timeout = -1;
} else {
validateUint32(timeout, 'options.timeout', true);
}
const { breakOnSigint = false } = options;
validateBoolean(breakOnSigint, 'options.breakOnSigint');
const status = this[kWrap].getStatus();
if (status !== kInstantiated &&
status !== kEvaluated &&
status !== kErrored) {
throw new ERR_VM_MODULE_STATUS(
'must be one of linked, evaluated, or errored',
);
}
return this[kWrap].evaluate(timeout, breakOnSigint);
} catch (e) {
return PromiseReject(e);
}
await this[kWrap].evaluate(timeout, breakOnSigint);
}

[customInspectSymbol](depth, options) {
Expand Down
167 changes: 167 additions & 0 deletions test/parallel/test-vm-module-evaluate-source-text-module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// Flags: --experimental-vm-modules
'use strict';

// This tests the result of evaluating a vm.SourceTextModule.
const common = require('../common');

const assert = require('assert');
// To make testing easier we just use the public inspect API. If the output format
// changes, update this test accordingly.
const { inspect } = require('util');
const vm = require('vm');

globalThis.callCount = {};
common.allowGlobals(globalThis.callCount);

// Synchronous error during evaluation results in a synchronously rejected promise.
{
globalThis.callCount.syncError = 0;
const mod = new vm.SourceTextModule(`
globalThis.callCount.syncError++;
throw new Error("synchronous source text module");
export const a = 1;
`);
mod.linkRequests([]);
mod.instantiate();
const promise = mod.evaluate();
assert.strictEqual(globalThis.callCount.syncError, 1);
assert.match(inspect(promise), /rejected/);
assert(mod.error, 'Expected mod.error to be set');
assert.strictEqual(mod.error.message, 'synchronous source text module');

promise.catch(common.mustCall((err) => {
assert.strictEqual(err, mod.error);
// Calling evaluate() again results in the same rejection synchronously.
const promise2 = mod.evaluate();
assert.match(inspect(promise2), /rejected/);
promise2.catch(common.mustCall((err2) => {
assert.strictEqual(err, err2);
// The module is only evaluated once.
assert.strictEqual(globalThis.callCount.syncError, 1);
}));
}));
}

// Successful evaluation of a module without top-level await results in a
// promise synchronously resolved to undefined.
{
globalThis.callCount.syncNamedExports = 0;
const mod = new vm.SourceTextModule(`
globalThis.callCount.syncNamedExports++;
export const a = 1, b = 2;
`);
mod.linkRequests([]);
mod.instantiate();
const promise = mod.evaluate();
assert.match(inspect(promise), /Promise { undefined }/);
assert.strictEqual(mod.namespace.a, 1);
assert.strictEqual(mod.namespace.b, 2);
assert.strictEqual(globalThis.callCount.syncNamedExports, 1);
promise.then(common.mustCall((value) => {
assert.strictEqual(value, undefined);

// Calling evaluate() again results in the same resolved promise synchronously.
const promise2 = mod.evaluate();
assert.match(inspect(promise2), /Promise { undefined }/);
assert.strictEqual(mod.namespace.a, 1);
assert.strictEqual(mod.namespace.b, 2);
promise2.then(common.mustCall((value) => {
assert.strictEqual(value, undefined);
// The module is only evaluated once.
assert.strictEqual(globalThis.callCount.syncNamedExports, 1);
}));
}));
}

{
globalThis.callCount.syncDefaultExports = 0;
// Modules with either named and default exports have the same behaviors.
const mod = new vm.SourceTextModule(`
globalThis.callCount.syncDefaultExports++;
export default 42;
`);
mod.linkRequests([]);
mod.instantiate();
const promise = mod.evaluate();
assert.match(inspect(promise), /Promise { undefined }/);
assert.strictEqual(mod.namespace.default, 42);
assert.strictEqual(globalThis.callCount.syncDefaultExports, 1);

promise.then(common.mustCall((value) => {
assert.strictEqual(value, undefined);

// Calling evaluate() again results in the same resolved promise synchronously.
const promise2 = mod.evaluate();
assert.match(inspect(promise2), /Promise { undefined }/);
assert.strictEqual(mod.namespace.default, 42);
promise2.then(common.mustCall((value) => {
assert.strictEqual(value, undefined);
// The module is only evaluated once.
assert.strictEqual(globalThis.callCount.syncDefaultExports, 1);
}));
}));
}

// Successful evaluation of a module with top-level await results in a promise
// that is fulfilled asynchronously with undefined.
{
globalThis.callCount.asyncEvaluation = 0;
const mod = new vm.SourceTextModule(`
globalThis.callCount.asyncEvaluation++;
await Promise.resolve();
export const a = 1;
`);
mod.linkRequests([]);
mod.instantiate();
const promise = mod.evaluate();
assert.match(inspect(promise), /<pending>/);
// Accessing the namespace before the promise is fulfilled throws ReferenceError.
assert.throws(() => mod.namespace.a, { name: 'ReferenceError' });
assert.strictEqual(globalThis.callCount.asyncEvaluation, 1);
promise.then(common.mustCall((value) => {
assert.strictEqual(value, undefined);
assert.strictEqual(globalThis.callCount.asyncEvaluation, 1);

// Calling evaluate() again results in a promise synchronously resolved to undefined.
const promise2 = mod.evaluate();
assert.match(inspect(promise2), /Promise { undefined }/);
assert.strictEqual(mod.namespace.a, 1);
promise2.then(common.mustCall((value) => {
assert.strictEqual(value, undefined);
// The module is only evaluated once.
assert.strictEqual(globalThis.callCount.asyncEvaluation, 1);
}));
}));
}

// Rejection of a top-level await promise results in a promise that is
// rejected asynchronously with the same reason.
{
globalThis.callCount.asyncRejection = 0;
const mod = new vm.SourceTextModule(`
globalThis.callCount.asyncRejection++;
await Promise.reject(new Error("asynchronous source text module"));
export const a = 1;
`);
mod.linkRequests([]);
mod.instantiate();
const promise = mod.evaluate();
assert.match(inspect(promise), /<pending>/);
// Accessing the namespace before the promise is fulfilled throws ReferenceError.
assert.throws(() => mod.namespace.a, { name: 'ReferenceError' });
promise.catch(common.mustCall((err) => {
assert.strictEqual(err, mod.error);
assert.strictEqual(err.message, 'asynchronous source text module');
assert.strictEqual(globalThis.callCount.asyncRejection, 1);

// Calling evaluate() again results in a promise synchronously rejected
// with the same reason.
const promise2 = mod.evaluate();
assert.match(inspect(promise2), /rejected/);
promise2.catch(common.mustCall((err2) => {
assert.strictEqual(err, err2);
// The module is only evaluated once.
assert.strictEqual(globalThis.callCount.asyncRejection, 1);
}));
}));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Flags: --experimental-vm-modules
'use strict';

// This tests the result of evaluating a vm.SyntheticModule with an async rejection
// in the evaluation step.

const common = require('../common');

const assert = require('assert');
// To make testing easier we just use the public inspect API. If the output format
// changes, update this test accordingly.
const { inspect } = require('util');
const vm = require('vm');

// The promise _synchronously_ resolves to undefined, because for a synthethic module,
// the evaluation operation can only either resolve or reject immediately.
// In this case, the asynchronously rejected promise can't be handled from the outside,
// so we'll catch it with the isolate-level unhandledRejection handler.
// See https://tc39.es/ecma262/#sec-smr-Evaluate
process.on('unhandledRejection', common.mustCall((err) => {
assert.strictEqual(err.message, 'asynchronous source text module');
}));

const mod = new vm.SyntheticModule(['a'], common.mustCall(async () => {
throw new Error('asynchronous source text module');
}));

const promise = mod.evaluate();
assert.match(inspect(promise), /Promise { undefined }/);
// Accessing the uninitialized export of a synthetic module returns undefined.
assert.strictEqual(mod.namespace.a, undefined);

promise.then(common.mustCall((value) => {
assert.strictEqual(value, undefined);
}));

// Calling evaluate() again results in a promise _synchronously_ resolved to undefined again.
const promise2 = mod.evaluate();
assert.match(inspect(promise2), /Promise { undefined }/);
promise2.then(common.mustCall((value) => {
assert.strictEqual(value, undefined);
}));
Loading