Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separate commonjs-related types from jsg/modules.h/c++ #3298

Merged
merged 1 commit into from
Jan 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = ([&] {
anonrig marked this conversation as resolved.
Show resolved Hide resolved
// 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 = ([&] {
anonrig marked this conversation as resolved.
Show resolved Hide resolved
// 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);
jasnell marked this conversation as resolved.
Show resolved Hide resolved
}

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();
jasnell marked this conversation as resolved.
Show resolved Hide resolved
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
Loading