diff --git a/src/node/internal/process.ts b/src/node/internal/process.ts index 76970ba788d..2c830e56508 100644 --- a/src/node/internal/process.ts +++ b/src/node/internal/process.ts @@ -16,6 +16,8 @@ import { ERR_INVALID_ARG_VALUE, } from 'node-internal:internal_errors' +import { default as utilImpl } from 'node-internal:util'; + export function nextTick(cb: Function, ...args: unknown[]) { queueMicrotask(() => { cb(...args); }); }; @@ -62,7 +64,12 @@ export const env = new Proxy({}, { } }); +export function getBuiltinModule(id: string) : any { + return utilImpl.getBuiltinModule(id); +} + export default { nextTick, env, + getBuiltinModule, }; diff --git a/src/node/internal/util.d.ts b/src/node/internal/util.d.ts index b137105a375..97221b4fbe6 100644 --- a/src/node/internal/util.d.ts +++ b/src/node/internal/util.d.ts @@ -107,3 +107,5 @@ export function isWeakMap(value: unknown): value is WeakMap; export function isWeakSet(value: unknown): value is WeakSet; export function isAnyArrayBuffer(value: unknown): value is ArrayBuffer | SharedArrayBuffer; export function isBoxedPrimitive(value: unknown): value is Number | String | Boolean | BigInt | Symbol; + +export function getBuiltinModule(id: string): any; diff --git a/src/workerd/api/global-scope.c++ b/src/workerd/api/global-scope.c++ index c3716dcb12a..6497c723766 100644 --- a/src/workerd/api/global-scope.c++ +++ b/src/workerd/api/global-scope.c++ @@ -777,6 +777,56 @@ void ServiceWorkerGlobalScope::reportError(jsg::Lock& js, jsg::JsValue error) { } } +namespace { +jsg::JsValue resolveFromRegistry(jsg::Lock& js, kj::StringPtr specifier) { + auto moduleRegistry = jsg::ModuleRegistry::from(js); + if (moduleRegistry == nullptr) { + // TODO: Return the known built-in instead? This gets a bit tricky as we currently + // have no mechanism defined for accessing and caching the built-in module without + // the module registry. Should we even support this case at all? Without the module + // registry the user can't access the other importable modules anyway. For now, just + // returning undefined in this case seems the most appropriate. + return js.undefined(); + } + + auto spec = kj::Path::parse(specifier); + auto& info = JSG_REQUIRE_NONNULL(moduleRegistry->resolve(js, spec), Error, + kj::str("No such module: ", specifier)); + auto module = info.module.getHandle(js); + jsg::instantiateModule(js, module); + + auto handle = jsg::check(module->Evaluate(js.v8Context())); + KJ_ASSERT(handle->IsPromise()); + auto prom = handle.As(); + KJ_ASSERT(prom->State() != v8::Promise::PromiseState::kPending); + if (module->GetStatus() == v8::Module::kErrored) { + jsg::throwTunneledException(js.v8Isolate, module->GetException()); + } + return jsg::JsValue(js.v8Get(module->GetModuleNamespace().As(), "default"_kj)); +} +} // namespace + +jsg::JsValue ServiceWorkerGlobalScope::getBuffer(jsg::Lock& js) { + KJ_ASSERT(FeatureFlags::get(js).getNodeJsCompatV2()); + auto value = resolveFromRegistry(js, "node:buffer"_kj); + auto obj = JSG_REQUIRE_NONNULL(value.tryCast(), TypeError, + "Invalid node:buffer implementation"); + // Unlike the getProcess() case below, this getter is returning an object that + // is exported by the node:buffer module and not the module itself, so we need + // this additional get to the grab the reference to the thing we're actually + // returning. + auto buffer = obj.get(js, "Buffer"_kj); + JSG_REQUIRE(buffer.isFunction(), TypeError, "Invalid node:buffer implementation"); + return buffer; +} + +jsg::JsValue ServiceWorkerGlobalScope::getProcess(jsg::Lock& js) { + KJ_ASSERT(FeatureFlags::get(js).getNodeJsCompatV2()); + auto value = resolveFromRegistry(js, "node:process"_kj); + JSG_REQUIRE(value.isObject(), TypeError, "Invalid node:process implementation"); + return value; +} + double Performance::now() { // We define performance.now() for compatibility purposes, but due to spectre concerns it // returns exactly what Date.now() returns. diff --git a/src/workerd/api/global-scope.h b/src/workerd/api/global-scope.h index 9fba69f433b..252d81a149d 100644 --- a/src/workerd/api/global-scope.h +++ b/src/workerd/api/global-scope.h @@ -454,6 +454,12 @@ class ServiceWorkerGlobalScope: public WorkerGlobalScope { void reportError(jsg::Lock& js, jsg::JsValue error); + // When the nodejs_compat_v2 compatibility flag is enabled, we expose the Node.js + // compat Buffer and process at the global scope in all modules as lazy instance + // properties. + jsg::JsValue getBuffer(jsg::Lock& js); + jsg::JsValue getProcess(jsg::Lock& js); + JSG_RESOURCE_TYPE(ServiceWorkerGlobalScope, CompatibilityFlags::Reader flags) { JSG_INHERIT(WorkerGlobalScope); @@ -527,6 +533,12 @@ class ServiceWorkerGlobalScope: public WorkerGlobalScope { JSG_NESTED_TYPE(TransformStreamDefaultController); } + if (flags.getNodeJsCompatV2()) { + JSG_LAZY_INSTANCE_PROPERTY(Buffer, getBuffer); + JSG_LAZY_INSTANCE_PROPERTY(process, getProcess); + JSG_LAZY_INSTANCE_PROPERTY(global, getSelf); + } + JSG_NESTED_TYPE(CompressionStream); JSG_NESTED_TYPE(DecompressionStream); JSG_NESTED_TYPE(TextEncoderStream); diff --git a/src/workerd/api/node/node-compat-v2-test.js b/src/workerd/api/node/node-compat-v2-test.js new file mode 100644 index 00000000000..65b962f6c37 --- /dev/null +++ b/src/workerd/api/node/node-compat-v2-test.js @@ -0,0 +1,64 @@ +// Copyright (c) 2017-2022 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 +// + +// Imports of Node.js built-ins should work both with and without +// the 'node:' prefix. +import { default as assert } from 'node:assert'; +import { default as assert2 } from 'assert'; +const assert3 = (await import('node:assert')).default; +const assert4 = (await import('assert')).default; + +assert.strictEqual(assert, assert2); +assert.strictEqual(assert, assert3); +assert.strictEqual(assert, assert4); + +export const nodeJsExpectedGlobals = { + async test() { + // Expected Node.js globals Buffer, process, and global should be present. + const { Buffer } = await import('node:buffer'); + assert.strictEqual(Buffer, globalThis.Buffer); + + const { default: process } = await import('node:process'); + assert.strictEqual(process, globalThis.process); + + assert.strictEqual(global, globalThis); + } +}; + +export const nodeJsGetBuiltins = { + async test() { + // node:* modules in the worker bundle should override the built-in modules... + const { default: fs } = await import('node:fs'); + const { default: path } = await import('node:path'); + + await import ('node:path'); + + // But process.getBuiltinModule should always return the built-in module. + const builtInPath = process.getBuiltinModule('node:path'); + const builtInFs = process.getBuiltinModule('node:fs'); + + // These are from the worker bundle.... + assert.strictEqual(fs, 1); + assert.strictEqual(path, 2); + + // But these are from the built-ins... + // node:fs is not implemented currently so it should be undefined here. + assert.strictEqual(builtInFs, undefined); + + // node:path is implemented tho... + assert.notStrictEqual(path, builtInPath); + assert.strictEqual(typeof builtInPath, 'object'); + assert.strictEqual(typeof builtInPath.join, 'function'); + + // While process.getBuiltinModule(...) in Node.js only returns Node.js + // built-ins, our impl will return cloudflare: and workerd: built-ins + // also, for completeness. A key difference, however, is that for non-Node.js + // built-ins, the return value is the module namespace rather than the default + // export. + + const socket = await import('cloudflare:sockets'); + assert.strictEqual(process.getBuiltinModule('cloudflare:sockets'), socket); + } +}; diff --git a/src/workerd/api/node/node-compat-v2-test.wd-test b/src/workerd/api/node/node-compat-v2-test.wd-test new file mode 100644 index 00000000000..1ac7159d845 --- /dev/null +++ b/src/workerd/api/node/node-compat-v2-test.wd-test @@ -0,0 +1,17 @@ +using Workerd = import "/workerd/workerd.capnp"; + +const unitTests :Workerd.Config = ( + services = [ + ( name = "node-compat-v2-test", + worker = ( + modules = [ + (name = "worker", esModule = embed "node-compat-v2-test.js"), + (name = "node:fs", esModule = "export default 1"), + (name = "node:path", esModule = "export default 2"), + ], + compatibilityDate = "2024-05-01", + compatibilityFlags = ["nodejs_compat_v2", "experimental"] + ) + ), + ], +); diff --git a/src/workerd/api/node/node.h b/src/workerd/api/node/node.h index 2c7b9cb02e6..d24b800cc0a 100644 --- a/src/workerd/api/node/node.h +++ b/src/workerd/api/node/node.h @@ -60,16 +60,19 @@ void registerNodeJsCompatModules( #undef V #undef NODEJS_MODULES + bool nodeJsCompatEnabled = featureFlags.getNodeJsCompat() || + featureFlags.getNodeJsCompatV2(); + // If the `nodejs_compat` flag isn't enabled, only register internal modules. // We need these for `console.log()`ing when running `workerd` locally. kj::Maybe maybeFilter; - if (!featureFlags.getNodeJsCompat()) maybeFilter = jsg::ModuleType::INTERNAL; + if (!nodeJsCompatEnabled) maybeFilter = jsg::ModuleType::INTERNAL; registry.addBuiltinBundle(NODE_BUNDLE, maybeFilter); // If the `nodejs_compat` flag is off, but the `nodejs_als` flag is on, we // need to register the `node:async_hooks` module from the bundle. - if (!featureFlags.getNodeJsCompat() && featureFlags.getNodeJsAls()) { + if (!nodeJsCompatEnabled && featureFlags.getNodeJsAls()) { jsg::Bundle::Reader reader = NODE_BUNDLE; for (auto module : reader.getModules()) { auto specifier = module.getName(); diff --git a/src/workerd/api/node/util.c++ b/src/workerd/api/node/util.c++ index 029e7965598..e42ef018b8b 100644 --- a/src/workerd/api/node/util.c++ +++ b/src/workerd/api/node/util.c++ @@ -3,6 +3,7 @@ // https://opensource.org/licenses/Apache-2.0 #include "util.h" #include +#include namespace workerd::api::node { @@ -181,5 +182,46 @@ jsg::Name UtilModule::getResourceTypeInspect(jsg::Lock& js) { return js.newApiSymbol("kResourceTypeInspect"_kj); } +jsg::JsValue UtilModule::getBuiltinModule(jsg::Lock& js, kj::String specifier) { + auto rawSpecifier = kj::str(specifier); + bool isNode = false; + KJ_IF_SOME(spec, jsg::checkNodeSpecifier(specifier)) { + isNode = true; + specifier = kj::mv(spec); + } + + auto registry = jsg::ModuleRegistry::from(js); + if (registry == nullptr) return js.undefined(); + auto path = kj::Path::parse(specifier); + + KJ_IF_SOME(info, registry->resolve(js, path, kj::none, + jsg::ModuleRegistry::ResolveOption::BUILTIN_ONLY, + jsg::ModuleRegistry::ResolveMethod::IMPORT, + rawSpecifier.asPtr())) { + auto module = info.module.getHandle(js); + jsg::instantiateModule(js, module); + auto handle = jsg::check(module->Evaluate(js.v8Context())); + KJ_ASSERT(handle->IsPromise()); + auto prom = handle.As(); + KJ_ASSERT(prom->State() != v8::Promise::PromiseState::kPending); + if (module->GetStatus() == v8::Module::kErrored) { + jsg::throwTunneledException(js.v8Isolate, module->GetException()); + } + + // For Node.js modules, we want to grab the default export and return that. + // For other built-ins, we'll return the module namespace instead. Can be + // a bit confusing but it's a side effect of Node.js modules originally + // being commonjs and the official getBuiltinModule returning what is + // expected to be the default export, while the behavior of other built-ins + // is not really defined by Node.js' implementation. + if (isNode) { + return jsg::JsValue(js.v8Get(module->GetModuleNamespace().As(), "default"_kj)); + } else { + return jsg::JsValue(module->GetModuleNamespace()); + } + } + + return js.undefined(); +} } // namespace workerd::api::node diff --git a/src/workerd/api/node/util.h b/src/workerd/api/node/util.h index 527649b01e1..cd0e717faf5 100644 --- a/src/workerd/api/node/util.h +++ b/src/workerd/api/node/util.h @@ -196,6 +196,8 @@ class UtilModule final: public jsg::Object { bool isAnyArrayBuffer(jsg::JsValue value); bool isBoxedPrimitive(jsg::JsValue value); + jsg::JsValue getBuiltinModule(jsg::Lock& js, kj::String specifier); + JSG_RESOURCE_TYPE(UtilModule) { JSG_NESTED_TYPE(MIMEType); JSG_NESTED_TYPE(MIMEParams); @@ -220,6 +222,8 @@ class UtilModule final: public jsg::Object { #undef V JSG_METHOD(isAnyArrayBuffer); JSG_METHOD(isBoxedPrimitive); + + JSG_METHOD(getBuiltinModule); } }; diff --git a/src/workerd/io/compatibility-date.capnp b/src/workerd/io/compatibility-date.capnp index f8e568d766f..384b9547d60 100644 --- a/src/workerd/io/compatibility-date.capnp +++ b/src/workerd/io/compatibility-date.capnp @@ -441,4 +441,12 @@ struct CompatibilityFlags @0x8f8c1b68151b6cef { $compatDisableFlag("fetch_legacy_url") $compatEnableDate("2024-06-03"); # Ensures that WHATWG standard URL parsing is used in the fetch API implementation. + + nodeJsCompatV2 @50 :Bool + $compatEnableFlag("nodejs_compat_v2") + $compatDisableFlag("no_nodejs_compat_v2") + $experimental; + # Implies nodeJSCompat with the following additional modifications: + # * Node.js Compat built-ins may be imported/required with or without the node: prefix + # * Node.js Compat the globals Buffer and process are available everywhere } diff --git a/src/workerd/io/worker.c++ b/src/workerd/io/worker.c++ index b0e8abc256f..2afb024fb1d 100644 --- a/src/workerd/io/worker.c++ +++ b/src/workerd/io/worker.c++ @@ -1018,6 +1018,9 @@ Worker::Isolate::Isolate(kj::Own apiParam, lock->setCaptureThrowsAsRejections(features.getCaptureThrowsAsRejections()); lock->setCommonJsExportDefault(features.getExportCommonJsDefaultNamespace()); + if (features.getNodeJsCompatV2()) { + lock->setNodeJsCompatEnabled(); + } if (impl->inspector != kj::none || ::kj::_::Debug::shouldLog(::kj::LogSeverity::INFO)) { lock->setLoggerCallback([this](jsg::Lock& js, kj::StringPtr message) { diff --git a/src/workerd/jsg/jsg.c++ b/src/workerd/jsg/jsg.c++ index 2af22d3ab2a..dc581d0ca26 100644 --- a/src/workerd/jsg/jsg.c++ +++ b/src/workerd/jsg/jsg.c++ @@ -192,6 +192,10 @@ void Lock::setCaptureThrowsAsRejections(bool capture) { IsolateBase::from(v8Isolate).setCaptureThrowsAsRejections({}, capture); } +void Lock::setNodeJsCompatEnabled() { + IsolateBase::from(v8Isolate).setNodeJsCompatEnabled({}, true); +} + void Lock::setCommonJsExportDefault(bool exportDefault) { IsolateBase::from(v8Isolate).setCommonJsExportDefault({}, exportDefault); } diff --git a/src/workerd/jsg/jsg.h b/src/workerd/jsg/jsg.h index 623dd8e8e5f..60f530cef2c 100644 --- a/src/workerd/jsg/jsg.h +++ b/src/workerd/jsg/jsg.h @@ -2296,6 +2296,8 @@ class Lock { void setCaptureThrowsAsRejections(bool capture); void setCommonJsExportDefault(bool exportDefault); + void setNodeJsCompatEnabled(); + using Logger = void(Lock&, kj::StringPtr); void setLoggerCallback(kj::Function&& logger); diff --git a/src/workerd/jsg/modules.c++ b/src/workerd/jsg/modules.c++ index 71dc0c42ae2..ffd25b30b04 100644 --- a/src/workerd/jsg/modules.c++ +++ b/src/workerd/jsg/modules.c++ @@ -11,6 +11,35 @@ namespace workerd::jsg { namespace { +// This list must be kept in sync with the list of builtins from Node.js. +// It should be unlikely that anything is ever removed from this list, and +// adding items to it is considered a semver-major change in Node.js. +static const std::set NODEJS_BUILTINS { + "_http_agent", "_http_client", "_http_common", + "_http_incoming", "_http_outgoing", "_http_server", + "_stream_duplex", "_stream_passthrough", "_stream_readable", + "_stream_transform", "_stream_wrap", "_stream_writable", + "_tls_common", "_tls_wrap", "assert", + "assert/strict", "async_hooks", "buffer", + "child_process", "cluster", "console", + "constants", "crypto", "dgram", + "diagnostics_channel", "dns", "dns/promises", + "domain", "events", "fs", + "fs/promises", "http", "http2", + "https", "inspector", "inspector/promises", + "module", "net", "os", + "path", "path/posix", "path/win32", + "perf_hooks", "process", "punycode", + "querystring", "readline", "readline/promises", + "repl", "stream", "stream/consumers", + "stream/promises", "stream/web", "string_decoder", + "sys", "timers", "timers/promises", + "tls", "trace_events", "tty", + "url", "util", "util/types", + "v8", "vm", "worker_threads", + "zlib" +}; + // The CompileCache is used to hold cached compilation data for built-in JavaScript modules. // // Importantly, this is a process-lifetime in-memory cache that is only appropriate for @@ -59,6 +88,12 @@ v8::MaybeLocal resolveCallback(v8::Local context, auto spec = kj::str(specifier); + if (isNodeJsCompatEnabled(js)) { + KJ_IF_SOME(nodeSpec, checkNodeSpecifier(spec)) { + spec = kj::mv(nodeSpec); + } + } + // If the referrer module is a built-in, it is only permitted to resolve // internal modules. If the worker bundle provided an override for a builtin, // then internalOnly will be false. @@ -247,6 +282,19 @@ v8::MaybeLocal evaluateSyntheticModuleCallback( } // namespace +kj::Maybe checkNodeSpecifier(kj::StringPtr specifier) { + if (NODEJS_BUILTINS.contains(specifier)) { + return kj::str("node:", specifier); + } else if (specifier.startsWith("node:")) { + return kj::str(specifier); + } + return kj::none; +} + +bool isNodeJsCompatEnabled(jsg::Lock& js) { + return IsolateBase::from(js.v8Isolate).isNodeJsCompatEnabled(); +} + ModuleRegistry* getModulesForResolveCallback(v8::Isolate* isolate) { return static_cast( isolate->GetCurrentContext()->GetAlignedPointerFromEmbedderData(2)); @@ -256,6 +304,12 @@ v8::Local CommonJsModuleContext::require(jsg::Lock& js, kj::String sp auto modulesForResolveCallback = getModulesForResolveCallback(js.v8Isolate); KJ_REQUIRE(modulesForResolveCallback != nullptr, "didn't expect resolveCallback() now"); + if (isNodeJsCompatEnabled(js)) { + KJ_IF_SOME(nodeSpec, checkNodeSpecifier(specifier)) { + specifier = kj::mv(nodeSpec); + } + } + kj::Path targetPath = ([&] { // If the specifier begins with one of our known prefixes, let's not resolve // it against the referrer. @@ -559,44 +613,12 @@ NodeJsModuleContext::NodeJsModuleContext(jsg::Lock& js, kj::Path path) exports(js.v8Ref(module->getExports(js))) {} v8::Local NodeJsModuleContext::require(jsg::Lock& js, kj::String specifier) { - // This list must be kept in sync with the list of builtins from Node.js. - // It should be unlikely that anything is ever removed from this list, and - // adding items to it is considered a semver-major change in Node.js. - static const std::set NODEJS_BUILTINS { - "_http_agent", "_http_client", "_http_common", - "_http_incoming", "_http_outgoing", "_http_server", - "_stream_duplex", "_stream_passthrough", "_stream_readable", - "_stream_transform", "_stream_wrap", "_stream_writable", - "_tls_common", "_tls_wrap", "assert", - "assert/strict", "async_hooks", "buffer", - "child_process", "cluster", "console", - "constants", "crypto", "dgram", - "diagnostics_channel", "dns", "dns/promises", - "domain", "events", "fs", - "fs/promises", "http", "http2", - "https", "inspector", "inspector/promises", - "module", "net", "os", - "path", "path/posix", "path/win32", - "perf_hooks", "process", "punycode", - "querystring", "readline", "readline/promises", - "repl", "stream", "stream/consumers", - "stream/promises", "stream/web", "string_decoder", - "sys", "timers", "timers/promises", - "tls", "trace_events", "tty", - "url", "util", "util/types", - "v8", "vm", "worker_threads", - "zlib" - }; - // If it is a bare specifier known to be a Node.js built-in, then prefix the // specifier with node: bool isNodeBuiltin = false; auto resolveOption = jsg::ModuleRegistry::ResolveOption::DEFAULT; - if (NODEJS_BUILTINS.contains(specifier)) { - specifier = kj::str("node:", specifier); - isNodeBuiltin = true; - resolveOption = jsg::ModuleRegistry::ResolveOption::BUILTIN_ONLY; - } else if (specifier.startsWith("node:")) { + KJ_IF_SOME(spec, checkNodeSpecifier(specifier)) { + specifier = kj::mv(spec); isNodeBuiltin = true; resolveOption = jsg::ModuleRegistry::ResolveOption::BUILTIN_ONLY; } diff --git a/src/workerd/jsg/modules.h b/src/workerd/jsg/modules.h index f037d33d972..2584566723e 100644 --- a/src/workerd/jsg/modules.h +++ b/src/workerd/jsg/modules.h @@ -14,6 +14,9 @@ namespace workerd::jsg { +kj::Maybe checkNodeSpecifier(kj::StringPtr specifier); +bool isNodeJsCompatEnabled(jsg::Lock& js); + class CommonJsModuleContext; class CommonJsModuleObject: public jsg::Object { @@ -441,12 +444,8 @@ class ModuleRegistryImpl final: public ModuleRegistry { void addBuiltinModule(Module::Reader module) { if (module.which() != Module::SRC) { - using Key = typename Entry::Key; auto specifier = module.getName(); auto path = kj::Path::parse(specifier); - if (module.getType() == Type::BUILTIN && entries.find(Key(path, Type::BUNDLE)) != kj::none) { - return; - } switch (module.which()) { case Module::WASM: // The body of this callback is copied from `compileWasmGlobal` in @@ -516,31 +515,15 @@ class ModuleRegistryImpl final: public ModuleRegistry { kj::ArrayPtr sourceCode, Type type = Type::BUILTIN) { KJ_ASSERT(type != Type::BUNDLE); - using Key = typename Entry::Key; - - // We need to make sure there is not an existing worker bundle module with the same - // name if type == Type::BUILTIN auto path = kj::Path::parse(specifier); - if (type == Type::BUILTIN && entries.find(Key(path, Type::BUNDLE)) != kj::none) { - return; - } - entries.insert(kj::heap(path, type, sourceCode)); } void addBuiltinModule(kj::StringPtr specifier, ModuleCallback factory, Type type = Type::BUILTIN) { - using Key = typename Entry::Key; - + KJ_ASSERT(type != Type::BUNDLE); auto path = kj::Path::parse(specifier); - - // We need to make sure there is not an existing worker bundle module with the same - // name if type == Type::BUILTIN - if (type == Type::BUILTIN && entries.find(Key(path, Type::BUNDLE)) != kj::none) { - return; - } - entries.insert(kj::heap(path, type, kj::mv(factory))); } @@ -567,6 +550,10 @@ class ModuleRegistryImpl final: public ModuleRegistry { return entry->module(js, observer, referrer, method); } return kj::none; + } else if (option == ResolveOption::BUILTIN_ONLY) { + KJ_IF_SOME(entry, entries.find(Key(specifier, Type::BUILTIN))) { + return entry->module(js, observer, referrer, method); + } } else { if (option == ResolveOption::DEFAULT) { // First, we try to resolve a worker bundle version of the module. @@ -832,6 +819,12 @@ v8::MaybeLocal dynamicImportCallback(v8::Local context })(); auto spec = kj::str(specifier); + if (isNodeJsCompatEnabled(js)) { + KJ_IF_SOME(nodeSpec, checkNodeSpecifier(spec)) { + spec = kj::mv(nodeSpec); + } + } + auto maybeSpecifierPath = ([&]() -> kj::Maybe { // If the specifier begins with one of our known prefixes, let's not resolve // it against the referrer. diff --git a/src/workerd/jsg/setup.h b/src/workerd/jsg/setup.h index 96e6c598723..ffc577a4240 100644 --- a/src/workerd/jsg/setup.h +++ b/src/workerd/jsg/setup.h @@ -124,9 +124,15 @@ class IsolateBase { exportCommonJsDefault = exportDefault; } + inline void setNodeJsCompatEnabled(kj::Badge, bool enabled) { + nodeJsCompatEnabled = enabled; + } + inline bool areWarningsLogged() const { return maybeLogger != kj::none; } inline bool areErrorsReported() const { return maybeErrorReporter != kj::none; } + inline bool isNodeJsCompatEnabled() const { return nodeJsCompatEnabled; } + // The logger will be optionally set by the isolate setup logic if there is anywhere // for the log to go (for instance, if debug logging is enabled or the inspector is // being used). @@ -201,6 +207,7 @@ class IsolateBase { bool captureThrowsAsRejections = false; bool exportCommonJsDefault = false; bool asyncContextTrackingEnabled = false; + bool nodeJsCompatEnabled = false; kj::Maybe> maybeLogger; kj::Maybe> maybeErrorReporter;