Compare commits

...

4 Commits

Author SHA1 Message Date
Claude Bot
4a113328c4 Fix __esModule handling: restore bundler interoperability
The initial PR incorrectly removed __esModule from the bundler's
__toCommonJS helper. While removing __esModule from runtime CJS
module handling was correct, the bundler still needs to add it when
converting ESM to CJS format for proper interoperability.

Changes:
- Restored __esModule: true in __toCommonJS (bundler helper)
- Kept __toESM changes (always set default, no __esModule check)
- Restored __toCommonJS wrapping in doStep5.zig for CJS entry points
- Updated test expectations for shorter __toESM code

The key distinction:
- Runtime CJS modules: No automatic __esModule (matches Node.js)
- Bundled ESM→CJS: Adds __esModule: true (matches esbuild, needed
  for CJS modules requiring the bundle to detect it was ESM)

This matches esbuild's behavior and fixes bundler tests.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-04 07:38:10 +00:00
Claude Bot
d75589f2d6 Merge main into claude/remove-esmodule-workaround 2025-10-04 05:39:07 +00:00
autofix-ci[bot]
8bdf0765c1 [autofix.ci] apply automated fixes 2025-09-19 20:36:22 +00:00
Claude Bot
18752929dc Remove __esModule workaround for Node.js compatibility
This is a breaking change that removes Bun's special handling of the __esModule annotation in CommonJS modules. The workaround was causing more issues than it solved and prevented full Node.js compatibility.

Key changes:
- CommonJS modules now always export module.exports as the default export
- __esModule is no longer treated specially and won't be automatically added
- Bundler no longer adds or filters __esModule properties
- Runtime helpers simplified to remove __esModule logic

This fixes several compatibility issues:
- Fixes #4506: CommonJS module.exports functions are now directly callable
- Fixes #6388: CLI tools work correctly without terminal crashes
- Fixes #7465: Default exports from packages like concurrently work properly
- May fix #3881: ESM/CommonJS edge cases with Vite
- May fix #4677: Module resolution for reflect-metadata

Breaking change: Code that relied on Bun's __esModule workaround behavior will need to be updated. The new behavior matches Node.js more closely.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-19 20:33:30 +00:00
12 changed files with 349 additions and 188 deletions

View File

@@ -945,130 +945,25 @@ void populateESMExports(
JSC::JSGlobalObject* globalObject,
JSValue result,
Vector<JSC::Identifier, 4>& exportNames,
JSC::MarkedArgumentBuffer& exportValues,
bool ignoreESModuleAnnotation)
JSC::MarkedArgumentBuffer& exportValues)
{
auto& vm = JSC::getVM(globalObject);
const Identifier& esModuleMarker = vm.propertyNames->__esModule;
// Bun's interpretation 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;
auto scope = DECLARE_THROW_SCOPE(vm);
if (auto* exports = result.getObject()) {
bool hasESModuleMarker = false;
if (!ignoreESModuleAnnotation) {
PropertySlot slot(exports, PropertySlot::InternalMethodType::VMInquiry, &vm);
auto has = exports->getPropertySlot(globalObject, esModuleMarker, slot);
scope.assertNoException();
if (has) {
JSValue value = slot.getValue(globalObject, esModuleMarker);
CLEAR_IF_EXCEPTION(scope);
if (!value.isUndefinedOrNull()) {
if (value.pureToBoolean() == TriState::True) {
hasESModuleMarker = true;
}
}
}
}
// After removing the __esModule workaround, CommonJS modules always
// export the entire exports object as the default export.
// Named exports are still extracted from the exports object.
if (auto* exports = result.getObject()) {
auto* structure = exports->structure();
uint32_t size = structure->inlineSize() + structure->outOfLineSize();
exportNames.reserveCapacity(size + 2);
exportValues.ensureCapacity(size + 2);
exportNames.reserveCapacity(size + 1);
exportValues.ensureCapacity(size + 1);
CLEAR_IF_EXCEPTION(scope);
if (hasESModuleMarker) {
if (canPerformFastEnumeration(structure)) {
exports->structure()->forEachProperty(vm, [&](const PropertyTableEntry& entry) -> bool {
auto key = entry.key();
if (key->isSymbol() || 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 (scope.exception()) [[unlikely]] {
if (!vm.hasPendingTerminationException()) scope.clearException();
return;
}
for (auto property : properties) {
if (property.isEmpty() || property.isNull() || property == esModuleMarker || property.isPrivateName() || property.isSymbol()) [[unlikely]]
continue;
// ignore constructor
if (property == vm.propertyNames->constructor)
continue;
JSC::PropertySlot slot(exports, PropertySlot::InternalMethodType::Get);
auto has = exports->getPropertySlot(globalObject, property, slot);
RETURN_IF_EXCEPTION(scope, );
if (!has) continue;
// Allow DontEnum properties which are not getter/setters
// https://github.com/oven-sh/bun/issues/4432
if (slot.attributes() & PropertyAttribute::DontEnum) {
if (!(slot.isValue() || slot.isCustom())) {
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 (scope.exception()) [[unlikely]] {
scope.clearException();
getterResult = jsUndefined();
}
exportValues.append(getterResult);
needsToAssignDefault = needsToAssignDefault && property != vm.propertyNames->defaultKeyword;
}
}
} else if (canPerformFastEnumeration(structure)) {
if (canPerformFastEnumeration(structure)) {
exports->structure()->forEachProperty(vm, [&](const PropertyTableEntry& entry) -> bool {
auto key = entry.key();
if (key->isSymbol() || key == vm.propertyNames->defaultKeyword)
@@ -1125,10 +1020,9 @@ void populateESMExports(
}
}
if (needsToAssignDefault) {
exportNames.append(vm.propertyNames->defaultKeyword);
exportValues.append(result);
}
// Always assign the entire exports object as the default export
exportNames.append(vm.propertyNames->defaultKeyword);
exportValues.append(result);
}
void JSCommonJSModule::toSyntheticSource(JSC::JSGlobalObject* globalObject,
@@ -1140,7 +1034,7 @@ void JSCommonJSModule::toSyntheticSource(JSC::JSGlobalObject* globalObject,
auto result = this->exportsObject();
RETURN_IF_EXCEPTION(scope, );
RELEASE_AND_RETURN(scope, populateESMExports(globalObject, result, exportNames, exportValues, this->ignoreESModuleAnnotation));
RELEASE_AND_RETURN(scope, populateESMExports(globalObject, result, exportNames, exportValues));
}
void JSCommonJSModule::setExportsObject(JSC::JSValue exportsObject)
@@ -1364,7 +1258,6 @@ void JSCommonJSModule::evaluate(
}
auto sourceProvider = Zig::SourceProvider::create(jsCast<Zig::GlobalObject*>(globalObject), source, JSC::SourceProviderSourceType::Program, isBuiltIn);
this->ignoreESModuleAnnotation = source.tag == ResolvedSourceTagPackageJSONTypeModule;
if (this->hasEvaluated)
return;
@@ -1433,7 +1326,6 @@ std::optional<JSC::SourceCode> createCommonJSModule(
JSValue entry = globalObject->requireMap()->get(globalObject, requireMapKey);
RETURN_IF_EXCEPTION(scope, {});
bool ignoreESModuleAnnotation = source.tag == ResolvedSourceTagPackageJSONTypeModule;
SourceOrigin sourceOrigin;
if (entry) {
@@ -1481,8 +1373,6 @@ std::optional<JSC::SourceCode> createCommonJSModule(
sourceOrigin = Zig::toSourceOrigin(sourceURL, isBuiltIn);
}
moduleObject->ignoreESModuleAnnotation = ignoreESModuleAnnotation;
return JSC::SourceCode(
JSC::SyntheticSourceProvider::create(
[](JSC::JSGlobalObject* lexicalGlobalObject,

View File

@@ -29,8 +29,7 @@ void populateESMExports(
JSC::JSGlobalObject* globalObject,
JSC::JSValue result,
WTF::Vector<JSC::Identifier, 4>& exportNames,
JSC::MarkedArgumentBuffer& exportValues,
bool ignoreESModuleAnnotation);
JSC::MarkedArgumentBuffer& exportValues);
class JSCommonJSModule final : public JSC::JSDestructibleObject {
public:
@@ -71,7 +70,6 @@ public:
// compile function is not stored here, but in
mutable JSC::WriteBarrier<Unknown> m_overriddenCompile;
bool ignoreESModuleAnnotation { false };
JSC::SourceCode sourceCode = JSC::SourceCode();
static size_t estimatedSize(JSC::JSCell* cell, JSC::VM& vm);

View File

@@ -416,11 +416,8 @@ pub fn createExportsForFile(
c.graph.ast.items(.flags)[id].uses_exports_ref = true;
}
// Decorate "module.exports" with the "__esModule" flag to indicate that
// we used to be an ES module. This is done by wrapping the exports object
// instead of by mutating the exports object because other modules in the
// bundle (including the entry point module) may do "import * as" to get
// access to the exports object and should NOT see the "__esModule" flag.
// Export the ES module exports as CommonJS exports.
// Wrap in __toCommonJS to add __esModule marker for interoperability
if (force_include_exports_for_entry_point) {
const toCommonJSRef = c.runtimeFunction("__toCommonJS");

View File

@@ -235,7 +235,6 @@ pub fn generateCodeForFileInChunkJS(
if (prop.key == null or prop.key.?.data != .e_string or prop.value == null) continue;
const name = prop.key.?.data.e_string.slice(temp_allocator);
if (strings.eqlComptime(name, "default") or
strings.eqlComptime(name, "__esModule") or
!bun.js_lexer.isIdentifier(name)) continue;
if (resolved_exports.get(name)) |export_data| {

View File

@@ -310,7 +310,7 @@ pub fn generateCodeForLazyExport(this: *LinkerContext, source_index: Index.Int)
for (expr.data.e_object.properties.slice()) |property_| {
const property: G.Property = property_;
if (property.key == null or property.key.?.data != .e_string or property.value == null or
property.key.?.data.e_string.eqlComptime("default") or property.key.?.data.e_string.eqlComptime("__esModule"))
property.key.?.data.e_string.eqlComptime("default"))
{
continue;
}

View File

@@ -116,21 +116,6 @@ export function overridableRequire(this: JSCommonJSModule, originalId: string, o
// If we can pull out a ModuleNamespaceObject, let's do it.
if (esm?.evaluated && (esm.state ?? 0) >= $ModuleReady) {
const namespace = Loader.getModuleNamespaceObject(esm!.module);
// In Bun, when __esModule is not defined, it's a CustomAccessor on the prototype.
// Various libraries expect __esModule to be set when using ESM from require().
// We don't want to always inject the __esModule export into every module,
// And creating an Object wrapper causes the actual exports to not be own properties.
// So instead of either of those, we make it so that the __esModule property can be set at runtime.
// It only supports "true" and undefined. Anything non-truthy is treated as undefined.
// https://github.com/oven-sh/bun/issues/14411
if (namespace.__esModule === undefined) {
try {
namespace.__esModule = true;
} catch {
// https://github.com/oven-sh/bun/issues/17816
}
}
return (mod.exports = namespace["module.exports"] ?? namespace);
}
}
@@ -323,21 +308,6 @@ export function requireESMFromHijackedExtension(this: JSCommonJSModule, id: stri
// 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;
}

View File

@@ -37,19 +37,14 @@ export var __reExport = (target, mod, secondTarget) => {
}
};
// Converts the module from CommonJS to ESM. When in node mode (i.e. in an
// ".mjs" file, package.json has "type: module", or the "__esModule" export
// in the CommonJS file is falsy or missing), the "default" property is
// overridden to point to the original CommonJS exports object instead.
// Converts the module from CommonJS to ESM. With the __esModule workaround removed,
// CommonJS modules are always treated as having a single default export.
export var __toESM = (mod, isNodeMode, target) => {
target = mod != null ? __create(__getProtoOf(mod)) : {};
const to =
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
// Always set "default" to the CommonJS "module.exports" for consistency
const to = __defProp(target, "default", { value: mod, enumerable: true });
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
// Copy all properties from the CommonJS module to the ESM namespace
for (let key of __getOwnPropNames(mod))
if (!__hasOwnProp.call(to, key))
__defProp(to, key, {

View File

@@ -1105,6 +1105,7 @@ describe("bundler", () => {
sideEffect(() => {});
`,
},
backend: "cli",
outdir: "/out",
sourceMap: "external",
minifySyntax: true,
@@ -1113,7 +1114,7 @@ describe("bundler", () => {
snapshotSourceMap: {
"entry.js.map": {
files: ["../node_modules/react/index.js", "../entry.js"],
mappingsExactMatch: "qYACA,WAAW,IAAQ,EAAE,ICDrB,eACA,QAAQ,IAAI,CAAK",
mappingsExactMatch: "8WACA,WAAW,IAAQ,EAAE,ICDrB,eACA,QAAQ,IAAI,CAAK",
},
},
});

View File

@@ -57,9 +57,9 @@ describe("bundler", () => {
"../entry.tsx",
],
mappings: [
["react.development.js:524:'getContextName'", "1:5412:Y1"],
["react.development.js:524:'getContextName'", "1:5386:Y1"],
["react.development.js:2495:'actScopeDepth'", "23:4082:GJ++"],
["react.development.js:696:''Component'", '1:7474:\'Component "%s"'],
["react.development.js:696:''Component'", '1:7448:\'Component "%s"'],
["entry.tsx:6:'\"Content-Type\"'", '100:18809:"Content-Type"'],
["entry.tsx:11:'<html>'", "100:19063:void"],
["entry.tsx:23:'await'", "100:19163:await"],
@@ -67,7 +67,7 @@ describe("bundler", () => {
},
},
expectExactFilesize: {
"out/entry.js": 221726,
"out/entry.js": 221700,
},
run: {
stdout: "<!DOCTYPE html><html><body><h1>Hello World</h1><p>This is an example.</p></body></html>",

View File

@@ -10,59 +10,71 @@ import * as WithoutTypeModuleExportEsModuleNoAnnotation from "./without-type-mod
describe('without type: "module"', () => {
test("module.exports = {}", () => {
// CommonJS always exports entire module.exports as default
expect(WithoutTypeModuleExportEsModuleAnnotationMissingDefault.default).toEqual({});
expect(WithoutTypeModuleExportEsModuleAnnotationMissingDefault.__esModule).toBeUndefined();
});
test("exports.__esModule = true", () => {
// CommonJS exports entire module.exports as default, including __esModule property
expect(WithoutTypeModuleExportEsModuleAnnotationNoDefault.default).toEqual({
__esModule: true,
});
// The module namespace object will not have the __esModule property.
expect(WithoutTypeModuleExportEsModuleAnnotationNoDefault.__esModule).toBeUndefined();
// The module namespace object should have __esModule as a named export
expect(WithoutTypeModuleExportEsModuleAnnotationNoDefault.__esModule).toBe(true);
});
test("exports.default = true; exports.__esModule = true;", () => {
expect(WithoutTypeModuleExportEsModuleAnnotation.default).toBeTrue();
expect(WithoutTypeModuleExportEsModuleAnnotation.__esModule).toBeUndefined();
// CommonJS exports entire module.exports as default
expect(WithoutTypeModuleExportEsModuleAnnotation.default).toEqual({
default: true,
__esModule: true,
});
expect(WithoutTypeModuleExportEsModuleAnnotation.__esModule).toBe(true);
});
test("exports.default = true;", () => {
// CommonJS exports entire module.exports as default
expect(WithoutTypeModuleExportEsModuleNoAnnotation.default).toEqual({
default: true,
});
expect(WithoutTypeModuleExportEsModuleAnnotation.__esModule).toBeUndefined();
expect(WithoutTypeModuleExportEsModuleNoAnnotation.__esModule).toBeUndefined();
});
});
describe('with type: "module"', () => {
test("module.exports = {}", () => {
// CommonJS always exports entire module.exports as default, regardless of type:module
expect(WithTypeModuleExportEsModuleAnnotationMissingDefault.default).toEqual({});
expect(WithTypeModuleExportEsModuleAnnotationMissingDefault.__esModule).toBeUndefined();
});
test("exports.__esModule = true", () => {
// CommonJS exports entire module.exports as default, including __esModule property
expect(WithTypeModuleExportEsModuleAnnotationNoDefault.default).toEqual({
__esModule: true,
});
// The module namespace object WILL have the __esModule property.
expect(WithTypeModuleExportEsModuleAnnotationNoDefault.__esModule).toBeTrue();
// The module namespace object should have __esModule as a named export
expect(WithTypeModuleExportEsModuleAnnotationNoDefault.__esModule).toBe(true);
});
test("exports.default = true; exports.__esModule = true;", () => {
// CommonJS exports entire module.exports as default
expect(WithTypeModuleExportEsModuleAnnotation.default).toEqual({
default: true,
__esModule: true,
});
expect(WithTypeModuleExportEsModuleAnnotation.__esModule).toBeTrue();
// __esModule should be available as a named export
expect(WithTypeModuleExportEsModuleAnnotation.__esModule).toBe(true);
});
test("exports.default = true;", () => {
// CommonJS exports entire module.exports as default
expect(WithTypeModuleExportEsModuleNoAnnotation.default).toEqual({
default: true,
});
expect(WithTypeModuleExportEsModuleAnnotation.__esModule).toBeTrue();
expect(WithTypeModuleExportEsModuleNoAnnotation.__esModule).toBeUndefined();
});
});

View File

@@ -16,12 +16,14 @@ test("__esModule is settable", () => {
Self.__esModule = undefined;
});
test("require of self sets __esModule", () => {
test("require of self does NOT automatically set __esModule", () => {
expect(Self.__esModule).toBeUndefined();
{
const Self = require("./esModule.test.ts");
expect(Self.__esModule).toBe(true);
// With new behavior, __esModule is not automatically added
expect(Self.__esModule).toBeUndefined();
}
expect(Self.__esModule).toBe(true);
// __esModule remains undefined since it's not automatically added
expect(Self.__esModule).toBeUndefined();
expect(Object.getOwnPropertyNames(Self)).toBeEmpty();
});

View File

@@ -0,0 +1,297 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, normalizeBunSnapshot, tempDir } from "harness";
test("CommonJS module.exports function should be directly callable (#4506)", async () => {
using dir = tempDir("test-cjs-function", {
"package.json": JSON.stringify({ name: "test-pkg" }),
"index.js": `
module.exports = function isNatural(value) {
return Number.isInteger(value) && value >= 0;
};
module.exports.isPositive = function(value) {
return Number.isInteger(value) && value > 0;
};
`,
"test.js": `
const isNatural = require('./index.js');
// Should be directly callable
console.log(typeof isNatural === 'function' ? 'PASS: is function' : 'FAIL: not function');
console.log(isNatural(5) === true ? 'PASS: isNatural(5)' : 'FAIL: isNatural(5)');
console.log(isNatural(-1) === false ? 'PASS: isNatural(-1)' : 'FAIL: isNatural(-1)');
// Named export should also work
console.log(typeof isNatural.isPositive === 'function' ? 'PASS: has isPositive' : 'FAIL: no isPositive');
console.log(isNatural.isPositive(1) === true ? 'PASS: isPositive(1)' : 'FAIL: isPositive(1)');
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
expect(normalizeBunSnapshot(stdout, dir)).toMatchInlineSnapshot(`
"PASS: is function
PASS: isNatural(5)
PASS: isNatural(-1)
PASS: has isPositive
PASS: isPositive(1)"
`);
});
test("CommonJS exports object should be directly accessible", async () => {
using dir = tempDir("test-cjs-object", {
"package.json": JSON.stringify({ name: "test-pkg" }),
"module.js": `
exports.foo = "bar";
exports.baz = 42;
exports.func = function() { return "hello"; };
`,
"test.js": `
const mod = require('./module.js');
console.log(mod.foo === "bar" ? 'PASS: foo' : 'FAIL: foo');
console.log(mod.baz === 42 ? 'PASS: baz' : 'FAIL: baz');
console.log(typeof mod.func === 'function' ? 'PASS: func is function' : 'FAIL: func not function');
console.log(mod.func() === "hello" ? 'PASS: func()' : 'FAIL: func()');
// Should not have __esModule added
console.log(mod.__esModule === undefined ? 'PASS: no __esModule' : 'FAIL: has __esModule');
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
expect(normalizeBunSnapshot(stdout, dir)).toMatchInlineSnapshot(`
"PASS: foo
PASS: baz
PASS: func is function
PASS: func()
PASS: no __esModule"
`);
});
test("ESM import of CommonJS default export", async () => {
using dir = tempDir("test-esm-cjs-default", {
"package.json": JSON.stringify({ name: "test-pkg", type: "module" }),
"cjs-module.cjs": `
module.exports = function myFunction() {
return "default export";
};
module.exports.namedExport = "named";
`,
"test.mjs": `
import myFunction from './cjs-module.cjs';
import * as mod from './cjs-module.cjs';
// Default import should be the entire module.exports
console.log(typeof myFunction === 'function' ? 'PASS: default is function' : 'FAIL: default not function');
console.log(myFunction() === 'default export' ? 'PASS: default()' : 'FAIL: default()');
// Named export should be accessible on the default
console.log(myFunction.namedExport === 'named' ? 'PASS: default.namedExport' : 'FAIL: default.namedExport');
// Star import should have default pointing to module.exports
console.log(typeof mod.default === 'function' ? 'PASS: mod.default is function' : 'FAIL: mod.default not function');
console.log(mod.default() === 'default export' ? 'PASS: mod.default()' : 'FAIL: mod.default()');
console.log(mod.namedExport === 'named' ? 'PASS: mod.namedExport' : 'FAIL: mod.namedExport');
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test.mjs"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
expect(normalizeBunSnapshot(stdout, dir)).toMatchInlineSnapshot(`
"PASS: default is function
PASS: default()
PASS: default.namedExport
PASS: mod.default is function
PASS: mod.default()
PASS: mod.namedExport"
`);
});
test("ESM import of CommonJS with exports object", async () => {
using dir = tempDir("test-esm-cjs-exports", {
"package.json": JSON.stringify({ name: "test-pkg", type: "module" }),
"cjs-module.cjs": `
exports.foo = "bar";
exports.baz = 42;
exports.func = function() { return "hello"; };
`,
"test.mjs": `
import defaultExport from './cjs-module.cjs';
import * as mod from './cjs-module.cjs';
// Default import should be the entire exports object
console.log(typeof defaultExport === 'object' ? 'PASS: default is object' : 'FAIL: default not object');
console.log(defaultExport.foo === 'bar' ? 'PASS: default.foo' : 'FAIL: default.foo');
console.log(defaultExport.baz === 42 ? 'PASS: default.baz' : 'FAIL: default.baz');
console.log(typeof defaultExport.func === 'function' ? 'PASS: default.func' : 'FAIL: default.func');
// Star import should have the same properties plus default
console.log(mod.default === defaultExport ? 'PASS: mod.default === default' : 'FAIL: mod.default !== default');
console.log(mod.foo === 'bar' ? 'PASS: mod.foo' : 'FAIL: mod.foo');
console.log(mod.baz === 42 ? 'PASS: mod.baz' : 'FAIL: mod.baz');
console.log(typeof mod.func === 'function' ? 'PASS: mod.func' : 'FAIL: mod.func');
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test.mjs"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
expect(normalizeBunSnapshot(stdout, dir)).toMatchInlineSnapshot(`
"PASS: default is object
PASS: default.foo
PASS: default.baz
PASS: default.func
PASS: mod.default === default
PASS: mod.foo
PASS: mod.baz
PASS: mod.func"
`);
});
test("CommonJS module with __esModule should be treated normally", async () => {
using dir = tempDir("test-esmodule-flag", {
"package.json": JSON.stringify({ name: "test-pkg", type: "module" }),
"cjs-with-flag.cjs": `
// This module manually sets __esModule, which should now be treated as a normal property
exports.__esModule = true;
exports.default = "explicit default";
exports.foo = "bar";
`,
"test.mjs": `
import defaultExport from './cjs-with-flag.cjs';
import * as mod from './cjs-with-flag.cjs';
// With __esModule workaround removed, default import should be the entire exports object
// NOT the value of exports.default
console.log(typeof defaultExport === 'object' ? 'PASS: default is object' : 'FAIL: default not object');
console.log(defaultExport.default === 'explicit default' ? 'PASS: has .default property' : 'FAIL: no .default property');
console.log(defaultExport.foo === 'bar' ? 'PASS: has .foo property' : 'FAIL: no .foo property');
console.log(defaultExport.__esModule === true ? 'PASS: has .__esModule property' : 'FAIL: no .__esModule property');
// Star import verification
console.log(mod.default === defaultExport ? 'PASS: mod.default is exports object' : 'FAIL: mod.default not exports object');
console.log(mod.foo === 'bar' ? 'PASS: mod.foo' : 'FAIL: mod.foo');
console.log(mod.__esModule === true ? 'PASS: mod.__esModule' : 'FAIL: mod.__esModule');
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "test.mjs"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(0);
expect(stderr).toBe("");
expect(normalizeBunSnapshot(stdout, dir)).toMatchInlineSnapshot(`
"PASS: default is object
PASS: has .default property
PASS: has .foo property
PASS: has .__esModule property
PASS: mod.default is exports object
PASS: mod.foo
PASS: mod.__esModule"
`);
});
test("Bundler should handle CommonJS correctly without __esModule", async () => {
using dir = tempDir("test-bundler-cjs", {
"package.json": JSON.stringify({ name: "test-pkg" }),
"module.js": `
module.exports = function() { return "bundled"; };
module.exports.extra = "data";
`,
"entry.js": `
const mod = require('./module.js');
console.log(typeof mod === 'function' ? 'PASS: is function' : 'FAIL: not function');
console.log(mod() === 'bundled' ? 'PASS: call result' : 'FAIL: call result');
console.log(mod.extra === 'data' ? 'PASS: extra property' : 'FAIL: extra property');
`,
});
// Build the bundle
await using buildProc = Bun.spawn({
cmd: [bunExe(), "build", "entry.js", "--outfile", "bundle.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [buildStdout, buildStderr, buildExitCode] = await Promise.all([
buildProc.stdout.text(),
buildProc.stderr.text(),
buildProc.exited,
]);
expect(buildExitCode).toBe(0);
// Run the bundle
await using runProc = Bun.spawn({
cmd: [bunExe(), "bundle.js"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});
const [runStdout, runStderr, runExitCode] = await Promise.all([
runProc.stdout.text(),
runProc.stderr.text(),
runProc.exited,
]);
expect(runExitCode).toBe(0);
expect(runStderr).toBe("");
expect(normalizeBunSnapshot(runStdout, dir)).toMatchInlineSnapshot(`
"PASS: is function
PASS: call result
PASS: extra property"
`);
});