-
Notifications
You must be signed in to change notification settings - Fork 334
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Separate commonjs-related types from jsg/modules.h/c++ (#3298)
- Loading branch information
Showing
5 changed files
with
329 additions
and
294 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.