From c29933f823c1fd613d96dc181f03c58df111028b Mon Sep 17 00:00:00 2001 From: chloe caruso Date: Tue, 1 Apr 2025 14:31:16 -0700 Subject: [PATCH] implement require.extensions attempt 2 (#18686) --- src/bun.js/bindings/BunProcess.h | 4 +- src/bun.js/bindings/JSCommonJSExtensions.cpp | 290 ++++++++++++++++++ src/bun.js/bindings/JSCommonJSExtensions.h | 58 ++++ src/bun.js/bindings/JSCommonJSModule.cpp | 103 ++++++- src/bun.js/bindings/JSCommonJSModule.h | 5 + src/bun.js/bindings/ModuleLoader.cpp | 114 ++++++- src/bun.js/bindings/ModuleLoader.h | 17 +- src/bun.js/bindings/NodeModuleModule.zig | 151 +++++++++ src/bun.js/bindings/ResolvedSource.zig | 6 +- src/bun.js/bindings/ZigGlobalObject.cpp | 14 +- src/bun.js/bindings/ZigGlobalObject.h | 13 + src/bun.js/bindings/bindings.zig | 40 +-- src/bun.js/bindings/headers-handwritten.h | 7 +- .../bindings/webcore/DOMClientIsoSubspaces.h | 1 + src/bun.js/bindings/webcore/DOMIsoSubspaces.h | 1 + src/bun.js/javascript.zig | 17 +- src/bun.js/module_loader.zig | 125 ++++++-- src/bun.js/modules/NodeModuleModule.cpp | 50 ++- src/bun.zig | 2 +- src/codegen/bundle-modules.ts | 7 +- src/js/builtins.d.ts | 2 +- src/js/builtins/CommonJS.ts | 35 +++ src/js_parser.zig | 11 +- src/options.zig | 19 +- src/resolver/package_json.zig | 4 +- src/resolver/resolver.zig | 159 ++++++---- src/transpiler.zig | 6 +- .../cli/run/module-type-fixture/cjs/hello.cjs | 1 + .../cli/run/module-type-fixture/cjs/hello.cts | 1 + test/cli/run/module-type-fixture/cjs/hello.js | 1 + .../cli/run/module-type-fixture/cjs/hello.jsx | 1 + .../cli/run/module-type-fixture/cjs/hello.mjs | 1 + .../cli/run/module-type-fixture/cjs/hello.mts | 1 + test/cli/run/module-type-fixture/cjs/hello.ts | 1 + .../cli/run/module-type-fixture/cjs/hello.tsx | 1 + .../run/module-type-fixture/cjs/import.cjs | 3 + .../run/module-type-fixture/cjs/package.json | 3 + .../cli/run/module-type-fixture/esm/hello.cjs | 1 + .../cli/run/module-type-fixture/esm/hello.cts | 1 + test/cli/run/module-type-fixture/esm/hello.js | 1 + .../cli/run/module-type-fixture/esm/hello.jsx | 1 + .../cli/run/module-type-fixture/esm/hello.mjs | 1 + .../cli/run/module-type-fixture/esm/hello.mts | 1 + test/cli/run/module-type-fixture/esm/hello.ts | 1 + .../cli/run/module-type-fixture/esm/hello.tsx | 1 + .../run/module-type-fixture/esm/import.cjs | 3 + .../run/module-type-fixture/esm/package.json | 3 + test/cli/run/run-detect-module-type.ts | 54 ++++ test/internal/ban-words.test.ts | 4 +- .../node/module/extensions-fixture/a.custom | 1 + test/js/node/module/extensions-fixture/a.js | 1 + test/js/node/module/extensions-fixture/a.json | 1 + test/js/node/module/extensions-fixture/a.ts | 4 + test/js/node/module/extensions-fixture/b.json | 1 + .../node/module/extensions-fixture/c.custom | 1 + test/js/node/module/extensions-fixture/d.js | 1 + test/js/node/module/extensions-fixture/e.js | 4 + .../extensions-fixture/secretly_esm.cjs | 1 + .../js/node/module/require-extensions.test.ts | 187 +++++++++++ .../node/test/test-module-multi-extensions.js | 93 ++++++ 60 files changed, 1448 insertions(+), 194 deletions(-) create mode 100644 src/bun.js/bindings/JSCommonJSExtensions.cpp create mode 100644 src/bun.js/bindings/JSCommonJSExtensions.h create mode 100644 test/cli/run/module-type-fixture/cjs/hello.cjs create mode 100644 test/cli/run/module-type-fixture/cjs/hello.cts create mode 100644 test/cli/run/module-type-fixture/cjs/hello.js create mode 100644 test/cli/run/module-type-fixture/cjs/hello.jsx create mode 100644 test/cli/run/module-type-fixture/cjs/hello.mjs create mode 100644 test/cli/run/module-type-fixture/cjs/hello.mts create mode 100644 test/cli/run/module-type-fixture/cjs/hello.ts create mode 100644 test/cli/run/module-type-fixture/cjs/hello.tsx create mode 100644 test/cli/run/module-type-fixture/cjs/import.cjs create mode 100644 test/cli/run/module-type-fixture/cjs/package.json create mode 100644 test/cli/run/module-type-fixture/esm/hello.cjs create mode 100644 test/cli/run/module-type-fixture/esm/hello.cts create mode 100644 test/cli/run/module-type-fixture/esm/hello.js create mode 100644 test/cli/run/module-type-fixture/esm/hello.jsx create mode 100644 test/cli/run/module-type-fixture/esm/hello.mjs create mode 100644 test/cli/run/module-type-fixture/esm/hello.mts create mode 100644 test/cli/run/module-type-fixture/esm/hello.ts create mode 100644 test/cli/run/module-type-fixture/esm/hello.tsx create mode 100644 test/cli/run/module-type-fixture/esm/import.cjs create mode 100644 test/cli/run/module-type-fixture/esm/package.json create mode 100644 test/cli/run/run-detect-module-type.ts create mode 100644 test/js/node/module/extensions-fixture/a.custom create mode 100644 test/js/node/module/extensions-fixture/a.js create mode 100644 test/js/node/module/extensions-fixture/a.json create mode 100644 test/js/node/module/extensions-fixture/a.ts create mode 100644 test/js/node/module/extensions-fixture/b.json create mode 100644 test/js/node/module/extensions-fixture/c.custom create mode 100644 test/js/node/module/extensions-fixture/d.js create mode 100644 test/js/node/module/extensions-fixture/e.js create mode 100644 test/js/node/module/extensions-fixture/secretly_esm.cjs create mode 100644 test/js/node/module/require-extensions.test.ts create mode 100644 test/js/node/test/test-module-multi-extensions.js diff --git a/src/bun.js/bindings/BunProcess.h b/src/bun.js/bindings/BunProcess.h index b9f7bd8265..589105f521 100644 --- a/src/bun.js/bindings/BunProcess.h +++ b/src/bun.js/bindings/BunProcess.h @@ -11,11 +11,10 @@ class GlobalObject; } namespace Bun { +using namespace JSC; extern "C" int getRSS(size_t* rss); -using namespace JSC; - class Process : public WebCore::JSEventEmitter { using Base = WebCore::JSEventEmitter; @@ -110,5 +109,6 @@ public: }; bool isSignalName(WTF::String input); +JSC_DECLARE_HOST_FUNCTION(Process_functionDlopen); } // namespace Bun diff --git a/src/bun.js/bindings/JSCommonJSExtensions.cpp b/src/bun.js/bindings/JSCommonJSExtensions.cpp new file mode 100644 index 0000000000..c8db8f7d68 --- /dev/null +++ b/src/bun.js/bindings/JSCommonJSExtensions.cpp @@ -0,0 +1,290 @@ +#include "JSCommonJSExtensions.h" +#include "ZigGlobalObject.h" +#include "BunProcess.h" +#include "ModuleLoader.h" +#include "JSCommonJSModule.h" + +namespace Bun { +using namespace JSC; + +const JSC::ClassInfo JSCommonJSExtensions::s_info = { "CommonJSExtensions"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(JSCommonJSExtensions) }; + +JSC::EncodedJSValue builtinLoader(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame, BunLoaderType loaderType); + +// These functions are separate so that assigning one to the other can be +// detected and use the corresponding loader. +JSC_DEFINE_HOST_FUNCTION(jsLoaderJS, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + return builtinLoader(globalObject, callFrame, BunLoaderTypeJS); +} +JSC_DEFINE_HOST_FUNCTION(jsLoaderTS, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + return builtinLoader(globalObject, callFrame, BunLoaderTypeTS); +} +JSC_DEFINE_HOST_FUNCTION(jsLoaderJSON, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) +{ + return builtinLoader(globalObject, callFrame, BunLoaderTypeJSON); +} +#define jsLoaderNode Process_functionDlopen + +// The few places that call the above functions directly are usually because the +// developer is using a package to allow injecting a transpiler into Node.js. An +// example is the Next.js require extensions hook: +// +// const oldJSHook = require.extensions['.js']; +// require.extensions['.js'] = function(mod, filename) { +// try { +// return oldJSHook(mod, filename); +// } catch (error) { +// if (error.code !== 'ERR_REQUIRE_ESM') { +// throw error; +// } +// const content = readFileSync(filename, 'utf8'); +// const { code } = transformSync(content, swcOptions); +// mod._compile(code, filename); +// } +// }; +// +// These sorts of hooks don't do their intended purpose. Since Bun has always +// supported requiring ESM+TypeScript+JSX, errors are never thrown. This +// is just asking to make the developer experience worse. +// +// Since developers are not even aware of some of these hooks, some are disabled +// automatically. Some hooks have genuine use cases, such as adding new loaders. +bool isAllowedToMutateExtensions(JSC::JSGlobalObject* globalObject) +{ + JSC::VM& vm = globalObject->vm(); + WTF::Vector stackFrames; + vm.interpreter.getStackTrace(globalObject, stackFrames, 0, 1); + if (stackFrames.size() == 0) return true; + JSC::StackFrame& frame = stackFrames[0]; + + WTF::String url = frame.sourceURL(vm); + if (!url) return true; + +#if OS(WINDOWS) +#define CHECK_PATH(url, _, windows) (url.contains(windows)) +#else +#define CHECK_PATH(url, posix, _) (url.contains(posix)) +#endif + + // When adding to this list, please comment why the package is using extensions incorrectly. + if (CHECK_PATH(url, "dist/build/next-config-ts/"_s, "dist\\build\\next-config-ts\\"_s)) + return false; // Next.js adds SWC support to add features Bun already has. + if (CHECK_PATH(url, "@meteorjs/babel"_s, "@meteorjs\\babel"_s)) + return false; // Wraps existing loaders to use Babel. + // NOTE: @babel/core is not on this list because it checks if extensions[".ts"] exists + // before adding it's own. + // NOTE: vitest uses extensions correctly + // NOTE: vite doesn't need to use extensions, but blocking them would make + // it slower as they already bundle the code before injecting the hook. + +#undef CHECK_PATH + return true; +} + +void JSCommonJSExtensions::finishCreation(JSC::VM& vm) +{ + Base::finishCreation(vm); + ASSERT(inherits(info())); + + Zig::GlobalObject* global = defaultGlobalObject(globalObject()); + JSC::JSFunction* fnLoadJS = JSC::JSFunction::create( + vm, + global, + 2, + ""_s, + jsLoaderJS, + JSC::ImplementationVisibility::Public, + JSC::Intrinsic::NoIntrinsic, + JSC::callHostFunctionAsConstructor); + JSC::JSFunction* fnLoadJSON = JSC::JSFunction::create( + vm, + global, + 2, + ""_s, + jsLoaderJSON, + JSC::ImplementationVisibility::Public, + JSC::Intrinsic::NoIntrinsic, + JSC::callHostFunctionAsConstructor); + JSC::JSFunction* fnLoadNode = JSC::JSFunction::create( + vm, + global, + 2, + ""_s, + jsLoaderNode, + JSC::ImplementationVisibility::Public, + JSC::Intrinsic::NoIntrinsic, + JSC::callHostFunctionAsConstructor); + JSC::JSFunction* fnLoadTS = JSC::JSFunction::create( + vm, + global, + 2, + ""_s, + jsLoaderTS, + JSC::ImplementationVisibility::Public, + JSC::Intrinsic::NoIntrinsic, + JSC::callHostFunctionAsConstructor); + + this->putDirect(vm, JSC::Identifier::fromString(vm, ".js"_s), fnLoadJS, 0); + this->putDirect(vm, JSC::Identifier::fromString(vm, ".json"_s), fnLoadJSON, 0); + this->putDirect(vm, JSC::Identifier::fromString(vm, ".node"_s), fnLoadNode, 0); + this->putDirect(vm, JSC::Identifier::fromString(vm, ".ts"_s), fnLoadTS, 0); + this->putDirect(vm, JSC::Identifier::fromString(vm, ".cts"_s), fnLoadTS, 0); + this->putDirect(vm, JSC::Identifier::fromString(vm, ".mjs"_s), fnLoadJS, 0); + this->putDirect(vm, JSC::Identifier::fromString(vm, ".mts"_s), fnLoadTS, 0); +} + +extern "C" void NodeModuleModule__onRequireExtensionModify( + Zig::GlobalObject* globalObject, + const BunString* key, + uint32_t kind, + JSC::JSValue value); + +void onAssign(Zig::GlobalObject* globalObject, JSC::PropertyName propertyName, JSC::JSValue value) +{ + if (propertyName.isSymbol()) return; + auto* name = propertyName.publicName(); + if (!name->startsWith('.')) return; + BunString ext = Bun::toString(name); + uint32_t kind = 0; + JSC::CallData callData = JSC::getCallData(value); + if (callData.type == JSC::CallData::Type::Native) { + auto* untaggedPtr = callData.native.function.untaggedPtr(); + if (untaggedPtr == &jsLoaderJS) { + kind = 1; + } else if (untaggedPtr == &jsLoaderJSON) { + kind = 2; + } else if (untaggedPtr == &jsLoaderNode) { + kind = 3; + } else if (untaggedPtr == &jsLoaderTS) { + kind = 4; + } + } else if (callData.type == JSC::CallData::Type::None) { + kind = -1; + } + NodeModuleModule__onRequireExtensionModify(globalObject, &ext, kind, value); +} + +bool JSCommonJSExtensions::defineOwnProperty(JSC::JSObject* object, JSC::JSGlobalObject* globalObject, JSC::PropertyName propertyName, const JSC::PropertyDescriptor& descriptor, bool shouldThrow) +{ + if (!isAllowedToMutateExtensions(globalObject)) return true; + JSValue value = descriptor.value(); + if (value) { + onAssign(defaultGlobalObject(globalObject), propertyName, value); + } else { + onAssign(defaultGlobalObject(globalObject), propertyName, JSC::jsUndefined()); + } + return Base::defineOwnProperty(object, globalObject, propertyName, descriptor, shouldThrow); +} + +bool JSCommonJSExtensions::put(JSC::JSCell* cell, JSC::JSGlobalObject* globalObject, JSC::PropertyName propertyName, JSC::JSValue value, JSC::PutPropertySlot& slot) +{ + if (!isAllowedToMutateExtensions(globalObject)) return true; + onAssign(defaultGlobalObject(globalObject), propertyName, value); + return Base::put(cell, globalObject, propertyName, value, slot); +} + +bool JSCommonJSExtensions::deleteProperty(JSC::JSCell* cell, JSC::JSGlobalObject* globalObject, JSC::PropertyName propertyName, JSC::DeletePropertySlot& slot) +{ + if (!isAllowedToMutateExtensions(globalObject)) return true; + bool deleted = Base::deleteProperty(cell, globalObject, propertyName, slot); + if (deleted) { + onAssign(defaultGlobalObject(globalObject), propertyName, JSC::jsUndefined()); + } + return deleted; +} + +extern "C" uint32_t JSCommonJSExtensions__appendFunction(Zig::GlobalObject* globalObject, JSC::JSValue value) +{ + JSCommonJSExtensions* extensions = globalObject->lazyRequireExtensionsObject(); + extensions->m_registeredFunctions.append(JSC::WriteBarrier()); + extensions->m_registeredFunctions.last().set(globalObject->vm(), extensions, value); + return extensions->m_registeredFunctions.size() - 1; +} + +extern "C" void JSCommonJSExtensions__setFunction(Zig::GlobalObject* globalObject, uint32_t index, JSC::JSValue value) +{ + JSCommonJSExtensions* extensions = globalObject->lazyRequireExtensionsObject(); + extensions->m_registeredFunctions[index].set(globalObject->vm(), globalObject, value); +} + +extern "C" uint32_t JSCommonJSExtensions__swapRemove(Zig::GlobalObject* globalObject, uint32_t index) +{ + JSCommonJSExtensions* extensions = globalObject->lazyRequireExtensionsObject(); + ASSERT(extensions->m_registeredFunctions.size() > 0); + if (extensions->m_registeredFunctions.size() == 1) { + extensions->m_registeredFunctions.clear(); + return index; + } + ASSERT(index < extensions->m_registeredFunctions.size()); + if (index < (extensions->m_registeredFunctions.size() - 1)) { + JSValue last = extensions->m_registeredFunctions.takeLast().get(); + extensions->m_registeredFunctions[index].set(globalObject->vm(), globalObject, last); + return extensions->m_registeredFunctions.size(); + } else { + extensions->m_registeredFunctions.removeLast(); + return index; + } +} + +// This implements `Module._extensions['.js']`, which +// - Loads source code from a file +// - [not supported] Calls `fs.readFileSync`, which is usually not overridden. +// - Evaluates the module +// - Calls `module._compile(code, filename)`, which is often overridden. +// - Returns `undefined` +JSC::EncodedJSValue builtinLoader(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame, BunLoaderType loaderType) +{ + auto scope = DECLARE_THROW_SCOPE(globalObject->vm()); + Zig::GlobalObject* global = defaultGlobalObject(globalObject); + JSC::JSObject* modValue = callFrame->argument(0).getObject(); + if (!modValue) { + throwTypeError(globalObject, scope, "Module._extensions['.js'] must be called with a CommonJS module object"_s); + return JSC::JSValue::encode({}); + } + Bun::JSCommonJSModule* mod = jsDynamicCast(modValue); + if (!mod) { + throwTypeError(globalObject, scope, "Module._extensions['.js'] must be called with a CommonJS module object"_s); + return JSC::JSValue::encode({}); + } + JSC::JSValue specifier = callFrame->argument(1); + WTF::String specifierWtfString = specifier.toWTFString(globalObject); + BunString specifierBunString = Bun::toString(specifierWtfString); + BunString empty = BunStringEmpty; + JSC::VM& vm = globalObject->vm(); + ErrorableResolvedSource res; + + JSValue result = fetchCommonJSModuleNonBuiltin( + global->bunVM(), + vm, + global, + &specifierBunString, + specifier, + &empty, + &empty, + &res, + mod, + specifierWtfString, + loaderType, + scope); + RETURN_IF_EXCEPTION(scope, {}); + if (result == jsNumber(-1)) { + // ESM + JSC::JSFunction* requireESM = global->requireESMFromHijackedExtension(); + JSC::MarkedArgumentBuffer args; + args.append(specifier); + JSC::CallData callData = JSC::getCallData(requireESM); + ASSERT(callData.type == JSC::CallData::Type::JS); + NakedPtr returnedException = nullptr; + JSC::profiledCall(global, JSC::ProfilingReason::API, requireESM, callData, mod, args, returnedException); + if (UNLIKELY(returnedException)) { + throwException(globalObject, scope, returnedException->value()); + return JSC::JSValue::encode({}); + } + } + + return JSC::JSValue::encode(jsUndefined()); +} + +} // namespace Bun diff --git a/src/bun.js/bindings/JSCommonJSExtensions.h b/src/bun.js/bindings/JSCommonJSExtensions.h new file mode 100644 index 0000000000..1e7f13b1e7 --- /dev/null +++ b/src/bun.js/bindings/JSCommonJSExtensions.h @@ -0,0 +1,58 @@ +#pragma once +#include "root.h" +#include "headers-handwritten.h" +#include "BunClientData.h" + +namespace Bun { + +// require.extensions & Module._extensions +class JSCommonJSExtensions : public JSC::JSDestructibleObject { +public: + using Base = JSC::JSDestructibleObject; + static constexpr unsigned StructureFlags = Base::StructureFlags | JSC::OverridesPut; + ~JSCommonJSExtensions(); + + WTF::Vector> m_registeredFunctions; + + static JSCommonJSExtensions* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure) + { + JSCommonJSExtensions* ptr = new (NotNull, JSC::allocateCell(vm)) JSCommonJSExtensions(vm, structure); + ptr->finishCreation(vm); + return ptr; + } + + DECLARE_INFO; + + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) + { + if constexpr (mode == JSC::SubspaceAccess::Concurrently) + return nullptr; + return WebCore::subspaceForImpl( + vm, + [](auto& spaces) { return spaces.m_clientSubspaceForJSCommonJSExtensions.get(); }, + [](auto& spaces, auto&& space) { spaces.m_clientSubspaceForJSCommonJSExtensions = std::forward(space); }, + [](auto& spaces) { return spaces.m_subspaceForJSCommonJSExtensions.get(); }, + [](auto& spaces, auto&& space) { spaces.m_subspaceForJSCommonJSExtensions = std::forward(space); }); + } + + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) + { + return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); + } + +protected: + static bool defineOwnProperty(JSC::JSObject*, JSC::JSGlobalObject*, JSC::PropertyName, const JSC::PropertyDescriptor&, bool shouldThrow); + static bool put(JSC::JSCell*, JSC::JSGlobalObject*, JSC::PropertyName, JSC::JSValue, JSC::PutPropertySlot&); + static bool deleteProperty(JSC::JSCell*, JSC::JSGlobalObject*, JSC::PropertyName, JSC::DeletePropertySlot&); + +private: + JSCommonJSExtensions(JSC::VM& vm, JSC::Structure* structure) + : Base(vm, structure) + { + } + + void finishCreation(JSC::VM&); +}; + +} // namespace Bun diff --git a/src/bun.js/bindings/JSCommonJSModule.cpp b/src/bun.js/bindings/JSCommonJSModule.cpp index 6b200709e2..d8d45de5b1 100644 --- a/src/bun.js/bindings/JSCommonJSModule.cpp +++ b/src/bun.js/bindings/JSCommonJSModule.cpp @@ -76,6 +76,7 @@ #include "wtf/NakedPtr.h" #include "wtf/URL.h" #include "wtf/text/StringImpl.h" +#include "JSCommonJSExtensions.h" extern "C" bool Bun__isBunMain(JSC::JSGlobalObject* global, const BunString*); @@ -298,12 +299,31 @@ JSC_DEFINE_CUSTOM_SETTER(jsRequireCacheSetter, return true; } +JSC_DEFINE_CUSTOM_GETTER(jsRequireExtensionsGetter, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) +{ + Zig::GlobalObject* thisObject = jsCast(globalObject); + return JSValue::encode(thisObject->lazyRequireExtensionsObject()); +} + +JSC_DEFINE_CUSTOM_SETTER(jsRequireExtensionsSetter, + (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, + JSC::EncodedJSValue value, JSC::PropertyName propertyName)) +{ + JSObject* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (!thisObject) + return false; + + thisObject->putDirect(globalObject->vm(), propertyName, JSValue::decode(value), 0); + return true; +} + static const HashTableValue RequireResolveFunctionPrototypeValues[] = { { "paths"_s, static_cast(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, requireResolvePathsFunction, 1 } }, }; static const HashTableValue RequireFunctionPrototypeValues[] = { { "cache"_s, static_cast(JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsRequireCacheGetter, jsRequireCacheSetter } }, + { "extensions"_s, static_cast(JSC::PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, jsRequireExtensionsGetter, jsRequireExtensionsSetter } }, }; Structure* RequireFunctionPrototype::createStructure( @@ -366,13 +386,6 @@ void RequireFunctionPrototype::finishCreation(JSC::VM& vm) JSC::Identifier::fromString(vm, "main"_s), JSC::GetterSetter::create(vm, globalObject, requireDotMainFunction, requireDotMainFunction), PropertyAttribute::Accessor | PropertyAttribute::ReadOnly | 0); - - auto extensions = constructEmptyObject(globalObject); - extensions->putDirect(vm, JSC::Identifier::fromString(vm, ".js"_s), jsBoolean(true), 0); - extensions->putDirect(vm, JSC::Identifier::fromString(vm, ".json"_s), jsBoolean(true), 0); - extensions->putDirect(vm, JSC::Identifier::fromString(vm, ".node"_s), jsBoolean(true), 0); - - this->putDirect(vm, JSC::Identifier::fromString(vm, "extensions"_s), extensions, 0); } JSC_DEFINE_CUSTOM_GETTER(getterFilename, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) @@ -593,6 +606,30 @@ JSC_DEFINE_CUSTOM_SETTER(setterLoaded, return true; } +JSC_DEFINE_CUSTOM_GETTER(getterUnderscoreCompile, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) +{ + JSCommonJSModule* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) { + return JSValue::encode(jsUndefined()); + } + if (thisObject->m_overriddenCompile) { + return JSValue::encode(thisObject->m_overriddenCompile.get()); + } + return JSValue::encode(defaultGlobalObject(globalObject)->modulePrototypeUnderscoreCompileFunction()); +} + +JSC_DEFINE_CUSTOM_SETTER(setterUnderscoreCompile, + (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, + JSC::EncodedJSValue value, JSC::PropertyName propertyName)) +{ + JSCommonJSModule* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (!thisObject) + return false; + JSValue decodedValue = JSValue::decode(value); + thisObject->m_overriddenCompile.set(globalObject->vm(), thisObject, decodedValue); + return true; +} + JSC_DEFINE_HOST_FUNCTION(functionJSCommonJSModule_compile, (JSGlobalObject * globalObject, CallFrame* callframe)) { auto* moduleObject = jsDynamicCast(callframe->thisValue()); @@ -655,7 +692,7 @@ JSC_DEFINE_HOST_FUNCTION(functionJSCommonJSModule_compile, (JSGlobalObject * glo } static const struct HashTableValue JSCommonJSModulePrototypeTableValues[] = { - { "_compile"_s, static_cast(PropertyAttribute::Function | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::NativeFunctionType, functionJSCommonJSModule_compile, 2 } }, + { "_compile"_s, static_cast(PropertyAttribute::CustomAccessor | PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, getterUnderscoreCompile, setterUnderscoreCompile } }, { "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 } }, @@ -1151,7 +1188,6 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionRequireCommonJS, (JSGlobalObject * lexicalGlo // This is always a new JSCommonJSModule object; cast cannot fail. JSCommonJSModule* child = jsCast(callframe->uncheckedArgument(1)); - BunString specifierStr = Bun::toString(specifier); BunString referrerStr = Bun::toString(referrer); BunString typeAttributeStr = { BunStringTag::Dead }; String typeAttribute = String(); @@ -1182,7 +1218,7 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionRequireCommonJS, (JSGlobalObject * lexicalGlo globalObject, child, specifierValue, - &specifierStr, + specifier, &referrerStr, LIKELY(typeAttribute.isEmpty()) ? nullptr @@ -1269,8 +1305,53 @@ void JSCommonJSModule::evaluate( this->sourceCode = JSC::SourceCode(WTFMove(sourceProvider)); evaluateCommonJSModuleOnce(vm, globalObject, this, this->m_dirname.get(), this->m_filename.get()); +} - return; +void JSCommonJSModule::evaluateWithPotentiallyOverriddenCompile( + Zig::GlobalObject* globalObject, + const WTF::String& key, + JSValue keyJSString, + ResolvedSource& source) +{ + if (JSValue compileFunction = this->m_overriddenCompile.get()) { + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + if (!compileFunction) { + throwTypeError(globalObject, scope, "overridden module._compile is not a function (called from overridden Module._extensions)"_s); + return; + } + JSC::CallData callData = JSC::getCallData(compileFunction.asCell()); + if (callData.type == JSC::CallData::Type::None) { + throwTypeError(globalObject, scope, "overridden module._compile is not a function (called from overridden Module._extensions)"_s); + return; + } + WTF::String sourceString = source.source_code.toWTFString(BunString::ZeroCopy); + RETURN_IF_EXCEPTION(scope, ); + if (source.needsDeref) { + source.needsDeref = false; + source.source_code.deref(); + } + // Remove the wrapper from the source string, since the transpiler has added it. + auto trimStart = sourceString.find('\n'); + WTF::String sourceStringWithoutWrapper; + if (trimStart != WTF::notFound) { + auto wrapperStart = globalObject->m_moduleWrapperStart; + auto wrapperEnd = globalObject->m_moduleWrapperEnd; + sourceStringWithoutWrapper = sourceString.substring(trimStart, sourceString.length() - trimStart - 4); + } else { + sourceStringWithoutWrapper = sourceString; + } + RETURN_IF_EXCEPTION(scope, ); + + // _compile(source, filename) + MarkedArgumentBuffer arguments; + arguments.append(jsString(vm, sourceStringWithoutWrapper)); + arguments.append(keyJSString); + JSC::profiledCall(globalObject, ProfilingReason::API, compileFunction, callData, this, arguments); + RETURN_IF_EXCEPTION(scope, ); + return; + } + this->evaluate(globalObject, key, source, false); } std::optional createCommonJSModule( diff --git a/src/bun.js/bindings/JSCommonJSModule.h b/src/bun.js/bindings/JSCommonJSModule.h index d8843132f3..0e12fe8316 100644 --- a/src/bun.js/bindings/JSCommonJSModule.h +++ b/src/bun.js/bindings/JSCommonJSModule.h @@ -23,6 +23,7 @@ using namespace JSC; JSC_DECLARE_HOST_FUNCTION(jsFunctionCreateCommonJSModule); JSC_DECLARE_HOST_FUNCTION(jsFunctionEvaluateCommonJSModule); +JSC_DECLARE_HOST_FUNCTION(functionJSCommonJSModule_compile); void populateESMExports( JSC::JSGlobalObject* globalObject, @@ -66,6 +67,9 @@ public: // When the module is assigned a JSCommonJSModule parent, it is assigned to this field. // This is the normal state. JSC::Weak m_parent {}; + // If compile is overridden, it is assigned to this field. The default + // compile function is not stored here, but in + mutable JSC::WriteBarrier m_overriddenCompile; bool ignoreESModuleAnnotation { false }; JSC::SourceCode sourceCode = JSC::SourceCode(); @@ -86,6 +90,7 @@ public: static JSC::Structure* createStructure(JSC::JSGlobalObject* globalObject); void evaluate(Zig::GlobalObject* globalObject, const WTF::String& sourceURL, ResolvedSource& resolvedSource, bool isBuiltIn); + void evaluateWithPotentiallyOverriddenCompile(Zig::GlobalObject* globalObject, const WTF::String& sourceURL, JSValue keyJSString, ResolvedSource& resolvedSource); inline void evaluate(Zig::GlobalObject* globalObject, const WTF::String& sourceURL, ResolvedSource& resolvedSource) { return evaluate(globalObject, sourceURL, resolvedSource, false); diff --git a/src/bun.js/bindings/ModuleLoader.cpp b/src/bun.js/bindings/ModuleLoader.cpp index baa549bf91..110997a8a7 100644 --- a/src/bun.js/bindings/ModuleLoader.cpp +++ b/src/bun.js/bindings/ModuleLoader.cpp @@ -37,6 +37,8 @@ #include "JSCommonJSModule.h" #include "../modules/_NativeModule.h" +#include "JSCommonJSExtensions.h" + namespace Bun { using namespace JSC; using namespace Zig; @@ -567,11 +569,39 @@ JSValue resolveAndFetchBuiltinModule( return {}; } +void evaluateCommonJSCustomExtension( + Zig::GlobalObject* globalObject, + JSCommonJSModule* target, + String filename, + JSValue filenameValue, + uint32_t extensionIndex) +{ + auto& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + Bun::JSCommonJSExtensions* extensions = globalObject->lazyRequireExtensionsObject(); + JSValue extension = extensions->m_registeredFunctions[extensionIndex].get(); + + if (!extension) { + throwTypeError(globalObject, scope, makeString("require.extension is not a function"_s)); + return; + } + JSC::CallData callData = JSC::getCallData(extension.asCell()); + if (callData.type == JSC::CallData::Type::None) { + throwTypeError(globalObject, scope, makeString("require.extension is not a function"_s)); + return; + } + MarkedArgumentBuffer arguments; + arguments.append(target); + arguments.append(filenameValue); + JSC::profiledCall(globalObject, ProfilingReason::API, extension, callData, target, arguments); + RETURN_IF_EXCEPTION(scope, ); +} + JSValue fetchCommonJSModule( Zig::GlobalObject* globalObject, JSCommonJSModule* target, JSValue specifierValue, - BunString* specifier, + String specifierWtfString, BunString* referrer, BunString* typeAttribute) { @@ -584,13 +614,15 @@ JSValue fetchCommonJSModule( ErrorableResolvedSource* res = &resValue; ResolvedSourceCodeHolder sourceCodeHolder(res); + BunString specifier = Bun::toString(specifierWtfString); + bool wasModuleMock = false; // When "bun test" is enabled, allow users to override builtin modules // This is important for being able to trivially mock things like the filesystem. if (isBunTest) { - if (JSC::JSValue virtualModuleResult = Bun::runVirtualModule(globalObject, specifier, wasModuleMock)) { - JSValue promiseOrCommonJSModule = handleVirtualModuleResult(globalObject, virtualModuleResult, res, specifier, referrer, wasModuleMock, target); + if (JSC::JSValue virtualModuleResult = Bun::runVirtualModule(globalObject, &specifier, wasModuleMock)) { + JSValue promiseOrCommonJSModule = handleVirtualModuleResult(globalObject, virtualModuleResult, res, &specifier, referrer, wasModuleMock, target); RETURN_IF_EXCEPTION(scope, {}); // If we assigned module.exports to the virtual module, we're done here. @@ -606,7 +638,7 @@ JSValue fetchCommonJSModule( RELEASE_AND_RETURN(scope, JSValue {}); } case JSPromise::Status::Pending: { - JSC::throwTypeError(globalObject, scope, makeString("require() async module \""_s, specifier->toWTFString(BunString::ZeroCopy), "\" is unsupported. use \"await import()\" instead."_s)); + JSC::throwTypeError(globalObject, scope, makeString("require() async module \""_s, specifierWtfString, "\" is unsupported. use \"await import()\" instead."_s)); RELEASE_AND_RETURN(scope, JSValue {}); } case JSPromise::Status::Fulfilled: { @@ -625,7 +657,7 @@ JSValue fetchCommonJSModule( } } - if (auto builtin = fetchBuiltinModuleWithoutResolution(globalObject, specifier, res)) { + if (auto builtin = fetchBuiltinModuleWithoutResolution(globalObject, &specifier, res)) { if (!res->success) { RELEASE_AND_RETURN(scope, builtin); } @@ -636,8 +668,8 @@ JSValue fetchCommonJSModule( // When "bun test" is NOT enabled, disable users from overriding builtin modules if (!isBunTest) { - if (JSC::JSValue virtualModuleResult = Bun::runVirtualModule(globalObject, specifier, wasModuleMock)) { - JSValue promiseOrCommonJSModule = handleVirtualModuleResult(globalObject, virtualModuleResult, res, specifier, referrer, wasModuleMock, target); + if (JSC::JSValue virtualModuleResult = Bun::runVirtualModule(globalObject, &specifier, wasModuleMock)) { + JSValue promiseOrCommonJSModule = handleVirtualModuleResult(globalObject, virtualModuleResult, res, &specifier, referrer, wasModuleMock, target); RETURN_IF_EXCEPTION(scope, {}); // If we assigned module.exports to the virtual module, we're done here. @@ -653,7 +685,7 @@ JSValue fetchCommonJSModule( RELEASE_AND_RETURN(scope, JSValue {}); } case JSPromise::Status::Pending: { - JSC::throwTypeError(globalObject, scope, makeString("require() async module \""_s, specifier->toWTFString(BunString::ZeroCopy), "\" is unsupported. use \"await import()\" instead."_s)); + JSC::throwTypeError(globalObject, scope, makeString("require() async module \""_s, specifierWtfString, "\" is unsupported. use \"await import()\" instead."_s)); RELEASE_AND_RETURN(scope, JSValue {}); } case JSPromise::Status::Fulfilled: { @@ -688,10 +720,31 @@ JSValue fetchCommonJSModule( if (hasAlreadyLoadedESMVersionSoWeShouldntTranspileItTwice()) { RELEASE_AND_RETURN(scope, jsNumber(-1)); } + return fetchCommonJSModuleNonBuiltin(bunVM, vm, globalObject, &specifier, specifierValue, referrer, typeAttribute, res, target, specifierWtfString, BunLoaderTypeNone, scope); +} - Bun__transpileFile(bunVM, globalObject, specifier, referrer, typeAttribute, res, false); +template +JSValue fetchCommonJSModuleNonBuiltin( + void* bunVM, + JSC::VM& vm, + Zig::GlobalObject* globalObject, + BunString* specifier, + JSC::JSValue specifierValue, + BunString* referrer, + BunString* typeAttribute, + ErrorableResolvedSource* res, + JSCommonJSModule* target, + String specifierWtfString, + BunLoaderType forceLoaderType, + JSC::ThrowScope& scope) +{ + Bun__transpileFile(bunVM, globalObject, specifier, referrer, typeAttribute, res, false, !isExtension, forceLoaderType); if (res->success && res->result.value.isCommonJSModule) { - target->evaluate(globalObject, specifier->toWTFString(BunString::ZeroCopy), res->result.value); + if constexpr (isExtension) { + target->evaluateWithPotentiallyOverriddenCompile(globalObject, specifierWtfString, specifierValue, res->result.value); + } else { + target->evaluate(globalObject, specifierWtfString, res->result.value); + } RETURN_IF_EXCEPTION(scope, {}); RELEASE_AND_RETURN(scope, target); } @@ -733,6 +786,15 @@ JSValue fetchCommonJSModule( target->putDirect(vm, WebCore::clientData(vm)->builtinNames().exportsPublicName(), value, 0); target->hasEvaluated = true; RELEASE_AND_RETURN(scope, target); + } else if (res->result.value.tag == SyntheticModuleType::CommonJSCustomExtension) { + if constexpr (isExtension) { + ASSERT_NOT_REACHED(); + JSC::throwException(globalObject, scope, JSC::createSyntaxError(globalObject, "Recursive extension. This is a bug in Bun"_s)); + RELEASE_AND_RETURN(scope, {}); + } + evaluateCommonJSCustomExtension(globalObject, target, specifierWtfString, specifierValue, res->result.value.cjsCustomExtensionIndex); + RETURN_IF_EXCEPTION(scope, {}); + RELEASE_AND_RETURN(scope, target); } auto&& provider = Zig::SourceProvider::create(globalObject, res->result.value); @@ -741,6 +803,34 @@ JSValue fetchCommonJSModule( RELEASE_AND_RETURN(scope, jsNumber(-1)); } +// Explicit instantiations of fetchCommonJSModuleNonBuiltin +template JSValue fetchCommonJSModuleNonBuiltin( + void* bunVM, + JSC::VM& vm, + Zig::GlobalObject* globalObject, + BunString* specifier, + JSC::JSValue specifierValue, + BunString* referrer, + BunString* typeAttribute, + ErrorableResolvedSource* res, + JSCommonJSModule* target, + String specifierWtfString, + BunLoaderType forceLoaderType, + JSC::ThrowScope& scope); +template JSValue fetchCommonJSModuleNonBuiltin( + void* bunVM, + JSC::VM& vm, + Zig::GlobalObject* globalObject, + BunString* specifier, + JSC::JSValue specifierValue, + BunString* referrer, + BunString* typeAttribute, + ErrorableResolvedSource* res, + JSCommonJSModule* target, + String specifierWtfString, + BunLoaderType forceLoaderType, + JSC::ThrowScope& scope); + extern "C" bool isBunTest; template @@ -860,12 +950,12 @@ static JSValue fetchESMSourceCode( } if constexpr (allowPromise) { - auto* pendingCtx = Bun__transpileFile(bunVM, globalObject, specifier, referrer, typeAttribute, res, true); + auto* pendingCtx = Bun__transpileFile(bunVM, globalObject, specifier, referrer, typeAttribute, res, true, false, BunLoaderTypeNone); if (pendingCtx) { return pendingCtx; } } else { - Bun__transpileFile(bunVM, globalObject, specifier, referrer, typeAttribute, res, false); + Bun__transpileFile(bunVM, globalObject, specifier, referrer, typeAttribute, res, false, false, BunLoaderTypeNone); } if (res->success && res->result.value.isCommonJSModule) { diff --git a/src/bun.js/bindings/ModuleLoader.h b/src/bun.js/bindings/ModuleLoader.h index 6285d428a2..5618386459 100644 --- a/src/bun.js/bindings/ModuleLoader.h +++ b/src/bun.js/bindings/ModuleLoader.h @@ -110,10 +110,25 @@ JSValue fetchCommonJSModule( Zig::GlobalObject* globalObject, JSCommonJSModule* moduleObject, JSValue specifierValue, - BunString* specifier, + String specifier, BunString* referrer, BunString* typeAttribute); +template +JSValue fetchCommonJSModuleNonBuiltin( + void* bunVM, + JSC::VM& vm, + Zig::GlobalObject* globalObject, + BunString* specifier, + JSC::JSValue specifierValue, + BunString* referrer, + BunString* typeAttribute, + ErrorableResolvedSource* res, + JSCommonJSModule* target, + String specifierWtfString, + BunLoaderType forceLoaderType, + JSC::ThrowScope& scope); + JSValue resolveAndFetchBuiltinModule( Zig::GlobalObject* globalObject, BunString* specifier); diff --git a/src/bun.js/bindings/NodeModuleModule.zig b/src/bun.js/bindings/NodeModuleModule.zig index b5f66759b1..1da4932984 100644 --- a/src/bun.js/bindings/NodeModuleModule.zig +++ b/src/bun.js/bindings/NodeModuleModule.zig @@ -81,3 +81,154 @@ pub fn _stat(path: []const u8) i32 { .directory => 1, // Returns 1 for directories. }; } + +pub const CustomLoader = union(enum) { + loader: bun.options.Loader, + /// Retrieve via WriteBarrier in `global->lazyRequireExtensionsObject().get(index)` + custom: u32, + + pub const Packed = enum(u32) { + const loader_start: u32 = std.math.maxInt(u32) - 4; + js = loader_start, + json = loader_start + 1, + napi = loader_start + 2, + ts = loader_start + 3, + /// custom + _, + + pub fn pack(loader: CustomLoader) Packed { + return switch (loader) { + .loader => |basic| switch (basic) { + .js => .js, + .json => .json, + .napi => .napi, + .ts => .ts, + else => brk: { + bun.debugAssert(false); + break :brk .js; + }, + }, + .custom => |custom| @enumFromInt(custom), + }; + } + + pub fn unpack(self: Packed) CustomLoader { + return switch (self) { + .js => .{ .loader = .js }, + .json => .{ .loader = .json }, + .napi => .{ .loader = .napi }, + .ts => .{ .loader = .ts }, + _ => .{ .custom = @intFromEnum(self) }, + }; + } + }; +}; + +extern fn JSCommonJSExtensions__appendFunction(global: *JSC.JSGlobalObject, value: JSC.JSValue) u32; +extern fn JSCommonJSExtensions__setFunction(global: *JSC.JSGlobalObject, index: u32, value: JSC.JSValue) void; +/// Returns the index of the last value, which must have it's references updated to `index` +extern fn JSCommonJSExtensions__swapRemove(global: *JSC.JSGlobalObject, index: u32) u32; + +// Memory management is complicated because JSValues are stored in gc-visitable +// WriteBarriers in C++ but the hash map for extensions is in Zig for flexibility. +fn onRequireExtensionModify(global: *JSC.JSGlobalObject, str: []const u8, kind: i32, value: JSC.JSValue) !void { + bun.assert(kind >= -1 and kind <= 4); + const vm = global.bunVM(); + const list = &vm.commonjs_custom_extensions; + defer vm.transpiler.resolver.opts.extra_cjs_extensions = list.keys(); + const is_built_in = bun.options.defaultLoaders.get(str) != null; + if (kind >= 0) { + const loader: CustomLoader = switch (kind) { + 1 => .{ .loader = .js }, + 2 => .{ .loader = .json }, + 3 => .{ .loader = .napi }, + 4 => .{ .loader = .ts }, + else => .{ .custom = undefined }, // to be filled in later + }; + const gop = try list.getOrPut(bun.default_allocator, str); + if (!gop.found_existing) { + const dupe = try bun.default_allocator.dupe(u8, str); + errdefer bun.default_allocator.free(dupe); + gop.key_ptr.* = dupe; + if (is_built_in) { + vm.has_mutated_built_in_extensions += 1; + } + gop.value_ptr.* = .pack(switch (loader) { + .loader => loader, + .custom => .{ + .custom = JSCommonJSExtensions__appendFunction(global, value), + }, + }); + } else { + const existing = gop.value_ptr.*.unpack(); + if (existing == .custom and loader != .custom) { + swapRemoveExtension(vm, existing.custom); + } + gop.value_ptr.* = .pack(switch (loader) { + .loader => loader, + .custom => .{ + .custom = if (existing == .custom) new: { + JSCommonJSExtensions__setFunction(global, existing.custom, value); + break :new existing.custom; + } else JSCommonJSExtensions__appendFunction(global, value), + }, + }); + } + } else if (list.fetchSwapRemove(str)) |prev| { + bun.default_allocator.free(prev.key); + if (is_built_in) { + vm.has_mutated_built_in_extensions -= 1; + } + switch (prev.value.unpack()) { + .loader => {}, + .custom => |index| swapRemoveExtension(vm, index), + } + } +} + +fn swapRemoveExtension(vm: *JSC.VirtualMachine, index: u32) void { + const last_index = JSCommonJSExtensions__swapRemove(vm.global, index); + if (last_index == index) return; + // Find and rewrite the last index to the new index. + // Since packed structs are sugar over the backing int, this code can use + // the simd path in the standard library search. + const find: u32 = @intFromEnum(CustomLoader.Packed.pack(.{ .custom = last_index })); + const values = vm.commonjs_custom_extensions.values(); + const values_reinterpret = bun.reinterpretSlice(u32, values); + const i = std.mem.indexOfScalar(u32, values_reinterpret, find) orelse + return bun.debugAssert(false); + values[i] = .pack(.{ .custom = last_index }); +} + +pub fn findLongestRegisteredExtension(vm: *JSC.VirtualMachine, filename: []const u8) ?CustomLoader { + const basename = std.fs.path.basename(filename); + var next: usize = 0; + while (bun.strings.indexOfCharPos(basename, '.', next)) |i| { + next = i + 1; + if (i == 0) continue; + const ext = basename[i..]; + if (vm.commonjs_custom_extensions.get(ext)) |value| { + return value.unpack(); + } + } + return null; +} + +fn onRequireExtensionModifyBinding( + global: *JSC.JSGlobalObject, + str: *const bun.String, + kind: i32, + value: JSC.JSValue, +) callconv(.c) void { + var sfa_state = std.heap.stackFallback(8192, bun.default_allocator); + const alloc = sfa_state.get(); + const str_slice = str.toUTF8(alloc); + defer str_slice.deinit(); + onRequireExtensionModify(global, str_slice.slice(), kind, value) catch |err| switch (err) { + error.OutOfMemory => bun.outOfMemory(), + }; +} + +comptime { + @export(&onRequireExtensionModifyBinding, .{ .name = "NodeModuleModule__onRequireExtensionModify" }); +} diff --git a/src/bun.js/bindings/ResolvedSource.zig b/src/bun.js/bindings/ResolvedSource.zig index ed35ac3fc1..f8f43999e0 100644 --- a/src/bun.js/bindings/ResolvedSource.zig +++ b/src/bun.js/bindings/ResolvedSource.zig @@ -13,13 +13,15 @@ pub const ResolvedSource = extern struct { is_commonjs_module: bool = false, - hash: u32 = 0, + /// When .tag is .common_js_custom_extension, this is special-cased to hold + /// the index of the extension, since the module is stored in a WriteBarrier. + cjs_custom_extension_index: u32 = 0, allocator: ?*anyopaque = null, jsvalue_for_export: JSValue = .zero, - tag: Tag = Tag.javascript, + tag: Tag = .javascript, /// This is for source_code source_code_needs_deref: bool = true, diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index ba4a8ae814..4f50452f4a 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -2939,18 +2939,6 @@ void GlobalObject::finishCreation(VM& vm) init.set(crypto); }); - m_lazyRequireCacheObject.initLater( - [](const Initializer& init) { - JSC::VM& vm = init.vm; - JSC::JSGlobalObject* globalObject = init.owner; - - 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); - init.set(result.toObject(globalObject)); - }); - m_lazyTestModuleObject.initLater( [](const Initializer& init) { JSC::JSGlobalObject* globalObject = init.owner; @@ -4026,6 +4014,8 @@ void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) visitor.append(thisObject->m_currentNapiHandleScopeImpl); thisObject->m_moduleResolveFilenameFunction.visit(visitor); + thisObject->m_modulePrototypeUnderscoreCompileFunction.visit(visitor); + thisObject->m_commonJSRequireESMFromHijackedExtensionFunction.visit(visitor); thisObject->m_moduleRunMainFunction.visit(visitor); thisObject->m_nodeModuleConstructor.visit(visitor); thisObject->m_asyncBoundFunctionStructure.visit(visitor); diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index 594e7bca5f..60e3940bc6 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -56,6 +56,13 @@ class GlobalInternals; #include #include +namespace Bun { +class JSCommonJSExtensions; +class InternalModuleRegistry; +class JSMockModule; +class JSMockFunction; +} + namespace WebCore { class WorkerGlobalScope; class SubtleCrypto; @@ -262,6 +269,9 @@ public: JSObject* processBindingFs() const { return m_processBindingFs.getInitializedOnMainThread(this); } JSObject* lazyRequireCacheObject() const { return m_lazyRequireCacheObject.getInitializedOnMainThread(this); } + Bun::JSCommonJSExtensions* lazyRequireExtensionsObject() const { return m_lazyRequireExtensionsObject.getInitializedOnMainThread(this); } + JSC::JSFunction* modulePrototypeUnderscoreCompileFunction() const { return m_modulePrototypeUnderscoreCompileFunction.getInitializedOnMainThread(this); } + JSC::JSFunction* requireESMFromHijackedExtension() const { return m_commonJSRequireESMFromHijackedExtensionFunction.getInitializedOnMainThread(this); } Structure* NodeVMGlobalObjectStructure() const { return m_cachedNodeVMGlobalObjectStructure.getInitializedOnMainThread(this); } Structure* globalProxyStructure() const { return m_cachedGlobalProxyStructure.getInitializedOnMainThread(this); } @@ -399,6 +409,8 @@ public: LazyProperty m_moduleResolveFilenameFunction; LazyProperty m_moduleRunMainFunction; + LazyProperty m_modulePrototypeUnderscoreCompileFunction; + LazyProperty m_commonJSRequireESMFromHijackedExtensionFunction; LazyProperty m_nodeModuleConstructor; mutable WriteBarrier m_nextTickQueue; @@ -588,6 +600,7 @@ public: LazyProperty m_JSResizableOrGrowableSharedBufferSubclassStructure; LazyProperty m_vmModuleContextMap; LazyProperty m_lazyRequireCacheObject; + LazyProperty m_lazyRequireExtensionsObject; LazyProperty m_lazyTestModuleObject; LazyProperty m_lazyPreloadTestModuleObject; LazyProperty m_testMatcherUtilsObject; diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index a44efc4450..9633829240 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -342,30 +342,30 @@ pub fn initialize(eval_mode: bool) void { JSCInitialize( std.os.environ.ptr, std.os.environ.len, - struct { - pub fn callback(name: [*]const u8, len: usize) callconv(.C) void { - Output.prettyErrorln( - \\error: invalid JSC environment variable - \\ - \\ {s} - \\ - \\For a list of options, see this file: - \\ - \\ https://github.com/oven-sh/webkit/blob/main/Source/JavaScriptCore/runtime/OptionsList.h - \\ - \\Environment variables must be prefixed with "BUN_JSC_". This code runs before .env files are loaded, so those won't work here. - \\ - \\Warning: options change between releases of Bun and WebKit without notice. This is not a stable API, you should not rely on it beyond debugging something, and it may be removed entirely in a future version of Bun. - , - .{name[0..len]}, - ); - bun.Global.exit(1); - } - }.callback, + onJSCInvalidEnvVar, eval_mode, ); } +pub fn onJSCInvalidEnvVar(name: [*]const u8, len: usize) callconv(.C) void { + Output.prettyErrorln( + \\error: invalid JSC environment variable + \\ + \\ {s} + \\ + \\For a list of options, see this file: + \\ + \\ https://github.com/oven-sh/webkit/blob/main/Source/JavaScriptCore/runtime/OptionsList.h + \\ + \\Environment variables must be prefixed with "BUN_JSC_". This code runs before .env files are loaded, so those won't work here. + \\ + \\Warning: options change between releases of Bun and WebKit without notice. This is not a stable API, you should not rely on it beyond debugging something, and it may be removed entirely in a future version of Bun. + , + .{name[0..len]}, + ); + bun.Global.exit(1); +} + /// Returns null on error. Use windows API to lookup the actual error. /// The reason this function is in zig is so that we can use our own utf16-conversion functions. /// diff --git a/src/bun.js/bindings/headers-handwritten.h b/src/bun.js/bindings/headers-handwritten.h index 55710a0ebf..b0513402e3 100644 --- a/src/bun.js/bindings/headers-handwritten.h +++ b/src/bun.js/bindings/headers-handwritten.h @@ -106,7 +106,7 @@ typedef struct ResolvedSource { BunString source_code; BunString source_url; bool isCommonJSModule; - uint32_t hash; + uint32_t cjsCustomExtensionIndex; void* allocator; JSC::EncodedJSValue jsvalue_for_export; uint32_t tag; @@ -347,7 +347,10 @@ extern "C" JSC::JSInternalPromise* Bun__transpileFile( BunString* specifier, BunString* referrer, const BunString* typeAttribute, - ErrorableResolvedSource* result, bool allowPromise); + ErrorableResolvedSource* result, + bool allowPromise, + bool isCommonJSRequire, + BunLoaderType forceLoaderType); extern "C" bool Bun__fetchBuiltinModule( void* bunVM, diff --git a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h index 21a4ed1392..159d2417cd 100644 --- a/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMClientIsoSubspaces.h @@ -37,6 +37,7 @@ public: std::unique_ptr m_clientSubspaceForBundlerPlugin; std::unique_ptr m_clientSubspaceForNodeVMScript; std::unique_ptr m_clientSubspaceForJSCommonJSModule; + std::unique_ptr m_clientSubspaceForJSCommonJSExtensions; 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 ea657733a0..6b64f5d0f3 100644 --- a/src/bun.js/bindings/webcore/DOMIsoSubspaces.h +++ b/src/bun.js/bindings/webcore/DOMIsoSubspaces.h @@ -37,6 +37,7 @@ public: std::unique_ptr m_subspaceForBundlerPlugin; std::unique_ptr m_subspaceForNodeVMScript; std::unique_ptr m_subspaceForJSCommonJSModule; + std::unique_ptr m_subspaceForJSCommonJSExtensions; std::unique_ptr m_subspaceForJSMockImplementation; std::unique_ptr m_subspaceForJSModuleMock; std::unique_ptr m_subspaceForJSMockFunction; diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index d72798803b..7c4ea3f710 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -74,6 +74,7 @@ const PackageManager = @import("../install/install.zig").PackageManager; const IPC = @import("ipc.zig"); const DNSResolver = @import("api/bun/dns_resolver.zig").DNSResolver; const Watcher = bun.Watcher; +const node_module_module = @import("./bindings/NodeModuleModule.zig"); const ModuleLoader = JSC.ModuleLoader; const FetchFlags = JSC.FetchFlags; @@ -900,7 +901,7 @@ pub const VirtualMachine = struct { is_inside_deferred_task_queue: bool = false, - // defaults off. .on("message") will set it to true unles overridden + // defaults off. .on("message") will set it to true unless overridden // process.channel.unref() will set it to false and mark it overridden // on disconnect it will be disabled channel_ref: bun.Async.KeepAlive = .{}, @@ -909,6 +910,18 @@ pub const VirtualMachine = struct { // if one disconnect event listener should be ignored channel_ref_should_ignore_one_disconnect_event_listener: bool = false, + /// A set of extensions that exist in the require.extensions map. Keys + /// contain the leading '.'. Value is either a loader for built in + /// functions, or an index into JSCommonJSExtensions. + /// + /// `.keys() == transpiler.resolver.opts.extra_cjs_extensions`, so + /// mutations in this map must update the resolver. + commonjs_custom_extensions: bun.StringArrayHashMapUnmanaged(node_module_module.CustomLoader.Packed) = .empty, + /// Incremented when the `require.extensions` for a built-in extension is mutated. + /// An example is mutating `require.extensions['.js']` to intercept all '.js' files. + /// The value is decremented when defaults are restored. + has_mutated_built_in_extensions: u32 = 0, + pub const OnUnhandledRejection = fn (*VirtualMachine, globalObject: *JSGlobalObject, JSValue) void; pub const OnException = fn (*ZigException) void; @@ -2367,7 +2380,6 @@ pub const VirtualMachine = struct { .source_code = bun.String.init(""), .specifier = specifier, .source_url = specifier.createIfDifferent(source_url), - .hash = 0, .allocator = null, .source_code_needs_deref = false, }; @@ -2382,7 +2394,6 @@ pub const VirtualMachine = struct { .source_code = bun.String.init(source.impl), .specifier = specifier, .source_url = specifier.createIfDifferent(source_url), - .hash = source.hash, .allocator = source, .source_code_needs_deref = false, }; diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index 9ffcd0798a..44e1a0526b 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -42,6 +42,7 @@ const JSC = bun.JSC; const MarkedArrayBuffer = @import("./base.zig").MarkedArrayBuffer; const getAllocator = @import("./base.zig").getAllocator; const JSValue = bun.JSC.JSValue; +const node_module_module = @import("./bindings/NodeModuleModule.zig"); const JSGlobalObject = bun.JSC.JSGlobalObject; const ExceptionValueRef = bun.JSC.ExceptionValueRef; @@ -78,7 +79,6 @@ inline fn jsSyntheticModule(name: ResolvedSource.Tag, specifier: String) Resolve .source_code = bun.String.empty, .specifier = specifier, .source_url = bun.String.static(@tagName(name)), - .hash = 0, .tag = name, .source_code_needs_deref = false, }; @@ -568,7 +568,6 @@ pub const RuntimeTranspilerStore = struct { break :brk result; }, }, - .hash = 0, .is_commonjs_module = entry.metadata.module_type == .cjs, .tag = this.resolved_source.tag, }; @@ -582,7 +581,6 @@ pub const RuntimeTranspilerStore = struct { .allocator = null, .source_code = bun.String.createLatin1(parse_result.source.contents), .already_bundled = true, - .hash = 0, .bytecode_cache = if (bytecode_slice.len > 0) bytecode_slice.ptr else null, .bytecode_cache_size = bytecode_slice.len, .is_commonjs_module = parse_result.already_bundled.isCommonJS(), @@ -684,7 +682,6 @@ pub const RuntimeTranspilerStore = struct { .allocator = null, .source_code = source_code, .is_commonjs_module = parse_result.ast.has_commonjs_export_names or parse_result.ast.exports_kind == .cjs, - .hash = 0, .tag = this.resolved_source.tag, }; } @@ -1447,8 +1444,6 @@ pub const ModuleLoader = struct { .specifier = String.init(specifier), .source_url = String.init(path.text), .is_commonjs_module = parse_result.ast.has_commonjs_export_names or parse_result.ast.exports_kind == .cjs, - - .hash = 0, }; } @@ -1509,7 +1504,6 @@ pub const ModuleLoader = struct { .source_code = bun.String.empty, .specifier = input_specifier, .source_url = input_specifier.createIfDifferent(path.text), - .hash = 0, }; } } @@ -1741,8 +1735,6 @@ pub const ModuleLoader = struct { .source_code = bun.String.createUTF8(parse_result.source.contents), .specifier = input_specifier, .source_url = input_specifier.createIfDifferent(path.text), - - .hash = 0, .tag = ResolvedSource.Tag.json_for_object_loader, }; } @@ -1757,7 +1749,6 @@ pub const ModuleLoader = struct { }, .specifier = input_specifier, .source_url = input_specifier.createIfDifferent(path.text), - .hash = 0, }; } @@ -1767,7 +1758,6 @@ pub const ModuleLoader = struct { .allocator = null, .specifier = input_specifier, .source_url = input_specifier.createIfDifferent(path.text), - .hash = 0, .jsvalue_for_export = JSValue.createEmptyObject(jsc_vm.global, 0), .tag = .exports_object, }; @@ -1777,7 +1767,6 @@ pub const ModuleLoader = struct { .allocator = null, .specifier = input_specifier, .source_url = input_specifier.createIfDifferent(path.text), - .hash = 0, .jsvalue_for_export = parse_result.ast.parts.@"[0]"().stmts[0].data.s_expr.value.toJS(allocator, globalObject orelse jsc_vm.global) catch |e| panic("Unexpected JS error: {s}", .{@errorName(e)}), .tag = .exports_object, }; @@ -1791,7 +1780,6 @@ pub const ModuleLoader = struct { .specifier = input_specifier, .source_url = input_specifier.createIfDifferent(path.text), .already_bundled = true, - .hash = 0, .bytecode_cache = if (bytecode_slice.len > 0) bytecode_slice.ptr else null, .bytecode_cache_size = bytecode_slice.len, .is_commonjs_module = parse_result.already_bundled.isCommonJS(), @@ -1810,7 +1798,6 @@ pub const ModuleLoader = struct { .specifier = input_specifier, .source_url = input_specifier.createIfDifferent(path.text), .is_commonjs_module = true, - .hash = 0, .tag = .javascript, }; } @@ -1839,7 +1826,6 @@ pub const ModuleLoader = struct { }, .specifier = input_specifier, .source_url = input_specifier.createIfDifferent(path.text), - .hash = 0, .is_commonjs_module = entry.metadata.module_type == .cjs, .tag = brk: { if (entry.metadata.module_type == .cjs and parse_result.source.path.isFile()) { @@ -1970,7 +1956,6 @@ pub const ModuleLoader = struct { .specifier = input_specifier, .source_url = input_specifier.createIfDifferent(path.text), .is_commonjs_module = parse_result.ast.has_commonjs_export_names or parse_result.ast.exports_kind == .cjs, - .hash = 0, .tag = tag, }; }, @@ -2013,7 +1998,6 @@ pub const ModuleLoader = struct { // .source_code = ZigString.init(jsc_vm.allocator.dupe(u8, parse_result.source.contents) catch unreachable), // .specifier = ZigString.init(specifier), // .source_url = input_specifier.createIfDifferent(path.text), - // .hash = 0, // .tag = ResolvedSource.Tag.wasm, // }; // }, @@ -2039,7 +2023,6 @@ pub const ModuleLoader = struct { .specifier = input_specifier, .source_url = input_specifier.createIfDifferent(path.text), .tag = .esm, - .hash = 0, }; } @@ -2099,7 +2082,6 @@ pub const ModuleLoader = struct { .specifier = input_specifier, .source_url = input_specifier.createIfDifferent(path.text), .tag = .esm, - .hash = 0, }; }, @@ -2110,7 +2092,6 @@ pub const ModuleLoader = struct { .source_code = bun.String.empty, .specifier = input_specifier, .source_url = input_specifier.createIfDifferent(path.text), - .hash = 0, .tag = .esm, }; } @@ -2125,7 +2106,6 @@ pub const ModuleLoader = struct { .jsvalue_for_export = html_bundle.toJS(globalObject.?), .specifier = input_specifier, .source_url = input_specifier.createIfDifferent(path.text), - .hash = 0, .tag = .export_default_object, }; }, @@ -2137,7 +2117,6 @@ pub const ModuleLoader = struct { .source_code = bun.String.empty, .specifier = input_specifier, .source_url = input_specifier.createIfDifferent(path.text), - .hash = 0, .tag = .esm, }; } @@ -2209,7 +2188,6 @@ pub const ModuleLoader = struct { .jsvalue_for_export = value, .specifier = input_specifier, .source_url = input_specifier.createIfDifferent(path.text), - .hash = 0, .tag = .export_default_object, }; }, @@ -2277,6 +2255,8 @@ pub const ModuleLoader = struct { type_attribute: ?*const bun.String, ret: *JSC.ErrorableResolvedSource, allow_promise: bool, + is_commonjs_require: bool, + force_loader_type: bun.options.Loader.Optional, ) ?*anyopaque { JSC.markBinding(@src()); var log = logger.Log.init(jsc_vm.transpiler.allocator); @@ -2294,13 +2274,66 @@ pub const ModuleLoader = struct { var virtual_source_to_use: ?logger.Source = null; var blob_to_deinit: ?JSC.WebCore.Blob = null; - const lr = options.getLoaderAndVirtualSource(_specifier.slice(), jsc_vm, &virtual_source_to_use, &blob_to_deinit, type_attribute_str) catch { + var lr = options.getLoaderAndVirtualSource(_specifier.slice(), jsc_vm, &virtual_source_to_use, &blob_to_deinit, type_attribute_str) catch { ret.* = JSC.ErrorableResolvedSource.err(error.JSErrorObject, globalObject.MODULE_NOT_FOUND("Blob not found", .{}).toJS().asVoid()); return null; }; defer if (blob_to_deinit) |*blob| blob.deinit(); - const module_type: options.ModuleType = if (lr.package_json) |pkg| pkg.module_type else .unknown; + if (force_loader_type.unwrap()) |loader_type| { + @branchHint(.unlikely); + bun.assert(!is_commonjs_require); + lr.loader = loader_type; + } else if (is_commonjs_require and jsc_vm.has_mutated_built_in_extensions > 0) { + @branchHint(.unlikely); + if (node_module_module.findLongestRegisteredExtension(jsc_vm, _specifier.slice())) |entry| { + switch (entry) { + .loader => |loader| { + lr.loader = loader; + }, + .custom => |index| { + ret.* = JSC.ErrorableResolvedSource.ok(ResolvedSource{ + .allocator = null, + .source_code = bun.String.empty, + .specifier = .empty, + .source_url = .empty, + .cjs_custom_extension_index = index, + .tag = .common_js_custom_extension, + }); + return null; + }, + } + } + } + + const module_type: options.ModuleType = brk: { + const ext = lr.path.name.ext; + // regular expression /.[cm][jt]s$/ + if (ext.len == ".cjs".len) { + if (strings.eqlComptimeIgnoreLen(ext, ".cjs")) + break :brk .cjs; + if (strings.eqlComptimeIgnoreLen(ext, ".mjs")) + break :brk .esm; + if (strings.eqlComptimeIgnoreLen(ext, ".cts")) + break :brk .cjs; + if (strings.eqlComptimeIgnoreLen(ext, ".mts")) + break :brk .esm; + } + // regular expression /.[jt]s$/ + if (ext.len == ".ts".len) { + if (strings.eqlComptimeIgnoreLen(ext, ".js") or + strings.eqlComptimeIgnoreLen(ext, ".ts")) + { + // Use the package.json module type if it exists + break :brk if (lr.package_json) |pkg| + pkg.module_type + else + .unknown; + } + } + // For JSX TSX and other extensions, let the file contents. + break :brk .unknown; + }; const pkg_name: ?[]const u8 = if (lr.package_json) |pkg| if (pkg.name.len > 0) pkg.name else null else @@ -2367,19 +2400,48 @@ pub const ModuleLoader = struct { } } - const synchronous_loader = lr.loader orelse loader: { + const synchronous_loader: options.Loader = lr.loader orelse loader: { if (jsc_vm.has_loaded or jsc_vm.is_in_preload) { // Extensionless files in this context are treated as the JS loader if (lr.path.name.ext.len == 0) { - break :loader options.Loader.tsx; + break :loader .tsx; } // Unknown extensions are to be treated as file loader - break :loader options.Loader.file; + if (is_commonjs_require) { + if (jsc_vm.commonjs_custom_extensions.entries.len > 0 and + jsc_vm.has_mutated_built_in_extensions == 0) + { + @branchHint(.unlikely); + if (node_module_module.findLongestRegisteredExtension(jsc_vm, lr.path.text)) |entry| { + switch (entry) { + .loader => |loader| break :loader loader, + .custom => |index| { + ret.* = JSC.ErrorableResolvedSource.ok(ResolvedSource{ + .allocator = null, + .source_code = bun.String.empty, + .specifier = .empty, + .source_url = .empty, + .cjs_custom_extension_index = index, + .tag = .common_js_custom_extension, + }); + return null; + }, + } + } + } + + // For Node.js compatibility, requiring a file with an + // unknown extension will be treated as a JS file + break :loader .ts; + } + + // For ESM, Bun treats unknown extensions as file loader + break :loader .file; } else { // Unless it's potentially the main module // This is important so that "bun run ./foo-i-have-no-extension" works - break :loader options.Loader.tsx; + break :loader .tsx; } }; @@ -2451,7 +2513,6 @@ pub const ModuleLoader = struct { .source_code = bun.String.createUTF8(jsc_vm.entry_point.source.contents), .specifier = specifier, .source_url = specifier, - .hash = 0, .tag = .esm, .source_code_needs_deref = true, }, @@ -2467,7 +2528,6 @@ pub const ModuleLoader = struct { .source_code = String.init(Runtime.Runtime.sourceCode()), .specifier = specifier, .source_url = specifier, - .hash = Runtime.Runtime.versionHash(), }, inline else => |tag| jsSyntheticModule(@field(ResolvedSource.Tag, @tagName(tag)), specifier), }; @@ -2487,7 +2547,6 @@ pub const ModuleLoader = struct { .source_code = bun.String.createUTF8(entry.source.contents), .specifier = specifier, .source_url = specifier.dupeRef(), - .hash = 0, }; } } else if (jsc_vm.standalone_module_graph) |graph| { @@ -2509,7 +2568,6 @@ pub const ModuleLoader = struct { .source_code = bun.String.static(code), .specifier = specifier, .source_url = specifier.dupeRef(), - .hash = 0, .source_code_needs_deref = false, }; } @@ -2519,7 +2577,6 @@ pub const ModuleLoader = struct { .source_code = file.toWTFString(), .specifier = specifier, .source_url = specifier.dupeRef(), - .hash = 0, .source_code_needs_deref = false, .bytecode_cache = if (file.bytecode.len > 0) file.bytecode.ptr else null, .bytecode_cache_size = file.bytecode.len, diff --git a/src/bun.js/modules/NodeModuleModule.cpp b/src/bun.js/modules/NodeModuleModule.cpp index e14600539a..8f7444d758 100644 --- a/src/bun.js/modules/NodeModuleModule.cpp +++ b/src/bun.js/modules/NodeModuleModule.cpp @@ -12,6 +12,7 @@ #include #include "JavaScriptCore/Completion.h" #include "JavaScriptCore/JSNativeStdFunction.h" +#include "JSCommonJSExtensions.h" #include "PathInlines.h" #include "ZigGlobalObject.h" @@ -562,6 +563,12 @@ static JSValue getModuleCacheObject(VM& vm, JSObject* moduleObject) ->lazyRequireCacheObject(); } +static JSValue getModuleExtensionsObject(VM& vm, JSObject* moduleObject) +{ + return jsCast(moduleObject->globalObject()) + ->lazyRequireExtensionsObject(); +} + static JSValue getModuleDebugObject(VM& vm, JSObject* moduleObject) { return JSC::constructEmptyObject(moduleObject->globalObject()); @@ -574,13 +581,6 @@ static JSValue getPathCacheObject(VM& vm, JSObject* moduleObject) vm, globalObject->nullPrototypeObjectStructure()); } -static JSValue getModuleExtensionsObject(VM& vm, JSObject* moduleObject) -{ - auto* globalObject = defaultGlobalObject(moduleObject->globalObject()); - return globalObject->requireFunctionUnbound()->getIfPropertyExists( - globalObject, Identifier::fromString(vm, "extensions"_s)); -} - static JSValue getSourceMapFunction(VM& vm, JSObject* moduleObject) { auto* globalObject = defaultGlobalObject(moduleObject->globalObject()); @@ -951,6 +951,42 @@ void addNodeModuleConstructorProperties(JSC::VM& vm, JSC::NoIntrinsic, jsFunctionResolveFileName); init.set(resolveFilenameFunction); }); + + globalObject->m_modulePrototypeUnderscoreCompileFunction.initLater( + [](const Zig::GlobalObject::Initializer& init) { + JSFunction* resolveFilenameFunction = JSFunction::create( + init.vm, init.owner, 2, "_compile"_s, + functionJSCommonJSModule_compile, JSC::ImplementationVisibility::Public, + JSC::NoIntrinsic, functionJSCommonJSModule_compile); + init.set(resolveFilenameFunction); + }); + + globalObject->m_commonJSRequireESMFromHijackedExtensionFunction.initLater( + [](const Zig::GlobalObject::Initializer& init) { + JSC::JSFunction* requireESM = JSC::JSFunction::create(init.vm, init.owner, commonJSRequireESMFromHijackedExtensionCodeGenerator(init.vm), init.owner); + init.set(requireESM); + }); + + globalObject->m_lazyRequireCacheObject.initLater( + [](const Zig::GlobalObject::Initializer& init) { + JSC::VM& vm = init.vm; + JSC::JSGlobalObject* globalObject = init.owner; + + 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); + ASSERT(!returnedException); + init.set(result.toObject(globalObject)); + }); + + globalObject->m_lazyRequireExtensionsObject.initLater( + [](const Zig::GlobalObject::Initializer& init) { + JSC::VM& vm = init.vm; + JSC::JSGlobalObject* globalObject = init.owner; + + init.set(JSCommonJSExtensions::create(vm, globalObject, JSCommonJSExtensions::createStructure(vm, globalObject, globalObject->nullPrototype()))); + }); } JSC_DEFINE_HOST_FUNCTION(jsFunctionIsModuleResolveFilenameSlowPathEnabled, diff --git a/src/bun.zig b/src/bun.zig index f7a344d64b..583f921f2d 100644 --- a/src/bun.zig +++ b/src/bun.zig @@ -3870,7 +3870,7 @@ pub fn WeakPtr(comptime T: type, comptime weakable_field: std.meta.FieldEnum(T)) }; } -pub const DebugThreadLock = if (Environment.allow_assert) +pub const DebugThreadLock = if (Environment.isDebug) struct { owning_thread: ?std.Thread.Id, locked_at: crash_handler.StoredTrace, diff --git a/src/codegen/bundle-modules.ts b/src/codegen/bundle-modules.ts index 44db6f130c..4e954e76bd 100644 --- a/src/codegen/bundle-modules.ts +++ b/src/codegen/bundle-modules.ts @@ -395,7 +395,6 @@ writeIfNotChanged( path.join(CODEGEN_DIR, "ResolvedSourceTag.zig"), `// zig fmt: off pub const ResolvedSourceTag = enum(u32) { - // Predefined javascript = 0, package_json_type_module = 1, package_json_type_commonjs = 2, @@ -404,11 +403,12 @@ pub const ResolvedSourceTag = enum(u32) { file = 5, esm = 6, json_for_object_loader = 7, - /// Generate an object with "default" set to all the exports, including a "default" propert + /// Generate an object with "default" set to all the exports, including a "default" property exports_object = 8, - /// Generate a module that only exports default the input JSValue export_default_object = 9, + /// Signal upwards that the matching value in 'require.extensions' should be used. + common_js_custom_extension = 10, // Built in modules are loaded through InternalModuleRegistry by numerical ID. // In this enum are represented as \`(1 << 9) & id\` @@ -438,6 +438,7 @@ writeIfNotChanged( JSONForObjectLoader = 7, ExportsObject = 8, ExportDefaultObject = 9, + CommonJSCustomExtension = 10, // Built in modules are loaded through InternalModuleRegistry by numerical ID. // In this enum are represented as \`(1 << 9) & id\` InternalModuleRegistryFlag = 1 << 9, diff --git a/src/js/builtins.d.ts b/src/js/builtins.d.ts index f6ae715204..d3a34cffec 100644 --- a/src/js/builtins.d.ts +++ b/src/js/builtins.d.ts @@ -484,7 +484,7 @@ declare function $createCommonJSModule( id: string, exports: any, hasEvaluated: boolean, - parent: ?JSCommonJSModule, + parent: JSCommonJSModule | undefined, ): JSCommonJSModule; declare function $evaluateCommonJSModule( moduleToEvaluate: JSCommonJSModule, diff --git a/src/js/builtins/CommonJS.ts b/src/js/builtins/CommonJS.ts index 0f42f753ad..f0405cc409 100644 --- a/src/js/builtins/CommonJS.ts +++ b/src/js/builtins/CommonJS.ts @@ -302,6 +302,41 @@ export function requireESM(this, resolved: string) { return exports; } +export function requireESMFromHijackedExtension(this: JSCommonJSModule, id: string) { + $assert(this); + try { + $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 + } + } + + this.exports = namespace["module.exports"] ?? namespace; + return; + } +} + $visibility = "Private"; export function createRequireCache() { var moduleMap = new Map(); diff --git a/src/js_parser.zig b/src/js_parser.zig index 949d064c70..ca0eded2c1 100644 --- a/src/js_parser.zig +++ b/src/js_parser.zig @@ -3590,9 +3590,6 @@ pub const Parser = struct { } } - const did_import_fast_refresh = false; - _ = did_import_fast_refresh; - // This is a workaround for broken module environment checks in packages like lodash-es // https://github.com/lodash/lodash/issues/5660 var force_esm = false; @@ -3964,7 +3961,13 @@ pub const Parser = struct { switch (p.options.module_type) { // ".cjs" or ".cts" or ("type: commonjs" and (".js" or ".jsx" or ".ts" or ".tsx")) .cjs => { - exports_kind = .cjs; + // There are no commonjs-only features used (require is allowed in ESM) + bun.assert(!uses_exports_ref and + !uses_module_ref and + !p.has_top_level_return and + !p.has_with_scope); + // Use ESM if the file has ES module syntax (import) + exports_kind = if (p.has_es_module_syntax) .esm else .cjs; }, .esm => { exports_kind = .esm; diff --git a/src/options.zig b/src/options.zig index 982b128bb7..5b4b76cc18 100644 --- a/src/options.zig +++ b/src/options.zig @@ -80,9 +80,9 @@ pub fn stringHashMapFromArrays(comptime t: type, allocator: std.mem.Allocator, k } pub const ExternalModules = struct { - node_modules: std.BufSet = undefined, - abs_paths: std.BufSet = undefined, - patterns: []const WildcardPattern = undefined, + node_modules: std.BufSet, + abs_paths: std.BufSet, + patterns: []const WildcardPattern, pub const WildcardPattern = struct { prefix: string, @@ -649,6 +649,14 @@ pub const Loader = enum(u8) { sqlite_embedded, html, + pub const Optional = enum(u8) { + none = 254, + _, + pub fn unwrap(opt: Optional) ?Loader { + return if (opt == .none) null else @enumFromInt(@intFromEnum(opt)); + } + }; + pub fn isCSS(this: Loader) bool { return this == .css; } @@ -1700,7 +1708,7 @@ pub const BundleOptions = struct { main_fields: []const string = Target.DefaultMainFields.get(Target.browser), /// TODO: remove this in favor accessing bundler.log log: *logger.Log, - external: ExternalModules = ExternalModules{}, + external: ExternalModules, entry_points: []const string, entry_naming: []const u8 = "", asset_naming: []const u8 = "", @@ -1708,6 +1716,9 @@ pub const BundleOptions = struct { public_path: []const u8 = "", extension_order: ResolveFileExtensions = .{}, main_field_extension_order: []const string = &Defaults.MainFieldExtensionOrder, + /// This list applies to all extension resolution cases. The runtime uses + /// this for implementing `require.extensions` + extra_cjs_extensions: []const []const u8 = &.{}, out_extensions: bun.StringHashMap(string), import_path_format: ImportPathFormat = ImportPathFormat.relative, defines_loaded: bool = false, diff --git a/src/resolver/package_json.zig b/src/resolver/package_json.zig index 7e86224048..7095b69572 100644 --- a/src/resolver/package_json.zig +++ b/src/resolver/package_json.zig @@ -604,7 +604,7 @@ pub const PackageJSON = struct { null, ) catch |err| { if (err != error.IsDir) { - r.log.addErrorFmt(null, logger.Loc.Empty, allocator, "Cannot read file \"{s}\": {s}", .{ r.prettyPath(fs.Path.init(input_path)), @errorName(err) }) catch unreachable; + r.log.addErrorFmt(null, logger.Loc.Empty, allocator, "Cannot read file \"{s}\": {s}", .{ input_path, @errorName(err) }) catch unreachable; } return null; @@ -618,7 +618,7 @@ pub const PackageJSON = struct { const key_path = fs.Path.init(package_json_path); var json_source = logger.Source.initPathString(key_path.text, entry.contents); - json_source.path.pretty = r.prettyPath(json_source.path); + json_source.path.pretty = json_source.path.text; const json: js_ast.Expr = (r.caches.json.parsePackageJSON(r.log, json_source, allocator, true) catch |err| { if (Environment.isDebug) { diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig index d4ba9cce07..aef35bdf38 100644 --- a/src/resolver/resolver.zig +++ b/src/resolver/resolver.zig @@ -2565,11 +2565,6 @@ pub const Resolver = struct { return result; } - // TODO: - pub fn prettyPath(_: *ThisResolver, path: Path) string { - return path.text; - } - pub fn binDirs(_: *const ThisResolver) []const string { if (!bin_folders_loaded) return &[_]string{}; return bin_folders.constSlice(); @@ -2833,7 +2828,7 @@ pub const Resolver = struct { r.dir_cache.markNotFound(queue_top.result); rfs.entries.markNotFound(cached_dir_entry_result); if (comptime enable_logging) { - const pretty = r.prettyPath(Path.init(queue_top.unsafe_path)); + const pretty = queue_top.unsafe_path; r.log.addErrorFmt( null, @@ -3471,54 +3466,68 @@ pub const Resolver = struct { } pub fn loadAsIndex(r: *ThisResolver, dir_info: *DirInfo, extension_order: []const string) ?MatchResult { - const rfs = &r.fs.fs; // Try the "index" file with extensions for (extension_order) |ext| { - var ext_buf = bufs(.extension_path); + if (loadIndexWithExtension(r, dir_info, ext)) |result| { + return result; + } + } + for (r.opts.extra_cjs_extensions) |ext| { + if (loadIndexWithExtension(r, dir_info, ext)) |result| { + return result; + } + } - var base = ext_buf[0 .. "index".len + ext.len]; - base[0.."index".len].* = "index".*; - bun.copy(u8, base["index".len..], ext); + return null; + } - if (dir_info.getEntries(r.generation)) |entries| { - if (entries.get(base)) |lookup| { - if (lookup.entry.kind(rfs, r.store_fd) == .file) { - const out_buf = brk: { - if (lookup.entry.abs_path.isEmpty()) { - const parts = [_]string{ dir_info.abs_path, base }; - const out_buf_ = r.fs.absBuf(&parts, bufs(.index)); - lookup.entry.abs_path = - PathString.init(r.fs.dirname_store.append(@TypeOf(out_buf_), out_buf_) catch unreachable); - } - break :brk lookup.entry.abs_path.slice(); - }; + fn loadIndexWithExtension(r: *ThisResolver, dir_info: *DirInfo, ext: string) ?MatchResult { + const rfs = &r.fs.fs; - if (r.debug_logs) |*debug| { - debug.addNoteFmt("Found file: \"{s}\"", .{out_buf}); + var ext_buf = bufs(.extension_path); + + var base = ext_buf[0 .. "index".len + ext.len]; + base[0.."index".len].* = "index".*; + bun.copy(u8, base["index".len..], ext); + + if (dir_info.getEntries(r.generation)) |entries| { + if (entries.get(base)) |lookup| { + if (lookup.entry.kind(rfs, r.store_fd) == .file) { + const out_buf = brk: { + if (lookup.entry.abs_path.isEmpty()) { + const parts = [_]string{ dir_info.abs_path, base }; + const out_buf_ = r.fs.absBuf(&parts, bufs(.index)); + lookup.entry.abs_path = + PathString.init(r.fs.dirname_store.append(@TypeOf(out_buf_), out_buf_) catch unreachable); } + break :brk lookup.entry.abs_path.slice(); + }; - if (dir_info.package_json) |package_json| { - return MatchResult{ - .path_pair = .{ .primary = Path.init(out_buf) }, - .diff_case = lookup.diff_case, - .package_json = package_json, - .dirname_fd = dir_info.getFileDescriptor(), - }; - } + if (r.debug_logs) |*debug| { + debug.addNoteFmt("Found file: \"{s}\"", .{out_buf}); + } + if (dir_info.package_json) |package_json| { return MatchResult{ .path_pair = .{ .primary = Path.init(out_buf) }, .diff_case = lookup.diff_case, - + .package_json = package_json, .dirname_fd = dir_info.getFileDescriptor(), }; } + + return MatchResult{ + .path_pair = .{ .primary = Path.init(out_buf) }, + .diff_case = lookup.diff_case, + + .dirname_fd = dir_info.getFileDescriptor(), + }; } } + } - if (r.debug_logs) |*debug| { - debug.addNoteFmt("Failed to find file: \"{s}/{s}\"", .{ dir_info.abs_path, base }); - } + if (r.debug_logs) |*debug| { + debug.addNoteFmt("Failed to find file: \"{s}/{s}\"", .{ dir_info.abs_path, base }); } return null; @@ -3742,7 +3751,7 @@ pub const Resolver = struct { } pub fn loadAsFile(r: *ThisResolver, path: string, extension_order: []const string) ?LoadResult { - var rfs: *Fs.FileSystem.RealFS = &r.fs.fs; + const rfs: *Fs.FileSystem.RealFS = &r.fs.fs; if (r.debug_logs) |*debug| { debug.addNoteFmt("Attempting to load \"{s}\" as a file", .{path}); @@ -3774,7 +3783,7 @@ pub const Resolver = struct { r.allocator, "Cannot read directory \"{s}\": {s}", .{ - r.prettyPath(Path.init(dir_path)), + dir_path, @errorName(dir_entry.err.original_err), }, ) catch {}; @@ -3818,35 +3827,14 @@ pub const Resolver = struct { // Try the path with extensions bun.copy(u8, bufs(.load_as_file), path); for (extension_order) |ext| { - var buffer = bufs(.load_as_file)[0 .. path.len + ext.len]; - bun.copy(u8, buffer[path.len..], ext); - const file_name = buffer[path.len - base.len .. buffer.len]; - - if (r.debug_logs) |*debug| { - debug.addNoteFmt("Checking for file \"{s}\" ", .{buffer}); + if (loadExtension(r, base, path, ext, entries)) |result| { + return result; } + } - if (entries.get(file_name)) |query| { - if (query.entry.kind(rfs, r.store_fd) == .file) { - if (r.debug_logs) |*debug| { - debug.addNoteFmt("Found file \"{s}\" ", .{buffer}); - } - - // now that we've found it, we allocate it. - return LoadResult{ - .path = brk: { - query.entry.abs_path = if (query.entry.abs_path.isEmpty()) - PathString.init(r.fs.dirname_store.append(@TypeOf(buffer), buffer) catch unreachable) - else - query.entry.abs_path; - - break :brk query.entry.abs_path.slice(); - }, - .diff_case = query.diff_case, - .dirname_fd = entries.fd, - .file_fd = query.entry.cache.fd, - }; - } + for (r.opts.extra_cjs_extensions) |ext| { + if (loadExtension(r, base, path, ext, entries)) |result| { + return result; } } @@ -3931,6 +3919,42 @@ pub const Resolver = struct { return null; } + fn loadExtension(r: *ThisResolver, base: string, path: string, ext: string, entries: *Fs.FileSystem.DirEntry) ?LoadResult { + const rfs: *Fs.FileSystem.RealFS = &r.fs.fs; + const buffer = bufs(.load_as_file)[0 .. path.len + ext.len]; + bun.copy(u8, buffer[path.len..], ext); + const file_name = buffer[path.len - base.len .. buffer.len]; + + if (r.debug_logs) |*debug| { + debug.addNoteFmt("Checking for file \"{s}\" ", .{buffer}); + } + + if (entries.get(file_name)) |query| { + if (query.entry.kind(rfs, r.store_fd) == .file) { + if (r.debug_logs) |*debug| { + debug.addNoteFmt("Found file \"{s}\" ", .{buffer}); + } + + // now that we've found it, we allocate it. + return .{ + .path = brk: { + query.entry.abs_path = if (query.entry.abs_path.isEmpty()) + PathString.init(r.fs.dirname_store.append(@TypeOf(buffer), buffer) catch unreachable) + else + query.entry.abs_path; + + break :brk query.entry.abs_path.slice(); + }, + .diff_case = query.diff_case, + .dirname_fd = entries.fd, + .file_fd = query.entry.cache.fd, + }; + } + } + + return null; + } + fn dirInfoUncached( r: *ThisResolver, info: *DirInfo, @@ -4138,8 +4162,7 @@ pub const Resolver = struct { tsconfigpath, if (FeatureFlags.store_file_descriptors) fd else .zero, ) catch |err| brk: { - const pretty = r.prettyPath(Path.init(tsconfigpath)); - + const pretty = tsconfigpath; if (err == error.ENOENT or err == error.FileNotFound) { r.log.addErrorFmt(null, logger.Loc.Empty, r.allocator, "Cannot find tsconfig file {}", .{bun.fmt.QuotedFormatter{ .text = pretty }}) catch {}; } else if (err != error.ParseErrorAlreadyLogged and err != error.IsDir and err != error.EISDIR) { diff --git a/src/transpiler.zig b/src/transpiler.zig index 5dbe50e1b8..fe19bed720 100644 --- a/src/transpiler.zig +++ b/src/transpiler.zig @@ -1180,21 +1180,21 @@ pub const Transpiler = struct { transpiler.log, &source, ) catch null) orelse return null) { - .ast => |value| ParseResult{ + .ast => |value| .{ .ast = value, .source = source, .loader = loader, .input_fd = input_fd, .runtime_transpiler_cache = this_parse.runtime_transpiler_cache, }, - .cached => ParseResult{ + .cached => .{ .ast = undefined, .runtime_transpiler_cache = this_parse.runtime_transpiler_cache, .source = source, .loader = loader, .input_fd = input_fd, }, - .already_bundled => |already_bundled| ParseResult{ + .already_bundled => |already_bundled| .{ .ast = undefined, .already_bundled = switch (already_bundled) { .bun => .source_code, diff --git a/test/cli/run/module-type-fixture/cjs/hello.cjs b/test/cli/run/module-type-fixture/cjs/hello.cjs new file mode 100644 index 0000000000..248453e484 --- /dev/null +++ b/test/cli/run/module-type-fixture/cjs/hello.cjs @@ -0,0 +1 @@ +console.log(eval("typeof module === 'undefined'")); diff --git a/test/cli/run/module-type-fixture/cjs/hello.cts b/test/cli/run/module-type-fixture/cjs/hello.cts new file mode 100644 index 0000000000..248453e484 --- /dev/null +++ b/test/cli/run/module-type-fixture/cjs/hello.cts @@ -0,0 +1 @@ +console.log(eval("typeof module === 'undefined'")); diff --git a/test/cli/run/module-type-fixture/cjs/hello.js b/test/cli/run/module-type-fixture/cjs/hello.js new file mode 100644 index 0000000000..72c037e9a6 --- /dev/null +++ b/test/cli/run/module-type-fixture/cjs/hello.js @@ -0,0 +1 @@ +console.log(eval("typeof module === 'undefined'")); \ No newline at end of file diff --git a/test/cli/run/module-type-fixture/cjs/hello.jsx b/test/cli/run/module-type-fixture/cjs/hello.jsx new file mode 100644 index 0000000000..72c037e9a6 --- /dev/null +++ b/test/cli/run/module-type-fixture/cjs/hello.jsx @@ -0,0 +1 @@ +console.log(eval("typeof module === 'undefined'")); \ No newline at end of file diff --git a/test/cli/run/module-type-fixture/cjs/hello.mjs b/test/cli/run/module-type-fixture/cjs/hello.mjs new file mode 100644 index 0000000000..248453e484 --- /dev/null +++ b/test/cli/run/module-type-fixture/cjs/hello.mjs @@ -0,0 +1 @@ +console.log(eval("typeof module === 'undefined'")); diff --git a/test/cli/run/module-type-fixture/cjs/hello.mts b/test/cli/run/module-type-fixture/cjs/hello.mts new file mode 100644 index 0000000000..248453e484 --- /dev/null +++ b/test/cli/run/module-type-fixture/cjs/hello.mts @@ -0,0 +1 @@ +console.log(eval("typeof module === 'undefined'")); diff --git a/test/cli/run/module-type-fixture/cjs/hello.ts b/test/cli/run/module-type-fixture/cjs/hello.ts new file mode 100644 index 0000000000..72c037e9a6 --- /dev/null +++ b/test/cli/run/module-type-fixture/cjs/hello.ts @@ -0,0 +1 @@ +console.log(eval("typeof module === 'undefined'")); \ No newline at end of file diff --git a/test/cli/run/module-type-fixture/cjs/hello.tsx b/test/cli/run/module-type-fixture/cjs/hello.tsx new file mode 100644 index 0000000000..72c037e9a6 --- /dev/null +++ b/test/cli/run/module-type-fixture/cjs/hello.tsx @@ -0,0 +1 @@ +console.log(eval("typeof module === 'undefined'")); \ No newline at end of file diff --git a/test/cli/run/module-type-fixture/cjs/import.cjs b/test/cli/run/module-type-fixture/cjs/import.cjs new file mode 100644 index 0000000000..08c3035d60 --- /dev/null +++ b/test/cli/run/module-type-fixture/cjs/import.cjs @@ -0,0 +1,3 @@ +import * as fs from "node:fs"; +console.log(eval("typeof module === 'undefined'")); ++fs; diff --git a/test/cli/run/module-type-fixture/cjs/package.json b/test/cli/run/module-type-fixture/cjs/package.json new file mode 100644 index 0000000000..5bbefffbab --- /dev/null +++ b/test/cli/run/module-type-fixture/cjs/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/test/cli/run/module-type-fixture/esm/hello.cjs b/test/cli/run/module-type-fixture/esm/hello.cjs new file mode 100644 index 0000000000..248453e484 --- /dev/null +++ b/test/cli/run/module-type-fixture/esm/hello.cjs @@ -0,0 +1 @@ +console.log(eval("typeof module === 'undefined'")); diff --git a/test/cli/run/module-type-fixture/esm/hello.cts b/test/cli/run/module-type-fixture/esm/hello.cts new file mode 100644 index 0000000000..248453e484 --- /dev/null +++ b/test/cli/run/module-type-fixture/esm/hello.cts @@ -0,0 +1 @@ +console.log(eval("typeof module === 'undefined'")); diff --git a/test/cli/run/module-type-fixture/esm/hello.js b/test/cli/run/module-type-fixture/esm/hello.js new file mode 100644 index 0000000000..72c037e9a6 --- /dev/null +++ b/test/cli/run/module-type-fixture/esm/hello.js @@ -0,0 +1 @@ +console.log(eval("typeof module === 'undefined'")); \ No newline at end of file diff --git a/test/cli/run/module-type-fixture/esm/hello.jsx b/test/cli/run/module-type-fixture/esm/hello.jsx new file mode 100644 index 0000000000..72c037e9a6 --- /dev/null +++ b/test/cli/run/module-type-fixture/esm/hello.jsx @@ -0,0 +1 @@ +console.log(eval("typeof module === 'undefined'")); \ No newline at end of file diff --git a/test/cli/run/module-type-fixture/esm/hello.mjs b/test/cli/run/module-type-fixture/esm/hello.mjs new file mode 100644 index 0000000000..248453e484 --- /dev/null +++ b/test/cli/run/module-type-fixture/esm/hello.mjs @@ -0,0 +1 @@ +console.log(eval("typeof module === 'undefined'")); diff --git a/test/cli/run/module-type-fixture/esm/hello.mts b/test/cli/run/module-type-fixture/esm/hello.mts new file mode 100644 index 0000000000..248453e484 --- /dev/null +++ b/test/cli/run/module-type-fixture/esm/hello.mts @@ -0,0 +1 @@ +console.log(eval("typeof module === 'undefined'")); diff --git a/test/cli/run/module-type-fixture/esm/hello.ts b/test/cli/run/module-type-fixture/esm/hello.ts new file mode 100644 index 0000000000..72c037e9a6 --- /dev/null +++ b/test/cli/run/module-type-fixture/esm/hello.ts @@ -0,0 +1 @@ +console.log(eval("typeof module === 'undefined'")); \ No newline at end of file diff --git a/test/cli/run/module-type-fixture/esm/hello.tsx b/test/cli/run/module-type-fixture/esm/hello.tsx new file mode 100644 index 0000000000..72c037e9a6 --- /dev/null +++ b/test/cli/run/module-type-fixture/esm/hello.tsx @@ -0,0 +1 @@ +console.log(eval("typeof module === 'undefined'")); \ No newline at end of file diff --git a/test/cli/run/module-type-fixture/esm/import.cjs b/test/cli/run/module-type-fixture/esm/import.cjs new file mode 100644 index 0000000000..08c3035d60 --- /dev/null +++ b/test/cli/run/module-type-fixture/esm/import.cjs @@ -0,0 +1,3 @@ +import * as fs from "node:fs"; +console.log(eval("typeof module === 'undefined'")); ++fs; diff --git a/test/cli/run/module-type-fixture/esm/package.json b/test/cli/run/module-type-fixture/esm/package.json new file mode 100644 index 0000000000..3dbc1ca591 --- /dev/null +++ b/test/cli/run/module-type-fixture/esm/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/test/cli/run/run-detect-module-type.ts b/test/cli/run/run-detect-module-type.ts new file mode 100644 index 0000000000..e9b447a198 --- /dev/null +++ b/test/cli/run/run-detect-module-type.ts @@ -0,0 +1,54 @@ +import { bunEnv, bunExe } from "harness"; +import { join } from "path"; +import { expect, test } from "bun:test"; + +// module type -> file extensions -> expected module type +const table = { + cjs: { + 'hello.cjs': 'commonjs', + 'hello.js': 'commonjs', + 'hello.mjs': 'module', + 'hello.ts': 'commonjs', + 'hello.tsx': 'module', + 'hello.cts': 'commonjs', + 'hello.jsx': 'module', + 'hello.mts': 'module', + // files using ES import and no exports will be detected as module + "import.cjs": "module", + }, + esm: { + 'hello.cjs': 'commonjs', + 'hello.js': 'module', + 'hello.mjs': 'module', + 'hello.ts': 'module', + 'hello.tsx': 'module', + 'hello.cts': 'commonjs', + 'hello.jsx': 'module', + 'hello.mts': 'module', + // files using ES import and no exports will be detected as module + "import.cjs": "module", + }, +}; + +test("detect module type", () => { + const expected = Object.entries(table).map(([moduleType, extensions]) => { + return Object.entries(extensions).map(([extension, expected]) => { + return `${moduleType} ${extension} -> ${expected}`; + }); + }).flat(); + + const actual = Object.entries(table).map(([moduleType, extensions]) => { + return Object.entries(extensions).map(([extension, expected]) => { + const proc = Bun.spawnSync({ + cmd: [bunExe(), "run", join(import.meta.dir, 'module-type-fixture', moduleType, extension)], + env: bunEnv, + }); + if (proc.exitCode !== 0) { + throw new Error(`Failed to run ${moduleType} ${extension}: ${proc.stderr.toString('utf8').trim()}`); + } + return `${moduleType} ${extension} -> ${proc.stdout.toString('utf8').trim() === "false" ? "commonjs" : "module"}`; + }); + }).flat(); + + expect(actual).toEqual(expected); +}); diff --git a/test/internal/ban-words.test.ts b/test/internal/ban-words.test.ts index ff08d7cd51..2acaeeba81 100644 --- a/test/internal/ban-words.test.ts +++ b/test/internal/ban-words.test.ts @@ -27,8 +27,8 @@ const words: Record "alloc.ptr !=": { reason: "The std.mem.Allocator context pointer can be undefined, which makes this comparison undefined behavior" }, "== alloc.ptr": { reason: "The std.mem.Allocator context pointer can be undefined, which makes this comparison undefined behavior" }, "!= alloc.ptr": { reason: "The std.mem.Allocator context pointer can be undefined, which makes this comparison undefined behavior" }, - [String.raw`: [a-zA-Z0-9_\.\*\?\[\]\(\)]+ = undefined,`]: { reason: "Do not default a struct field to undefined", limit: 246, regex: true }, - "usingnamespace": { reason: "This brings Bun away from incremental / faster compile times.", limit: 492 }, + [String.raw`: [a-zA-Z0-9_\.\*\?\[\]\(\)]+ = undefined,`]: { reason: "Do not default a struct field to undefined", limit: 244, regex: true }, + "usingnamespace": { reason: "Zig deprecates this, and will not support it in incremental compilation.", limit: 492 }, }; const words_keys = [...Object.keys(words)]; diff --git a/test/js/node/module/extensions-fixture/a.custom b/test/js/node/module/extensions-fixture/a.custom new file mode 100644 index 0000000000..e5d94c4c86 --- /dev/null +++ b/test/js/node/module/extensions-fixture/a.custom @@ -0,0 +1 @@ +module.exports = 'fail' \ No newline at end of file diff --git a/test/js/node/module/extensions-fixture/a.js b/test/js/node/module/extensions-fixture/a.js new file mode 100644 index 0000000000..b4c5011785 --- /dev/null +++ b/test/js/node/module/extensions-fixture/a.js @@ -0,0 +1 @@ +module.exports = 'a unmodified'; diff --git a/test/js/node/module/extensions-fixture/a.json b/test/js/node/module/extensions-fixture/a.json new file mode 100644 index 0000000000..6314134f33 --- /dev/null +++ b/test/js/node/module/extensions-fixture/a.json @@ -0,0 +1 @@ +{ "hello": "world" } \ No newline at end of file diff --git a/test/js/node/module/extensions-fixture/a.ts b/test/js/node/module/extensions-fixture/a.ts new file mode 100644 index 0000000000..ae8b40fb8e --- /dev/null +++ b/test/js/node/module/extensions-fixture/a.ts @@ -0,0 +1,4 @@ +declare const y: string; +enum J { x = "hello " } +const hello: string = "world"; +module.exports = J.x + hello!; diff --git a/test/js/node/module/extensions-fixture/b.json b/test/js/node/module/extensions-fixture/b.json new file mode 100644 index 0000000000..7b37d4c855 --- /dev/null +++ b/test/js/node/module/extensions-fixture/b.json @@ -0,0 +1 @@ +{ "hello \ No newline at end of file diff --git a/test/js/node/module/extensions-fixture/c.custom b/test/js/node/module/extensions-fixture/c.custom new file mode 100644 index 0000000000..fabebadd37 --- /dev/null +++ b/test/js/node/module/extensions-fixture/c.custom @@ -0,0 +1 @@ +module.exports = 'c dot custom' \ No newline at end of file diff --git a/test/js/node/module/extensions-fixture/d.js b/test/js/node/module/extensions-fixture/d.js new file mode 100644 index 0000000000..608be5dd43 --- /dev/null +++ b/test/js/node/module/extensions-fixture/d.js @@ -0,0 +1 @@ +module.exports = 'd.js' \ No newline at end of file diff --git a/test/js/node/module/extensions-fixture/e.js b/test/js/node/module/extensions-fixture/e.js new file mode 100644 index 0000000000..95c587acc9 --- /dev/null +++ b/test/js/node/module/extensions-fixture/e.js @@ -0,0 +1,4 @@ +declare const y: string; +enum J { x = "hello" } +const hello: string = " world"; +module.exports = J.x + hello!; diff --git a/test/js/node/module/extensions-fixture/secretly_esm.cjs b/test/js/node/module/extensions-fixture/secretly_esm.cjs new file mode 100644 index 0000000000..aef22247d7 --- /dev/null +++ b/test/js/node/module/extensions-fixture/secretly_esm.cjs @@ -0,0 +1 @@ +export default 1; diff --git a/test/js/node/module/require-extensions.test.ts b/test/js/node/module/require-extensions.test.ts new file mode 100644 index 0000000000..c93b35d6d9 --- /dev/null +++ b/test/js/node/module/require-extensions.test.ts @@ -0,0 +1,187 @@ +import assert from "assert"; +import { test, mock, expect } from "bun:test"; +import { tempDirWithFiles } from "harness"; +import path from "path"; + +test("require.extensions shape makes sense", () => { + const extensions = require.extensions; + expect(extensions).toBeDefined(); + expect(typeof extensions).toBe("object"); + expect(extensions[".js"]).toBeFunction(); + expect(extensions[".json"]).toBeFunction(); + expect(extensions[".node"]).toBeFunction(); + // When --experimental-strip-types is passed, TypeScript files can be loaded. + expect(extensions[".cts"]).toBeFunction(); + expect(extensions[".ts"]).toBeFunction(); + expect(extensions[".mjs"]).toBeFunction(); + expect(extensions[".mts"]).toBeFunction(); + expect(require('module')._extensions === require.extensions).toBe(true); +}); +test("custom require extension 1", () => { + const custom = require.extensions['.custom'] = mock(function (module, filename) { + expect(filename).toBe(path.join(import.meta.dir, 'extensions-fixture', 'c.custom')); + (module as any)._compile(`module.exports = 'custom';`, filename); + }); + const mod = require('./extensions-fixture/c'); + expect(mod).toBe('custom'); + expect(custom.mock.calls.length).toBe(1); + delete require.extensions['.custom']; + expect(() => require('./extensions-fixture/c')).toThrow(/Cannot find module/); + expect(require('./extensions-fixture/c.custom')).toBe('custom'); // already loaded + delete require.cache[require.resolve('./extensions-fixture/c.custom')]; + expect(custom.mock.calls.length).toBe(1); + expect(require('./extensions-fixture/c.custom')).toBe('c dot custom'); // use js loader +}); +test("custom require extension overwrite default loader", () => { + const original = require.extensions['.js']; + try { + const custom = require.extensions['.js'] = mock(function (module, filename) { + expect(filename).toBe(path.join(import.meta.dir, 'extensions-fixture', 'd.js')); + (module as any)._compile(`module.exports = 'custom';`, filename); + }); + const mod = require('./extensions-fixture/d'); + expect(mod).toBe('custom'); + expect(custom.mock.calls.length).toBe(1); + require.extensions['.js'] = original; + expect(require('./extensions-fixture/d')).toBe('custom'); // already loaded + delete require.cache[require.resolve('./extensions-fixture/d')]; + expect(custom.mock.calls.length).toBe(1); + expect(require('./extensions-fixture/d')).toBe('d.js'); // use js loader + } finally { + require.extensions['.js'] = original; + } +}); +test("custom require extension overwrite default loader with other default loader", () => { + const original = require.extensions['.js']; + try { + require.extensions['.js'] = require.extensions['.ts']!; + const mod = require('./extensions-fixture/e.js'); // should not enter JS + expect(mod).toBe('hello world'); + } finally { + require.extensions['.js'] = original; + } +}); +test("test that assigning properties weirdly wont do anything bad", () => { + const original = require.extensions['.js']; + try { + function f1() {} + function f2() {} + require.extensions['.js'] = f1; + require.extensions['.abc'] = f2; + require.extensions['.js'] = f2; + require.extensions['.js'] = undefined!; + require.extensions['.abc'] = undefined!; + require.extensions['.abc'] = f1; + require.extensions['.js'] = f2; + } finally { + require.extensions['.js'] = original; + } +}); +test("wrapping an existing extension with no logic", () => { + const original = require.extensions['.js']; + try { + delete require.cache[require.resolve('./extensions-fixture/d')]; + const mocked = require.extensions['.js'] = mock(function (module, filename) { + expect(module).toBeDefined(); + expect(filename).toBe(path.join(import.meta.dir, 'extensions-fixture', 'd.js')); + original(module, filename); + }); + const mod = require('./extensions-fixture/d'); + expect(mod).toBe('d.js'); + expect(mocked).toBeCalled(); + } finally { + require.extensions['.js'] = original; + } +}); +test("wrapping an existing extension with mutated compile function", () => { + const original = require.extensions['.js']; + try { + delete require.cache[require.resolve('./extensions-fixture/d')]; + const mocked = require.extensions['.js'] = mock(function (module, filename) { + expect(module).toBeDefined(); + expect(filename).toBe(path.join(import.meta.dir, 'extensions-fixture', 'd.js')); + const originalCompile = module._compile; + module._compile = function (code, filename) { + expect(code).toBe('\n module.exports = \"d.js\";\n'); + expect(filename).toBe(path.join(import.meta.dir, 'extensions-fixture', 'd.js')); + originalCompile.call(module, 'module.exports = "new";', filename); + }; + original(module, filename); + }); + const mod = require('./extensions-fixture/d'); + expect(mod).toBe('new'); + expect(mocked).toBeCalled(); + } finally { + require.extensions['.js'] = original; + } +}); +test("wrapping an existing extension with mutated compile function ts", () => { + const original = require.extensions['.ts']; + assert(original); + try { + delete require.cache[require.resolve('./extensions-fixture/e.js')]; + const mocked = require.extensions['.js'] = mock(function (module, filename) { + expect(module).toBeDefined(); + expect(filename).toBe(path.join(import.meta.dir, 'extensions-fixture', 'e.js')); + const originalCompile = module._compile; + module._compile = function (code, filename) { + expect(code).toBe('\n var J;\n ((J) => J.x = \"hello\")(J ||= {});\n const hello = \" world\";\n module.exports = \"hello world\";\n'); + expect(filename).toBe(path.join(import.meta.dir, 'extensions-fixture', 'e.js')); + originalCompile.call(module, 'module.exports = "new";', filename); + }; + original(module, filename); + }); + const mod = require('./extensions-fixture/e'); + expect(mod).toBe('new'); + expect(mocked).toBeCalled(); + } finally { + require.extensions['.js'] = original; + } +}); +test("wrapping an existing extension but it's secretly sync esm", () => { + const original = require.extensions['.ts']; + assert(original); + try { + delete require.cache[require.resolve('./extensions-fixture/secretly_esm.cjs')]; + let called = false; + const mocked = require.extensions['.cjs'] = mock(function (module, filename) { + expect(module).toBeDefined(); + expect(filename).toBe(path.join(import.meta.dir, 'extensions-fixture', 'secretly_esm.cjs')); + module._compile = function (code, filename) { + called = true; + throw new Error('should not be called'); + }; + original(module, filename); + }); + const mod = require('./extensions-fixture/secretly_esm'); + expect(mod).toEqual({ default: 1 }); + expect(mocked).toBeCalled(); + } finally { + require.extensions['.cjs'] = original; + } +}); +test("mutating extensions is banned by some files", () => { + // vercel is not allowed to mutate require.extensions + const files = [ + 'node_modules/next/dist/build/next-config-ts/index.js', + 'node_modules/@meteorjs/babel/index.js', + ]; + const fixture = tempDirWithFiles('extensions-fixture', + Object.fromEntries(files.map(file => [file, ` + const assert = require('assert'); + const mock = function (module, filename) { + throw new Error('should not be called'); + }; + require.extensions['.js'] = mock; + assert(require.extensions['.js'] !== mock); + globalThis.pass += 1; + `]))); + globalThis.pass = 0; + + let n = 0; + for (const file of files) { + require(path.join(fixture, file)); + n++; + expect(globalThis.pass).toBe(n); + } +}); \ No newline at end of file diff --git a/test/js/node/test/test-module-multi-extensions.js b/test/js/node/test/test-module-multi-extensions.js new file mode 100644 index 0000000000..205a030c57 --- /dev/null +++ b/test/js/node/test/test-module-multi-extensions.js @@ -0,0 +1,93 @@ +'use strict'; + +// Refs: https://github.com/nodejs/node/issues/4778 + +const common = require('../common'); +const assert = require('assert'); +const fs = require('fs'); +const Module = require('module'); +const tmpdir = require('../common/tmpdir'); +const file = tmpdir.resolve('test-extensions.foo.bar'); +const dotfile = tmpdir.resolve('.bar'); +const dotfileWithExtension = tmpdir.resolve('.foo.bar'); + +tmpdir.refresh(); +fs.writeFileSync(file, 'console.log(__filename);', 'utf8'); +fs.writeFileSync(dotfile, 'console.log(__filename);', 'utf8'); +fs.writeFileSync(dotfileWithExtension, 'console.log(__filename);', 'utf8'); + +{ + require.extensions['.bar'] = common.mustNotCall(); + require.extensions['.foo.bar'] = common.mustCall(); + const modulePath = tmpdir.resolve('test-extensions'); + require(modulePath); + require(file); + delete require.cache[file]; + delete require.extensions['.bar']; + delete require.extensions['.foo.bar']; + Module._pathCache = { __proto__: null }; +} + +{ + require.extensions['.foo.bar'] = common.mustCall(); + const modulePath = tmpdir.resolve('test-extensions'); + require(modulePath); + assert.throws( + () => require(`${modulePath}.foo`), + (err) => err.message.startsWith(`Cannot find module '${modulePath}.foo'`) + ); + require(`${modulePath}.foo.bar`); + delete require.cache[file]; + delete require.extensions['.foo.bar']; + Module._pathCache = { __proto__: null }; +} + +{ + const modulePath = tmpdir.resolve('test-extensions'); + assert.throws( + () => require(modulePath), + (err) => err.message.startsWith(`Cannot find module '${modulePath}'`) + ); + delete require.cache[file]; + Module._pathCache = { __proto__: null }; +} + +{ + require.extensions['.bar'] = common.mustNotCall(); + require.extensions['.foo.bar'] = common.mustCall(); + const modulePath = tmpdir.resolve('test-extensions.foo'); + require(modulePath); + delete require.cache[file]; + delete require.extensions['.bar']; + delete require.extensions['.foo.bar']; + Module._pathCache = { __proto__: null }; +} + +{ + require.extensions['.foo.bar'] = common.mustNotCall(); + const modulePath = tmpdir.resolve('test-extensions.foo'); + assert.throws( + () => require(modulePath), + (err) => err.message.startsWith(`Cannot find module '${modulePath}'`) + ); + delete require.extensions['.foo.bar']; + Module._pathCache = { __proto__: null }; +} + +{ + require.extensions['.bar'] = common.mustNotCall(); + require(dotfile); + delete require.cache[dotfile]; + delete require.extensions['.bar']; + Module._pathCache = { __proto__: null }; +} + +{ + require.extensions['.bar'] = common.mustCall(); + require.extensions['.foo.bar'] = common.mustNotCall(); + require(dotfileWithExtension); + delete require.cache[dotfileWithExtension]; + delete require.extensions['.bar']; + delete require.extensions['.foo.bar']; + Module._pathCache = { __proto__: null }; +}