-
Notifications
You must be signed in to change notification settings - Fork 29.8k
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
vm: add experimental NodeRealm implementation #47855
Changes from 25 commits
5aebfda
a624c7e
b04d52c
156fad5
8ff9d0a
1050cb2
1b5978b
4211750
8929ecd
7f56539
db8bf72
62e534b
bb0a04a
75618e6
1b8059f
6184855
0ffaba0
aa46778
c3f4321
39e58b3
8474e51
8240d61
f371400
9f1d1a5
57409e6
db0c89b
3988738
db395ac
7ba9bbd
4209518
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -530,6 +530,22 @@ changes: | |
Specify the `module` of a custom experimental [ECMAScript module loader][]. | ||
`module` may be any string accepted as an [`import` specifier][]. | ||
|
||
### `--experimental-noderealm` | ||
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
<!-- YAML | ||
added: REPLACEME | ||
--> | ||
|
||
Enable experimental support for `vm.NodeRealm`. | ||
|
||
### `--no-experimental-noderealm` | ||
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. We usually document one or the other, never both IIRC |
||
|
||
<!-- YAML | ||
added: REPLACEME | ||
--> | ||
|
||
Disable experimental support for `vm.NodeRealm`. | ||
|
||
### `--experimental-network-imports` | ||
|
||
<!-- YAML | ||
|
@@ -2113,6 +2129,7 @@ Node.js options that are allowed are: | |
* `--experimental-import-meta-resolve` | ||
* `--experimental-json-modules` | ||
* `--experimental-loader` | ||
* `--experimental-noderealm` | ||
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. I think 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. What update will be necessary? 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. The test is failing in this PR (you can see at the bottom of this view). Looking closer though, I think the issue is that this list needs to be in sort order, and this new flag is in the wrong position. It should go between |
||
* `--experimental-modules` | ||
* `--experimental-network-imports` | ||
* `--experimental-permission` | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -1573,6 +1573,78 @@ 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-noderealm` 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. | ||||||
|
||||||
```mjs | ||||||
import { NodeRealm } from 'node:vm'; | ||||||
const noderealm = new NodeRealm(); | ||||||
const myAsyncFunction = noderealm.createImport(import.meta.url)('my-module'); | ||||||
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. Could this be simplified to 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. I don't think retrieving a function caller's location reliably is possible. |
||||||
console.log(await myAsyncFunction()); | ||||||
``` | ||||||
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. I do think the docs should clarify the difference between this and a 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. 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?) |
||||||
|
||||||
#### `new NodeRealm()` | ||||||
|
||||||
<!-- YAML | ||||||
added: REPLACEME | ||||||
--> | ||||||
|
||||||
#### `noderealm.stop()` | ||||||
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.
Suggested change
And for all following. |
||||||
|
||||||
<!-- YAML | ||||||
added: REPLACEME | ||||||
--> | ||||||
|
||||||
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
* 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(filename)` | ||||||
|
||||||
<!-- YAML | ||||||
added: REPLACEME | ||||||
--> | ||||||
|
||||||
* `specifier` {string} A module specifier like './file.js' or 'my-package' | ||||||
|
||||||
Create a function that can be used for loading | ||||||
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
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. | ||||||
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 | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
'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 { | ||
NodeRealm: NodeRealmImpl, | ||
} = internalBinding('contextify'); | ||
|
||
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(); | ||
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
this.#handle = new NodeRealmImpl(); | ||
this.#handle.onexit = (code) => { | ||
this.stop(); | ||
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. Should we wait for the |
||
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 { | ||
|
||
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 Error('createImport() must be called with a string or URL'); | ||
} | ||
|
||
return (specifiers, importAssertions) => { | ||
return this.#loader.import(specifiers, parentURL, importAssertions || {}); | ||
}; | ||
} | ||
} | ||
|
||
module.exports = NodeRealm; |
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.
This needs to be updated to point to the correct location via the license builder.
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.
Could you point me at the docs for this?
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 don't know if there are docs for it, but I think you can change this line to
lib/internal/vm/localworker.js
where it currently sayslib/worker_threads.js
, rerun the license builder, and this should be updated.