mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 02:48:50 +00:00
### What does this PR do? Fixes printing `import.meta.url` and others with `--bytecode`. Fixes #14954. Fixes printing `__toESM` when output module format is CJS and input module format is ESM. The key change is that `__toESM`'s `isNodeMode` parameter now depends on the **input module type** (whether the importing file uses ESM syntax like `import`/`export`) rather than the output format. This matches Node.js ESM behavior where importing CommonJS from `.mjs` files always wraps the entire `module.exports` object as the default export, ignoring `__esModule` markers. ### How did you verify your code works? Added comprehensive test suite in `test/bundler/bundler_cjs.test.ts` with **23 tests** covering: #### Core Behaviors: - ✅ Files using `import` syntax always get `isNodeMode=1`, which **ignores `__esModule`** markers and wraps the entire CJS module as default - ✅ This matches Node.js ESM semantics for importing CJS from `.mjs` files - ✅ Different CJS export patterns (`exports.x`, `module.exports = ...`, functions, primitives) - ✅ Named, default, and namespace (`import *`) imports - ✅ Different targets (node, browser, bun) - all behave the same - ✅ Different output formats (esm, cjs) - format doesn't affect the behavior - ✅ `.mjs` files re-exporting from `.cjs` - ✅ Deep re-export chains - ✅ Edge cases (non-boolean `__esModule`, `__esModule=false`, etc.) #### Test Results: - **With this PR's changes**: All 23 tests pass ✅ - **Without this PR (system bun)**: 22 pass, 1 fails (the one testing that `__esModule` is ignored with import syntax + CJS format) The failing test with system bun demonstrates the bug being fixed: currently, format=cjs with import syntax still respects `__esModule`, but it should ignore it (matching Node.js behavior). --------- Co-authored-by: Jarred Sumner <jarred@jarredsumner.com> Co-authored-by: Claude Bot <claude-bot@bun.sh>
495 lines
14 KiB
TypeScript
495 lines
14 KiB
TypeScript
import { describe } from "bun:test";
|
|
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
|
|
//
|
|
// 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
|
|
//
|
|
// The __esModule marker is only respected in non-bundled scenarios or when using
|
|
// actual CommonJS require() syntax.
|
|
|
|
describe("bundler", () => {
|
|
// ============================================================================
|
|
// Tests with ESM syntax (import statements)
|
|
// These all use isNodeMode=1, which IGNORES __esModule
|
|
// ============================================================================
|
|
|
|
// Test 1: import with __esModule marker - IGNORED
|
|
itBundled("cjs/__toESM_import_syntax_with_esModule", {
|
|
files: {
|
|
"/entry.js": /* js */ `
|
|
import lib from './lib.cjs';
|
|
console.log(JSON.stringify(lib));
|
|
`,
|
|
"/lib.cjs": /* js */ `
|
|
exports.__esModule = true;
|
|
exports.default = { value: 'default export' };
|
|
exports.named = 'named export';
|
|
`,
|
|
},
|
|
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"}',
|
|
},
|
|
});
|
|
|
|
// Test 2: import WITHOUT __esModule marker
|
|
itBundled("cjs/__toESM_import_syntax_without_esModule", {
|
|
files: {
|
|
"/entry.js": /* js */ `
|
|
import lib from './lib.cjs';
|
|
console.log(JSON.stringify(lib));
|
|
`,
|
|
"/lib.cjs": /* js */ `
|
|
exports.foo = 'foo';
|
|
exports.bar = 'bar';
|
|
`,
|
|
},
|
|
run: {
|
|
// Same behavior - entire module wrapped as default
|
|
stdout: '{"foo":"foo","bar":"bar"}',
|
|
},
|
|
});
|
|
|
|
// Test 3: import with module.exports = function
|
|
itBundled("cjs/__toESM_import_syntax_function", {
|
|
files: {
|
|
"/entry.js": /* js */ `
|
|
import lib from './lib.cjs';
|
|
console.log(lib.name + ':' + lib());
|
|
`,
|
|
"/lib.cjs": /* js */ `
|
|
module.exports = function myFunc() { return 'result'; };
|
|
`,
|
|
},
|
|
run: {
|
|
stdout: "myFunc:result",
|
|
},
|
|
});
|
|
|
|
// Test 4: import with module.exports = primitive
|
|
itBundled("cjs/__toESM_import_syntax_primitive", {
|
|
files: {
|
|
"/entry.js": /* js */ `
|
|
import lib from './lib.cjs';
|
|
console.log(lib);
|
|
`,
|
|
"/lib.cjs": /* js */ `
|
|
module.exports = 42;
|
|
`,
|
|
},
|
|
run: {
|
|
stdout: "42",
|
|
},
|
|
});
|
|
|
|
// Test 5: import with named + default
|
|
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.foo = 'foo value';
|
|
exports.bar = 'bar value';
|
|
`,
|
|
},
|
|
run: {
|
|
stdout: '{"default":{"foo":"foo value","bar":"bar value"},"named":"foo value"}',
|
|
},
|
|
});
|
|
|
|
// Test 6: Namespace import (import *)
|
|
itBundled("cjs/__toESM_import_syntax_namespace", {
|
|
files: {
|
|
"/entry.js": /* js */ `
|
|
import * as lib from './lib.cjs';
|
|
console.log(JSON.stringify(lib));
|
|
`,
|
|
"/lib.cjs": /* js */ `
|
|
exports.foo = 'foo';
|
|
exports.bar = 'bar';
|
|
`,
|
|
},
|
|
run: {
|
|
// Namespace import only gets the CJS exports as-is, no default wrapper
|
|
stdout: '{"foo":"foo","bar":"bar"}',
|
|
},
|
|
});
|
|
|
|
// ============================================================================
|
|
// Tests with different targets
|
|
// Target doesn't affect isNodeMode - it's based on syntax
|
|
// ============================================================================
|
|
|
|
// Test 7: target=node
|
|
itBundled("cjs/__toESM_target_node", {
|
|
files: {
|
|
"/entry.js": /* js */ `
|
|
import lib from './lib.cjs';
|
|
console.log(JSON.stringify(lib));
|
|
`,
|
|
"/lib.cjs": /* js */ `
|
|
exports.x = 1;
|
|
exports.y = 2;
|
|
`,
|
|
},
|
|
target: "node",
|
|
run: {
|
|
stdout: '{"x":1,"y":2}',
|
|
},
|
|
});
|
|
|
|
// Test 8: target=browser
|
|
itBundled("cjs/__toESM_target_browser", {
|
|
files: {
|
|
"/entry.js": /* js */ `
|
|
import lib from './lib.cjs';
|
|
console.log(JSON.stringify(lib));
|
|
`,
|
|
"/lib.cjs": /* js */ `
|
|
exports.x = 1;
|
|
exports.y = 2;
|
|
`,
|
|
},
|
|
target: "browser",
|
|
run: {
|
|
stdout: '{"x":1,"y":2}',
|
|
},
|
|
});
|
|
|
|
// Test 9: target=bun
|
|
itBundled("cjs/__toESM_target_bun", {
|
|
files: {
|
|
"/entry.js": /* js */ `
|
|
import lib from './lib.cjs';
|
|
console.log(JSON.stringify(lib));
|
|
`,
|
|
"/lib.cjs": /* js */ `
|
|
exports.x = 1;
|
|
exports.y = 2;
|
|
`,
|
|
},
|
|
target: "bun",
|
|
run: {
|
|
stdout: '{"x":1,"y":2}',
|
|
},
|
|
});
|
|
|
|
// ============================================================================
|
|
// Tests with different output formats
|
|
// Output format doesn't affect isNodeMode either
|
|
// ============================================================================
|
|
|
|
// Test 10: format=esm
|
|
itBundled("cjs/__toESM_format_esm", {
|
|
files: {
|
|
"/entry.js": /* js */ `
|
|
import lib from './lib.cjs';
|
|
console.log(JSON.stringify(lib));
|
|
`,
|
|
"/lib.cjs": /* js */ `
|
|
exports.__esModule = true;
|
|
exports.default = 'the default';
|
|
exports.other = 'other';
|
|
`,
|
|
},
|
|
format: "esm",
|
|
run: {
|
|
// __esModule ignored because we're using import syntax
|
|
stdout: '{"__esModule":true,"default":"the default","other":"other"}',
|
|
},
|
|
});
|
|
|
|
// Test 11: format=cjs with import syntax
|
|
itBundled("cjs/__toESM_format_cjs_with_import", {
|
|
files: {
|
|
"/entry.js": /* js */ `
|
|
import lib from './lib.cjs';
|
|
console.log(JSON.stringify(lib));
|
|
`,
|
|
"/lib.cjs": /* js */ `
|
|
exports.__esModule = true;
|
|
exports.default = 'the default';
|
|
exports.other = 'other';
|
|
`,
|
|
},
|
|
format: "cjs",
|
|
run: {
|
|
// Still ignores __esModule because entry uses import syntax
|
|
stdout: '{"__esModule":true,"default":"the default","other":"other"}',
|
|
},
|
|
});
|
|
|
|
// ============================================================================
|
|
// Tests for .mjs files re-exporting from .cjs
|
|
// ============================================================================
|
|
|
|
// Test 12: .mjs re-exporting default from CJS
|
|
itBundled("cjs/__toESM_mjs_reexport", {
|
|
files: {
|
|
"/entry.js": /* js */ `
|
|
import lib from './wrapper.mjs';
|
|
console.log(JSON.stringify(lib));
|
|
`,
|
|
"/wrapper.mjs": /* js */ `
|
|
export { default } from './lib.cjs';
|
|
`,
|
|
"/lib.cjs": /* js */ `
|
|
exports.foo = 'foo';
|
|
exports.bar = 'bar';
|
|
`,
|
|
},
|
|
run: {
|
|
stdout: '{"foo":"foo","bar":"bar"}',
|
|
},
|
|
});
|
|
|
|
// Test 13: .mjs re-exporting with __esModule (still ignored)
|
|
itBundled("cjs/__toESM_mjs_reexport_with_esModule", {
|
|
files: {
|
|
"/entry.js": /* js */ `
|
|
import lib from './wrapper.mjs';
|
|
console.log(JSON.stringify(lib));
|
|
`,
|
|
"/wrapper.mjs": /* js */ `
|
|
export { default } from './lib.cjs';
|
|
`,
|
|
"/lib.cjs": /* js */ `
|
|
exports.__esModule = true;
|
|
exports.default = { value: 'from cjs' };
|
|
exports.other = 'other';
|
|
`,
|
|
},
|
|
run: {
|
|
// __esModule ignored - entire module wrapped as default
|
|
stdout: '{"__esModule":true,"default":{"value":"from cjs"},"other":"other"}',
|
|
},
|
|
});
|
|
|
|
// Test 14: Deep re-export chain
|
|
itBundled("cjs/__toESM_deep_reexport_chain", {
|
|
files: {
|
|
"/entry.js": /* js */ `
|
|
import lib from './layer1.mjs';
|
|
console.log(JSON.stringify(lib));
|
|
`,
|
|
"/layer1.mjs": /* js */ `
|
|
export { default } from './layer2.mjs';
|
|
`,
|
|
"/layer2.mjs": /* js */ `
|
|
export { default } from './lib.cjs';
|
|
`,
|
|
"/lib.cjs": /* js */ `
|
|
exports.deep = 'value';
|
|
`,
|
|
},
|
|
run: {
|
|
stdout: '{"deep":"value"}',
|
|
},
|
|
});
|
|
|
|
// Test 15: Re-export with rename
|
|
itBundled("cjs/__toESM_reexport_with_rename", {
|
|
files: {
|
|
"/entry.js": /* js */ `
|
|
import { myDefault } from './wrapper.mjs';
|
|
console.log(JSON.stringify(myDefault));
|
|
`,
|
|
"/wrapper.mjs": /* js */ `
|
|
export { default as myDefault } from './lib.cjs';
|
|
`,
|
|
"/lib.cjs": /* js */ `
|
|
exports.x = 1;
|
|
`,
|
|
},
|
|
run: {
|
|
stdout: '{"x":1}',
|
|
},
|
|
});
|
|
|
|
// ============================================================================
|
|
// Edge cases
|
|
// ============================================================================
|
|
|
|
// Test 16: CJS with a property named "default" but no __esModule
|
|
itBundled("cjs/__toESM_default_prop_no_esModule", {
|
|
files: {
|
|
"/entry.js": /* js */ `
|
|
import lib from './lib.cjs';
|
|
console.log(JSON.stringify(lib));
|
|
`,
|
|
"/lib.cjs": /* js */ `
|
|
exports.default = 'I am a prop named default';
|
|
exports.other = 'other';
|
|
`,
|
|
},
|
|
run: {
|
|
// Entire module wrapped, including the .default property
|
|
stdout: '{"default":"I am a prop named default","other":"other"}',
|
|
},
|
|
});
|
|
|
|
// Test 17: Mixed import styles
|
|
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.foo = 'foo';
|
|
exports.bar = 'bar';
|
|
`,
|
|
},
|
|
run: {
|
|
stdout:
|
|
'{"default":{"foo":"foo","bar":"bar"},"named":"foo","namespace":{"default":{"foo":"foo","bar":"bar"},"foo":"foo","bar":"bar"}}',
|
|
},
|
|
});
|
|
|
|
// Test 18: __esModule with non-true value
|
|
itBundled("cjs/__toESM_esModule_non_true", {
|
|
files: {
|
|
"/entry.js": /* js */ `
|
|
import lib from './lib.cjs';
|
|
console.log(JSON.stringify(lib));
|
|
`,
|
|
"/lib.cjs": /* js */ `
|
|
exports.__esModule = 'truthy';
|
|
exports.default = { value: 'default' };
|
|
exports.other = 'other';
|
|
`,
|
|
},
|
|
run: {
|
|
// Even if __esModule were respected, only `true` would work
|
|
// But it's ignored anyway due to import syntax
|
|
stdout: '{"__esModule":"truthy","default":{"value":"default"},"other":"other"}',
|
|
},
|
|
});
|
|
|
|
// Test 19: __esModule = false
|
|
itBundled("cjs/__toESM_esModule_false", {
|
|
files: {
|
|
"/entry.js": /* js */ `
|
|
import lib from './lib.cjs';
|
|
console.log(JSON.stringify(lib));
|
|
`,
|
|
"/lib.cjs": /* js */ `
|
|
exports.__esModule = false;
|
|
exports.default = { value: 'ignored' };
|
|
exports.foo = 'foo';
|
|
`,
|
|
},
|
|
run: {
|
|
// Entire module wrapped as default (since we use import syntax)
|
|
stdout: '{"__esModule":false,"default":{"value":"ignored"},"foo":"foo"}',
|
|
},
|
|
});
|
|
|
|
// Test 20: module.exports with __esModule
|
|
itBundled("cjs/__toESM_module_exports_with_esModule", {
|
|
files: {
|
|
"/entry.js": /* js */ `
|
|
import lib from './lib.cjs';
|
|
console.log(JSON.stringify(lib));
|
|
`,
|
|
"/lib.cjs": /* js */ `
|
|
module.exports = {
|
|
__esModule: true,
|
|
default: { value: 'nested' },
|
|
other: 'prop'
|
|
};
|
|
`,
|
|
},
|
|
run: {
|
|
// __esModule is in the object but ignored due to import syntax
|
|
stdout: '{"__esModule":true,"default":{"value":"nested"},"other":"prop"}',
|
|
},
|
|
});
|
|
|
|
// 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 */ `
|
|
import lib, { named } from "./lib.cjs";
|
|
console.log(JSON.stringify({ default: lib, named }));
|
|
`,
|
|
"/lib.cjs": /* js */ `
|
|
exports.__esModule = true;
|
|
exports.default = { value: "default" };
|
|
exports.named = "named export";
|
|
`,
|
|
},
|
|
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"}',
|
|
},
|
|
});
|
|
|
|
// Test 22: Star import with __esModule
|
|
itBundled("cjs/__toESM_star_import_with_esModule", {
|
|
files: {
|
|
"/entry.js": /* js */ `
|
|
import * as lib from './lib.cjs';
|
|
console.log(JSON.stringify(lib));
|
|
`,
|
|
"/lib.cjs": /* js */ `
|
|
exports.__esModule = true;
|
|
exports.default = 'default';
|
|
exports.named = 'named';
|
|
`,
|
|
},
|
|
run: {
|
|
// Star import gets the exports as-is, no wrapper
|
|
stdout: '{"named":"named","default":"default","__esModule":true}',
|
|
},
|
|
});
|
|
|
|
// Test 23: Practical example - importing lodash-like library
|
|
itBundled("cjs/__toESM_practical_lodash_style", {
|
|
files: {
|
|
"/entry.js": /* js */ `
|
|
import _ from './lodash.cjs';
|
|
import { map } from './lodash.cjs';
|
|
console.log(JSON.stringify({
|
|
hasMap: typeof _.map === 'function',
|
|
same: _.map === map
|
|
}));
|
|
`,
|
|
"/lodash.cjs": /* js */ `
|
|
exports.map = function(arr, fn) { return arr.map(fn); };
|
|
exports.filter = function(arr, fn) { return arr.filter(fn); };
|
|
`,
|
|
},
|
|
run: {
|
|
// Default gets entire module, named import gets specific function
|
|
// Both reference the same function
|
|
stdout: '{"hasMap":true,"same":true}',
|
|
},
|
|
});
|
|
});
|