-
Notifications
You must be signed in to change notification settings - Fork 30.1k
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 16 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,77 @@ 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 'vm'; | ||||||
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
import { fileURLToPath } from 'url'; | ||||||
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
const w = new NodeRealm(); | ||||||
const myAsyncFunction = w.createRequire(fileURLToPath(import.meta.url))('my-module'); | ||||||
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
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
|
||||||
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 | ||||||
--> | ||||||
|
||||||
* `filename` {string} | ||||||
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
Create a dynamic `import()` function that can be used for loading EcmaScript | ||||||
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
modules inside 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. What is the signature of the returned function? Does it return a promise? Does it match the signature of |
||||||
|
||||||
#### `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,115 @@ | ||
'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. | ||
// TODO(@mcollina): refactor to use a close callback | ||
setTimeout(tryClosing, 100); | ||
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 choice of 100ms for the timeouts here and on line 89 seem kind of arbitrary.
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} 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. | ||
setTimeout(tryClosing, 100); | ||
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}); | ||
} | ||
|
||
get process() { | ||
return this.#process; | ||
} | ||
|
||
get globalThis() { | ||
return this.#global; | ||
} | ||
|
||
/** | ||
* @param {string} path | ||
*/ | ||
createImport(path) { | ||
const parentURL = pathToFileURL(path); | ||
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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.