Skip to content

Commit

Permalink
Separate commonjs-related types from jsg/modules.h/c++ (#3298)
Browse files Browse the repository at this point in the history
  • Loading branch information
jasnell authored Jan 7, 2025
1 parent 8c228f5 commit 19fc71c
Show file tree
Hide file tree
Showing 5 changed files with 329 additions and 294 deletions.
2 changes: 2 additions & 0 deletions src/workerd/jsg/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ wd_cc_library(
srcs = [
"async-context.c++",
"buffersource.c++",
"commonjs.c++",
"compile-cache.c++",
"dom-exception.c++",
"inspector.c++",
Expand All @@ -31,6 +32,7 @@ wd_cc_library(
hdrs = [
"async-context.h",
"buffersource.h",
"commonjs.h",
"compile-cache.h",
"dom-exception.h",
"function.h",
Expand Down
163 changes: 163 additions & 0 deletions src/workerd/jsg/commonjs.c++
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
#include "commonjs.h"

#include "modules.h"

namespace workerd::jsg {

v8::Local<v8::Value> CommonJsModuleContext::require(jsg::Lock& js, kj::String specifier) {
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.
if (specifier.startsWith("node:") || specifier.startsWith("cloudflare:") ||
specifier.startsWith("workerd:")) {
return kj::Path::parse(specifier);
}
return path.parent().eval(specifier);
})();

// require() is only exposed to worker bundle modules so the resolve here is only
// permitted to require worker bundle or built-in modules. Internal modules are
// excluded.
auto& info = JSG_REQUIRE_NONNULL(modulesForResolveCallback->resolve(js, targetPath, path,
ModuleRegistry::ResolveOption::DEFAULT,
ModuleRegistry::ResolveMethod::REQUIRE, specifier.asPtr()),
Error, "No such module \"", targetPath.toString(), "\".");
// 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().

ModuleRegistry::RequireImplOptions options = ModuleRegistry::RequireImplOptions::DEFAULT;
if (getCommonJsExportDefault(js.v8Isolate)) {
options = ModuleRegistry::RequireImplOptions::EXPORT_DEFAULT;
}

return ModuleRegistry::requireImpl(js, info, options);
}

CommonJsModuleObject::CommonJsModuleObject(jsg::Lock& js)
: exports(js.v8Isolate, v8::Object::New(js.v8Isolate)) {}

v8::Local<v8::Value> CommonJsModuleObject::getExports(jsg::Lock& js) {
return exports.getHandle(js);
}
void CommonJsModuleObject::setExports(jsg::Value value) {
exports = kj::mv(value);
}

void CommonJsModuleObject::visitForMemoryInfo(MemoryTracker& tracker) const {
tracker.trackField("exports", exports);
}

// ======================================================================================

NodeJsModuleContext::NodeJsModuleContext(jsg::Lock& js, kj::Path path)
: module(jsg::alloc<NodeJsModuleObject>(js, path.toString(true))),
path(kj::mv(path)),
exports(js.v8Ref(module->getExports(js))) {}

v8::Local<v8::Value> NodeJsModuleContext::require(jsg::Lock& js, kj::String specifier) {
// 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;
KJ_IF_SOME(spec, checkNodeSpecifier(specifier)) {
specifier = kj::mv(spec);
isNodeBuiltin = true;
resolveOption = jsg::ModuleRegistry::ResolveOption::BUILTIN_ONLY;
}

// TODO(cleanup): This implementation from here on is identical to the
// CommonJsModuleContext::require. We should consolidate these as the
// next step.

auto modulesForResolveCallback = jsg::getModulesForResolveCallback(js.v8Isolate);
KJ_REQUIRE(modulesForResolveCallback != nullptr, "didn't expect resolveCallback() now");

kj::Path targetPath = ([&] {
// If the specifier begins with one of our known prefixes, let's not resolve
// it against the referrer.
if (specifier.startsWith("node:") || specifier.startsWith("cloudflare:") ||
specifier.startsWith("workerd:")) {
return kj::Path::parse(specifier);
}
return path.parent().eval(specifier);
})();

// require() is only exposed to worker bundle modules so the resolve here is only
// permitted to require worker bundle or built-in modules. Internal modules are
// excluded.
auto& info =
JSG_REQUIRE_NONNULL(modulesForResolveCallback->resolve(js, targetPath, path, resolveOption,
ModuleRegistry::ResolveMethod::REQUIRE, specifier.asPtr()),
Error, "No such module \"", targetPath.toString(), "\".");
// 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().

if (!isNodeBuiltin) {
JSG_REQUIRE_NONNULL(
info.maybeSynthetic, TypeError, "Cannot use require() to import an ES Module.");
}

return ModuleRegistry::requireImpl(js, info, ModuleRegistry::RequireImplOptions::EXPORT_DEFAULT);
}

v8::Local<v8::Value> NodeJsModuleContext::getBuffer(jsg::Lock& js) {
auto value = require(js, kj::str("node:buffer"));
JSG_REQUIRE(value->IsObject(), TypeError, "Invalid node:buffer implementation");
auto module = value.As<v8::Object>();
auto buffer = js.v8Get(module, "Buffer"_kj);
JSG_REQUIRE(buffer->IsFunction(), TypeError, "Invalid node:buffer implementation");
return buffer;
}

v8::Local<v8::Value> NodeJsModuleContext::getProcess(jsg::Lock& js) {
auto value = require(js, kj::str("node:process"));
JSG_REQUIRE(value->IsObject(), TypeError, "Invalid node:process implementation");
return value;
}

kj::String NodeJsModuleContext::getFilename() {
return path.toString(true);
}

kj::String NodeJsModuleContext::getDirname() {
return path.parent().toString(true);
}

jsg::Ref<NodeJsModuleObject> NodeJsModuleContext::getModule(jsg::Lock& js) {
return module.addRef();
}

v8::Local<v8::Value> NodeJsModuleContext::getExports(jsg::Lock& js) {
return exports.getHandle(js);
}

void NodeJsModuleContext::setExports(jsg::Value value) {
exports = kj::mv(value);
}

NodeJsModuleObject::NodeJsModuleObject(jsg::Lock& js, kj::String path)
: exports(js.v8Isolate, v8::Object::New(js.v8Isolate)),
path(kj::mv(path)) {}

v8::Local<v8::Value> NodeJsModuleObject::getExports(jsg::Lock& js) {
return exports.getHandle(js);
}

void NodeJsModuleObject::setExports(jsg::Value value) {
exports = kj::mv(value);
}

kj::StringPtr NodeJsModuleObject::getPath() {
return path;
}

} // namespace workerd::jsg
159 changes: 159 additions & 0 deletions src/workerd/jsg/commonjs.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
#pragma once

#include <workerd/jsg/jsg.h>

#include <kj/filesystem.h>

namespace workerd::jsg {

class CommonJsModuleObject: public jsg::Object {
public:
CommonJsModuleObject(jsg::Lock& js);

v8::Local<v8::Value> getExports(jsg::Lock& js);
void setExports(jsg::Value value);

JSG_RESOURCE_TYPE(CommonJsModuleObject) {
JSG_INSTANCE_PROPERTY(exports, getExports, setExports);
}

void visitForMemoryInfo(MemoryTracker& tracker) const;

private:
jsg::Value exports;
};

class CommonJsModuleContext: public jsg::Object {
public:
CommonJsModuleContext(jsg::Lock& js, kj::Path path)
: module(jsg::alloc<CommonJsModuleObject>(js)),
path(kj::mv(path)),
exports(js.v8Isolate, module->getExports(js)) {}

v8::Local<v8::Value> require(jsg::Lock& js, kj::String specifier);

jsg::Ref<CommonJsModuleObject> getModule(jsg::Lock& js) {
return module.addRef();
}

v8::Local<v8::Value> getExports(jsg::Lock& js) {
return exports.getHandle(js);
}
void setExports(jsg::Value value) {
exports = kj::mv(value);
}

JSG_RESOURCE_TYPE(CommonJsModuleContext) {
JSG_METHOD(require);
JSG_READONLY_INSTANCE_PROPERTY(module, getModule);
JSG_INSTANCE_PROPERTY(exports, getExports, setExports);
}

jsg::Ref<CommonJsModuleObject> module;

void visitForMemoryInfo(MemoryTracker& tracker) const {
tracker.trackField("exports", exports);
tracker.trackFieldWithSize("path", path.size());
}

private:
kj::Path path;
jsg::Value exports;
};

// ======================================================================================

// TODO(cleanup): Ideally these would exist over with the rest of the Node.js
// compat related stuff in workerd/api/node but there's a dependency cycle issue
// to work through there. Specifically, these are needed in jsg but jsg cannot
// depend on workerd/api. We should revisit to see if we can get these moved over.

// The NodeJsModuleContext is used in support of the NodeJsCompatModule type.
// It adds additional extensions to the global context that would normally be
// expected within the global scope of a Node.js compatible module (such as
// Buffer and process).

// TODO(cleanup): There's a fair amount of duplicated code between the CommonJsModule
// and NodeJsModule types... should be deduplicated.
class NodeJsModuleObject: public jsg::Object {
public:
NodeJsModuleObject(jsg::Lock& js, kj::String path);

v8::Local<v8::Value> getExports(jsg::Lock& js);
void setExports(jsg::Value value);
kj::StringPtr getPath();

// TODO(soon): Additional properties... We can likely get by without implementing most
// of these (if any).
// * children https://nodejs.org/dist/latest-v20.x/docs/api/modules.html#modulechildren
// * filename https://nodejs.org/dist/latest-v20.x/docs/api/modules.html#modulefilename
// * id https://nodejs.org/dist/latest-v20.x/docs/api/modules.html#moduleid
// * isPreloading https://nodejs.org/dist/latest-v20.x/docs/api/modules.html#moduleispreloading
// * loaded https://nodejs.org/dist/latest-v20.x/docs/api/modules.html#moduleloaded
// * parent https://nodejs.org/dist/latest-v20.x/docs/api/modules.html#moduleparent
// * paths https://nodejs.org/dist/latest-v20.x/docs/api/modules.html#modulepaths
// * require https://nodejs.org/dist/latest-v20.x/docs/api/modules.html#modulerequireid

JSG_RESOURCE_TYPE(NodeJsModuleObject) {
JSG_INSTANCE_PROPERTY(exports, getExports, setExports);
JSG_READONLY_INSTANCE_PROPERTY(path, getPath);
}

void visitForMemoryInfo(MemoryTracker& tracker) const {
tracker.trackField("exports", exports);
tracker.trackField("path", path);
}

private:
jsg::Value exports;
kj::String path;
};

// The NodeJsModuleContext is similar in structure to CommonJsModuleContext
// with the exception that:
// (a) Node.js-compat built-in modules can be required without the `node:` specifier-prefix
// (meaning that worker-bundle modules whose names conflict with the Node.js built-ins
// are ignored), and
// (b) The common Node.js globals that we implement are exposed. For instance, `process`
// and `Buffer` will be found at the global scope.
class NodeJsModuleContext: public jsg::Object {
public:
NodeJsModuleContext(jsg::Lock& js, kj::Path path);

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

// TODO(soon): Implement setImmediate/clearImmediate

jsg::Ref<NodeJsModuleObject> getModule(jsg::Lock& js);

v8::Local<v8::Value> getExports(jsg::Lock& js);
void setExports(jsg::Value value);

kj::String getFilename();
kj::String getDirname();

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);
JSG_LAZY_INSTANCE_PROPERTY(__filename, getFilename);
JSG_LAZY_INSTANCE_PROPERTY(__dirname, getDirname);
}

jsg::Ref<NodeJsModuleObject> module;

void visitForMemoryInfo(MemoryTracker& tracker) const {
tracker.trackField("exports", exports);
tracker.trackFieldWithSize("path", path.size());
}

private:
kj::Path path;
jsg::Value exports;
};

} // namespace workerd::jsg
Loading

0 comments on commit 19fc71c

Please sign in to comment.