Skip to content

Commit

Permalink
More implementation for Node.js module type
Browse files Browse the repository at this point in the history
  • Loading branch information
jasnell committed Apr 24, 2023
1 parent 5fbab36 commit 9398d12
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 14 deletions.
28 changes: 28 additions & 0 deletions samples/nodejs-compat-module/README.md
Original file line number Diff line number Diff line change
@@ -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
```
36 changes: 36 additions & 0 deletions samples/nodejs-compat-module/config.capnp
Original file line number Diff line number Diff line change
@@ -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"]
);
10 changes: 10 additions & 0 deletions samples/nodejs-compat-module/foo.js
Original file line number Diff line number Diff line change
@@ -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'),
}
13 changes: 13 additions & 0 deletions samples/nodejs-compat-module/worker.js
Original file line number Diff line number Diff line change
@@ -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");
}
};
7 changes: 7 additions & 0 deletions src/node/process.ts
Original file line number Diff line number Diff line change
@@ -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';
60 changes: 47 additions & 13 deletions src/workerd/jsg/modules.c++
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,25 @@ v8::MaybeLocal<v8::Value> 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<ModuleRegistry::NodeJsModuleInfo&>(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,
Expand Down Expand Up @@ -433,6 +451,9 @@ v8::Local<v8::WasmModuleObject> compileWasmModule(jsg::Lock& js,
v8::MemorySpan<const uint8_t>(code.begin(), code.size())));
}

// ======================================================================================
// Node.js module types

jsg::Ref<NodeJsModuleContext> ModuleRegistry::NodeJsModuleInfo::initModuleContext(
jsg::Lock& js,
kj::StringPtr name) {
Expand Down Expand Up @@ -468,8 +489,12 @@ v8::Local<v8::Value> 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
Expand All @@ -490,8 +515,10 @@ v8::Local<v8::Value> 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();
Expand All @@ -509,16 +536,23 @@ v8::Local<v8::Value> 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<v8::Object>()
->Get(context, v8StrIntern(isolate, "default")));
} else {
return module->GetModuleNamespace();
}
return check(module->GetModuleNamespace().As<v8::Object>()
->Get(context, v8StrIntern(isolate, "default")));
}

v8::Local<v8::Value> 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<v8::Object>();
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<v8::Value> 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
75 changes: 75 additions & 0 deletions src/workerd/jsg/modules.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,88 @@ class NodeJsModuleContext: public jsg::Object {
exports(isolate, module->getExports(isolate)) {}

v8::Local<v8::Value> require(kj::String specifier, v8::Isolate* isolate);
v8::Local<v8::Value> getBuffer(jsg::Lock& js);
v8::Local<v8::Value> getProcess(jsg::Lock& js);

jsg::Ref<CommonJsModuleObject> getModule(v8::Isolate* isolate) { return module.addRef(); }

v8::Local<v8::Value> 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<CommonJsModuleObject> module;
private:
kj::Path path;
Expand Down
3 changes: 2 additions & 1 deletion src/workerd/server/workerd-api.c++
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ JSG_DECLARE_ISOLATE_TYPE(JsgWorkerdIsolate,
jsg::InjectConfiguration<CompatibilityFlags::Reader>,
Worker::ApiIsolate::ErrorInterface,
jsg::CommonJsModuleObject,
jsg::CommonJsModuleContext);
jsg::CommonJsModuleContext,
jsg::NodeJsModuleContext);

struct WorkerdApiIsolate::Impl {
kj::Own<CompatibilityFlags::Reader> features;
Expand Down

0 comments on commit 9398d12

Please # to comment.