From ff635551436123022ba3980b39580d53973c80a2 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 24 Jun 2023 06:02:16 -0700 Subject: Rewrite Bun's runtime CommonJS loader (#3379) * wip changes for CommonJS * this rewrite is almost complete * even more code * wip * Remove usages of `import.meta.require` from builtins * Remove usages of require * Regenerate * :scissors: builtin rewrite commonjs in printer * Use lazy custom getters for import.meta * fixups * Remove depd * ugh * still crashing * fixup undici * comment out import.meta.require.resolve temporarily not a real solution but it stops the crashes * Redo import.meta.primordials * Builtins now have a `builtin://` protocol in source origin * Seems to work? * Finsih getting rid of primordials * switcharoo * No more function * just one more bug * Update launch.json * Implement `require.main` * :scissors: * Bump WebKit * Fixup import cycles * Fixup improt cycles * export more things * Implement `createCommonJSModule` builtin * More exports * regenerate * i broke some stuff * some of these tests work now * We lost the encoding * Sort of fix zlib * Sort of fix util * Update events.js * bump * bump * bump * Fix missing export in fs * fix some bugs with builtin esm modules (stream, worker_threads, events). its not perfect yet. * fix some other internal module bugs * oops * fix some extra require default stuff * uncomment this file but it crsahes on my machine * tidy code here * fixup tls exports * make simdutf happier * Add hasPrefix binding * Add test for `require.main` * Fix CommonJS evaluation order race condition * Make node:http load faster * Add missing exports to tls.js * Use the getter * Regenerate builtins * Fix assertion failure in Bun.write() * revamp dotEnv parser (#3347) - fixes `strings.indexOfAny()` - fixes OOB array access fixes #411 fixes #2823 fixes #3042 * fix tests for `expect()` (#3384) - extend test job time-out for `darwin-aarch64` * `expect().resolves` and `expect().rejects` (#3318) * Move expect and snapshots to their own files * expect().resolves and expect().rejects * Fix promise being added to unhandled rejection list * Handle timeouts in expect() * wip merge * Fix merge issue --------- Co-authored-by: Jarred Sumner Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> * fixup min/memcopy (#3388) * Fix crash in builtins * Don't attempt to evaluate modules with no source code * Update WebCoreJSBuiltins.cpp * Update WebCoreJSBuiltins.cpp * Update WebCoreJSBuiltins.cpp * Fix crash * cleanup * Fix test cc @paperdave * Fixup Undici * Fix issue in node:http * Create util-deprecate.mjs * Fix several bugs * Use the identifier * Support error.code in `util.deprecate` * make the CJs loader slightly more resilient * Update WebCoreJSBuiltins.cpp * Fix macros --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> Co-authored-by: dave caruso Co-authored-by: Alex Lam S.L Co-authored-by: Ashcon Partovi Co-authored-by: Ciro Spaciari --- src/bun.js/bindings/CommonJSModuleRecord.h | 78 +++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) (limited to 'src/bun.js/bindings/CommonJSModuleRecord.h') diff --git a/src/bun.js/bindings/CommonJSModuleRecord.h b/src/bun.js/bindings/CommonJSModuleRecord.h index 86daf875d..48f14b39c 100644 --- a/src/bun.js/bindings/CommonJSModuleRecord.h +++ b/src/bun.js/bindings/CommonJSModuleRecord.h @@ -6,14 +6,90 @@ class GlobalObject; } namespace JSC { class SourceCode; +class JSSourceCode; +class ProgramExecutable; +class AbstractModuleRecord; } namespace Bun { +JSC_DECLARE_HOST_FUNCTION(jsFunctionCreateCommonJSModule); +JSC_DECLARE_HOST_FUNCTION(jsFunctionLoadModule); + +class JSCommonJSModule final : public JSC::JSDestructibleObject { +public: + using Base = JSC::JSDestructibleObject; + static constexpr unsigned StructureFlags = Base::StructureFlags | JSC::OverridesPut; + + mutable JSC::WriteBarrier m_id; + mutable JSC::WriteBarrier m_filename; + mutable JSC::WriteBarrier m_dirname; + mutable JSC::WriteBarrier sourceCode; + + static void destroy(JSC::JSCell*); + ~JSCommonJSModule(); + + void finishCreation(JSC::VM& vm, + JSC::JSString* id, JSC::JSString* filename, + JSC::JSString* dirname, JSC::JSSourceCode* sourceCode); + + static JSC::Structure* createStructure(JSC::JSGlobalObject* globalObject); + + bool evaluate(Zig::GlobalObject* globalObject, const WTF::String& sourceURL, ResolvedSource resolvedSource); + bool evaluate(Zig::GlobalObject* globalObject, const WTF::String& key, const SyntheticSourceProvider::SyntheticSourceGenerator& generator); + + static JSCommonJSModule* create(JSC::VM& vm, JSC::Structure* structure, + JSC::JSString* id, + JSC::JSString* filename, + JSC::JSString* dirname, JSC::JSSourceCode* sourceCode); + + static JSCommonJSModule* create( + Zig::GlobalObject* globalObject, + const WTF::String& key, + JSValue exportsObject, + bool hasEvaluated = false); + + static JSCommonJSModule* create( + Zig::GlobalObject* globalObject, + const WTF::String& key, + ResolvedSource resolvedSource); + + void toSyntheticSource(JSC::JSGlobalObject* globalObject, + JSC::Identifier moduleKey, + Vector& exportNames, + JSC::MarkedArgumentBuffer& exportValues); + + JSValue exportsObject(); + JSValue id(); + + DECLARE_VISIT_CHILDREN; + + static bool put(JSC::JSCell* cell, JSC::JSGlobalObject* globalObject, + JSC::PropertyName propertyName, JSC::JSValue value, + JSC::PutPropertySlot& slot); + + DECLARE_INFO; + template + static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm); + + bool hasEvaluated = false; + + JSCommonJSModule(JSC::VM& vm, JSC::Structure* structure) + : Base(vm, structure) + { + } +}; + +JSCommonJSModule* createCommonJSModuleWithoutRunning( + Zig::GlobalObject* globalObject, + Ref sourceProvider, + const WTF::String& sourceURL, + ResolvedSource source); + JSC::Structure* createCommonJSModuleStructure( Zig::GlobalObject* globalObject); -JSC::SourceCode createCommonJSModule( +std::optional createCommonJSModule( Zig::GlobalObject* globalObject, ResolvedSource source); -- cgit v1.2.3 From ec3ed67bc9ad8cbb0e59234564d57265d5423fce Mon Sep 17 00:00:00 2001 From: Dylan Conway <35280289+dylan-conway@users.noreply.github.com> Date: Mon, 26 Jun 2023 08:12:37 -0700 Subject: implement `_nodeModulePaths` and `require.main.paths` (#3411) * tests in progress * add `require.main.paths`, add every dir up to root * remove imports --- src/bun.js/bindings/CommonJSModuleRecord.cpp | 31 +++++++++ src/bun.js/bindings/CommonJSModuleRecord.h | 1 + src/bun.js/modules/NodeModuleModule.cpp | 15 ++-- src/resolver/resolver.zig | 95 ++++++++++++++++++++++++++ test/js/node/module/node-module-module.test.js | 19 ++++++ 5 files changed, 150 insertions(+), 11 deletions(-) (limited to 'src/bun.js/bindings/CommonJSModuleRecord.h') diff --git a/src/bun.js/bindings/CommonJSModuleRecord.cpp b/src/bun.js/bindings/CommonJSModuleRecord.cpp index 8d4fe0a1e..3615db774 100644 --- a/src/bun.js/bindings/CommonJSModuleRecord.cpp +++ b/src/bun.js/bindings/CommonJSModuleRecord.cpp @@ -295,6 +295,35 @@ JSC_DEFINE_CUSTOM_SETTER(setterPath, return true; } +extern "C" EncodedJSValue Resolver__propForRequireMainPaths(JSGlobalObject*); + +JSC_DEFINE_CUSTOM_GETTER(getterPaths, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::PropertyName)) +{ + JSCommonJSModule* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (UNLIKELY(!thisObject)) { + return JSValue::encode(jsUndefined()); + } + + if (!thisObject->m_paths) { + JSValue paths = JSValue::decode(Resolver__propForRequireMainPaths(globalObject)); + thisObject->m_paths.set(globalObject->vm(), thisObject, paths); + } + + return JSValue::encode(thisObject->m_paths.get()); +} + +JSC_DEFINE_CUSTOM_SETTER(setterPaths, + (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, + JSC::EncodedJSValue value, JSC::PropertyName propertyName)) +{ + JSCommonJSModule* thisObject = jsDynamicCast(JSValue::decode(thisValue)); + if (!thisObject) + return false; + + thisObject->m_paths.set(globalObject->vm(), thisObject, JSValue::decode(value)); + return true; +} + JSC_DEFINE_CUSTOM_SETTER(setterFilename, (JSC::JSGlobalObject * globalObject, JSC::EncodedJSValue thisValue, JSC::EncodedJSValue value, JSC::PropertyName propertyName)) @@ -340,6 +369,7 @@ static const struct HashTableValue JSCommonJSModulePrototypeTableValues[] = { { "loaded"_s, static_cast(PropertyAttribute::PropertyCallback | PropertyAttribute::DontEnum | 0), NoIntrinsic, { HashTableValue::LazyPropertyType, createLoaded } }, { "parent"_s, static_cast(PropertyAttribute::PropertyCallback | PropertyAttribute::DontEnum | 0), NoIntrinsic, { HashTableValue::LazyPropertyType, createParent } }, { "path"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, getterPath, setterPath } }, + { "paths"_s, static_cast(PropertyAttribute::CustomAccessor), NoIntrinsic, { HashTableValue::GetterSetterType, getterPaths, setterPaths } }, }; class JSCommonJSModulePrototype final : public JSC::JSNonFinalObject { @@ -675,6 +705,7 @@ void JSCommonJSModule::visitChildrenImpl(JSCell* cell, Visitor& visitor) visitor.append(thisObject->sourceCode); visitor.append(thisObject->m_filename); visitor.append(thisObject->m_dirname); + visitor.append(thisObject->m_paths); } DEFINE_VISIT_CHILDREN(JSCommonJSModule); diff --git a/src/bun.js/bindings/CommonJSModuleRecord.h b/src/bun.js/bindings/CommonJSModuleRecord.h index 48f14b39c..a96ab5f75 100644 --- a/src/bun.js/bindings/CommonJSModuleRecord.h +++ b/src/bun.js/bindings/CommonJSModuleRecord.h @@ -24,6 +24,7 @@ public: mutable JSC::WriteBarrier m_id; mutable JSC::WriteBarrier m_filename; mutable JSC::WriteBarrier m_dirname; + mutable JSC::WriteBarrier m_paths; mutable JSC::WriteBarrier sourceCode; static void destroy(JSC::JSCell*); diff --git a/src/bun.js/modules/NodeModuleModule.cpp b/src/bun.js/modules/NodeModuleModule.cpp index 8b278ddd8..34d45698f 100644 --- a/src/bun.js/modules/NodeModuleModule.cpp +++ b/src/bun.js/modules/NodeModuleModule.cpp @@ -26,15 +26,8 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeModuleCreateRequire, scope, JSValue::encode(Zig::ImportMetaObject::createRequireFunction( vm, globalObject, val))); } -JSC_DEFINE_HOST_FUNCTION(jsFunctionNodeModulePaths, - (JSC::JSGlobalObject * globalObject, - JSC::CallFrame *callFrame)) { - return JSC::JSValue::encode(JSC::JSArray::create( - globalObject->vm(), - globalObject->arrayStructureForIndexingTypeDuringAllocation( - ArrayWithContiguous), - 0)); -} +extern "C" EncodedJSValue Resolver__nodeModulePathsForJS(JSGlobalObject *, + CallFrame *); JSC_DEFINE_HOST_FUNCTION(jsFunctionFindSourceMap, (JSGlobalObject * globalObject, @@ -114,7 +107,7 @@ void generateNodeModuleModule(JSC::JSGlobalObject *globalObject, vm, globalObject, 1, String("createRequire"_s), jsFunctionNodeModuleCreateRequire, ImplementationVisibility::Public)); exportValues.append(JSFunction::create(vm, globalObject, 1, String("paths"_s), - jsFunctionNodeModulePaths, + Resolver__nodeModulePathsForJS, ImplementationVisibility::Public)); exportValues.append(JSFunction::create( vm, globalObject, 1, String("findSourceMap"_s), jsFunctionFindSourceMap, @@ -143,7 +136,7 @@ void generateNodeModuleModule(JSC::JSGlobalObject *globalObject, exportNames.append(JSC::Identifier::fromString(vm, "_nodeModulePaths"_s)); exportValues.append(JSFunction::create( vm, globalObject, 0, String("_nodeModulePaths"_s), - jsFunctionNodeModulePaths, ImplementationVisibility::Public)); + Resolver__nodeModulePathsForJS, ImplementationVisibility::Public)); exportNames.append(JSC::Identifier::fromString(vm, "_cache"_s)); exportValues.append( diff --git a/src/resolver/resolver.zig b/src/resolver/resolver.zig index 14bc358d0..40b106f3a 100644 --- a/src/resolver/resolver.zig +++ b/src/resolver/resolver.zig @@ -94,6 +94,7 @@ const bufs = struct { threadlocal var remap_path_trailing_slash: [bun.MAX_PATH_BYTES]u8 = undefined; threadlocal var path_in_global_disk_cache: [bun.MAX_PATH_BYTES]u8 = undefined; threadlocal var abs_to_rel: [bun.MAX_PATH_BYTES]u8 = undefined; + threadlocal var node_modules_paths_buf: [bun.MAX_PATH_BYTES]u8 = undefined; pub inline fn bufs(comptime field: std.meta.DeclEnum(@This())) *@TypeOf(@field(@This(), @tagName(field))) { return &@field(@This(), @tagName(field)); @@ -3107,6 +3108,93 @@ pub const Resolver = struct { }; } + pub export fn Resolver__nodeModulePathsForJS(globalThis: *bun.JSC.JSGlobalObject, callframe: *bun.JSC.CallFrame) callconv(.C) bun.JSC.JSValue { + bun.JSC.markBinding(@src()); + const argument: bun.JSC.JSValue = callframe.argument(0); + + if (argument.isEmpty() or !argument.isString()) { + globalThis.throwInvalidArgumentType("nodeModulePaths", "path", "string"); + return .zero; + } + + const in_str = argument.toBunString(globalThis); + var r = &globalThis.bunVM().bundler.resolver; + return nodeModulePathsJSValue(r, in_str, globalThis); + } + + pub export fn Resolver__propForRequireMainPaths(globalThis: *bun.JSC.JSGlobalObject) callconv(.C) bun.JSC.JSValue { + bun.JSC.markBinding(@src()); + + const in_str = bun.String.create("."); + var r = &globalThis.bunVM().bundler.resolver; + return nodeModulePathsJSValue(r, in_str, globalThis); + } + + pub fn nodeModulePathsJSValue( + r: *ThisResolver, + in_str: bun.String, + globalObject: *bun.JSC.JSGlobalObject, + ) bun.JSC.JSValue { + var list = std.ArrayList(bun.String).init(bun.default_allocator); + defer list.deinit(); + + const sliced = in_str.toUTF8(bun.default_allocator); + defer sliced.deinit(); + + const str = brk: { + if (std.fs.path.isAbsolute(sliced.slice())) break :brk sliced.slice(); + var dir_path_buf = bufs(.node_modules_paths_buf); + break :brk r.fs.joinBuf(&[_]string{ r.fs.top_level_dir, sliced.slice() }, dir_path_buf); + }; + var arena = std.heap.ArenaAllocator.init(bun.default_allocator); + defer arena.deinit(); + var stack_fallback_allocator = std.heap.stackFallback(1024, arena.allocator()); + + if (r.readDirInfo(strings.withoutTrailingSlash(str)) catch null) |result| { + var dir_info = result; + + while (true) { + const path_without_trailing_slash = strings.withoutTrailingSlash(dir_info.abs_path); + const path_parts = brk: { + if (path_without_trailing_slash.len == 1 and path_without_trailing_slash[0] == '/') { + break :brk [2]string{ "", "/node_modules" }; + } + + break :brk [2]string{ path_without_trailing_slash, "/node_modules" }; + }; + list.append( + bun.String.create( + bun.strings.concat(stack_fallback_allocator.get(), &path_parts) catch unreachable, + ), + ) catch unreachable; + dir_info = (r.readDirInfo(std.fs.path.dirname(path_without_trailing_slash) orelse break) catch null) orelse break; + } + } else { + // does not exist + const full_path = std.fs.path.resolve(r.allocator, &[1][]const u8{str}) catch unreachable; + var path = full_path; + while (true) { + const path_without_trailing_slash = strings.withoutTrailingSlash(path); + + list.append( + bun.String.create( + bun.strings.concat( + stack_fallback_allocator.get(), + &[_]string{ + path_without_trailing_slash, + "/node_modules", + }, + ) catch unreachable, + ), + ) catch unreachable; + + path = path[0 .. strings.lastIndexOfChar(path, '/') orelse break]; + } + } + + return bun.String.toJSArray(globalObject, list.items); + } + pub fn loadAsIndex(r: *ThisResolver, dir_info: *DirInfo, extension_order: []const string) ?MatchResult { var rfs = &r.fs.fs; // Try the "index" file with extensions @@ -3892,3 +3980,10 @@ pub const GlobalCache = enum { }; } }; + +comptime { + if (!bun.JSC.is_bindgen) { + _ = Resolver.Resolver__nodeModulePathsForJS; + _ = Resolver.Resolver__propForRequireMainPaths; + } +} diff --git a/test/js/node/module/node-module-module.test.js b/test/js/node/module/node-module-module.test.js index 549b5e085..3ced63da1 100644 --- a/test/js/node/module/node-module-module.test.js +++ b/test/js/node/module/node-module-module.test.js @@ -1,5 +1,24 @@ import { expect, test } from "bun:test"; +import { _nodeModulePaths } from "module"; test("module.globalPaths exists", () => { expect(Array.isArray(require("module").globalPaths)).toBe(true); }); + +test("_nodeModulePaths() works", () => { + expect(() => { + _nodeModulePaths(); + }).toThrow(); + expect(_nodeModulePaths(".").length).toBeGreaterThan(0); + expect(_nodeModulePaths(".").pop()).toBe("/node_modules"); + expect(_nodeModulePaths("")).toEqual(_nodeModulePaths(".")); + expect(_nodeModulePaths("/")).toEqual(["/node_modules"]); + expect(_nodeModulePaths("/a/b/c/d")).toEqual([ + "/a/b/c/d/node_modules", + "/a/b/c/node_modules", + "/a/b/node_modules", + "/a/node_modules", + "/node_modules", + ]); + expect(_nodeModulePaths("/a/b/../d")).toEqual(["/a/d/node_modules", "/a/node_modules", "/node_modules"]); +}); -- cgit v1.2.3 From a732999da578ca92a1d9e633036225a32e77529d Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Mon, 26 Jun 2023 12:49:20 -0700 Subject: Runtime support for `__esModule` annotations (#3393) * Runtime support for `__esModule` annotations * Ignore `__esModule` annotation when `"type": "module"` is set --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- src/bun.js/bindings/CommonJSModuleRecord.cpp | 113 +++++++++++++++++++-- src/bun.js/bindings/CommonJSModuleRecord.h | 1 + src/bun.js/bindings/exports.zig | 7 +- src/bun.js/bindings/headers-handwritten.h | 1 + src/bun.js/module_loader.zig | 21 ++++ src/js/builtins/BunBuiltinNames.h | 7 +- test/js/bun/resolve/esModule-annotation.test.js | 68 +++++++++++++ .../export-esModule-annotation-empty.cjs | 1 + .../export-esModule-annotation-no-default.cjs | 1 + .../export-esModule-annotation.cjs | 2 + .../export-esModule-no-annotation.cjs | 1 + test/js/bun/resolve/with-type-module/package.json | 4 + .../export-esModule-annotation-empty.cjs | 1 + .../export-esModule-annotation-no-default.cjs | 1 + .../export-esModule-annotation.cjs | 2 + .../export-esModule-no-annotation.cjs | 1 + .../bun/resolve/without-type-module/package.json | 4 + 17 files changed, 221 insertions(+), 15 deletions(-) create mode 100644 test/js/bun/resolve/esModule-annotation.test.js create mode 100644 test/js/bun/resolve/with-type-module/export-esModule-annotation-empty.cjs create mode 100644 test/js/bun/resolve/with-type-module/export-esModule-annotation-no-default.cjs create mode 100644 test/js/bun/resolve/with-type-module/export-esModule-annotation.cjs create mode 100644 test/js/bun/resolve/with-type-module/export-esModule-no-annotation.cjs create mode 100644 test/js/bun/resolve/with-type-module/package.json create mode 100644 test/js/bun/resolve/without-type-module/export-esModule-annotation-empty.cjs create mode 100644 test/js/bun/resolve/without-type-module/export-esModule-annotation-no-default.cjs create mode 100644 test/js/bun/resolve/without-type-module/export-esModule-annotation.cjs create mode 100644 test/js/bun/resolve/without-type-module/export-esModule-no-annotation.cjs create mode 100644 test/js/bun/resolve/without-type-module/package.json (limited to 'src/bun.js/bindings/CommonJSModuleRecord.h') diff --git a/src/bun.js/bindings/CommonJSModuleRecord.cpp b/src/bun.js/bindings/CommonJSModuleRecord.cpp index 3615db774..c7dac89c2 100644 --- a/src/bun.js/bindings/CommonJSModuleRecord.cpp +++ b/src/bun.js/bindings/CommonJSModuleRecord.cpp @@ -586,13 +586,41 @@ void JSCommonJSModule::toSyntheticSource(JSC::JSGlobalObject* globalObject, auto result = this->exportsObject(); auto& vm = globalObject->vm(); - exportNames.append(vm.propertyNames->defaultKeyword); - exportValues.append(result); // This exists to tell ImportMetaObject.ts that this is a CommonJS module. exportNames.append(Identifier::fromUid(vm.symbolRegistry().symbolForKey("CommonJS"_s))); exportValues.append(jsNumber(0)); + // Bun's intepretation of the "__esModule" annotation: + // + // - If a "default" export does not exist OR the __esModule annotation is not present, then we + // set the default export to the exports object + // + // - If a "default" export also exists, then we set the default export + // to the value of it (matching Babel behavior) + // + // https://stackoverflow.com/questions/50943704/whats-the-purpose-of-object-definepropertyexports-esmodule-value-0 + // https://github.com/nodejs/node/issues/40891 + // https://github.com/evanw/bundler-esm-cjs-tests + // https://github.com/evanw/esbuild/issues/1591 + // https://github.com/oven-sh/bun/issues/3383 + // + // Note that this interpretation is slightly different + // + // - We do not ignore when "type": "module" or when the file + // extension is ".mjs". Build tools determine that based on the + // caller's behavior, but in a JS runtime, there is only one ModuleNamespaceObject. + // + // It would be possible to match the behavior at runtime, but + // it would need further engine changes which do not match the ES Module spec + // + // - We ignore the value of the annotation. We only look for the + // existence of the value being set. This is for performance reasons, but also + // this annotation is meant for tooling and the only usages of setting + // it to something that does NOT evaluate to "true" I could find were in + // unit tests of build tools. Happy to revisit this if users file an issue. + bool needsToAssignDefault = true; + if (result.isObject()) { auto* exports = asObject(result); @@ -601,21 +629,78 @@ void JSCommonJSModule::toSyntheticSource(JSC::JSGlobalObject* globalObject, exportNames.reserveCapacity(size + 2); exportValues.ensureCapacity(size + 2); - if (canPerformFastEnumeration(structure)) { + auto catchScope = DECLARE_CATCH_SCOPE(vm); + + Identifier esModuleMarker = builtinNames(vm).__esModulePublicName(); + bool hasESModuleMarker = !this->ignoreESModuleAnnotation && exports->hasProperty(globalObject, esModuleMarker); + if (catchScope.exception()) { + catchScope.clearException(); + } + + if (hasESModuleMarker) { + if (canPerformFastEnumeration(structure)) { + exports->structure()->forEachProperty(vm, [&](const PropertyTableEntry& entry) -> bool { + auto key = entry.key(); + if (key->isSymbol() || entry.attributes() & PropertyAttribute::DontEnum || key == esModuleMarker) + return true; + + needsToAssignDefault = needsToAssignDefault && key != vm.propertyNames->defaultKeyword; + + JSValue value = exports->getDirect(entry.offset()); + exportNames.append(Identifier::fromUid(vm, key)); + exportValues.append(value); + return true; + }); + } else { + JSC::PropertyNameArray properties(vm, JSC::PropertyNameMode::Strings, JSC::PrivateSymbolMode::Exclude); + exports->methodTable()->getOwnPropertyNames(exports, globalObject, properties, DontEnumPropertiesMode::Exclude); + if (catchScope.exception()) { + catchScope.clearExceptionExceptTermination(); + return; + } + + for (auto property : properties) { + if (UNLIKELY(property.isEmpty() || property.isNull() || property == esModuleMarker || property.isPrivateName() || property.isSymbol())) + continue; + + // ignore constructor + if (property == vm.propertyNames->constructor) + continue; + + JSC::PropertySlot slot(exports, PropertySlot::InternalMethodType::Get); + if (!exports->getPropertySlot(globalObject, property, slot)) + continue; + + exportNames.append(property); + + JSValue getterResult = slot.getValue(globalObject, property); + + // If it throws, we keep them in the exports list, but mark it as undefined + // This is consistent with what Node.js does. + if (catchScope.exception()) { + catchScope.clearException(); + getterResult = jsUndefined(); + } + + exportValues.append(getterResult); + + needsToAssignDefault = needsToAssignDefault && property != vm.propertyNames->defaultKeyword; + } + } + + } else if (canPerformFastEnumeration(structure)) { exports->structure()->forEachProperty(vm, [&](const PropertyTableEntry& entry) -> bool { auto key = entry.key(); - if (key->isSymbol() || key == vm.propertyNames->defaultKeyword || entry.attributes() & PropertyAttribute::DontEnum) + if (key->isSymbol() || entry.attributes() & PropertyAttribute::DontEnum || key == vm.propertyNames->defaultKeyword) return true; - exportNames.append(Identifier::fromUid(vm, key)); - JSValue value = exports->getDirect(entry.offset()); + exportNames.append(Identifier::fromUid(vm, key)); exportValues.append(value); return true; }); } else { - auto catchScope = DECLARE_CATCH_SCOPE(vm); JSC::PropertyNameArray properties(vm, JSC::PropertyNameMode::Strings, JSC::PrivateSymbolMode::Exclude); exports->methodTable()->getOwnPropertyNames(exports, globalObject, properties, DontEnumPropertiesMode::Exclude); if (catchScope.exception()) { @@ -624,11 +709,11 @@ void JSCommonJSModule::toSyntheticSource(JSC::JSGlobalObject* globalObject, } for (auto property : properties) { - if (UNLIKELY(property.isEmpty() || property.isNull() || property.isPrivateName() || property.isSymbol())) + if (UNLIKELY(property.isEmpty() || property.isNull() || property == vm.propertyNames->defaultKeyword || property.isPrivateName() || property.isSymbol())) continue; // ignore constructor - if (property == vm.propertyNames->constructor || property == vm.propertyNames->defaultKeyword) + if (property == vm.propertyNames->constructor) continue; JSC::PropertySlot slot(exports, PropertySlot::InternalMethodType::Get); @@ -650,6 +735,11 @@ void JSCommonJSModule::toSyntheticSource(JSC::JSGlobalObject* globalObject, } } } + + if (needsToAssignDefault) { + exportNames.append(vm.propertyNames->defaultKeyword); + exportValues.append(result); + } } JSValue JSCommonJSModule::exportsObject() @@ -759,6 +849,7 @@ bool JSCommonJSModule::evaluate( { auto& vm = globalObject->vm(); auto sourceProvider = Zig::SourceProvider::create(jsCast(globalObject), source, JSC::SourceProviderSourceType::Program); + this->ignoreESModuleAnnotation = source.tag == ResolvedSourceTagPackageJSONTypeModule; JSC::SourceCode rawInputSource( WTFMove(sourceProvider)); @@ -766,6 +857,7 @@ bool JSCommonJSModule::evaluate( return true; this->sourceCode.set(vm, this, JSC::JSSourceCode::create(vm, WTFMove(rawInputSource))); + WTF::NakedPtr exception; evaluateCommonJSModuleOnce(vm, globalObject, this, this->m_dirname.get(), this->m_filename.get(), exception); @@ -796,6 +888,7 @@ std::optional createCommonJSModule( JSValue entry = globalObject->requireMap()->get(globalObject, specifierValue); auto sourceProvider = Zig::SourceProvider::create(jsCast(globalObject), source, JSC::SourceProviderSourceType::Program); + bool ignoreESModuleAnnotation = source.tag == ResolvedSourceTagPackageJSONTypeModule; SourceOrigin sourceOrigin = sourceProvider->sourceOrigin(); if (entry) { @@ -827,6 +920,8 @@ std::optional createCommonJSModule( globalObject->requireMap()->set(globalObject, requireMapKey, moduleObject); } + moduleObject->ignoreESModuleAnnotation = ignoreESModuleAnnotation; + return JSC::SourceCode( JSC::SyntheticSourceProvider::create( [](JSC::JSGlobalObject* lexicalGlobalObject, diff --git a/src/bun.js/bindings/CommonJSModuleRecord.h b/src/bun.js/bindings/CommonJSModuleRecord.h index a96ab5f75..15792f9da 100644 --- a/src/bun.js/bindings/CommonJSModuleRecord.h +++ b/src/bun.js/bindings/CommonJSModuleRecord.h @@ -26,6 +26,7 @@ public: mutable JSC::WriteBarrier m_dirname; mutable JSC::WriteBarrier m_paths; mutable JSC::WriteBarrier sourceCode; + bool ignoreESModuleAnnotation { false }; static void destroy(JSC::JSCell*); ~JSCommonJSModule(); diff --git a/src/bun.js/bindings/exports.zig b/src/bun.js/bindings/exports.zig index f77b57216..213291e7b 100644 --- a/src/bun.js/bindings/exports.zig +++ b/src/bun.js/bindings/exports.zig @@ -216,9 +216,10 @@ pub const ResolvedSource = extern struct { pub const Tag = enum(u64) { javascript = 0, - wasm = 1, - object = 2, - file = 3, + package_json_type_module = 1, + wasm = 2, + object = 3, + file = 4, @"node:buffer" = 1024, @"node:process" = 1025, diff --git a/src/bun.js/bindings/headers-handwritten.h b/src/bun.js/bindings/headers-handwritten.h index 57940550f..db1e38d3e 100644 --- a/src/bun.js/bindings/headers-handwritten.h +++ b/src/bun.js/bindings/headers-handwritten.h @@ -72,6 +72,7 @@ typedef struct ResolvedSource { void* allocator; uint64_t tag; } ResolvedSource; +static const uint64_t ResolvedSourceTagPackageJSONTypeModule = 1; typedef union ErrorableResolvedSourceResult { ResolvedSource value; ZigErrorType err; diff --git a/src/bun.js/module_loader.zig b/src/bun.js/module_loader.zig index 5838d8a49..b25bb4b10 100644 --- a/src/bun.js/module_loader.zig +++ b/src/bun.js/module_loader.zig @@ -1225,6 +1225,25 @@ pub const ModuleLoader = struct { return resolved_source; } + // Pass along package.json type "module" if set. + const tag = brk: { + if (parse_result.ast.exports_kind == .cjs and parse_result.source.path.isFile()) { + var actual_package_json: *PackageJSON = package_json orelse brk2: { + // this should already be cached virtually always so it's fine to do this + var dir_info = (jsc_vm.bundler.resolver.readDirInfo(parse_result.source.path.name.dir) catch null) orelse + break :brk .javascript; + + break :brk2 dir_info.package_json orelse dir_info.enclosing_package_json; + } orelse break :brk .javascript; + + if (actual_package_json.module_type == .esm) { + break :brk ResolvedSource.Tag.package_json_type_module; + } + } + + break :brk ResolvedSource.Tag.javascript; + }; + return .{ .allocator = null, .source_code = bun.String.createLatin1(printer.ctx.getWritten()), @@ -1245,6 +1264,8 @@ pub const ModuleLoader = struct { // having JSC own the memory causes crashes .hash = 0, + + .tag = tag, }; }, // provideFetch() should be called diff --git a/src/js/builtins/BunBuiltinNames.h b/src/js/builtins/BunBuiltinNames.h index af9291918..1897f939e 100644 --- a/src/js/builtins/BunBuiltinNames.h +++ b/src/js/builtins/BunBuiltinNames.h @@ -37,6 +37,7 @@ using namespace JSC; macro(WritableStream) \ macro(WritableStreamDefaultController) \ macro(WritableStreamDefaultWriter) \ + macro(__esModule) \ macro(_events) \ macro(abortAlgorithm) \ macro(abortSteps) \ @@ -59,11 +60,11 @@ using namespace JSC; macro(cloneArrayBuffer) \ macro(close) \ macro(closeAlgorithm) \ + macro(closeRequest) \ + macro(closeRequested) \ macro(closed) \ macro(closedPromise) \ macro(closedPromiseCapability) \ - macro(closeRequest) \ - macro(closeRequested) \ macro(code) \ macro(commonJSSymbol) \ macro(connect) \ @@ -94,6 +95,7 @@ using namespace JSC; macro(end) \ macro(errno) \ macro(errorSteps) \ + macro(evaluateCommonJSModule) \ macro(execArgv) \ macro(exports) \ macro(extname) \ @@ -137,7 +139,6 @@ using namespace JSC; macro(lazyLoad) \ macro(lazyStreamPrototypeMap) \ macro(loadCJS2ESM) \ - macro(evaluateCommonJSModule) \ macro(localStreams) \ macro(main) \ macro(makeDOMException) \ diff --git a/test/js/bun/resolve/esModule-annotation.test.js b/test/js/bun/resolve/esModule-annotation.test.js new file mode 100644 index 000000000..33c84be5d --- /dev/null +++ b/test/js/bun/resolve/esModule-annotation.test.js @@ -0,0 +1,68 @@ +import { test, expect, describe } from "bun:test"; +import * as WithTypeModuleExportEsModuleAnnotationMissingDefault from "./with-type-module/export-esModule-annotation-empty.cjs"; +import * as WithTypeModuleExportEsModuleAnnotationNoDefault from "./with-type-module/export-esModule-annotation-no-default.cjs"; +import * as WithTypeModuleExportEsModuleAnnotation from "./with-type-module/export-esModule-annotation.cjs"; +import * as WithTypeModuleExportEsModuleNoAnnotation from "./with-type-module/export-esModule-no-annotation.cjs"; +import * as WithoutTypeModuleExportEsModuleAnnotationMissingDefault from "./without-type-module/export-esModule-annotation-empty.cjs"; +import * as WithoutTypeModuleExportEsModuleAnnotationNoDefault from "./without-type-module/export-esModule-annotation-no-default.cjs"; +import * as WithoutTypeModuleExportEsModuleAnnotation from "./without-type-module/export-esModule-annotation.cjs"; +import * as WithoutTypeModuleExportEsModuleNoAnnotation from "./without-type-module/export-esModule-no-annotation.cjs"; + +describe('without type: "module"', () => { + test("module.exports = {}", () => { + expect(WithoutTypeModuleExportEsModuleAnnotationMissingDefault.default).toEqual({}); + expect(WithoutTypeModuleExportEsModuleAnnotationMissingDefault.__esModule).toBeUndefined(); + }); + + test("exports.__esModule = true", () => { + expect(WithoutTypeModuleExportEsModuleAnnotationNoDefault.default).toEqual({ + __esModule: true, + }); + + // The module namespace object will not have the __esModule property. + expect(WithoutTypeModuleExportEsModuleAnnotationNoDefault).not.toHaveProperty("__esModule"); + }); + + test("exports.default = true; exports.__esModule = true;", () => { + expect(WithoutTypeModuleExportEsModuleAnnotation.default).toBeTrue(); + expect(WithoutTypeModuleExportEsModuleAnnotation.__esModule).toBeUndefined(); + }); + + test("exports.default = true;", () => { + expect(WithoutTypeModuleExportEsModuleNoAnnotation.default).toEqual({ + default: true, + }); + expect(WithoutTypeModuleExportEsModuleAnnotation.__esModule).toBeUndefined(); + }); +}); + +describe('with type: "module"', () => { + test("module.exports = {}", () => { + expect(WithTypeModuleExportEsModuleAnnotationMissingDefault.default).toEqual({}); + expect(WithTypeModuleExportEsModuleAnnotationMissingDefault.__esModule).toBeUndefined(); + }); + + test("exports.__esModule = true", () => { + expect(WithTypeModuleExportEsModuleAnnotationNoDefault.default).toEqual({ + __esModule: true, + }); + + // The module namespace object WILL have the __esModule property. + expect(WithTypeModuleExportEsModuleAnnotationNoDefault).toHaveProperty("__esModule"); + }); + + test("exports.default = true; exports.__esModule = true;", () => { + expect(WithTypeModuleExportEsModuleAnnotation.default).toEqual({ + default: true, + __esModule: true, + }); + expect(WithTypeModuleExportEsModuleAnnotation.__esModule).toBeTrue(); + }); + + test("exports.default = true;", () => { + expect(WithTypeModuleExportEsModuleNoAnnotation.default).toEqual({ + default: true, + }); + expect(WithTypeModuleExportEsModuleAnnotation.__esModule).toBeTrue(); + }); +}); diff --git a/test/js/bun/resolve/with-type-module/export-esModule-annotation-empty.cjs b/test/js/bun/resolve/with-type-module/export-esModule-annotation-empty.cjs new file mode 100644 index 000000000..f053ebf79 --- /dev/null +++ b/test/js/bun/resolve/with-type-module/export-esModule-annotation-empty.cjs @@ -0,0 +1 @@ +module.exports = {}; diff --git a/test/js/bun/resolve/with-type-module/export-esModule-annotation-no-default.cjs b/test/js/bun/resolve/with-type-module/export-esModule-annotation-no-default.cjs new file mode 100644 index 000000000..32b83d4a5 --- /dev/null +++ b/test/js/bun/resolve/with-type-module/export-esModule-annotation-no-default.cjs @@ -0,0 +1 @@ +exports.__esModule = true; diff --git a/test/js/bun/resolve/with-type-module/export-esModule-annotation.cjs b/test/js/bun/resolve/with-type-module/export-esModule-annotation.cjs new file mode 100644 index 000000000..bc0625a0c --- /dev/null +++ b/test/js/bun/resolve/with-type-module/export-esModule-annotation.cjs @@ -0,0 +1,2 @@ +exports.default = true; +exports.__esModule = true; diff --git a/test/js/bun/resolve/with-type-module/export-esModule-no-annotation.cjs b/test/js/bun/resolve/with-type-module/export-esModule-no-annotation.cjs new file mode 100644 index 000000000..a4b65815f --- /dev/null +++ b/test/js/bun/resolve/with-type-module/export-esModule-no-annotation.cjs @@ -0,0 +1 @@ +exports.default = true; diff --git a/test/js/bun/resolve/with-type-module/package.json b/test/js/bun/resolve/with-type-module/package.json new file mode 100644 index 000000000..f1863a426 --- /dev/null +++ b/test/js/bun/resolve/with-type-module/package.json @@ -0,0 +1,4 @@ +{ + "name": "with-type-module", + "type": "module" +} diff --git a/test/js/bun/resolve/without-type-module/export-esModule-annotation-empty.cjs b/test/js/bun/resolve/without-type-module/export-esModule-annotation-empty.cjs new file mode 100644 index 000000000..f053ebf79 --- /dev/null +++ b/test/js/bun/resolve/without-type-module/export-esModule-annotation-empty.cjs @@ -0,0 +1 @@ +module.exports = {}; diff --git a/test/js/bun/resolve/without-type-module/export-esModule-annotation-no-default.cjs b/test/js/bun/resolve/without-type-module/export-esModule-annotation-no-default.cjs new file mode 100644 index 000000000..32b83d4a5 --- /dev/null +++ b/test/js/bun/resolve/without-type-module/export-esModule-annotation-no-default.cjs @@ -0,0 +1 @@ +exports.__esModule = true; diff --git a/test/js/bun/resolve/without-type-module/export-esModule-annotation.cjs b/test/js/bun/resolve/without-type-module/export-esModule-annotation.cjs new file mode 100644 index 000000000..bc0625a0c --- /dev/null +++ b/test/js/bun/resolve/without-type-module/export-esModule-annotation.cjs @@ -0,0 +1,2 @@ +exports.default = true; +exports.__esModule = true; diff --git a/test/js/bun/resolve/without-type-module/export-esModule-no-annotation.cjs b/test/js/bun/resolve/without-type-module/export-esModule-no-annotation.cjs new file mode 100644 index 000000000..a4b65815f --- /dev/null +++ b/test/js/bun/resolve/without-type-module/export-esModule-no-annotation.cjs @@ -0,0 +1 @@ +exports.default = true; diff --git a/test/js/bun/resolve/without-type-module/package.json b/test/js/bun/resolve/without-type-module/package.json new file mode 100644 index 000000000..5b290db1c --- /dev/null +++ b/test/js/bun/resolve/without-type-module/package.json @@ -0,0 +1,4 @@ +{ + "name": "without-type-module", + "type": "commonjs" +} -- cgit v1.2.3