Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 34.3k
vm: add experimental NodeRealm implementation#47855
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
5aebfdaa624c7eb04d52c156fad58ff9d0a1050cb21b5978b42117508929ecd7f56539db8bf7262e534bbb0a04a75618e61b8059f61848550ffaba0aa46778c3f432139e58b38474e518240d61f3714009f1d1a557409e6db0c89b3988738db395ac7ba9bbd4209518File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1573,6 +1573,84 @@ inside a `vm.Context`, functions passed to them will be added to global queues, | ||
| which are shared by all contexts. Therefore, callbacks passed to those functions | ||
| are not controllable through the timeout either. | ||
| ### Class: `NodeRealm` | ||
| > Stability: 1 - Experimental. Use `--experimental-node-realm` CLI flag to | ||
| > enable this feature. | ||
| <!-- YAML | ||
| added: REPLACEME | ||
| --> | ||
| * Extends:{EventEmitter} | ||
| A `NodeRealm` is effectively a Node.js environment that runs within the | ||
| same thread. It similar to a [ShadowRealm][], but with a few main differences: | ||
| * `NodeRealm` supports loading both CommonJS and ES modules. | ||
| * Full interoperability between the host realm and the `NodeRealm` instance | ||
| is allowed. | ||
| * There is a deliberate `stop()` function. | ||
| ```mjs | ||
| import{NodeRealm } from 'node:vm' | ||
| const nodeRealm = new NodeRealm(); | ||
| const{myAsyncFunction } = await nodeRealm.createImport(import.meta.url)('my-module'); | ||
| console.log(await myAsyncFunction()); | ||
| ``` | ||
| #### `new NodeRealm()` | ||
| <!-- YAML | ||
| added: REPLACEME | ||
| --> | ||
| #### `nodeRealm.stop()` | ||
| <!-- YAML | ||
| added: REPLACEME | ||
| --> | ||
mcollina marked this conversation as resolved. Show resolvedHide resolvedUh oh!There was an error while loading. Please reload this page. | ||
| * Returns: <Promise> | ||
| This will render the inner Node.js instance unusable. | ||
| and is generally comparable to running `process.exit()`. | ||
| This method returns a promise that will be resolved when all resources | ||
| associated with this Node.js instance are released. This promise resolves on | ||
| the event loop of the _outer_ Node.js instance. | ||
| #### `nodeRealm.createImport(specifier)` | ||
| <!-- YAML | ||
| added: REPLACEME | ||
| --> | ||
| * `specifier`{string} A module specifier like './file.js' or 'my-package' | ||
| Creates a function that can be used for loading | ||
| modules inside the inner Node.js instance. | ||
| #### `nodeRealm.globalThis` | ||
| <!-- YAML | ||
| added: REPLACEME | ||
| --> | ||
| * Type:{Object} | ||
| Returns a reference to the global object of the inner Node.js instance. | ||
Member There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It should be clarified whether this value is mutable. e.g. is it possible to | ||
| #### `nodeRealm.process` | ||
| <!-- YAML | ||
| added: REPLACEME | ||
| --> | ||
| * Type:{Object} | ||
| Returns a reference to the `process` object of the inner Node.js instance. | ||
| [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 | ||
| @@ -1599,3 +1677,4 @@ are not controllable through the timeout either. | ||
| [global object]: https://es5.github.io/#x15.1 | ||
| [indirect `eval()` call]: https://es5.github.io/#x10.4.2 | ||
| [origin]: https://developer.mozilla.org/en-US/docs/Glossary/Origin | ||
| [ShadowRealm]: https://github.com/tc39/proposal-shadowrealm | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| 'use strict' | ||
| // NodeRealm was originally a separate module developed by | ||
| // Anna Henningsen and published separately on npm as the | ||
| // synchronous-worker module under the MIT license. It has been | ||
| // incorporated into Node.js with Anna's permission. | ||
| // See the LICENSE file for LICENSE and copyright attribution. | ||
| const{ | ||
| Promise, | ||
| } = primordials; | ||
| const{ | ||
| emitExperimentalWarning, | ||
| } = require('internal/util'); | ||
| const{ | ||
| ERR_VM_NODE_REALM_INVALID_PARENT, | ||
| } = require('internal/errors').codes; | ||
| const{ | ||
| NodeRealm: NodeRealmImpl, | ||
| } = internalBinding('contextify'); | ||
| const{URL } = require('internal/url'); | ||
| const EventEmitter = require('events'); | ||
| const{setTimeout } = require('timers'); | ||
| const{pathToFileURL } = require('url'); | ||
| let debug = require('internal/util/debuglog').debuglog('noderealm', (fn) =>{ | ||
| debug = fn; | ||
| }); | ||
| class NodeRealm extends EventEmitter{ | ||
| #handle = undefined; | ||
| #process = undefined; | ||
| #global = undefined; | ||
| #stoppedPromise = undefined; | ||
| #loader = undefined; | ||
| constructor(){ | ||
| super(); | ||
| emitExperimentalWarning('NodeRealm'); | ||
| this.#handle = new NodeRealmImpl(); | ||
| this.#handle.onexit = (code) =>{ | ||
| this.stop(); | ||
| this.emit('exit', code); | ||
| }; | ||
| try{ | ||
| this.#handle.start(); | ||
| this.#handle.load((process, nativeRequire, globalThis) =>{ | ||
| this.#process = process; | ||
| this.#global = globalThis; | ||
| process.on('uncaughtException', (err) =>{ | ||
| if (process.listenerCount('uncaughtException') === 1){ | ||
| // If we are stopping, silence all errors | ||
| if (!this.#stoppedPromise){ | ||
| this.emit('error', err); | ||
| } | ||
| process.exit(1); | ||
| } | ||
| }); | ||
| }); | ||
| const req = this.#handle.internalRequire(); | ||
| this.#loader = req('internal/process/esm_loader').esmLoader; | ||
| } catch (err){ | ||
| this.#handle.stop(); | ||
| throw err; | ||
| } | ||
| } | ||
| /** | ||
| * @returns{Promise<void>} | ||
| */ | ||
| async stop(){ | ||
| // TODO(@mcollina): add support for AbortController, we want to abort this, | ||
| // or add a timeout. | ||
| return this.#stoppedPromise ??= new Promise((resolve) =>{ | ||
| const tryClosing = () =>{ | ||
| const closed = this.#handle.tryCloseAllHandles(); | ||
| debug('closed %d handles', closed); | ||
| if (closed > 0){ | ||
| // This is an active wait for the handles to close. | ||
| // We might want to change this in the future to use a callback, | ||
| // but at this point it seems like a premature optimization. | ||
| // We cannot unref() this because we need to shut this down properly. | ||
| // TODO(@mcollina): refactor to use a close callback | ||
| setTimeout(tryClosing, 100); | ||
| } else{ | ||
| this.#handle.stop(); | ||
| resolve(); | ||
| } | ||
| }; | ||
| // We use setTimeout instead of setImmediate because it runs in a different | ||
| // phase of the event loop. This is important because the immediate queue | ||
| // would crash if the environment it refers to has been already closed. | ||
| // We cannot unref() this because we need to shut this down properly. | ||
| setTimeout(tryClosing, 100); | ||
| }); | ||
| } | ||
| get process(){ | ||
| return this.#process; | ||
| } | ||
| get globalThis(){ | ||
| return this.#global; | ||
| } | ||
| /** | ||
| * @param{string|URL} parentURL | ||
| */ | ||
| createImport(parentURL){ | ||
| if (typeof parentURL === 'string'){ | ||
| if (parentURL.indexOf('file://') === 0){ | ||
| parentURL = new URL(parentURL); | ||
| } else{ | ||
| parentURL = pathToFileURL(parentURL); | ||
| } | ||
| } else if (!(parentURL instanceof URL)){ | ||
| throw new ERR_VM_NODE_REALM_INVALID_PARENT(parentURL); | ||
| } | ||
| return (specifiers, importAssertions) =>{ | ||
| return this.#loader.import(specifiers, parentURL, importAssertions ||{}); | ||
| }; | ||
| } | ||
| } | ||
| module.exports = NodeRealm; |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do think the docs should clarify the difference between this and a
ShadowRealm.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would also like to understand the differences (and similarities) between this and a worker. Because they look very similar. For example, does a realm have an event loop? Does it share globals? (I'm assuming yes and no?)