Skip to content

Commit 19fc71c

Browse files
authored
Separate commonjs-related types from jsg/modules.h/c++ (#3298)
1 parent 8c228f5 commit 19fc71c

File tree

5 files changed

+329
-294
lines changed

5 files changed

+329
-294
lines changed

src/workerd/jsg/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ wd_cc_library(
1111
srcs = [
1212
"async-context.c++",
1313
"buffersource.c++",
14+
"commonjs.c++",
1415
"compile-cache.c++",
1516
"dom-exception.c++",
1617
"inspector.c++",
@@ -31,6 +32,7 @@ wd_cc_library(
3132
hdrs = [
3233
"async-context.h",
3334
"buffersource.h",
35+
"commonjs.h",
3436
"compile-cache.h",
3537
"dom-exception.h",
3638
"function.h",

src/workerd/jsg/commonjs.c++

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
#include "commonjs.h"
2+
3+
#include "modules.h"
4+
5+
namespace workerd::jsg {
6+
7+
v8::Local<v8::Value> CommonJsModuleContext::require(jsg::Lock& js, kj::String specifier) {
8+
auto modulesForResolveCallback = getModulesForResolveCallback(js.v8Isolate);
9+
KJ_REQUIRE(modulesForResolveCallback != nullptr, "didn't expect resolveCallback() now");
10+
11+
if (isNodeJsCompatEnabled(js)) {
12+
KJ_IF_SOME(nodeSpec, checkNodeSpecifier(specifier)) {
13+
specifier = kj::mv(nodeSpec);
14+
}
15+
}
16+
17+
kj::Path targetPath = ([&] {
18+
// If the specifier begins with one of our known prefixes, let's not resolve
19+
// it against the referrer.
20+
if (specifier.startsWith("node:") || specifier.startsWith("cloudflare:") ||
21+
specifier.startsWith("workerd:")) {
22+
return kj::Path::parse(specifier);
23+
}
24+
return path.parent().eval(specifier);
25+
})();
26+
27+
// require() is only exposed to worker bundle modules so the resolve here is only
28+
// permitted to require worker bundle or built-in modules. Internal modules are
29+
// excluded.
30+
auto& info = JSG_REQUIRE_NONNULL(modulesForResolveCallback->resolve(js, targetPath, path,
31+
ModuleRegistry::ResolveOption::DEFAULT,
32+
ModuleRegistry::ResolveMethod::REQUIRE, specifier.asPtr()),
33+
Error, "No such module \"", targetPath.toString(), "\".");
34+
// Adding imported from suffix here not necessary like it is for resolveCallback, since we have a
35+
// js stack that will include the parent module's name and location of the failed require().
36+
37+
ModuleRegistry::RequireImplOptions options = ModuleRegistry::RequireImplOptions::DEFAULT;
38+
if (getCommonJsExportDefault(js.v8Isolate)) {
39+
options = ModuleRegistry::RequireImplOptions::EXPORT_DEFAULT;
40+
}
41+
42+
return ModuleRegistry::requireImpl(js, info, options);
43+
}
44+
45+
CommonJsModuleObject::CommonJsModuleObject(jsg::Lock& js)
46+
: exports(js.v8Isolate, v8::Object::New(js.v8Isolate)) {}
47+
48+
v8::Local<v8::Value> CommonJsModuleObject::getExports(jsg::Lock& js) {
49+
return exports.getHandle(js);
50+
}
51+
void CommonJsModuleObject::setExports(jsg::Value value) {
52+
exports = kj::mv(value);
53+
}
54+
55+
void CommonJsModuleObject::visitForMemoryInfo(MemoryTracker& tracker) const {
56+
tracker.trackField("exports", exports);
57+
}
58+
59+
// ======================================================================================
60+
61+
NodeJsModuleContext::NodeJsModuleContext(jsg::Lock& js, kj::Path path)
62+
: module(jsg::alloc<NodeJsModuleObject>(js, path.toString(true))),
63+
path(kj::mv(path)),
64+
exports(js.v8Ref(module->getExports(js))) {}
65+
66+
v8::Local<v8::Value> NodeJsModuleContext::require(jsg::Lock& js, kj::String specifier) {
67+
// If it is a bare specifier known to be a Node.js built-in, then prefix the
68+
// specifier with node:
69+
bool isNodeBuiltin = false;
70+
auto resolveOption = jsg::ModuleRegistry::ResolveOption::DEFAULT;
71+
KJ_IF_SOME(spec, checkNodeSpecifier(specifier)) {
72+
specifier = kj::mv(spec);
73+
isNodeBuiltin = true;
74+
resolveOption = jsg::ModuleRegistry::ResolveOption::BUILTIN_ONLY;
75+
}
76+
77+
// TODO(cleanup): This implementation from here on is identical to the
78+
// CommonJsModuleContext::require. We should consolidate these as the
79+
// next step.
80+
81+
auto modulesForResolveCallback = jsg::getModulesForResolveCallback(js.v8Isolate);
82+
KJ_REQUIRE(modulesForResolveCallback != nullptr, "didn't expect resolveCallback() now");
83+
84+
kj::Path targetPath = ([&] {
85+
// If the specifier begins with one of our known prefixes, let's not resolve
86+
// it against the referrer.
87+
if (specifier.startsWith("node:") || specifier.startsWith("cloudflare:") ||
88+
specifier.startsWith("workerd:")) {
89+
return kj::Path::parse(specifier);
90+
}
91+
return path.parent().eval(specifier);
92+
})();
93+
94+
// require() is only exposed to worker bundle modules so the resolve here is only
95+
// permitted to require worker bundle or built-in modules. Internal modules are
96+
// excluded.
97+
auto& info =
98+
JSG_REQUIRE_NONNULL(modulesForResolveCallback->resolve(js, targetPath, path, resolveOption,
99+
ModuleRegistry::ResolveMethod::REQUIRE, specifier.asPtr()),
100+
Error, "No such module \"", targetPath.toString(), "\".");
101+
// Adding imported from suffix here not necessary like it is for resolveCallback, since we have a
102+
// js stack that will include the parent module's name and location of the failed require().
103+
104+
if (!isNodeBuiltin) {
105+
JSG_REQUIRE_NONNULL(
106+
info.maybeSynthetic, TypeError, "Cannot use require() to import an ES Module.");
107+
}
108+
109+
return ModuleRegistry::requireImpl(js, info, ModuleRegistry::RequireImplOptions::EXPORT_DEFAULT);
110+
}
111+
112+
v8::Local<v8::Value> NodeJsModuleContext::getBuffer(jsg::Lock& js) {
113+
auto value = require(js, kj::str("node:buffer"));
114+
JSG_REQUIRE(value->IsObject(), TypeError, "Invalid node:buffer implementation");
115+
auto module = value.As<v8::Object>();
116+
auto buffer = js.v8Get(module, "Buffer"_kj);
117+
JSG_REQUIRE(buffer->IsFunction(), TypeError, "Invalid node:buffer implementation");
118+
return buffer;
119+
}
120+
121+
v8::Local<v8::Value> NodeJsModuleContext::getProcess(jsg::Lock& js) {
122+
auto value = require(js, kj::str("node:process"));
123+
JSG_REQUIRE(value->IsObject(), TypeError, "Invalid node:process implementation");
124+
return value;
125+
}
126+
127+
kj::String NodeJsModuleContext::getFilename() {
128+
return path.toString(true);
129+
}
130+
131+
kj::String NodeJsModuleContext::getDirname() {
132+
return path.parent().toString(true);
133+
}
134+
135+
jsg::Ref<NodeJsModuleObject> NodeJsModuleContext::getModule(jsg::Lock& js) {
136+
return module.addRef();
137+
}
138+
139+
v8::Local<v8::Value> NodeJsModuleContext::getExports(jsg::Lock& js) {
140+
return exports.getHandle(js);
141+
}
142+
143+
void NodeJsModuleContext::setExports(jsg::Value value) {
144+
exports = kj::mv(value);
145+
}
146+
147+
NodeJsModuleObject::NodeJsModuleObject(jsg::Lock& js, kj::String path)
148+
: exports(js.v8Isolate, v8::Object::New(js.v8Isolate)),
149+
path(kj::mv(path)) {}
150+
151+
v8::Local<v8::Value> NodeJsModuleObject::getExports(jsg::Lock& js) {
152+
return exports.getHandle(js);
153+
}
154+
155+
void NodeJsModuleObject::setExports(jsg::Value value) {
156+
exports = kj::mv(value);
157+
}
158+
159+
kj::StringPtr NodeJsModuleObject::getPath() {
160+
return path;
161+
}
162+
163+
} // namespace workerd::jsg

src/workerd/jsg/commonjs.h

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
#pragma once
2+
3+
#include <workerd/jsg/jsg.h>
4+
5+
#include <kj/filesystem.h>
6+
7+
namespace workerd::jsg {
8+
9+
class CommonJsModuleObject: public jsg::Object {
10+
public:
11+
CommonJsModuleObject(jsg::Lock& js);
12+
13+
v8::Local<v8::Value> getExports(jsg::Lock& js);
14+
void setExports(jsg::Value value);
15+
16+
JSG_RESOURCE_TYPE(CommonJsModuleObject) {
17+
JSG_INSTANCE_PROPERTY(exports, getExports, setExports);
18+
}
19+
20+
void visitForMemoryInfo(MemoryTracker& tracker) const;
21+
22+
private:
23+
jsg::Value exports;
24+
};
25+
26+
class CommonJsModuleContext: public jsg::Object {
27+
public:
28+
CommonJsModuleContext(jsg::Lock& js, kj::Path path)
29+
: module(jsg::alloc<CommonJsModuleObject>(js)),
30+
path(kj::mv(path)),
31+
exports(js.v8Isolate, module->getExports(js)) {}
32+
33+
v8::Local<v8::Value> require(jsg::Lock& js, kj::String specifier);
34+
35+
jsg::Ref<CommonJsModuleObject> getModule(jsg::Lock& js) {
36+
return module.addRef();
37+
}
38+
39+
v8::Local<v8::Value> getExports(jsg::Lock& js) {
40+
return exports.getHandle(js);
41+
}
42+
void setExports(jsg::Value value) {
43+
exports = kj::mv(value);
44+
}
45+
46+
JSG_RESOURCE_TYPE(CommonJsModuleContext) {
47+
JSG_METHOD(require);
48+
JSG_READONLY_INSTANCE_PROPERTY(module, getModule);
49+
JSG_INSTANCE_PROPERTY(exports, getExports, setExports);
50+
}
51+
52+
jsg::Ref<CommonJsModuleObject> module;
53+
54+
void visitForMemoryInfo(MemoryTracker& tracker) const {
55+
tracker.trackField("exports", exports);
56+
tracker.trackFieldWithSize("path", path.size());
57+
}
58+
59+
private:
60+
kj::Path path;
61+
jsg::Value exports;
62+
};
63+
64+
// ======================================================================================
65+
66+
// TODO(cleanup): Ideally these would exist over with the rest of the Node.js
67+
// compat related stuff in workerd/api/node but there's a dependency cycle issue
68+
// to work through there. Specifically, these are needed in jsg but jsg cannot
69+
// depend on workerd/api. We should revisit to see if we can get these moved over.
70+
71+
// The NodeJsModuleContext is used in support of the NodeJsCompatModule type.
72+
// It adds additional extensions to the global context that would normally be
73+
// expected within the global scope of a Node.js compatible module (such as
74+
// Buffer and process).
75+
76+
// TODO(cleanup): There's a fair amount of duplicated code between the CommonJsModule
77+
// and NodeJsModule types... should be deduplicated.
78+
class NodeJsModuleObject: public jsg::Object {
79+
public:
80+
NodeJsModuleObject(jsg::Lock& js, kj::String path);
81+
82+
v8::Local<v8::Value> getExports(jsg::Lock& js);
83+
void setExports(jsg::Value value);
84+
kj::StringPtr getPath();
85+
86+
// TODO(soon): Additional properties... We can likely get by without implementing most
87+
// of these (if any).
88+
// * children https://nodejs.org/dist/latest-v20.x/docs/api/modules.html#modulechildren
89+
// * filename https://nodejs.org/dist/latest-v20.x/docs/api/modules.html#modulefilename
90+
// * id https://nodejs.org/dist/latest-v20.x/docs/api/modules.html#moduleid
91+
// * isPreloading https://nodejs.org/dist/latest-v20.x/docs/api/modules.html#moduleispreloading
92+
// * loaded https://nodejs.org/dist/latest-v20.x/docs/api/modules.html#moduleloaded
93+
// * parent https://nodejs.org/dist/latest-v20.x/docs/api/modules.html#moduleparent
94+
// * paths https://nodejs.org/dist/latest-v20.x/docs/api/modules.html#modulepaths
95+
// * require https://nodejs.org/dist/latest-v20.x/docs/api/modules.html#modulerequireid
96+
97+
JSG_RESOURCE_TYPE(NodeJsModuleObject) {
98+
JSG_INSTANCE_PROPERTY(exports, getExports, setExports);
99+
JSG_READONLY_INSTANCE_PROPERTY(path, getPath);
100+
}
101+
102+
void visitForMemoryInfo(MemoryTracker& tracker) const {
103+
tracker.trackField("exports", exports);
104+
tracker.trackField("path", path);
105+
}
106+
107+
private:
108+
jsg::Value exports;
109+
kj::String path;
110+
};
111+
112+
// The NodeJsModuleContext is similar in structure to CommonJsModuleContext
113+
// with the exception that:
114+
// (a) Node.js-compat built-in modules can be required without the `node:` specifier-prefix
115+
// (meaning that worker-bundle modules whose names conflict with the Node.js built-ins
116+
// are ignored), and
117+
// (b) The common Node.js globals that we implement are exposed. For instance, `process`
118+
// and `Buffer` will be found at the global scope.
119+
class NodeJsModuleContext: public jsg::Object {
120+
public:
121+
NodeJsModuleContext(jsg::Lock& js, kj::Path path);
122+
123+
v8::Local<v8::Value> require(jsg::Lock& js, kj::String specifier);
124+
v8::Local<v8::Value> getBuffer(jsg::Lock& js);
125+
v8::Local<v8::Value> getProcess(jsg::Lock& js);
126+
127+
// TODO(soon): Implement setImmediate/clearImmediate
128+
129+
jsg::Ref<NodeJsModuleObject> getModule(jsg::Lock& js);
130+
131+
v8::Local<v8::Value> getExports(jsg::Lock& js);
132+
void setExports(jsg::Value value);
133+
134+
kj::String getFilename();
135+
kj::String getDirname();
136+
137+
JSG_RESOURCE_TYPE(NodeJsModuleContext) {
138+
JSG_METHOD(require);
139+
JSG_READONLY_INSTANCE_PROPERTY(module, getModule);
140+
JSG_INSTANCE_PROPERTY(exports, getExports, setExports);
141+
JSG_LAZY_INSTANCE_PROPERTY(Buffer, getBuffer);
142+
JSG_LAZY_INSTANCE_PROPERTY(process, getProcess);
143+
JSG_LAZY_INSTANCE_PROPERTY(__filename, getFilename);
144+
JSG_LAZY_INSTANCE_PROPERTY(__dirname, getDirname);
145+
}
146+
147+
jsg::Ref<NodeJsModuleObject> module;
148+
149+
void visitForMemoryInfo(MemoryTracker& tracker) const {
150+
tracker.trackField("exports", exports);
151+
tracker.trackFieldWithSize("path", path.size());
152+
}
153+
154+
private:
155+
kj::Path path;
156+
jsg::Value exports;
157+
};
158+
159+
} // namespace workerd::jsg

0 commit comments

Comments
 (0)