diff --git a/LICENSE b/LICENSE
index b2f25c4e4b8974..2bd3fc96d95a63 100644
--- a/LICENSE
+++ b/LICENSE
@@ -2186,3 +2186,28 @@ The externally maintained libraries used by Node.js are:
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""
+
+- synchronous-worker, located at lib/internal/vm/node_realm.js, is licensed as follows:
+ """
+ The MIT License (MIT)
+
+ Copyright (c) 2020 Anna Henningsen
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+ """
diff --git a/doc/api/cli.md b/doc/api/cli.md
index 8e26ae16ae6233..de1f3401a5db3d 100644
--- a/doc/api/cli.md
+++ b/doc/api/cli.md
@@ -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-node-realm`
+
+
+
+Enable experimental support for `vm.NodeRealm`.
+
+### `--no-experimental-node-realm`
+
+
+
+Disable experimental support for `vm.NodeRealm`.
+
### `--experimental-network-imports`
+
+The `createImport()` function was passed a value that was neither
+a string nor a `URL`.
+
### `ERR_WORKER_UNSUPPORTED_EXTENSION`
diff --git a/doc/api/vm.md b/doc/api/vm.md
index 1b8bb71715250c..b440bd946014a0 100644
--- a/doc/api/vm.md
+++ b/doc/api/vm.md
@@ -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.
+
+
+
+* 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()`
+
+
+
+#### `nodeRealm.stop()`
+
+
+
+* Returns:
+
+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)`
+
+
+
+* `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`
+
+
+
+* Type: {Object}
+
+Returns a reference to the global object of the inner Node.js instance.
+
+#### `nodeRealm.process`
+
+
+
+* 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
diff --git a/doc/node.1 b/doc/node.1
index 90d0e42e90deb4..face062ee14301 100644
--- a/doc/node.1
+++ b/doc/node.1
@@ -166,6 +166,9 @@ to use as a custom module loader.
.It Fl -experimental-network-imports
Enable experimental support for loading modules using `import` over `https:`.
.
+.It Fl -experimental-node-realm
+Enable experimental support for vm.NodeRealm.
+.
.It Fl -experimental-permission
Enable the experimental permission model.
.
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index 18f4a2c42c39b1..e12d6fc2e1382b 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -1706,6 +1706,7 @@ E('ERR_VM_MODULE_LINK_FAILURE', function(message, cause) {
E('ERR_VM_MODULE_NOT_MODULE',
'Provided module is not an instance of Module', Error);
E('ERR_VM_MODULE_STATUS', 'Module status %s', Error);
+E('ERR_VM_NODE_REALM_INVALID_PARENT', 'createImport() must be called with a string or URL; received "%s"', Error);
E('ERR_WASI_ALREADY_STARTED', 'WASI instance has already started', Error);
E('ERR_WEBASSEMBLY_RESPONSE', 'WebAssembly response %s', TypeError);
E('ERR_WORKER_INIT_FAILED', 'Worker initialization failure: %s', Error);
diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js
index fc6d9ccf4f517b..61fe05490693de 100644
--- a/lib/internal/process/pre_execution.js
+++ b/lib/internal/process/pre_execution.js
@@ -67,6 +67,7 @@ function prepareExecution(options) {
setupInspectorHooks();
setupWarningHandler();
setupFetch();
+ setupNodeRealm();
setupWebCrypto();
setupCustomEvent();
setupCodeCoverage();
@@ -267,6 +268,16 @@ function setupFetch() {
});
}
+function setupNodeRealm() {
+ // Patch the vm module when --experimental-node-realm is on.
+ // Please update the comments in vm.js when this block changes.
+ if (getOptionValue('--experimental-node-realm')) {
+ const NodeRealm = require('internal/vm/node_realm');
+ const vm = require('vm');
+ vm.NodeRealm = NodeRealm;
+ }
+}
+
// TODO(aduh95): move this to internal/bootstrap/web/* when the CLI flag is
// removed.
function setupWebCrypto() {
diff --git a/lib/internal/vm/node_realm.js b/lib/internal/vm/node_realm.js
new file mode 100644
index 00000000000000..0fee2234e4af40
--- /dev/null
+++ b/lib/internal/vm/node_realm.js
@@ -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}
+ */
+ 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;
diff --git a/lib/vm.js b/lib/vm.js
index 1fdea8433495b2..798603ab1daee7 100644
--- a/lib/vm.js
+++ b/lib/vm.js
@@ -343,3 +343,5 @@ module.exports = {
// The vm module is patched to include vm.Module, vm.SourceTextModule
// and vm.SyntheticModule in the pre-execution phase when
// --experimental-vm-modules is on.
+// The vm module is also patched to include vm.NodeRealm in the
+// pre-execution phase when --experimental-node-realm is on.
diff --git a/src/env.cc b/src/env.cc
index ff8d3952cf20a3..6e0429b86cc6c5 100644
--- a/src/env.cc
+++ b/src/env.cc
@@ -1019,21 +1019,40 @@ void Environment::CleanupHandles() {
RunAndClearNativeImmediates(true /* skip unrefed SetImmediate()s */);
- for (ReqWrapBase* request : req_wrap_queue_)
+ CleanupHandlesNoUvRun();
+
+ while (handle_cleanup_waiting_ != 0 || request_waiting_ != 0 ||
+ !handle_wrap_queue_.IsEmpty()) {
+ uv_run(event_loop(), UV_RUN_ONCE);
+ }
+}
+
+int Environment::CleanupHandlesNoUvRun() {
+ {
+ Mutex::ScopedLock lock(native_immediates_threadsafe_mutex_);
+ task_queues_async_initialized_ = false;
+ }
+
+ int count = 0;
+
+ for (ReqWrapBase* request : req_wrap_queue_) {
+ count++;
request->Cancel();
+ }
- for (HandleWrap* handle : handle_wrap_queue_)
+ for (HandleWrap* handle : handle_wrap_queue_) {
+ count++;
handle->Close();
+ }
- for (HandleCleanup& hc : handle_cleanup_queue_)
+ for (HandleCleanup& hc : handle_cleanup_queue_) {
+ count++;
hc.cb_(this, hc.handle_, hc.arg_);
+ }
+
handle_cleanup_queue_.clear();
- while (handle_cleanup_waiting_ != 0 ||
- request_waiting_ != 0 ||
- !handle_wrap_queue_.IsEmpty()) {
- uv_run(event_loop(), UV_RUN_ONCE);
- }
+ return count;
}
void Environment::StartProfilerIdleNotifier() {
diff --git a/src/env.h b/src/env.h
index 5359436be31e76..413b831752c4b2 100644
--- a/src/env.h
+++ b/src/env.h
@@ -627,6 +627,7 @@ class Environment : public MemoryRetainer {
void RegisterHandleCleanups();
void CleanupHandles();
+ int CleanupHandlesNoUvRun();
void Exit(ExitCode code);
void ExitEnv(StopFlags::Flags flags);
@@ -857,6 +858,9 @@ class Environment : public MemoryRetainer {
inline HandleWrapQueue* handle_wrap_queue() { return &handle_wrap_queue_; }
inline ReqWrapQueue* req_wrap_queue() { return &req_wrap_queue_; }
+ inline int handle_cleanup_waiting() const {
+ return handle_cleanup_waiting_;
+ }
// https://w3c.github.io/hr-time/#dfn-time-origin
inline uint64_t time_origin() {
diff --git a/src/env_properties.h b/src/env_properties.h
index 320d08d22577c8..36c64d9b46e4a2 100644
--- a/src/env_properties.h
+++ b/src/env_properties.h
@@ -366,6 +366,7 @@
V(socketaddress_constructor_template, v8::FunctionTemplate) \
V(streambaseentry_ctor_template, v8::FunctionTemplate) \
V(streambaseoutputstream_constructor_template, v8::ObjectTemplate) \
+ V(node_realm_constructor_template, v8::FunctionTemplate) \
V(streamentry_ctor_template, v8::FunctionTemplate) \
V(streamentry_opaque_ctor_template, v8::FunctionTemplate) \
V(qlogoutputstream_constructor_template, v8::ObjectTemplate) \
diff --git a/src/node_contextify.cc b/src/node_contextify.cc
index 7f9f71ba74223a..0a612c7fc73ab8 100644
--- a/src/node_contextify.cc
+++ b/src/node_contextify.cc
@@ -21,6 +21,7 @@
#include "node_contextify.h"
+#include "async_wrap-inl.h"
#include "base_object-inl.h"
#include "memory_tracker-inl.h"
#include "module_wrap.h"
@@ -41,13 +42,16 @@ using v8::Array;
using v8::ArrayBufferView;
using v8::Boolean;
using v8::Context;
+using v8::DeserializeInternalFieldsCallback;
using v8::EscapableHandleScope;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
+using v8::Global;
using v8::HandleScope;
using v8::IndexedPropertyHandlerConfiguration;
using v8::Int32;
+using v8::Integer;
using v8::Isolate;
using v8::Just;
using v8::Local;
@@ -1391,6 +1395,260 @@ void MicrotaskQueueWrap::RegisterExternalReferences(
registry->Register(New);
}
+Local NodeRealm::GetConstructorTemplate(
+ IsolateData* isolate_data) {
+ Local tmpl =
+ isolate_data->node_realm_constructor_template();
+ if (tmpl.IsEmpty()) {
+ Isolate* isolate = isolate_data->isolate();
+ tmpl = NewFunctionTemplate(isolate, New);
+ tmpl->Inherit(AsyncWrap::GetConstructorTemplate(isolate_data));
+ tmpl->InstanceTemplate()->SetInternalFieldCount(1);
+
+ SetProtoMethod(isolate, tmpl, "start", Start);
+ SetProtoMethod(isolate, tmpl, "load", Load);
+ SetProtoMethod(isolate, tmpl, "stop", Stop);
+ SetProtoMethod(isolate, tmpl, "signalStop", SignalStop);
+ SetProtoMethod(isolate, tmpl, "tryCloseAllHandles", TryCloseAllHandles);
+ SetProtoMethod(isolate, tmpl, "internalRequire", InternalRequire);
+
+ isolate_data->set_node_realm_constructor_template(tmpl);
+ }
+ return tmpl;
+}
+
+void NodeRealm::CreatePerIsolateProperties(
+ IsolateData* isolate_data, v8::Local target) {
+ SetConstructorFunction(isolate_data->isolate(),
+ target,
+ "NodeRealm",
+ GetConstructorTemplate(isolate_data),
+ SetConstructorFunctionFlag::NONE);
+}
+
+void NodeRealm::RegisterExternalReferences(
+ ExternalReferenceRegistry* registry) {
+ registry->Register(New);
+ registry->Register(Start);
+ registry->Register(Load);
+ registry->Register(SignalStop);
+ registry->Register(Stop);
+ registry->Register(TryCloseAllHandles);
+ registry->Register(InternalRequire);
+}
+
+NodeRealm::NodeRealmScope::NodeRealmScope(NodeRealm* w)
+ : EscapableHandleScope(w->isolate_),
+ Scope(w->context()),
+ Isolate::SafeForTerminationScope(w->isolate_),
+ w_(w),
+ orig_can_be_terminated_(w->can_be_terminated_) {
+ w_->can_be_terminated_ = true;
+}
+
+NodeRealm::NodeRealmScope::~NodeRealmScope() {
+ w_->can_be_terminated_ = orig_can_be_terminated_;
+}
+
+Local NodeRealm::context() const {
+ return context_.Get(isolate_);
+}
+
+NodeRealm::NodeRealm(Environment* env, Local