diff --git a/samples/nodejs-compat-module/README.md b/samples/nodejs-compat-module/README.md new file mode 100644 index 000000000000..9ae540830ea3 --- /dev/null +++ b/samples/nodejs-compat-module/README.md @@ -0,0 +1,28 @@ +# Node.js Compat Example + +To run the example on http://localhost:8080 + +```sh +$ ./workerd serve config.capnp +``` + +To run using bazel + +```sh +$ bazel run //src/workerd/server:workerd -- serve ~/cloudflare/workerd/samples/nodejs-compat/config.capnp +``` + +To create a standalone binary that can be run: + +```sh +$ ./workerd compile config.capnp > nodejs-compat + +$ ./nodejs-compat +``` + +To test: + +```sh +% curl http://localhost:8080 +Hello World +``` diff --git a/samples/nodejs-compat-module/config.capnp b/samples/nodejs-compat-module/config.capnp new file mode 100644 index 000000000000..8f2d1ac17758 --- /dev/null +++ b/samples/nodejs-compat-module/config.capnp @@ -0,0 +1,36 @@ +# Imports the base schema for workerd configuration files. + +# Refer to the comments in /src/workerd/server/workerd.capnp for more details. + +using Workerd = import "/workerd/workerd.capnp"; + +# A constant of type Workerd.Config defines the top-level configuration for an +# instance of the workerd runtime. A single config file can contain multiple +# Workerd.Config definitions and must have at least one. +const helloWorldExample :Workerd.Config = ( + + # Every workerd instance consists of a set of named services. A worker, for instance, + # is a type of service. Other types of services can include external servers, the + # ability to talk to a network, or accessing a disk directory. Here we create a single + # worker service. The configuration details for the worker are defined below. + services = [ (name = "main", worker = .helloWorld) ], + + # Every configuration defines the one or more sockets on which the server will listene. + # Here, we create a single socket that will listen on localhost port 8080, and will + # dispatch to the "main" service that we defined above. + sockets = [ ( name = "http", address = "*:8080", http = (), service = "main" ) ] +); + +# The definition of the actual helloWorld worker exposed using the "main" service. +# In this example the worker is implemented as a single simple script (see worker.js). +# The compatibilityDate is required. For more details on compatibility dates see: +# https://developers.cloudflare.com/workers/platform/compatibility-dates/ + +const helloWorld :Workerd.Worker = ( + modules = [ + (name = "worker", esModule = embed "worker.js"), + (name = "foo", nodeJsModule = embed "foo.js") + ], + compatibilityDate = "2023-02-28", + compatibilityFlags = ["nodejs_compat"] +); diff --git a/samples/nodejs-compat-module/foo.js b/samples/nodejs-compat-module/foo.js new file mode 100644 index 000000000000..c42bd8864b5a --- /dev/null +++ b/samples/nodejs-compat-module/foo.js @@ -0,0 +1,10 @@ +process.nextTick(() => { + console.log('this should work'); +}); + +module.exports = { + // Buffer is a global... + Buffer, + // require can be called synchronously inline. + util: require('util'), +} diff --git a/samples/nodejs-compat-module/worker.js b/samples/nodejs-compat-module/worker.js new file mode 100644 index 000000000000..e84d9f65c831 --- /dev/null +++ b/samples/nodejs-compat-module/worker.js @@ -0,0 +1,13 @@ +import { default as foo } from 'foo'; +const { + Buffer, + util, +} = foo; + +export default { + async fetch(request) { + console.log(Buffer); + console.log(util); + return new Response("ok"); + } +}; diff --git a/src/node/process.ts b/src/node/process.ts new file mode 100644 index 000000000000..b7681fbd8943 --- /dev/null +++ b/src/node/process.ts @@ -0,0 +1,7 @@ +// 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 +// + +export * from 'node-internal:process'; +export { default } from 'node-internal:process'; diff --git a/src/workerd/jsg/modules.c++ b/src/workerd/jsg/modules.c++ index 7534359c149c..6c83b2168bb6 100644 --- a/src/workerd/jsg/modules.c++ +++ b/src/workerd/jsg/modules.c++ @@ -157,7 +157,25 @@ v8::MaybeLocal evaluateSyntheticModuleCallback( } } KJ_CASE_ONEOF(info, ModuleRegistry::NodeJsModuleInfo) { - KJ_UNIMPLEMENTED("Node.js module type is not yet implemented"); + v8::TryCatch catcher(isolate); + // const_cast is safe here because we're protected by the isolate js. + auto& nodejs = const_cast(info); + try { + nodejs.evalFunc(js); + } catch (const JsExceptionThrown&) { + if (catcher.CanContinue()) catcher.ReThrow(); + // leave `result` empty to propagate the JS exception + return; + } + + if (module->SetSyntheticModuleExport( + isolate, + v8StrIntern(isolate, "default"_kj), + nodejs.moduleContext->module->getExports(js.v8Isolate)).IsJust()) { + result = makeResolvedPromise(); + } else { + // leave `result` empty to propagate the JS exception + } } KJ_CASE_ONEOF(info, ModuleRegistry::TextModuleInfo) { if (module->SetSyntheticModuleExport(isolate, @@ -433,6 +451,9 @@ v8::Local compileWasmModule(jsg::Lock& js, v8::MemorySpan(code.begin(), code.size()))); } +// ====================================================================================== +// Node.js module types + jsg::Ref ModuleRegistry::NodeJsModuleInfo::initModuleContext( jsg::Lock& js, kj::StringPtr name) { @@ -468,8 +489,12 @@ v8::Local NodeJsModuleContext::require(kj::String specifier, v8::Isol // If it is a bare specifier known to be a Node.js built-in, then prefix the // specifier with node: + bool isNodeBuiltin = false; if (NODEJS_BUILTINS.contains(specifier)) { specifier = kj::str("node:", specifier); + isNodeBuiltin = true; + } else if (specifier.startsWith("node:")) { + isNodeBuiltin = true; } // TODO(cleanup): This implementation from here on is identical to the @@ -490,8 +515,10 @@ v8::Local NodeJsModuleContext::require(kj::String specifier, v8::Isol // Adding imported from suffix here not necessary like it is for resolveCallback, since we have a // js stack that will include the parent module's name and location of the failed require(). - JSG_REQUIRE_NONNULL(info.maybeSynthetic, TypeError, - "Cannot use require() to import an ES Module."); + if (!isNodeBuiltin) { + JSG_REQUIRE_NONNULL(info.maybeSynthetic, TypeError, + "Cannot use require() to import an ES Module."); + } auto module = info.module.getHandle(js); auto context = isolate->GetCurrentContext(); @@ -509,16 +536,23 @@ v8::Local NodeJsModuleContext::require(kj::String specifier, v8::Isol throwTunneledException(isolate, module->GetException()); } - // Originally, This returned an object like `{default: module.exports}` when we really - // intended to return the module exports raw. We should be extracting `default` here. - // Unfortunately, there is a user depending on the wrong behavior in production, so we - // needed a compatibility flag to fix. - if (getCommonJsExportDefault(isolate)) { - return check(module->GetModuleNamespace().As() - ->Get(context, v8StrIntern(isolate, "default"))); - } else { - return module->GetModuleNamespace(); - } + return check(module->GetModuleNamespace().As() + ->Get(context, v8StrIntern(isolate, "default"))); +} + +v8::Local NodeJsModuleContext::getBuffer(jsg::Lock& js) { + auto value = require(kj::str("node:buffer"), js.v8Isolate); + JSG_REQUIRE(value->IsObject(), TypeError, "Invalid node:buffer implementation"); + auto module = value.As(); + auto buffer = jsg::check(module->Get(js.v8Context(), jsg::v8Str(js.v8Isolate, "Buffer"))); + JSG_REQUIRE(buffer->IsFunction(), TypeError, "Invalid node:buffer implementation"); + return buffer; +} + +v8::Local NodeJsModuleContext::getProcess(jsg::Lock& js) { + auto value = require(kj::str("node:process"), js.v8Isolate); + JSG_REQUIRE(value->IsObject(), TypeError, "Invalid node:process implementation"); + return value; } } // namespace workerd::jsg diff --git a/src/workerd/jsg/modules.h b/src/workerd/jsg/modules.h index d1beccb903ac..6c6d08426ec6 100644 --- a/src/workerd/jsg/modules.h +++ b/src/workerd/jsg/modules.h @@ -66,13 +66,88 @@ class NodeJsModuleContext: public jsg::Object { exports(isolate, module->getExports(isolate)) {} v8::Local require(kj::String specifier, v8::Isolate* isolate); + v8::Local getBuffer(jsg::Lock& js); + v8::Local getProcess(jsg::Lock& js); + + jsg::Ref getModule(v8::Isolate* isolate) { return module.addRef(); } + + v8::Local getExports(v8::Isolate* isolate) { return exports.getHandle(isolate); } + void setExports(jsg::Value value) { exports = kj::mv(value); } JSG_RESOURCE_TYPE(NodeJsModuleContext) { JSG_METHOD(require); JSG_READONLY_INSTANCE_PROPERTY(module, getModule); JSG_INSTANCE_PROPERTY(exports, getExports, setExports); + JSG_LAZY_INSTANCE_PROPERTY(Buffer, getBuffer); + JSG_LAZY_INSTANCE_PROPERTY(process, getProcess); } + // The additional Node.js globals include... + // process + // Buffer + // queueMicrotask + // clearImmediate + // setImmediate + // structuredClone + // URL + // URLSearchParams + // DOMException + // AbortController + // AbortSignal + // Event + // EventTarget + // TextEncoder + // TextDecoder + // TransformStream + // TransformStreamDefaultController + // WritableStream + // WritableStreamDefaultController + // WritableStreamDefaultWriter + // ReadableStream + // ReadableStreamDefaultReader + // ReadableStreamBYOBReader + // ReadableStreamBYOBRequest + // ReadableByteStreamController + // ReadableStreamDefaultController + // ByteLengthQueuingStrategy + // CountQueuingStrategy + // TextEncoderStream + // TextDecoderStream + // CompressionStream + // DecompressionStream: + // clearInterval + // clearTimeout + // setInterval + // setTimeout + // atob + // btoa + // Blob + // File + // fetch + // FormData + // Headers + // Request + // Response + // crypto + // Crypto + // CryptoKey + // SubtleCrypto + // CustomEvent + + // Additional Node.js globals that we do not implement include: + // BroadcastChannel + // MessageChannel + // MessagePort + // MessageEvent + // Performance + // PerformanceEntry + // PerformanceMark + // PerformanceMeasure + // PerformanceObserver + // PerformanceObserverEntryList + // PerformanceResourceTiming + // performance + jsg::Ref module; private: kj::Path path; diff --git a/src/workerd/server/workerd-api.c++ b/src/workerd/server/workerd-api.c++ index baef3f3dac6a..2062247ea212 100644 --- a/src/workerd/server/workerd-api.c++ +++ b/src/workerd/server/workerd-api.c++ @@ -74,7 +74,8 @@ JSG_DECLARE_ISOLATE_TYPE(JsgWorkerdIsolate, jsg::InjectConfiguration, Worker::ApiIsolate::ErrorInterface, jsg::CommonJsModuleObject, - jsg::CommonJsModuleContext); + jsg::CommonJsModuleContext, + jsg::NodeJsModuleContext); struct WorkerdApiIsolate::Impl { kj::Own features;