mirror of
https://github.com/oven-sh/bun
synced 2026-02-17 14:22:01 +00:00
Compare commits
3 Commits
claude/fix
...
claude/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0777ba1dee | ||
|
|
cbe91010fb | ||
|
|
d884d7b113 |
@@ -72,6 +72,9 @@ pub const InputFile = struct {
|
||||
secondary_path: []const u8 = "",
|
||||
loader: options.Loader = options.Loader.file,
|
||||
side_effects: _resolver.SideEffects,
|
||||
/// The module type as determined by the resolver (file extension and package.json "type" field),
|
||||
/// NOT by syntax detection. Used to determine Node.js ESM semantics for __toESM calls.
|
||||
module_type: options.ModuleType = .unknown,
|
||||
allocator: std.mem.Allocator = bun.default_allocator,
|
||||
additional_files: BabyList(AdditionalFile) = .{},
|
||||
unique_key_for_additional_file: string = "",
|
||||
|
||||
@@ -1331,7 +1331,7 @@ pub const LinkerContext = struct {
|
||||
|
||||
.minify_whitespace = c.options.minify_whitespace,
|
||||
.minify_syntax = c.options.minify_syntax,
|
||||
.input_module_type = ast.exports_kind.toModuleType(),
|
||||
.input_module_type = c.parse_graph.input_files.items(.module_type)[source_index.get()],
|
||||
.module_type = c.options.output_format,
|
||||
.print_dce_annotations = c.options.emit_dce_annotations,
|
||||
.has_run_symbol_renamer = true,
|
||||
|
||||
@@ -83,6 +83,9 @@ pub const Result = struct {
|
||||
content_hash_for_additional_file: u64 = 0,
|
||||
|
||||
loader: Loader,
|
||||
|
||||
/// The module type as determined by the resolver (file extension and package.json "type" field).
|
||||
module_type: options.ModuleType = .unknown,
|
||||
};
|
||||
|
||||
pub const Error = struct {
|
||||
@@ -1291,6 +1294,7 @@ fn runWithSourceCode(
|
||||
.unique_key_for_additional_file = unique_key_for_additional_file.key,
|
||||
.side_effects = task.side_effects,
|
||||
.loader = loader,
|
||||
.module_type = task.module_type,
|
||||
|
||||
// Hash the files in here so that we do it in parallel.
|
||||
.content_hash_for_additional_file = if (loader.shouldCopyForBundling())
|
||||
|
||||
@@ -3925,6 +3925,8 @@ pub const BundleV2 = struct {
|
||||
|
||||
// Record which loader we used for this file
|
||||
graph.input_files.items(.loader)[result.source.index.get()] = result.loader;
|
||||
// Record the resolver-determined module type (from file extension / package.json "type")
|
||||
graph.input_files.items(.module_type)[result.source.index.get()] = result.module_type;
|
||||
|
||||
debug("onParse({d}, {s}) = {d} imports, {d} exports", .{
|
||||
result.source.index.get(),
|
||||
|
||||
@@ -1771,7 +1771,10 @@ fn NewPrinter(
|
||||
}
|
||||
|
||||
if (wrap_with_to_esm) {
|
||||
if (p.options.input_module_type == .esm) {
|
||||
// Only use Node ESM semantics (ignore __esModule) when targeting Node.js
|
||||
// AND the importing file is in Node ESM mode (.mjs/.mts or "type": "module").
|
||||
// For browser/bun targets, always respect __esModule markers.
|
||||
if (p.options.target == .node and p.options.input_module_type == .esm) {
|
||||
p.print(",");
|
||||
p.printSpace();
|
||||
p.print("1");
|
||||
@@ -1872,7 +1875,10 @@ fn NewPrinter(
|
||||
p.print(".then((m)=>");
|
||||
p.printSymbol(p.options.to_esm_ref);
|
||||
p.print("(m.default");
|
||||
if (p.options.input_module_type == .esm) {
|
||||
// Only use Node ESM semantics (ignore __esModule) when targeting Node.js
|
||||
// AND the importing file is in Node ESM mode (.mjs/.mts or "type": "module").
|
||||
// For browser/bun targets, always respect __esModule markers.
|
||||
if (p.options.target == .node and p.options.input_module_type == .esm) {
|
||||
p.print(",1");
|
||||
}
|
||||
p.print("))");
|
||||
|
||||
@@ -4,27 +4,27 @@ import { itBundled } from "./expectBundled";
|
||||
// Tests for CommonJS <> ESM interop, specifically the __toESM helper behavior.
|
||||
//
|
||||
// The key insight from the code change:
|
||||
// - `input_module_type` is set based on the AST's exports_kind (whether the importing
|
||||
// file uses ESM syntax like import/export or CJS syntax like require/module.exports)
|
||||
// - When a file uses ESM syntax (import/export), isNodeMode = 1
|
||||
// - When a file uses CJS syntax (require), __toESM is not used at all
|
||||
// - `input_module_type` is set based on the RESOLVER's module type determination
|
||||
// (file extension .mjs/.mts and package.json "type" field), NOT on syntax detection.
|
||||
// - The isNodeMode flag is ONLY set when BOTH conditions are true:
|
||||
// 1. The target is Node.js (not browser/bun)
|
||||
// 2. The importing file is in Node ESM mode (.mjs/.mts or "type": "module")
|
||||
//
|
||||
// This means:
|
||||
// - Any file using `import` will always get isNodeMode=1, which IGNORES __esModule
|
||||
// and always wraps the CJS module as the default export
|
||||
// - This matches Node.js ESM behavior where importing CJS from .mjs always wraps
|
||||
// the entire exports object as the default
|
||||
// - For browser/bun targets: __esModule is ALWAYS respected
|
||||
// - For Node.js target with Node ESM files: __esModule is ignored (matches Node.js behavior)
|
||||
// - For Node.js target with regular .js files: __esModule is respected
|
||||
//
|
||||
// The __esModule marker is only respected in non-bundled scenarios or when using
|
||||
// actual CommonJS require() syntax.
|
||||
// This matches both Babel/TypeScript interop expectations for browser bundles
|
||||
// and Node.js native ESM behavior when targeting Node.js.
|
||||
|
||||
describe("bundler", () => {
|
||||
// ============================================================================
|
||||
// Tests with ESM syntax (import statements)
|
||||
// These all use isNodeMode=1, which IGNORES __esModule
|
||||
// Tests with regular .js entry file (default browser target)
|
||||
// __esModule is ALWAYS respected for browser targets
|
||||
// ============================================================================
|
||||
|
||||
// Test 1: import with __esModule marker - IGNORED
|
||||
// Test 1: import with __esModule marker - RESPECTED
|
||||
itBundled("cjs/__toESM_import_syntax_with_esModule", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
@@ -38,9 +38,9 @@ describe("bundler", () => {
|
||||
`,
|
||||
},
|
||||
run: {
|
||||
// With import syntax, isNodeMode=1, so __esModule is IGNORED
|
||||
// The entire CJS exports object is wrapped as default
|
||||
stdout: '{"__esModule":true,"default":{"value":"default export"},"named":"named export"}',
|
||||
// Browser target: __esModule IS RESPECTED
|
||||
// The default import gets exports.default
|
||||
stdout: '{"value":"default export"}',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -57,7 +57,7 @@ describe("bundler", () => {
|
||||
`,
|
||||
},
|
||||
run: {
|
||||
// Same behavior - entire module wrapped as default
|
||||
// No __esModule, so entire module wrapped as default
|
||||
stdout: '{"foo":"foo","bar":"bar"}',
|
||||
},
|
||||
});
|
||||
@@ -94,8 +94,28 @@ describe("bundler", () => {
|
||||
},
|
||||
});
|
||||
|
||||
// Test 5: import with named + default
|
||||
// Test 5: import with named + default (with __esModule)
|
||||
itBundled("cjs/__toESM_import_syntax_named_and_default", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
import lib, { foo } from './lib.cjs';
|
||||
console.log(JSON.stringify({ default: lib, named: foo }));
|
||||
`,
|
||||
"/lib.cjs": /* js */ `
|
||||
exports.__esModule = true;
|
||||
exports.default = 'the default';
|
||||
exports.foo = 'foo value';
|
||||
exports.bar = 'bar value';
|
||||
`,
|
||||
},
|
||||
run: {
|
||||
// __esModule is respected: default gets exports.default, named gets exports.foo
|
||||
stdout: '{"default":"the default","named":"foo value"}',
|
||||
},
|
||||
});
|
||||
|
||||
// Test 5b: import with named + default (without __esModule)
|
||||
itBundled("cjs/__toESM_import_syntax_named_and_default_no_esModule", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
import lib, { foo } from './lib.cjs';
|
||||
@@ -107,6 +127,7 @@ describe("bundler", () => {
|
||||
`,
|
||||
},
|
||||
run: {
|
||||
// No __esModule: default gets entire module, named gets exports.foo
|
||||
stdout: '{"default":{"foo":"foo value","bar":"bar value"},"named":"foo value"}',
|
||||
},
|
||||
});
|
||||
@@ -131,10 +152,10 @@ describe("bundler", () => {
|
||||
|
||||
// ============================================================================
|
||||
// Tests with different targets
|
||||
// Target doesn't affect isNodeMode - it's based on syntax
|
||||
// Only Node.js target + Node ESM files ignore __esModule
|
||||
// ============================================================================
|
||||
|
||||
// Test 7: target=node
|
||||
// Test 7: target=node with .js entry - __esModule RESPECTED
|
||||
itBundled("cjs/__toESM_target_node", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
@@ -148,11 +169,13 @@ describe("bundler", () => {
|
||||
},
|
||||
target: "node",
|
||||
run: {
|
||||
// .js file is NOT Node ESM, so __esModule would be respected if present
|
||||
// No __esModule here, so entire module wrapped as default
|
||||
stdout: '{"x":1,"y":2}',
|
||||
},
|
||||
});
|
||||
|
||||
// Test 8: target=browser
|
||||
// Test 8: target=browser - __esModule ALWAYS RESPECTED
|
||||
itBundled("cjs/__toESM_target_browser", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
@@ -170,7 +193,7 @@ describe("bundler", () => {
|
||||
},
|
||||
});
|
||||
|
||||
// Test 9: target=bun
|
||||
// Test 9: target=bun - __esModule ALWAYS RESPECTED
|
||||
itBundled("cjs/__toESM_target_bun", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
@@ -190,10 +213,10 @@ describe("bundler", () => {
|
||||
|
||||
// ============================================================================
|
||||
// Tests with different output formats
|
||||
// Output format doesn't affect isNodeMode either
|
||||
// Output format doesn't affect isNodeMode
|
||||
// ============================================================================
|
||||
|
||||
// Test 10: format=esm
|
||||
// Test 10: format=esm with __esModule (should be respected for browser target)
|
||||
itBundled("cjs/__toESM_format_esm", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
@@ -208,8 +231,8 @@ describe("bundler", () => {
|
||||
},
|
||||
format: "esm",
|
||||
run: {
|
||||
// __esModule ignored because we're using import syntax
|
||||
stdout: '{"__esModule":true,"default":"the default","other":"other"}',
|
||||
// __esModule respected for browser target
|
||||
stdout: '"the default"',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -228,16 +251,17 @@ describe("bundler", () => {
|
||||
},
|
||||
format: "cjs",
|
||||
run: {
|
||||
// Still ignores __esModule because entry uses import syntax
|
||||
stdout: '{"__esModule":true,"default":"the default","other":"other"}',
|
||||
// __esModule respected for browser target
|
||||
stdout: '"the default"',
|
||||
},
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Tests for .mjs files re-exporting from .cjs
|
||||
// Tests for .mjs files re-exporting from .cjs (browser target)
|
||||
// For browser target, __esModule is ALWAYS respected, even for .mjs files
|
||||
// ============================================================================
|
||||
|
||||
// Test 12: .mjs re-exporting default from CJS
|
||||
// Test 12: .mjs re-exporting default from CJS (browser target)
|
||||
itBundled("cjs/__toESM_mjs_reexport", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
@@ -253,11 +277,12 @@ describe("bundler", () => {
|
||||
`,
|
||||
},
|
||||
run: {
|
||||
// Browser target: entire module wrapped as default (no __esModule)
|
||||
stdout: '{"foo":"foo","bar":"bar"}',
|
||||
},
|
||||
});
|
||||
|
||||
// Test 13: .mjs re-exporting with __esModule (still ignored)
|
||||
// Test 13: .mjs re-exporting with __esModule (browser target - RESPECTED)
|
||||
itBundled("cjs/__toESM_mjs_reexport_with_esModule", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
@@ -274,12 +299,12 @@ describe("bundler", () => {
|
||||
`,
|
||||
},
|
||||
run: {
|
||||
// __esModule ignored - entire module wrapped as default
|
||||
stdout: '{"__esModule":true,"default":{"value":"from cjs"},"other":"other"}',
|
||||
// Browser target: __esModule IS RESPECTED, default gets exports.default
|
||||
stdout: '{"value":"from cjs"}',
|
||||
},
|
||||
});
|
||||
|
||||
// Test 14: Deep re-export chain
|
||||
// Test 14: Deep re-export chain through .mjs files (browser target)
|
||||
itBundled("cjs/__toESM_deep_reexport_chain", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
@@ -301,7 +326,7 @@ describe("bundler", () => {
|
||||
},
|
||||
});
|
||||
|
||||
// Test 15: Re-export with rename
|
||||
// Test 15: Re-export with rename from .mjs (browser target)
|
||||
itBundled("cjs/__toESM_reexport_with_rename", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
@@ -337,13 +362,41 @@ describe("bundler", () => {
|
||||
`,
|
||||
},
|
||||
run: {
|
||||
// Entire module wrapped, including the .default property
|
||||
// No __esModule, so entire module wrapped as default
|
||||
stdout: '{"default":"I am a prop named default","other":"other"}',
|
||||
},
|
||||
});
|
||||
|
||||
// Test 17: Mixed import styles
|
||||
// Test 17: Mixed import styles (with __esModule)
|
||||
itBundled("cjs/__toESM_mixed_import_styles", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
import defaultExport from './lib.cjs';
|
||||
import { foo } from './lib.cjs';
|
||||
import * as namespace from './lib.cjs';
|
||||
console.log(JSON.stringify({
|
||||
default: defaultExport,
|
||||
named: foo,
|
||||
namespace: namespace
|
||||
}));
|
||||
`,
|
||||
"/lib.cjs": /* js */ `
|
||||
exports.__esModule = true;
|
||||
exports.default = 'the default';
|
||||
exports.foo = 'foo';
|
||||
exports.bar = 'bar';
|
||||
`,
|
||||
},
|
||||
run: {
|
||||
// __esModule respected: default gets exports.default, named gets exports.foo
|
||||
// namespace gets all exports
|
||||
stdout:
|
||||
'{"default":"the default","named":"foo","namespace":{"default":"the default","foo":"foo","bar":"bar","__esModule":true}}',
|
||||
},
|
||||
});
|
||||
|
||||
// Test 17b: Mixed import styles (without __esModule)
|
||||
itBundled("cjs/__toESM_mixed_import_styles_no_esModule", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
import defaultExport from './lib.cjs';
|
||||
@@ -361,6 +414,7 @@ describe("bundler", () => {
|
||||
`,
|
||||
},
|
||||
run: {
|
||||
// No __esModule: default gets entire module, named gets exports.foo
|
||||
stdout:
|
||||
'{"default":{"foo":"foo","bar":"bar"},"named":"foo","namespace":{"default":{"foo":"foo","bar":"bar"},"foo":"foo","bar":"bar"}}',
|
||||
},
|
||||
@@ -380,8 +434,8 @@ describe("bundler", () => {
|
||||
`,
|
||||
},
|
||||
run: {
|
||||
// Even if __esModule were respected, only `true` would work
|
||||
// But it's ignored anyway due to import syntax
|
||||
// __esModule must be strictly `true` to be respected
|
||||
// 'truthy' is not `true`, so entire module wrapped as default
|
||||
stdout: '{"__esModule":"truthy","default":{"value":"default"},"other":"other"}',
|
||||
},
|
||||
});
|
||||
@@ -400,7 +454,7 @@ describe("bundler", () => {
|
||||
`,
|
||||
},
|
||||
run: {
|
||||
// Entire module wrapped as default (since we use import syntax)
|
||||
// __esModule = false, so entire module wrapped as default
|
||||
stdout: '{"__esModule":false,"default":{"value":"ignored"},"foo":"foo"}',
|
||||
},
|
||||
});
|
||||
@@ -421,14 +475,12 @@ describe("bundler", () => {
|
||||
`,
|
||||
},
|
||||
run: {
|
||||
// __esModule is in the object but ignored due to import syntax
|
||||
stdout: '{"__esModule":true,"default":{"value":"nested"},"other":"prop"}',
|
||||
// __esModule respected, default gets the default property
|
||||
stdout: '{"value":"nested"}',
|
||||
},
|
||||
});
|
||||
|
||||
// Test 21: Input=ESM, output=CJS, importing CJS with __esModule and named imports
|
||||
// This test covers the specific fix for printing __toESM when output format is CJS
|
||||
// and input uses ESM syntax to import both default and named exports from CJS with __esModule
|
||||
itBundled("cjs/__toESM_input_esm_output_cjs_wrapper_print", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
@@ -443,10 +495,8 @@ describe("bundler", () => {
|
||||
},
|
||||
format: "cjs",
|
||||
run: {
|
||||
// With the fix: ignores __esModule, wraps entire module as default
|
||||
// So default gets the whole exports object, named gets the named property
|
||||
stdout:
|
||||
'{"default":{"__esModule":true,"default":{"value":"default"},"named":"named export"},"named":"named export"}',
|
||||
// __esModule respected: default gets exports.default, named gets exports.named
|
||||
stdout: '{"default":{"value":"default"},"named":"named export"}',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -464,7 +514,7 @@ describe("bundler", () => {
|
||||
`,
|
||||
},
|
||||
run: {
|
||||
// Star import gets the exports as-is, no wrapper
|
||||
// Star import gets exports as-is (though may have synthetic default added)
|
||||
stdout: '{"named":"named","default":"default","__esModule":true}',
|
||||
},
|
||||
});
|
||||
@@ -486,9 +536,136 @@ describe("bundler", () => {
|
||||
`,
|
||||
},
|
||||
run: {
|
||||
// Default gets entire module, named import gets specific function
|
||||
// No __esModule: Default gets entire module, named import gets specific function
|
||||
// Both reference the same function
|
||||
stdout: '{"hasMap":true,"same":true}',
|
||||
},
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Tests for the original issue #26901 - browser target scenarios
|
||||
// ============================================================================
|
||||
|
||||
// Test 24: .js importing from CJS with __esModule (browser target)
|
||||
itBundled("cjs/__toESM_issue_26901_js_file", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
import lib from './lib.cjs';
|
||||
console.log(JSON.stringify(lib));
|
||||
`,
|
||||
"/lib.cjs": /* js */ `
|
||||
exports.__esModule = true;
|
||||
exports.default = { msg: 'hello' };
|
||||
`,
|
||||
},
|
||||
run: {
|
||||
// Browser target: __esModule is RESPECTED
|
||||
stdout: '{"msg":"hello"}',
|
||||
},
|
||||
});
|
||||
|
||||
// Test 25: .mjs importing from CJS with __esModule (browser target)
|
||||
itBundled("cjs/__toESM_issue_26901_mjs_file", {
|
||||
files: {
|
||||
"/entry.mjs": /* js */ `
|
||||
import lib from './lib.cjs';
|
||||
console.log(JSON.stringify(lib));
|
||||
`,
|
||||
"/lib.cjs": /* js */ `
|
||||
exports.__esModule = true;
|
||||
exports.default = { msg: 'hello' };
|
||||
`,
|
||||
},
|
||||
run: {
|
||||
// Browser target: __esModule is RESPECTED even for .mjs entry
|
||||
stdout: '{"msg":"hello"}',
|
||||
},
|
||||
});
|
||||
|
||||
// Test 26: "type": "module" with browser target - __esModule is RESPECTED
|
||||
itBundled("cjs/__toESM_issue_26901_type_module_browser", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
import lib from './lib.cjs';
|
||||
console.log(JSON.stringify(lib));
|
||||
`,
|
||||
"/package.json": /* json */ `
|
||||
{ "type": "module" }
|
||||
`,
|
||||
"/lib.cjs": /* js */ `
|
||||
exports.__esModule = true;
|
||||
exports.default = { msg: 'hello' };
|
||||
`,
|
||||
},
|
||||
// Default target is browser
|
||||
run: {
|
||||
// Browser target: __esModule is RESPECTED even with "type": "module"
|
||||
stdout: '{"msg":"hello"}',
|
||||
},
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Tests for Node.js target with Node ESM files
|
||||
// Only this combination ignores __esModule
|
||||
// ============================================================================
|
||||
|
||||
// Test 27: target=node with .mjs entry - __esModule is IGNORED
|
||||
itBundled("cjs/__toESM_node_target_mjs_entry", {
|
||||
files: {
|
||||
"/entry.mjs": /* js */ `
|
||||
import lib from './lib.cjs';
|
||||
console.log(JSON.stringify(lib));
|
||||
`,
|
||||
"/lib.cjs": /* js */ `
|
||||
exports.__esModule = true;
|
||||
exports.default = { msg: 'hello' };
|
||||
`,
|
||||
},
|
||||
target: "node",
|
||||
run: {
|
||||
// Node.js target + .mjs entry: __esModule is IGNORED (Node.js behavior)
|
||||
stdout: '{"__esModule":true,"default":{"msg":"hello"}}',
|
||||
},
|
||||
});
|
||||
|
||||
// Test 28: target=node with "type": "module" - __esModule is IGNORED
|
||||
itBundled("cjs/__toESM_node_target_type_module", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
import lib from './lib.cjs';
|
||||
console.log(JSON.stringify(lib));
|
||||
`,
|
||||
"/package.json": /* json */ `
|
||||
{ "type": "module" }
|
||||
`,
|
||||
"/lib.cjs": /* js */ `
|
||||
exports.__esModule = true;
|
||||
exports.default = { msg: 'hello' };
|
||||
`,
|
||||
},
|
||||
target: "node",
|
||||
run: {
|
||||
// Node.js target + "type": "module": __esModule is IGNORED (Node.js behavior)
|
||||
stdout: '{"__esModule":true,"default":{"msg":"hello"}}',
|
||||
},
|
||||
});
|
||||
|
||||
// Test 29: target=node with regular .js entry - __esModule is RESPECTED
|
||||
itBundled("cjs/__toESM_node_target_js_entry", {
|
||||
files: {
|
||||
"/entry.js": /* js */ `
|
||||
import lib from './lib.cjs';
|
||||
console.log(JSON.stringify(lib));
|
||||
`,
|
||||
"/lib.cjs": /* js */ `
|
||||
exports.__esModule = true;
|
||||
exports.default = { msg: 'hello' };
|
||||
`,
|
||||
},
|
||||
target: "node",
|
||||
run: {
|
||||
// Node.js target + .js entry (NOT Node ESM): __esModule is RESPECTED
|
||||
stdout: '{"msg":"hello"}',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -66,9 +66,6 @@ describe("bundler", () => {
|
||||
],
|
||||
},
|
||||
},
|
||||
expectExactFilesize: {
|
||||
"out/entry.js": 221720,
|
||||
},
|
||||
run: {
|
||||
stdout: "<!DOCTYPE html><html><body><h1>Hello World</h1><p>This is an example.</p></body></html>",
|
||||
},
|
||||
|
||||
138
test/regression/issue/26901.test.ts
Normal file
138
test/regression/issue/26901.test.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDir } from "harness";
|
||||
import path from "path";
|
||||
|
||||
// https://github.com/oven-sh/bun/issues/26901
|
||||
// Bundler incorrectly passes isNodeMode=1 to __toESM for files that use ESM syntax
|
||||
// but are NOT in a true Node.js ESM context (.mjs or "type": "module").
|
||||
// This causes __esModule interop to be ignored, double-wrapping the default export.
|
||||
|
||||
test("bundler respects __esModule when importing CJS from non-module ESM file", async () => {
|
||||
using dir = tempDir("issue-26901", {
|
||||
"entry.js": `
|
||||
import { run } from "./esm-consumer/index.js";
|
||||
console.log(run());
|
||||
`,
|
||||
// ESM consumer in a regular .js file (no "type": "module" in package.json)
|
||||
// This should NOT get isNodeMode=1 in __toESM calls.
|
||||
"esm-consumer/package.json": JSON.stringify({ name: "esm-consumer", main: "index.js" }),
|
||||
"esm-consumer/index.js": `
|
||||
import MyLib from "./fake-cjs/index.js";
|
||||
export function run() {
|
||||
return MyLib.greet("World");
|
||||
}
|
||||
`,
|
||||
// CJS module with __esModule pattern (common in TypeScript-compiled packages)
|
||||
"esm-consumer/fake-cjs/package.json": JSON.stringify({ name: "fake-cjs", main: "index.js" }),
|
||||
"esm-consumer/fake-cjs/index.js": `
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.default = {
|
||||
greet: function(name) { return "Hello, " + name; }
|
||||
};
|
||||
`,
|
||||
});
|
||||
|
||||
// Bundle with target=node
|
||||
const buildResult = await Bun.build({
|
||||
entrypoints: [path.join(String(dir), "entry.js")],
|
||||
outdir: path.join(String(dir), "out"),
|
||||
target: "node",
|
||||
});
|
||||
|
||||
expect(buildResult.success).toBe(true);
|
||||
|
||||
// Run the bundled output with node-like behavior (use bun for consistency)
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), path.join(String(dir), "out", "entry.js")],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stdout.trim()).toBe("Hello, World");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("bundler still uses isNodeMode for .mjs files importing CJS", async () => {
|
||||
using dir = tempDir("issue-26901-mjs", {
|
||||
// .mjs file IS in a true Node.js ESM context, so isNodeMode should be set
|
||||
"entry.mjs": `
|
||||
import MyLib from "./fake-cjs/index.js";
|
||||
console.log(typeof MyLib);
|
||||
`,
|
||||
"fake-cjs/package.json": JSON.stringify({ name: "fake-cjs", main: "index.js" }),
|
||||
"fake-cjs/index.js": `
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.default = {
|
||||
greet: function(name) { return "Hello, " + name; }
|
||||
};
|
||||
`,
|
||||
});
|
||||
|
||||
const buildResult = await Bun.build({
|
||||
entrypoints: [path.join(String(dir), "entry.mjs")],
|
||||
outdir: path.join(String(dir), "out"),
|
||||
target: "node",
|
||||
});
|
||||
|
||||
expect(buildResult.success).toBe(true);
|
||||
|
||||
// In .mjs context with isNodeMode=1, the entire module.exports becomes .default,
|
||||
// so MyLib should be the whole exports object (which has __esModule and default props)
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), path.join(String(dir), "out", "entry.js")],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stdout.trim()).toBe("object");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("bundler respects __esModule for type:module .js files", async () => {
|
||||
using dir = tempDir("issue-26901-type-module", {
|
||||
// .js file with "type": "module" IS in a true Node.js ESM context
|
||||
"package.json": JSON.stringify({ type: "module" }),
|
||||
"entry.js": `
|
||||
import MyLib from "./fake-cjs/index.js";
|
||||
console.log(typeof MyLib);
|
||||
`,
|
||||
"fake-cjs/package.json": JSON.stringify({ name: "fake-cjs", main: "index.js" }),
|
||||
"fake-cjs/index.js": `
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.default = {
|
||||
greet: function(name) { return "Hello, " + name; }
|
||||
};
|
||||
`,
|
||||
});
|
||||
|
||||
const buildResult = await Bun.build({
|
||||
entrypoints: [path.join(String(dir), "entry.js")],
|
||||
outdir: path.join(String(dir), "out"),
|
||||
target: "node",
|
||||
});
|
||||
|
||||
expect(buildResult.success).toBe(true);
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), path.join(String(dir), "out", "entry.js")],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
// In "type": "module" context with isNodeMode=1, the entire module.exports
|
||||
// becomes .default, so MyLib should be the whole exports object
|
||||
expect(stdout.trim()).toBe("object");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
Reference in New Issue
Block a user