Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
84 changes: 84 additions & 0 deletions benchmark/vm/source-text-module-leaf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
'use strict';

const vm = require('vm');
const common = require('../common.js');
const assert = require('assert');

const bench = common.createBenchmark(main, {
stage: ['all', 'compile', 'link', 'instantiate', 'evaluate'],
type: ['sync', 'async'],
n: [1000],
}, {
flags: ['--experimental-vm-modules'],
});

function main({ stage, type, n }) {
const arr = [];
if (stage === 'all' || stage === 'compile') {
bench.start();
}

for (let i = 0; i < n; i++) {
let source = `export const value${i} = 1;`;
if (type === 'async') {
source += `\nawait Promise.resolve();\n`;
}
const m = new vm.SourceTextModule(source);
arr.push(m);
}

if (stage === 'compile') {
bench.end(n);
return;
}

if (stage === 'link') {
bench.start();
}

for (let i = 0; i < n; i++) {
arr[i].linkRequests([]);
}

if (stage === 'link') {
bench.end(n);
return;
}

if (stage === 'instantiate') {
bench.start();
}

for (let i = 0; i < n; i++) {
arr[i].instantiate();
}

if (stage === 'instantiate') {
bench.end(n);
return;
}

if (stage === 'evaluate') {
bench.start();
}

function finalize() {
bench.end(n);
for (let i = 0; i < n; i++) {
assert.strictEqual(arr[i].namespace[`value${i}`], 1);
}
}

if (type === 'sync') {
for (let i = 0; i < n; i++) {
arr[i].evaluate();
}
finalize();
} else {
const promises = [];
for (let i = 0; i < n; i++) {
promises.push(arr[i].evaluate());
}
Promise.all(promises).then(finalize);
}
}
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 at least linked;
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.
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 @@ -2221,6 +2249,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);
}));
}));
}
Loading
Loading