diff --git a/doc/api/vm.md b/doc/api/vm.md index 23424a786b294b..6b1f86b1ef95e3 100644 --- a/doc/api/vm.md +++ b/doc/api/vm.md @@ -25,23 +25,42 @@ object. The invoked code treats any property in the context like a global variable. Any changes to global variables caused by the invoked code are reflected in the context object. -```js -const vm = require('node:vm'); +```mjs +import { createContext, runInContext } from 'node:vm'; + +const x = 1; + +const context = { x: 2 }; +createContext(context); // Contextify the object. + +const code = 'x += 40; var y = 17;'; +// `x` and `y` are global variables in the context. +// Initially, x has the value 2 because that is the value of context.x. +runInContext(code, context); + +console.log(context.x); // 42 +console.log(context.y); // 17 + +console.log(x); // 1; y is not defined +``` + +```cjs +const { createContext, runInContext } = require('node:vm'); const x = 1; const context = { x: 2 }; -vm.createContext(context); // Contextify the object. +createContext(context); // Contextify the object. const code = 'x += 40; var y = 17;'; // `x` and `y` are global variables in the context. // Initially, x has the value 2 because that is the value of context.x. -vm.runInContext(code, context); +runInContext(code, context); console.log(context.x); // 42 console.log(context.y); // 17 -console.log(x); // 1; y is not defined. +console.log(x); // 1; y is not defined ``` ## Class: `vm.Script` @@ -201,17 +220,36 @@ The following example compiles code that increments a global variable, sets the value of another global variable, then execute the code multiple times. The globals are contained in the `context` object. -```js -const vm = require('node:vm'); +```mjs +import { createContext, Script } from 'node:vm'; const context = { animal: 'cat', count: 2, }; -const script = new vm.Script('count += 1; name = "kitty";'); +const script = new Script('count += 1; name = "kitty";'); -vm.createContext(context); +createContext(context); +for (let i = 0; i < 10; ++i) { + script.runInContext(context); +} + +console.log(context); +// Prints: { animal: 'cat', count: 12, name: 'kitty' } +``` + +```cjs +const { createContext, Script } = require('node:vm'); + +const context = { + animal: 'cat', + count: 2, +}; + +const script = new Script('count += 1; name = "kitty";'); + +createContext(context); for (let i = 0; i < 10; ++i) { script.runInContext(context); } @@ -296,10 +334,30 @@ The following example compiles code that sets a global variable, then executes the code multiple times in different contexts. The globals are set on and contained within each individual `context`. -```js -const vm = require('node:vm'); +```mjs +import { constants, Script } from 'node:vm'; + +const script = new Script('globalVar = "set"'); + +const contexts = [{}, {}, {}]; +contexts.forEach((context) => { + script.runInNewContext(context); +}); + +console.log(contexts); +// Prints: [{ globalVar: 'set' }, { globalVar: 'set' }, { globalVar: 'set' }] + +// This would throw if the context is created from a contextified object. +// constants.DONT_CONTEXTIFY allows creating contexts with ordinary +// global objects that can be frozen. +const freezeScript = new Script('Object.freeze(globalThis); globalThis;'); +const frozenContext = freezeScript.runInNewContext(constants.DONT_CONTEXTIFY); +``` + +```cjs +const { constants, Script } = require('node:vm'); -const script = new vm.Script('globalVar = "set"'); +const script = new Script('globalVar = "set"'); const contexts = [{}, {}, {}]; contexts.forEach((context) => { @@ -310,10 +368,10 @@ console.log(contexts); // Prints: [{ globalVar: 'set' }, { globalVar: 'set' }, { globalVar: 'set' }] // This would throw if the context is created from a contextified object. -// vm.constants.DONT_CONTEXTIFY allows creating contexts with ordinary +// constants.DONT_CONTEXTIFY allows creating contexts with ordinary // global objects that can be frozen. -const freezeScript = new vm.Script('Object.freeze(globalThis); globalThis;'); -const frozenContext = freezeScript.runInNewContext(vm.constants.DONT_CONTEXTIFY); +const freezeScript = new Script('Object.freeze(globalThis); globalThis;'); +const frozenContext = freezeScript.runInNewContext(constants.DONT_CONTEXTIFY); ``` ### `script.runInThisContext([options])` @@ -347,12 +405,28 @@ _does_ have access to the current `global` object. The following example compiles code that increments a `global` variable then executes that code multiple times: -```js -const vm = require('node:vm'); +```mjs +import { Script } from 'node:vm'; global.globalVar = 0; -const script = new vm.Script('globalVar += 1', { filename: 'myfile.vm' }); +const script = new Script('globalVar += 1', { filename: 'myfile.vm' }); + +for (let i = 0; i < 1000; ++i) { + script.runInThisContext(); +} + +console.log(globalVar); + +// 1000 +``` + +```cjs +const { Script } = require('node:vm'); + +global.globalVar = 0; + +const script = new Script('globalVar += 1', { filename: 'myfile.vm' }); for (let i = 0; i < 1000; ++i) { script.runInThisContext(); @@ -1126,16 +1200,40 @@ defined in the WebIDL specification. The purpose of synthetic modules is to provide a generic interface for exposing non-JavaScript sources to ECMAScript module graphs. -```js -const vm = require('node:vm'); +```mjs +import { SyntheticModule } from 'node:vm'; + +const source = '{ "a": 1 }'; +const syntheticModule = new SyntheticModule(['default'], function() { + const obj = JSON.parse(source); + this.setExport('default', obj); +}); + +// Use `syntheticModule` in linking +(async () => { + await syntheticModule.link(() => {}); + await syntheticModule.evaluate(); + + console.log('Default export:', syntheticModule.namespace.default); +})(); +``` + +```cjs +const { SyntheticModule } = require('node:vm'); const source = '{ "a": 1 }'; -const module = new vm.SyntheticModule(['default'], function() { +const syntheticModule = new SyntheticModule(['default'], function() { const obj = JSON.parse(source); this.setExport('default', obj); }); -// Use `module` in linking... +// Use `syntheticModule` in linking +(async () => { + await syntheticModule.link(() => {}); + await syntheticModule.evaluate(); + + console.log('Default export:', syntheticModule.namespace.default); +})(); ``` ### `new vm.SyntheticModule(exportNames, evaluateCallback[, options])` @@ -1395,15 +1493,32 @@ existing properties but also having the built-in objects and functions any standard [global object][] has. Outside of scripts run by the vm module, global variables will remain unchanged. -```js -const vm = require('node:vm'); +```mjs +import { createContext, runInContext } from 'node:vm'; + +global.globalVar = 3; + +const context = { globalVar: 1 }; +createContext(context); + +runInContext('globalVar *= 2;', context); + +console.log(context); +// Prints: { globalVar: 2 } + +console.log(global.globalVar); +// Prints: 3 +``` + +```cjs +const { createContext, runInContext } = require('node:vm'); global.globalVar = 3; const context = { globalVar: 1 }; -vm.createContext(context); +createContext(context); -vm.runInContext('globalVar *= 2;', context); +runInContext('globalVar *= 2;', context); console.log(context); // Prints: { globalVar: 2 } @@ -1478,45 +1593,62 @@ memory reachable by each V8 specific contexts in the current instance of the V8 engine, while the result of `v8.getHeapSpaceStatistics()` measure the memory occupied by each heap space in the current V8 instance. -```js -const vm = require('node:vm'); +```mjs +import { createContext, measureMemory } from 'node:vm'; // Measure the memory used by the main context. -vm.measureMemory({ mode: 'summary' }) +measureMemory({ mode: 'summary' }) // This is the same as vm.measureMemory() .then((result) => { // The current format is: // { - // total: { - // jsMemoryEstimate: 2418479, jsMemoryRange: [ 2418479, 2745799 ] - // } + // total: { jsMemoryEstimate: 1601828, jsMemoryRange: [1601828, 5275288] }, + // WebAssembly: { code: 0, metadata: 33962 }, // } console.log(result); }); -const context = vm.createContext({ a: 1 }); -vm.measureMemory({ mode: 'detailed', execution: 'eager' }) +const context = createContext({ a: 1 }); +measureMemory({ mode: 'detailed', execution: 'eager' }).then((result) => { + // Reference the context here so that it won't be GC'ed + // until the measurement is complete. + console.log('Context:', context.a); + // { + // total: { jsMemoryEstimate: 1767100, jsMemoryRange: [1767100, 5440560] }, + // WebAssembly: { code: 0, metadata: 33962 }, + // current: { jsMemoryEstimate: 1601828, jsMemoryRange: [1601828, 5275288] }, + // other: [{ jsMemoryEstimate: 165272, jsMemoryRange: [Array] }], + // } + console.log(result); +}); +``` + +```cjs +const { createContext, measureMemory } = require('node:vm'); +// Measure the memory used by the main context. +measureMemory({ mode: 'summary' }) + // This is the same as vm.measureMemory() .then((result) => { - // Reference the context here so that it won't be GC'ed - // until the measurement is complete. - console.log(context.a); + // The current format is: // { - // total: { - // jsMemoryEstimate: 2574732, - // jsMemoryRange: [ 2574732, 2904372 ] - // }, - // current: { - // jsMemoryEstimate: 2438996, - // jsMemoryRange: [ 2438996, 2768636 ] - // }, - // other: [ - // { - // jsMemoryEstimate: 135736, - // jsMemoryRange: [ 135736, 465376 ] - // } - // ] + // total: { jsMemoryEstimate: 1601828, jsMemoryRange: [1601828, 5275288] }, + // WebAssembly: { code: 0, metadata: 33962 }, // } console.log(result); }); + +const context = createContext({ a: 1 }); +measureMemory({ mode: 'detailed', execution: 'eager' }).then((result) => { + // Reference the context here so that it won't be GC'ed + // until the measurement is complete. + console.log('Context:', context.a); + // { + // total: { jsMemoryEstimate: 1767100, jsMemoryRange: [1767100, 5440560] }, + // WebAssembly: { code: 0, metadata: 33962 }, + // current: { jsMemoryEstimate: 1601828, jsMemoryRange: [1601828, 5275288] }, + // other: [{ jsMemoryEstimate: 165272, jsMemoryRange: [Array] }], + // } + console.log(result); +}); ``` ## `vm.runInContext(code, contextifiedObject[, options])` @@ -1583,14 +1715,27 @@ If `options` is a string, then it specifies the filename. The following example compiles and executes different scripts using a single [contextified][] object: -```js -const vm = require('node:vm'); +```mjs +import { createContext, runInContext } from 'node:vm'; const contextObject = { globalVar: 1 }; -vm.createContext(contextObject); +createContext(contextObject); for (let i = 0; i < 10; ++i) { - vm.runInContext('globalVar *= 2;', contextObject); + runInContext('globalVar *= 2;', contextObject); +} +console.log(contextObject); +// Prints: { globalVar: 1024 } +``` + +```cjs +const { createContext, runInContext } = require('node:vm'); + +const contextObject = { globalVar: 1 }; +createContext(contextObject); + +for (let i = 0; i < 10; ++i) { + runInContext('globalVar *= 2;', contextObject); } console.log(contextObject); // Prints: { globalVar: 1024 } @@ -1700,22 +1845,46 @@ It does several things at once: The following example compiles and executes code that increments a global variable and sets a new one. These globals are contained in the `contextObject`. -```js -const vm = require('node:vm'); +```mjs +import { runInNewContext, constants } from 'node:vm'; const contextObject = { animal: 'cat', count: 2, }; -vm.runInNewContext('count += 1; name = "kitty"', contextObject); +runInNewContext('count += 1; name = "kitty"', contextObject); console.log(contextObject); // Prints: { animal: 'cat', count: 3, name: 'kitty' } // This would throw if the context is created from a contextified object. // vm.constants.DONT_CONTEXTIFY allows creating contexts with ordinary global objects that // can be frozen. -const frozenContext = vm.runInNewContext('Object.freeze(globalThis); globalThis;', vm.constants.DONT_CONTEXTIFY); +const frozenContext = runInNewContext( + 'Object.freeze(globalThis); globalThis;', + constants.DONT_CONTEXTIFY, +); +``` + +```cjs +const { runInNewContext, constants } = require('node:vm'); + +const contextObject = { + animal: 'cat', + count: 2, +}; + +runInNewContext('count += 1; name = "kitty"', contextObject); +console.log(contextObject); +// Prints: { animal: 'cat', count: 3, name: 'kitty' } + +// This would throw if the context is created from a contextified object. +// vm.constants.DONT_CONTEXTIFY allows creating contexts with ordinary global objects that +// can be frozen. +const frozenContext = runInNewContext( + 'Object.freeze(globalThis); globalThis;', + constants.DONT_CONTEXTIFY, +); ``` ## `vm.runInThisContext(code[, options])` @@ -1782,11 +1951,26 @@ the JavaScript [`eval()`][] function to run the same code: -```js -const vm = require('node:vm'); +```mjs +import { runInThisContext } from 'node:vm'; let localVar = 'initial value'; -const vmResult = vm.runInThisContext('localVar = "vm";'); +const vmResult = runInThisContext('localVar = "vm";'); +console.log(`vmResult: '${vmResult}', localVar: '${localVar}'`); +// Prints: vmResult: 'vm', localVar: 'initial value' + +const evalResult = eval('localVar = "eval";'); +console.log(`evalResult: '${evalResult}', localVar: '${localVar}'`); +// Prints: evalResult: 'eval', localVar: 'eval' +``` + + + +```cjs +const { runInThisContext } = require('node:vm'); +let localVar = 'initial value'; + +const vmResult = runInThisContext('localVar = "vm";'); console.log(`vmResult: '${vmResult}', localVar: '${localVar}'`); // Prints: vmResult: 'vm', localVar: 'initial value' @@ -1811,15 +1995,17 @@ In order to run a simple web server using the `node:http` module the code passed to the context must either call `require('node:http')` on its own, or have a reference to the `node:http` module passed to it. For instance: -```js -'use strict'; -const vm = require('node:vm'); +```mjs +import { runInThisContext } from 'node:vm'; +import { createRequire } from 'node:module'; + +const require = createRequire(import.meta.url); const code = ` ((require) => { - const http = require('node:http'); + const { createServer } = require('node:http'); - http.createServer((request, response) => { + createServer((request, response) => { response.writeHead(200, { 'Content-Type': 'text/plain' }); response.end('Hello World\\n'); }).listen(8124); @@ -1827,7 +2013,25 @@ const code = ` console.log('Server running at http://127.0.0.1:8124/'); })`; -vm.runInThisContext(code)(require); +runInThisContext(code)(require); +``` + +```cjs +const { runInThisContext } = require('node:vm'); + +const code = ` +((require) => { + const { createServer } = require('node:http'); + + createServer((request, response) => { + response.writeHead(200, { 'Content-Type': 'text/plain' }); + response.end('Hello World\\n'); + }).listen(8124); + + console.log('Server running at http://127.0.0.1:8124/'); +})`; + +runInThisContext(code)(require); ``` The `require()` in the above case shares the state with the context it is @@ -1855,19 +2059,34 @@ The contextifying would introduce some quirks to the `globalThis` value in the c For example, it cannot be frozen, and it is not reference equal to the `contextObject` in the outer context. -```js -const vm = require('node:vm'); +```mjs +import { createContext, runInContext } from 'node:vm'; // An undefined `contextObject` option makes the global object contextified. -const context = vm.createContext(); -console.log(vm.runInContext('globalThis', context) === context); // false +const context = createContext(); +console.log(runInContext('globalThis', context) === context); // false // A contextified global object cannot be frozen. try { - vm.runInContext('Object.freeze(globalThis);', context); + runInContext('Object.freeze(globalThis);', context); } catch (e) { - console.log(e); // TypeError: Cannot freeze + console.log(`${e.constructor.name}: ${e.message}`); // TypeError: Cannot freeze } -console.log(vm.runInContext('globalThis.foo = 1; foo;', context)); // 1 +console.log(runInContext('globalThis.foo = 1; foo;', context)); // 1 +``` + +```cjs +const { createContext, runInContext } = require('node:vm'); + +// An undefined `contextObject` option makes the global object contextified. +const context = createContext(); +console.log(runInContext('globalThis', context) === context); // false +// A contextified global object cannot be frozen. +try { + runInContext('Object.freeze(globalThis);', context); +} catch (e) { + console.log(`${e.constructor.name}: ${e.message}`); // TypeError: Cannot freeze +} +console.log(runInContext('globalThis.foo = 1; foo;', context)); // 1 ``` To create a context with an ordinary global object and get access to a global proxy in @@ -1881,16 +2100,29 @@ a context without wrapping its global object with another object in a Node.js-sp As a result, the `globalThis` value inside the new context would behave more closely to an ordinary one. -```js -const vm = require('node:vm'); +```mjs +import { createContext, runInContext, constants } from 'node:vm'; + +// Use vm.constants.DONT_CONTEXTIFY to freeze the global object. +const context = createContext(constants.DONT_CONTEXTIFY); +runInContext('Object.freeze(globalThis);', context); +try { + runInContext('bar = 1; bar;', context); +} catch (e) { + console.log(`${e.constructor.name}: ${e.message}`); // ReferenceError: bar is not defined +} +``` + +```cjs +const { createContext, runInContext, constants } = require('node:vm'); // Use vm.constants.DONT_CONTEXTIFY to freeze the global object. -const context = vm.createContext(vm.constants.DONT_CONTEXTIFY); -vm.runInContext('Object.freeze(globalThis);', context); +const context = createContext(constants.DONT_CONTEXTIFY); +runInContext('Object.freeze(globalThis);', context); try { - vm.runInContext('bar = 1; bar;', context); + runInContext('bar = 1; bar;', context); } catch (e) { - console.log(e); // Uncaught ReferenceError: bar is not defined + console.log(`${e.constructor.name}: ${e.message}`); // ReferenceError: bar is not defined } ``` @@ -1899,27 +2131,51 @@ the returned object is a proxy-like object to the global object in the newly cre fewer Node.js-specific quirks. It is reference equal to the `globalThis` value in the new context, can be modified from outside the context, and can be used to access built-ins in the new context directly. -```js -const vm = require('node:vm'); +```mjs +import { createContext, runInContext, constants } from 'node:vm'; + +const context = createContext(constants.DONT_CONTEXTIFY); + +// Returned object is reference equal to globalThis in the new context. +console.log(runInContext('globalThis', context) === context); // true + +// Can be used to access globals in the new context directly. +console.log(context.Array); // [Function: Array] +runInContext('foo = 1;', context); +console.log(context.foo); // 1 +context.bar = 1; +console.log(runInContext('bar;', context)); // 1 + +// Can be frozen and it affects the inner context. +Object.freeze(context); +try { + runInContext('baz = 1; baz;', context); +} catch (e) { + console.log(`${e.constructor.name}: ${e.message}`); // ReferenceError: baz is not defined +} +``` -const context = vm.createContext(vm.constants.DONT_CONTEXTIFY); +```cjs +const { createContext, runInContext, constants } = require('node:vm'); + +const context = createContext(constants.DONT_CONTEXTIFY); // Returned object is reference equal to globalThis in the new context. -console.log(vm.runInContext('globalThis', context) === context); // true +console.log(runInContext('globalThis', context) === context); // true // Can be used to access globals in the new context directly. console.log(context.Array); // [Function: Array] -vm.runInContext('foo = 1;', context); +runInContext('foo = 1;', context); console.log(context.foo); // 1 context.bar = 1; -console.log(vm.runInContext('bar;', context)); // 1 +console.log(runInContext('bar;', context)); // 1 // Can be frozen and it affects the inner context. Object.freeze(context); try { - vm.runInContext('baz = 1; baz;', context); + runInContext('baz = 1; baz;', context); } catch (e) { - console.log(e); // Uncaught ReferenceError: baz is not defined + console.log(`${e.constructor.name}: ${e.message}`); // ReferenceError: baz is not defined } ``` @@ -1935,34 +2191,65 @@ For example, the following code executed by `vm.runInNewContext()` with a timeout of 5 milliseconds schedules an infinite loop to run after a promise resolves. The scheduled loop is never interrupted by the timeout: -```js -const vm = require('node:vm'); +```mjs +import { runInNewContext } from 'node:vm'; + +function loop() { + console.log('entering loop'); + while (1) console.log(Date.now()); +} + +runInNewContext( + 'Promise.resolve().then(() => loop());', + { loop, console }, + { timeout: 5 }, +); +// This is printed *before* 'entering infinite loop' (!) +console.log('done executing'); +``` + +```cjs +const { runInNewContext } = require('node:vm'); function loop() { console.log('entering loop'); while (1) console.log(Date.now()); } -vm.runInNewContext( +runInNewContext( 'Promise.resolve().then(() => loop());', { loop, console }, { timeout: 5 }, ); -// This is printed *before* 'entering loop' (!) +// This is printed *before* 'entering infinite loop' (!) console.log('done executing'); ``` This can be addressed by passing `microtaskMode: 'afterEvaluate'` to the code that creates the `Context`: -```js -const vm = require('node:vm'); +```mjs +import { runInNewContext } from 'node:vm'; + +function loop() { + while (1) console.log(Date.now()); +} + +runInNewContext( + 'Promise.resolve().then(() => loop());', + { loop, console }, + { timeout: 5, microtaskMode: 'afterEvaluate' }, +); +``` + +```cjs +const { runInNewContext } = require('node:vm'); function loop() { while (1) console.log(Date.now()); } -vm.runInNewContext( +runInNewContext( 'Promise.resolve().then(() => loop());', { loop, console }, { timeout: 5, microtaskMode: 'afterEvaluate' }, @@ -1999,15 +2286,12 @@ flow of the outer context is disrupted in a surprising way: the log statement is never executed. ```mjs -import * as vm from 'node:vm'; +import { createContext, runInContext } from 'node:vm'; -const inner_context = vm.createContext({}, { microtaskMode: 'afterEvaluate' }); +const inner_context = createContext({}, { microtaskMode: 'afterEvaluate' }); // runInContext() returns a Promise created in the inner context. -const inner_promise = vm.runInContext( - 'Promise.resolve()', - context, -); +const inner_promise = runInContext('Promise.resolve()', inner_context); // As part of performing `await`, the JavaScript runtime must enqueue a task // on the microtask queue of the context where `inner_promise` was created. @@ -2021,6 +2305,28 @@ await inner_promise; console.log('this will NOT be printed'); ``` +```cjs +const { createContext, runInContext } = require('node:vm'); + +// runInContext() returns a Promise created in the inner context. +const inner_context = createContext({}, { microtaskMode: 'afterEvaluate' }); + +(async () => { + const inner_promise = runInContext('Promise.resolve()', inner_context); + + // As part of performing `await`, the JavaScript runtime must enqueue a task + // on the microtask queue of the context where `inner_promise` was created. + // A task is added on the inner microtask queue, but **it will not be run + // automatically**: this task will remain pending indefinitely. + // + // Since the outer microtask queue is empty, execution in the outer module + // falls through, and the log statement below is never executed. + await inner_promise; + + console.log('this will NOT be printed'); +})(); +``` + To successfully share promises between contexts with different microtask queues, it is necessary to ensure that tasks on the inner microtask queue will be run **whenever** the outer context enqueues a task on the inner microtask queue.