diff --git a/src/bun.js/bindings/BunPlugin.cpp b/src/bun.js/bindings/BunPlugin.cpp index 1124e61958..d8d95b3415 100644 --- a/src/bun.js/bindings/BunPlugin.cpp +++ b/src/bun.js/bindings/BunPlugin.cpp @@ -28,7 +28,7 @@ #include #include "BunClientData.h" -#include "CommonJSModuleRecord.h" +#include "JSCommonJSModule.h" #include "isBuiltinModule.h" #include "ImportMetaObject.h" diff --git a/src/bun.js/bindings/BunProcess.cpp b/src/bun.js/bindings/BunProcess.cpp index 4713202a83..d3dcefe6c0 100644 --- a/src/bun.js/bindings/BunProcess.cpp +++ b/src/bun.js/bindings/BunProcess.cpp @@ -6,7 +6,7 @@ #include #include #include -#include "CommonJSModuleRecord.h" +#include "JSCommonJSModule.h" #include "ErrorCode+List.h" #include "JavaScriptCore/ArgList.h" #include "JavaScriptCore/CallData.h" diff --git a/src/bun.js/bindings/ImportMetaObject.cpp b/src/bun.js/bindings/ImportMetaObject.cpp index 31aeec3de9..229d735dc0 100644 --- a/src/bun.js/bindings/ImportMetaObject.cpp +++ b/src/bun.js/bindings/ImportMetaObject.cpp @@ -45,7 +45,7 @@ #include #include #include -#include "CommonJSModuleRecord.h" +#include "JSCommonJSModule.h" #include #include "PathInlines.h" #include "wtf/text/StringView.h" diff --git a/src/bun.js/bindings/CommonJSModuleRecord.cpp b/src/bun.js/bindings/JSCommonJSModule.cpp similarity index 99% rename from src/bun.js/bindings/CommonJSModuleRecord.cpp rename to src/bun.js/bindings/JSCommonJSModule.cpp index 02a5450f53..6b200709e2 100644 --- a/src/bun.js/bindings/CommonJSModuleRecord.cpp +++ b/src/bun.js/bindings/JSCommonJSModule.cpp @@ -67,7 +67,7 @@ #include #include "ZigSourceProvider.h" #include -#include "CommonJSModuleRecord.h" +#include "JSCommonJSModule.h" #include #include #include @@ -358,7 +358,7 @@ void RequireFunctionPrototype::finishCreation(JSC::VM& vm) JSC::JSFunction* requireDotMainFunction = JSFunction::create( vm, globalObject, - moduleMainCodeGenerator(vm), + commonJSMainCodeGenerator(vm), globalObject->globalScope()); this->putDirectAccessor( @@ -593,7 +593,7 @@ JSC_DEFINE_CUSTOM_SETTER(setterLoaded, return true; } -JSC_DEFINE_HOST_FUNCTION(functionCommonJSModuleRecord_compile, (JSGlobalObject * globalObject, CallFrame* callframe)) +JSC_DEFINE_HOST_FUNCTION(functionJSCommonJSModule_compile, (JSGlobalObject * globalObject, CallFrame* callframe)) { auto* moduleObject = jsDynamicCast(callframe->thisValue()); if (!moduleObject) { @@ -655,7 +655,7 @@ JSC_DEFINE_HOST_FUNCTION(functionCommonJSModuleRecord_compile, (JSGlobalObject * } static const struct HashTableValue JSCommonJSModulePrototypeTableValues[] = { - { "_compile"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, functionCommonJSModuleRecord_compile, 2 } }, + { "_compile"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, functionJSCommonJSModule_compile, 2 } }, { "children"_s, static_cast(PropertyAttribute::CustomAccessor | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, getterChildren, setterChildren } }, { "filename"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, getterFilename, setterFilename } }, { "id"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, getterId, setterId } }, diff --git a/src/bun.js/bindings/CommonJSModuleRecord.h b/src/bun.js/bindings/JSCommonJSModule.h similarity index 96% rename from src/bun.js/bindings/CommonJSModuleRecord.h rename to src/bun.js/bindings/JSCommonJSModule.h index 76e714b91a..d8843132f3 100644 --- a/src/bun.js/bindings/CommonJSModuleRecord.h +++ b/src/bun.js/bindings/JSCommonJSModule.h @@ -140,10 +140,10 @@ public: return nullptr; return WebCore::subspaceForImpl( vm, - [](auto& spaces) { return spaces.m_clientSubspaceForCommonJSModuleRecord.get(); }, - [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForCommonJSModuleRecord = std::forward(space); }, - [](auto& spaces) { return spaces.m_subspaceForCommonJSModuleRecord.get(); }, - [](auto& spaces, auto&& space) { spaces.m_subspaceForCommonJSModuleRecord = std::forward(space); }); + [](auto& spaces) { return spaces.m_clientSubspaceForJSCommonJSModule.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForJSCommonJSModule = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForJSCommonJSModule.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForJSCommonJSModule = std::forward(space); }); } bool hasEvaluated = false; diff --git a/src/bun.js/bindings/ModuleLoader.cpp b/src/bun.js/bindings/ModuleLoader.cpp index 05e313eef3..baa549bf91 100644 --- a/src/bun.js/bindings/ModuleLoader.cpp +++ b/src/bun.js/bindings/ModuleLoader.cpp @@ -34,7 +34,7 @@ #include #include "../modules/ObjectModule.h" -#include "CommonJSModuleRecord.h" +#include "JSCommonJSModule.h" #include "../modules/_NativeModule.h" namespace Bun { diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 266f101ad4..5ad5e11373 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -65,7 +65,7 @@ #include "BunWorkerGlobalScope.h" #include "CallSite.h" #include "CallSitePrototype.h" -#include "CommonJSModuleRecord.h" +#include "JSCommonJSModule.h" #include "ConsoleObject.h" #include "DOMWrapperWorld-class.h" #include "ErrorStackTrace.h" @@ -2943,7 +2943,7 @@ void GlobalObject::finishCreation(VM& vm) JSC::VM& vm = init.vm; JSC::JSGlobalObject* globalObject = init.owner; - auto* function = JSFunction::create(vm, globalObject, static_cast(importMetaObjectCreateRequireCacheCodeGenerator(vm)), globalObject); + auto* function = JSFunction::create(vm, globalObject, static_cast(commonJSCreateRequireCacheCodeGenerator(vm)), globalObject); NakedPtr returnedException = nullptr; auto result = JSC::profiledCall(globalObject, ProfilingReason::API, function, JSC::getCallData(function), globalObject, ArgList(), returnedException); @@ -3315,7 +3315,7 @@ void GlobalObject::finishCreation(VM& vm) JSFunction::create( init.vm, init.owner, - moduleRequireCodeGenerator(init.vm), + commonJSRequireCodeGenerator(init.vm), init.owner->globalScope(), JSFunction::createStructure(init.vm, init.owner, RequireFunctionPrototype::create(init.owner)))); }); @@ -3326,7 +3326,7 @@ void GlobalObject::finishCreation(VM& vm) JSFunction::create( init.vm, init.owner, - moduleRequireResolveCodeGenerator(init.vm), + commonJSRequireResolveCodeGenerator(init.vm), init.owner->globalScope(), JSFunction::createStructure(init.vm, init.owner, RequireResolveFunctionPrototype::create(init.owner)))); }); @@ -3824,11 +3824,11 @@ void GlobalObject::addBuiltinGlobals(JSC::VM& vm) putDirectBuiltinFunction(vm, this, builtinNames.createEmptyReadableStreamPrivateName(), readableStreamCreateEmptyReadableStreamCodeGenerator(vm), PropertyAttribute::Builtin | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); putDirectBuiltinFunction(vm, this, builtinNames.createUsedReadableStreamPrivateName(), readableStreamCreateUsedReadableStreamCodeGenerator(vm), PropertyAttribute::Builtin | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); putDirectBuiltinFunction(vm, this, builtinNames.createNativeReadableStreamPrivateName(), readableStreamCreateNativeReadableStreamCodeGenerator(vm), PropertyAttribute::Builtin | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); - putDirectBuiltinFunction(vm, this, builtinNames.requireESMPrivateName(), importMetaObjectRequireESMCodeGenerator(vm), PropertyAttribute::Builtin | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); - putDirectBuiltinFunction(vm, this, builtinNames.loadCJS2ESMPrivateName(), importMetaObjectLoadCJS2ESMCodeGenerator(vm), PropertyAttribute::Builtin | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); - putDirectBuiltinFunction(vm, this, builtinNames.internalRequirePrivateName(), importMetaObjectInternalRequireCodeGenerator(vm), PropertyAttribute::Builtin | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); + putDirectBuiltinFunction(vm, this, builtinNames.requireESMPrivateName(), commonJSRequireESMCodeGenerator(vm), PropertyAttribute::Builtin | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); + putDirectBuiltinFunction(vm, this, builtinNames.loadEsmIntoCjsPrivateName(), commonJSLoadEsmIntoCjsCodeGenerator(vm), PropertyAttribute::Builtin | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); + putDirectBuiltinFunction(vm, this, builtinNames.internalRequirePrivateName(), commonJSInternalRequireCodeGenerator(vm), PropertyAttribute::Builtin | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); - putDirectBuiltinFunction(vm, this, builtinNames.overridableRequirePrivateName(), moduleOverridableRequireCodeGenerator(vm), 0); + putDirectBuiltinFunction(vm, this, builtinNames.overridableRequirePrivateName(), commonJSOverridableRequireCodeGenerator(vm), 0); putDirectNativeFunction(vm, this, builtinNames.createUninitializedArrayBufferPrivateName(), 1, functionCreateUninitializedArrayBuffer, ImplementationVisibility::Public, NoIntrinsic, PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); putDirectNativeFunction(vm, this, builtinNames.resolveSyncPrivateName(), 1, functionImportMeta__resolveSyncPrivate, ImplementationVisibility::Public, NoIntrinsic, PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); diff --git a/src/bun.js/bindings/napi.cpp b/src/bun.js/bindings/napi.cpp index 0e8cf0bca5..ecaef3d3c6 100644 --- a/src/bun.js/bindings/napi.cpp +++ b/src/bun.js/bindings/napi.cpp @@ -69,7 +69,7 @@ #include "wtf/NakedPtr.h" #include #include -#include "CommonJSModuleRecord.h" +#include "JSCommonJSModule.h" #include "wtf/text/ASCIIFastPath.h" #include "JavaScriptCore/WeakInlines.h" #include diff --git a/src/bun.js/bindings/v8/node.cpp b/src/bun.js/bindings/v8/node.cpp index d00402ab5b..618ed8ba7a 100644 --- a/src/bun.js/bindings/v8/node.cpp +++ b/src/bun.js/bindings/v8/node.cpp @@ -2,7 +2,7 @@ #include "V8HandleScope.h" #include "JavaScriptCore/ObjectConstructor.h" -#include "CommonJSModuleRecord.h" +#include "JSCommonJSModule.h" #include "node/node_version.h" diff --git a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h index 74ae382bf8..21a4ed1392 100644 --- a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h @@ -36,7 +36,7 @@ public: std::unique_ptr m_clientSubspaceForRequireResolveFunction; std::unique_ptr m_clientSubspaceForBundlerPlugin; std::unique_ptr m_clientSubspaceForNodeVMScript; - std::unique_ptr m_clientSubspaceForCommonJSModuleRecord; + std::unique_ptr m_clientSubspaceForJSCommonJSModule; std::unique_ptr m_clientSubspaceForJSMockImplementation; std::unique_ptr m_clientSubspaceForJSModuleMock; std::unique_ptr m_clientSubspaceForJSMockFunction; diff --git a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h index 8cbd7f7641..ea657733a0 100644 --- a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h @@ -36,7 +36,7 @@ public: std::unique_ptr m_subspaceForRequireResolveFunction; std::unique_ptr m_subspaceForBundlerPlugin; std::unique_ptr m_subspaceForNodeVMScript; - std::unique_ptr m_subspaceForCommonJSModuleRecord; + std::unique_ptr m_subspaceForJSCommonJSModule; std::unique_ptr m_subspaceForJSMockImplementation; std::unique_ptr m_subspaceForJSModuleMock; std::unique_ptr m_subspaceForJSMockFunction; diff --git a/src/bun.js/modules/NodeModuleModule.cpp b/src/bun.js/modules/NodeModuleModule.cpp index be4a440557..e14600539a 100644 --- a/src/bun.js/modules/NodeModuleModule.cpp +++ b/src/bun.js/modules/NodeModuleModule.cpp @@ -658,7 +658,7 @@ JSC_DEFINE_CUSTOM_GETTER(nodeModuleWrapper, { // This does not cache anything because it is assumed nobody reads it more than once. VM& vm = global->vm(); - JSC::JSFunction* cb = JSC::JSFunction::create(vm, global, WebCore::moduleGetWrapperArrayProxyCodeGenerator(vm), global); + JSC::JSFunction* cb = JSC::JSFunction::create(vm, global, WebCore::commonJSGetWrapperArrayProxyCodeGenerator(vm), global); JSC::CallData callData = JSC::getCallData(cb); JSC::MarkedArgumentBuffer args; diff --git a/src/bun.js/modules/NodeModuleModule.h b/src/bun.js/modules/NodeModuleModule.h index 6750ff5ad5..56a9513a50 100644 --- a/src/bun.js/modules/NodeModuleModule.h +++ b/src/bun.js/modules/NodeModuleModule.h @@ -3,7 +3,7 @@ #include "root.h" -#include "CommonJSModuleRecord.h" +#include "JSCommonJSModule.h" #include "ImportMetaObject.h" #include "JavaScriptCore/ArgList.h" #include "JavaScriptCore/JSCJSValue.h" diff --git a/src/js/builtins.d.ts b/src/js/builtins.d.ts index 39cd4e50d3..f6ae715204 100644 --- a/src/js/builtins.d.ts +++ b/src/js/builtins.d.ts @@ -92,7 +92,7 @@ declare function $getInternalField number: N, ): Fields[N]; declare function $fulfillPromise(...args: any[]): TODO; -declare function $loadCJS2ESM(...args: any[]): TODO; +declare function $loadEsmIntoCjs(...args: any[]): TODO; declare function $getGeneratorInternalField(): TODO; declare function $getAsyncGeneratorInternalField(): TODO; declare function $getAbstractModuleRecordInternalField(): TODO; @@ -355,7 +355,7 @@ declare function $importer(): TODO; declare function $inFlightCloseRequest(): TODO; declare function $inFlightWriteRequest(): TODO; declare function $initializeWith(): TODO; -declare function $internalRequire(path: string): TODO; +declare function $internalRequire(id: string, parent: JSCommonJSModule): TODO; declare function $internalStream(): TODO; declare function $internalWritable(): TODO; declare function $isAbortSignal(signal: unknown): signal is AbortSignal; @@ -418,7 +418,7 @@ declare function $releaseLock(): TODO; declare function $removeEventListener(): TODO; declare function $require(): TODO; declare function $requireESM(path: string): any; -declare const $requireMap: Map; +declare const $requireMap: Map; declare const $internalModuleRegistry: InternalFieldObject; declare function $resolve(name: string, from: string): Promise; declare function $resolveSync(name: string, from: string, isESM?: boolean, isUserRequireResolve?: boolean): string; @@ -484,14 +484,14 @@ declare function $createCommonJSModule( id: string, exports: any, hasEvaluated: boolean, - parent: CommonJSModuleRecord, -): CommonJSModuleRecord; + parent: ?JSCommonJSModule, +): JSCommonJSModule; declare function $evaluateCommonJSModule( - moduleToEvaluate: CommonJSModuleRecord, - sourceModule: CommonJSModuleRecord -): CommonJSModuleRecord[]; + moduleToEvaluate: JSCommonJSModule, + sourceModule: JSCommonJSModule +): JSCommonJSModule[]; -declare function $overridableRequire(this: CommonJSModuleRecord, id: string): any; +declare function $overridableRequire(this: JSCommonJSModule, id: string): any; // The following I cannot find any definitions of, but they are functional. declare function $toLength(length: number): number; diff --git a/src/js/builtins/BunBuiltinNames.h b/src/js/builtins/BunBuiltinNames.h index a39525d95c..39f912a38b 100644 --- a/src/js/builtins/BunBuiltinNames.h +++ b/src/js/builtins/BunBuiltinNames.h @@ -154,7 +154,7 @@ using namespace JSC; macro(lazy) \ macro(lazyStreamPrototypeMap) \ macro(lineText) \ - macro(loadCJS2ESM) \ + macro(loadEsmIntoCjs) \ macro(localStreams) \ macro(main) \ macro(makeAbortError) \ diff --git a/src/js/builtins/CommonJS.ts b/src/js/builtins/CommonJS.ts new file mode 100644 index 0000000000..0f42f753ad --- /dev/null +++ b/src/js/builtins/CommonJS.ts @@ -0,0 +1,392 @@ +// This file contains functions used for the CommonJS module loader + +$getter; +export function main() { + return $requireMap.$get(Bun.main); +} + +// This function is bound when constructing instances of CommonJSModule +$visibility = "Private"; +export function require(this: JSCommonJSModule, id: string) { + // Do not use $tailCallForwardArguments here, it causes https://github.com/oven-sh/bun/issues/9225 + return $overridableRequire.$apply(this, arguments); +} + +// overridableRequire can be overridden by setting `Module.prototype.require` +$overriddenName = "require"; +$visibility = "Private"; +export function overridableRequire(this: JSCommonJSModule, originalId: string) { + const id = $resolveSync(originalId, this.filename, false); + if (id.startsWith('node:')) { + if (id !== originalId) { + // A terrible special case where Node.js allows non-prefixed built-ins to + // read the require cache. Though they never write to it, which is so silly. + const existing = $requireMap.$get(originalId); + if (existing) { + const c = $evaluateCommonJSModule(existing, this); + if (c && c.indexOf(existing) === -1) { + c.push(existing); + } + return existing.exports; + } + } + + return this.$requireNativeModule(id); + } else { + const existing = $requireMap.$get(id); + if (existing) { + // Scenario where this is necessary: + // + // In an ES Module, we have: + // + // import "react-dom/server" + // import "react" + // + // Synchronously, the "react" import is created first, and then the + // "react-dom/server" import is created. Then, at ES Module link time, they + // are evaluated. The "react-dom/server" import is evaluated first, and + // require("react") was previously created as an ESM module, so we wait + // for the ESM module to load + // + // ...and then when this code is reached, unless + // we evaluate it "early", we'll get an empty object instead of the module + // exports. + // + const c = $evaluateCommonJSModule(existing, this); + if (c && c.indexOf(existing) === -1) { + c.push(existing); + } + return existing.exports; + } + } + + if (id.endsWith(".node")) { + return $internalRequire(id, this); + } + + if (id === "bun:test") { + return Bun.jest(this.filename); + } + + // To handle import/export cycles, we need to create a module object and put + // it into the map before we import it. + const mod = $createCommonJSModule(id, {}, false, this); + $requireMap.$set(id, mod); + + // This is where we load the module. We will see if Module._load and + // Module._compile are actually important for compatibility. + // + // Note: we do not need to wrap this in a try/catch for release, if it throws + // the C++ code will clear the module from the map. + // + if (IS_BUN_DEVELOPMENT) { + $assert(mod.id === id); + try { + var out = this.$require( + id, + mod, + // did they pass a { type } object? + $argumentCount(), + // the object containing a "type" attribute, if they passed one + // maybe this will be "paths" in the future too. + $argument(1), + ); + } catch (E) { + $assert($requireMap.$get(id) === undefined, "Module " + JSON.stringify(id) + " should no longer be in the map"); + throw E; + } + } else { + var out = this.$require( + id, + mod, + $argumentCount(), + $argument(1), + ); + } + + // -1 means we need to lookup the module from the ESM registry. + if (out === -1) { + try { + out = $requireESM(id); + } catch (exception) { + // Since the ESM code is mostly JS, we need to handle exceptions here. + $requireMap.$delete(id); + throw exception; + } + + const esm = Loader.registry.$get(id); + + // If we can pull out a ModuleNamespaceObject, let's do it. + if (esm?.evaluated && (esm.state ?? 0) >= $ModuleReady) { + const namespace = Loader.getModuleNamespaceObject(esm!.module); + // In Bun, when __esModule is not defined, it's a CustomAccessor on the prototype. + // Various libraries expect __esModule to be set when using ESM from require(). + // We don't want to always inject the __esModule export into every module, + // And creating an Object wrapper causes the actual exports to not be own properties. + // So instead of either of those, we make it so that the __esModule property can be set at runtime. + // It only supports "true" and undefined. Anything non-truthy is treated as undefined. + // https://github.com/oven-sh/bun/issues/14411 + if (namespace.__esModule === undefined) { + try { + namespace.__esModule = true; + } catch { + // https://github.com/oven-sh/bun/issues/17816 + } + } + + return (mod.exports = namespace["module.exports"] ?? namespace); + } + } + + const c = $evaluateCommonJSModule(mod, this); + if (c && c.indexOf(mod) === -1) { + c.push(mod); + } + return mod.exports; +} + +$visibility = "Private"; +export function requireResolve(this: string | { filename?: string; id?: string }, id: string) { + return $resolveSync(id, typeof this === "string" ? this : this?.filename ?? this?.id ?? "", false, true); +} + +$visibility = "Private"; +export function internalRequire(id: string, parent: JSCommonJSModule) { + $assert($requireMap.$get(id) === undefined, "Module " + JSON.stringify(id) + " should not be in the map"); + $assert(id.endsWith(".node")); + + const module = $createCommonJSModule(id, {}, true, parent); + process.dlopen(module, id); + $requireMap.$set(id, module); + return module.exports; +} + + +$visibility = "Private"; +export function loadEsmIntoCjs(resolvedSpecifier: string) { + var loader = Loader; + var queue = $createFIFO(); + let key = resolvedSpecifier; + const registry = loader.registry; + + while (key) { + // we need to explicitly check because state could be $ModuleFetch + // it will throw this error if we do not: + // $throwTypeError("Requested module is already fetched."); + let entry = registry.$get(key)!, + moduleRecordPromise, + state = 0, + // entry.fetch is a Promise + // SourceCode is not a string, it's a JSC::SourceCode object + fetch: Promise | undefined; + + if (entry) { + ({ state, fetch } = entry); + } + + if ( + !entry || + // if we need to fetch it + (state <= $ModuleFetch && + // either: + // - we've never fetched it + // - a fetch is in progress + (!$isPromise(fetch) || + ($getPromiseInternalField(fetch, $promiseFieldFlags) & $promiseStateMask) === $promiseStatePending)) + ) { + // force it to be no longer pending + $fulfillModuleSync(key); + + entry = registry.$get(key)!; + + // the state can transition here + // https://github.com/oven-sh/bun/issues/8965 + if (entry) { + ({ state = 0, fetch } = entry); + } + } + + if (state < $ModuleLink && $isPromise(fetch)) { + // This will probably never happen, but just in case + if (($getPromiseInternalField(fetch, $promiseFieldFlags) & $promiseStateMask) === $promiseStatePending) { + throw new TypeError(`require() async module "${key}" is unsupported. use "await import()" instead.`); + } + + // this pulls it out of the promise without delaying by a tick + // the promise is already fulfilled by $fulfillModuleSync + const sourceCodeObject = $getPromiseInternalField(fetch, $promiseFieldReactionsOrResult); + moduleRecordPromise = loader.parseModule(key, sourceCodeObject); + } + let mod = entry?.module; + + if (moduleRecordPromise && $isPromise(moduleRecordPromise)) { + let reactionsOrResult = $getPromiseInternalField(moduleRecordPromise, $promiseFieldReactionsOrResult); + let flags = $getPromiseInternalField(moduleRecordPromise, $promiseFieldFlags); + let state = flags & $promiseStateMask; + // this branch should never happen, but just to be safe + if (state === $promiseStatePending || (reactionsOrResult && $isPromise(reactionsOrResult))) { + throw new TypeError(`require() async module "${key}" is unsupported. use "await import()" instead.`); + } else if (state === $promiseStateRejected) { + if (!reactionsOrResult?.message) { + throw new TypeError( + `${ + reactionsOrResult + "" ? reactionsOrResult : "An error occurred" + } occurred while parsing module \"${key}\"`, + ); + } + + throw reactionsOrResult; + } + entry.module = mod = reactionsOrResult; + } else if (moduleRecordPromise && !mod) { + entry.module = mod = moduleRecordPromise as LoaderModule; + } + + // This is very similar to "requestInstantiate" in ModuleLoader.js in JavaScriptCore. + $setStateToMax(entry, $ModuleLink); + const dependenciesMap = mod.dependenciesMap; + const requestedModules = loader.requestedModules(mod); + const dependencies = $newArrayWithSize(requestedModules.length); + for (var i = 0, length = requestedModules.length; i < length; ++i) { + const depName = requestedModules[i]; + // optimization: if it starts with a slash then it's an absolute path + // we don't need to run the resolver a 2nd time + const depKey = depName[0] === "/" ? depName : loader.resolve(depName, key); + const depEntry = loader.ensureRegistered(depKey); + + if (depEntry.state < $ModuleLink) { + queue.push(depKey); + } + + $putByValDirect(dependencies, i, depEntry); + dependenciesMap.$set(depName, depEntry); + } + + entry.dependencies = dependencies; + // All dependencies resolved, set instantiate and satisfy field directly. + entry.instantiate = Promise.$resolve(entry); + entry.satisfy = Promise.$resolve(entry); + entry.isSatisfied = true; + + key = queue.shift(); + while (key && (registry.$get(key)?.state ?? $ModuleFetch) >= $ModuleLink) { + key = queue.shift(); + } + } + + var linkAndEvaluateResult = loader.linkAndEvaluateModule(resolvedSpecifier, undefined); + if (linkAndEvaluateResult && $isPromise(linkAndEvaluateResult)) { + // if you use top-level await, or any dependencies use top-level await, then we throw here + // this means the module will still actually load eventually, but that's okay. + throw new TypeError( + `require() async module \"${resolvedSpecifier}\" is unsupported. use "await import()" instead.`, + ); + } + + return registry.$get(resolvedSpecifier); +} + +$visibility = "Private"; +export function requireESM(this, resolved: string) { + var entry = Loader.registry.$get(resolved); + + if (!entry || !entry.evaluated) { + entry = $loadEsmIntoCjs(resolved); + } + + if (!entry || !entry.evaluated || !entry.module) { + throw new TypeError(`require() failed to evaluate module "${resolved}". This is an internal consistentency error.`); + } + var exports = Loader.getModuleNamespaceObject(entry.module); + + return exports; +} + +$visibility = "Private"; +export function createRequireCache() { + var moduleMap = new Map(); + var inner = { + [Symbol.for("nodejs.util.inspect.custom")]() { + return { ...proxy }; + }, + }; + var proxy = new Proxy(inner, { + get(target, key: string) { + const entry = $requireMap.$get(key); + if (entry) return entry; + + const esm = Loader.registry.$get(key); + if (esm?.evaluated) { + const namespace = Loader.getModuleNamespaceObject(esm.module); + const mod = $createCommonJSModule(key, namespace, true, undefined); + $requireMap.$set(key, mod); + return mod; + } + + return inner[key]; + }, + set(target, key: string, value) { + $requireMap.$set(key, value); + return true; + }, + + has(target, key: string) { + return $requireMap.$has(key) || Boolean(Loader.registry.$get(key)?.evaluated); + }, + + deleteProperty(target, key: string) { + moduleMap.$delete(key); + $requireMap.$delete(key); + Loader.registry.$delete(key); + return true; + }, + + ownKeys(target) { + var array = [...$requireMap.$keys()]; + for (const key of Loader.registry.$keys()) { + if (!array.includes(key) && Loader.registry.$get(key)?.evaluated) { + $arrayPush(array, key); + } + } + return array; + }, + + // In Node, require.cache has a null prototype + getPrototypeOf(target) { + return null; + }, + + getOwnPropertyDescriptor(target, key: string) { + if ($requireMap.$has(key) || Loader.registry.$get(key)?.evaluated) { + return { + configurable: true, + enumerable: true, + }; + } + }, + }); + + return proxy; +} + +type WrapperMutate = (start: string, end: string) => void; +export function getWrapperArrayProxy(onMutate: WrapperMutate) { + const wrapper = ["(function(exports,require,module,__filename,__dirname){", "})"]; + return new Proxy(wrapper, { + set(target, prop, value, receiver) { + Reflect.set(target, prop, value, receiver); + onMutate(wrapper[0], wrapper[1]); + return true; + }, + defineProperty(target, prop, descriptor) { + Reflect.defineProperty(target, prop, descriptor); + onMutate(wrapper[0], wrapper[1]); + return true; + }, + deleteProperty(target, prop) { + Reflect.deleteProperty(target, prop); + onMutate(wrapper[0], wrapper[1]); + return true; + }, + }); +} diff --git a/src/js/builtins/ImportMetaObject.ts b/src/js/builtins/ImportMetaObject.ts index 7e06be69a5..ca8be76176 100644 --- a/src/js/builtins/ImportMetaObject.ts +++ b/src/js/builtins/ImportMetaObject.ts @@ -1,247 +1,5 @@ type ImportMetaObject = Partial; -$visibility = "Private"; -export function loadCJS2ESM(this: ImportMetaObject, resolvedSpecifier: string) { - var loader = Loader; - var queue = $createFIFO(); - let key = resolvedSpecifier; - const registry = loader.registry; - - while (key) { - // we need to explicitly check because state could be $ModuleFetch - // it will throw this error if we do not: - // $throwTypeError("Requested module is already fetched."); - let entry = registry.$get(key)!, - moduleRecordPromise, - state = 0, - // entry.fetch is a Promise - // SourceCode is not a string, it's a JSC::SourceCode object - fetch: Promise | undefined; - - if (entry) { - ({ state, fetch } = entry); - } - - if ( - !entry || - // if we need to fetch it - (state <= $ModuleFetch && - // either: - // - we've never fetched it - // - a fetch is in progress - (!$isPromise(fetch) || - ($getPromiseInternalField(fetch, $promiseFieldFlags) & $promiseStateMask) === $promiseStatePending)) - ) { - // force it to be no longer pending - $fulfillModuleSync(key); - - entry = registry.$get(key)!; - - // the state can transition here - // https://github.com/oven-sh/bun/issues/8965 - if (entry) { - ({ state = 0, fetch } = entry); - } - } - - if (state < $ModuleLink && $isPromise(fetch)) { - // This will probably never happen, but just in case - if (($getPromiseInternalField(fetch, $promiseFieldFlags) & $promiseStateMask) === $promiseStatePending) { - throw new TypeError(`require() async module "${key}" is unsupported. use "await import()" instead.`); - } - - // this pulls it out of the promise without delaying by a tick - // the promise is already fulfilled by $fulfillModuleSync - const sourceCodeObject = $getPromiseInternalField(fetch, $promiseFieldReactionsOrResult); - moduleRecordPromise = loader.parseModule(key, sourceCodeObject); - } - let mod = entry?.module; - - if (moduleRecordPromise && $isPromise(moduleRecordPromise)) { - let reactionsOrResult = $getPromiseInternalField(moduleRecordPromise, $promiseFieldReactionsOrResult); - let flags = $getPromiseInternalField(moduleRecordPromise, $promiseFieldFlags); - let state = flags & $promiseStateMask; - // this branch should never happen, but just to be safe - if (state === $promiseStatePending || (reactionsOrResult && $isPromise(reactionsOrResult))) { - throw new TypeError(`require() async module "${key}" is unsupported. use "await import()" instead.`); - } else if (state === $promiseStateRejected) { - if (!reactionsOrResult?.message) { - throw new TypeError( - `${ - reactionsOrResult + "" ? reactionsOrResult : "An error occurred" - } occurred while parsing module \"${key}\"`, - ); - } - - throw reactionsOrResult; - } - entry.module = mod = reactionsOrResult; - } else if (moduleRecordPromise && !mod) { - entry.module = mod = moduleRecordPromise as LoaderModule; - } - - // This is very similar to "requestInstantiate" in ModuleLoader.js in JavaScriptCore. - $setStateToMax(entry, $ModuleLink); - const dependenciesMap = mod.dependenciesMap; - const requestedModules = loader.requestedModules(mod); - const dependencies = $newArrayWithSize(requestedModules.length); - for (var i = 0, length = requestedModules.length; i < length; ++i) { - const depName = requestedModules[i]; - // optimization: if it starts with a slash then it's an absolute path - // we don't need to run the resolver a 2nd time - const depKey = depName[0] === "/" ? depName : loader.resolve(depName, key); - const depEntry = loader.ensureRegistered(depKey); - - if (depEntry.state < $ModuleLink) { - queue.push(depKey); - } - - $putByValDirect(dependencies, i, depEntry); - dependenciesMap.$set(depName, depEntry); - } - - entry.dependencies = dependencies; - // All dependencies resolved, set instantiate and satisfy field directly. - entry.instantiate = Promise.$resolve(entry); - entry.satisfy = Promise.$resolve(entry); - entry.isSatisfied = true; - - key = queue.shift(); - while (key && (registry.$get(key)?.state ?? $ModuleFetch) >= $ModuleLink) { - key = queue.shift(); - } - } - - var linkAndEvaluateResult = loader.linkAndEvaluateModule(resolvedSpecifier, undefined); - if (linkAndEvaluateResult && $isPromise(linkAndEvaluateResult)) { - // if you use top-level await, or any dependencies use top-level await, then we throw here - // this means the module will still actually load eventually, but that's okay. - throw new TypeError( - `require() async module \"${resolvedSpecifier}\" is unsupported. use "await import()" instead.`, - ); - } - - return registry.$get(resolvedSpecifier); -} - -$visibility = "Private"; -export function requireESM(this: ImportMetaObject, resolved: string) { - var entry = Loader.registry.$get(resolved); - - if (!entry || !entry.evaluated) { - entry = $loadCJS2ESM(resolved); - } - - if (!entry || !entry.evaluated || !entry.module) { - throw new TypeError(`require() failed to evaluate module "${resolved}". This is an internal consistentency error.`); - } - var exports = Loader.getModuleNamespaceObject(entry.module); - - return exports; -} - -$visibility = "Private"; -export function internalRequire(this: ImportMetaObject, id) { - var cached = $requireMap.$get(id); - const last5 = id.substring(id.length - 5); - if (cached) { - return cached.exports; - } - - // TODO: remove this hardcoding - if (last5 === ".json" && !id.endsWith?.("package.json")) { - var fs = (globalThis[Symbol.for("_fs")] ||= Bun.fs()); - var exports = JSON.parse(fs.readFileSync(id, "utf8")); - $requireMap.$set(id, $createCommonJSModule(id, exports, true, undefined)); - return exports; - } else if (last5 === ".node") { - const module = $createCommonJSModule(id, {}, true, undefined); - process.dlopen(module, id); - $requireMap.$set(id, module); - return module.exports; - } else if (last5 === ".toml") { - var fs = (globalThis[Symbol.for("_fs")] ||= Bun.fs()); - var exports = Bun.TOML.parse(fs.readFileSync(id, "utf8")); - $requireMap.$set(id, $createCommonJSModule(id, exports, true, undefined)); - return exports; - } else { - var exports = $requireESM(id); - const cachedModule = $requireMap.$get(id); - if (cachedModule) { - return cachedModule.exports; - } - $requireMap.$set(id, $createCommonJSModule(id, exports, true, undefined)); - return exports; - } -} - -$visibility = "Private"; -export function createRequireCache() { - var moduleMap = new Map(); - var inner = { - [Symbol.for("nodejs.util.inspect.custom")]() { - return { ...proxy }; - }, - }; - var proxy = new Proxy(inner, { - get(target, key: string) { - const entry = $requireMap.$get(key); - if (entry) return entry; - - const esm = Loader.registry.$get(key); - if (esm?.evaluated) { - const namespace = Loader.getModuleNamespaceObject(esm.module); - const mod = $createCommonJSModule(key, namespace, true, undefined); - $requireMap.$set(key, mod); - return mod; - } - - return inner[key]; - }, - set(target, key: string, value) { - $requireMap.$set(key, value); - return true; - }, - - has(target, key: string) { - return $requireMap.$has(key) || Boolean(Loader.registry.$get(key)?.evaluated); - }, - - deleteProperty(target, key: string) { - moduleMap.$delete(key); - $requireMap.$delete(key); - Loader.registry.$delete(key); - return true; - }, - - ownKeys(target) { - var array = [...$requireMap.$keys()]; - for (const key of Loader.registry.$keys()) { - if (!array.includes(key) && Loader.registry.$get(key)?.evaluated) { - $arrayPush(array, key); - } - } - return array; - }, - - // In Node, require.cache has a null prototype - getPrototypeOf(target) { - return null; - }, - - getOwnPropertyDescriptor(target, key: string) { - if ($requireMap.$has(key) || Loader.registry.$get(key)?.evaluated) { - return { - configurable: true, - enumerable: true, - }; - } - }, - }); - - return proxy; -} - $getter; export function main(this: ImportMetaObject) { return this.path === Bun.main && Bun.isMainThread; diff --git a/src/js/builtins/Module.ts b/src/js/builtins/Module.ts deleted file mode 100644 index 6233ce6307..0000000000 --- a/src/js/builtins/Module.ts +++ /dev/null @@ -1,170 +0,0 @@ -$getter; -export function main() { - return $requireMap.$get(Bun.main); -} - -$visibility = "Private"; -export function require(this: CommonJSModuleRecord, id: string) { - // Do not use $tailCallForwardArguments here, it causes https://github.com/oven-sh/bun/issues/9225 - return $overridableRequire.$apply(this, arguments); -} - -// overridableRequire can be overridden by setting `Module.prototype.require` -$overriddenName = "require"; -$visibility = "Private"; -export function overridableRequire(this: CommonJSModuleRecord, originalId: string) { - const id = $resolveSync(originalId, this.filename, false); - if (id.startsWith('node:')) { - if (id !== originalId) { - // A terrible special case where Node.js allows non-prefixed built-ins to - // read the require cache. Though they never write to it, which is so silly. - const existing = $requireMap.$get(originalId); - if (existing) { - const c = $evaluateCommonJSModule(existing, this); - if (c && c.indexOf(existing) === -1) { - c.push(existing); - } - return existing.exports; - } - } - - return this.$requireNativeModule(id); - } else { - const existing = $requireMap.$get(id); - if (existing) { - // Scenario where this is necessary: - // - // In an ES Module, we have: - // - // import "react-dom/server" - // import "react" - // - // Synchronously, the "react" import is created first, and then the - // "react-dom/server" import is created. Then, at ES Module link time, they - // are evaluated. The "react-dom/server" import is evaluated first, and - // require("react") was previously created as an ESM module, so we wait - // for the ESM module to load - // - // ...and then when this code is reached, unless - // we evaluate it "early", we'll get an empty object instead of the module - // exports. - // - const c = $evaluateCommonJSModule(existing, this); - if (c && c.indexOf(existing) === -1) { - c.push(existing); - } - return existing.exports; - } - } - - if (id.endsWith(".node")) { - return $internalRequire(id); - } - - if (id === "bun:test") { - return Bun.jest(this.filename); - } - - // To handle import/export cycles, we need to create a module object and put - // it into the map before we import it. - const mod = $createCommonJSModule(id, {}, false, this); - $requireMap.$set(id, mod); - - // This is where we load the module. We will see if Module._load and - // Module._compile are actually important for compatibility. - // - // Note: we do not need to wrap this in a try/catch for release, if it throws - // the C++ code will clear the module from the map. - // - if (IS_BUN_DEVELOPMENT) { - $assert(mod.id === id); - try { - var out = this.$require( - id, - mod, - // did they pass a { type } object? - $argumentCount(), - // the object containing a "type" attribute, if they passed one - // maybe this will be "paths" in the future too. - $argument(1), - ); - } catch (E) { - $assert($requireMap.$get(id) === undefined, "Module " + JSON.stringify(id) + " should no longer be in the map"); - throw E; - } - } else { - var out = this.$require( - id, - mod, - $argumentCount(), - $argument(1), - ); - } - - // -1 means we need to lookup the module from the ESM registry. - if (out === -1) { - try { - out = $requireESM(id); - } catch (exception) { - // Since the ESM code is mostly JS, we need to handle exceptions here. - $requireMap.$delete(id); - throw exception; - } - - const esm = Loader.registry.$get(id); - - // If we can pull out a ModuleNamespaceObject, let's do it. - if (esm?.evaluated && (esm.state ?? 0) >= $ModuleReady) { - const namespace = Loader.getModuleNamespaceObject(esm!.module); - // In Bun, when __esModule is not defined, it's a CustomAccessor on the prototype. - // Various libraries expect __esModule to be set when using ESM from require(). - // We don't want to always inject the __esModule export into every module, - // And creating an Object wrapper causes the actual exports to not be own properties. - // So instead of either of those, we make it so that the __esModule property can be set at runtime. - // It only supports "true" and undefined. Anything non-truthy is treated as undefined. - // https://github.com/oven-sh/bun/issues/14411 - if (namespace.__esModule === undefined) { - try { - namespace.__esModule = true; - } catch { - // https://github.com/oven-sh/bun/issues/17816 - } - } - - return (mod.exports = namespace["module.exports"] ?? namespace); - } - } - - const c = $evaluateCommonJSModule(mod, this); - if (c && c.indexOf(mod) === -1) { - c.push(mod); - } - return mod.exports; -} - -$visibility = "Private"; -export function requireResolve(this: string | { filename?: string; id?: string }, id: string) { - return $resolveSync(id, typeof this === "string" ? this : this?.filename ?? this?.id ?? "", false, true); -} - -type WrapperMutate = (start: string, end: string) => void; -export function getWrapperArrayProxy(onMutate: WrapperMutate) { - const wrapper = ["(function(exports,require,module,__filename,__dirname){", "})"]; - return new Proxy(wrapper, { - set(target, prop, value, receiver) { - Reflect.set(target, prop, value, receiver); - onMutate(wrapper[0], wrapper[1]); - return true; - }, - defineProperty(target, prop, descriptor) { - Reflect.defineProperty(target, prop, descriptor); - onMutate(wrapper[0], wrapper[1]); - return true; - }, - deleteProperty(target, prop) { - Reflect.deleteProperty(target, prop); - onMutate(wrapper[0], wrapper[1]); - return true; - }, - }); -} diff --git a/src/js/private.d.ts b/src/js/private.d.ts index 052e58bbe1..c12ec7f6a9 100644 --- a/src/js/private.d.ts +++ b/src/js/private.d.ts @@ -149,10 +149,10 @@ declare interface Error { code?: string; } -interface CommonJSModuleRecord { +interface JSCommonJSModule { $require(id: string, mod: any, args_count: number, args: Array): any; $requireNativeModule(id: string): any; - children: CommonJSModuleRecord[]; + children: JSCommonJSModule[]; exports: any; id: string; loaded: boolean;