Files
bun.sh/test/bundler/esbuild/splitting.test.ts
Alistair Smith 497a4d4818 Fix duplicate exports when two entrypoints share symbols (#26089)
### What does this PR do?

Fixes #5344
Fixes #6356

### How did you verify your code works?

Some test coverage

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude Bot <claude-bot@bun.sh>
2026-01-20 12:40:33 -08:00

636 lines
19 KiB
TypeScript

import assert from "assert";
import { describe, expect } from "bun:test";
import { readdirSync } from "fs";
import { itBundled } from "../expectBundled";
// Tests ported from:
// https://github.com/evanw/esbuild/blob/main/internal/bundler_tests/bundler_splitting_test.go
// For debug, all files are written to $TEMP/bun-bundle-tests/splitting
describe("bundler", () => {
itBundled("splitting/SharedES6IntoES6", {
files: {
"/a.js": /* js */ `
import {foo} from "./shared.js"
console.log(foo)
`,
"/b.js": /* js */ `
import {foo} from "./shared.js"
console.log(foo)
`,
"/shared.js": `export let foo = 123`,
},
entryPoints: ["/a.js", "/b.js"],
splitting: true,
run: [
{ file: "/out/a.js", stdout: "123" },
{ file: "/out/b.js", stdout: "123" },
],
assertNotPresent: {
"/out/a.js": "123",
"/out/b.js": "123",
},
});
itBundled("splitting/SharedCommonJSIntoES6", {
files: {
"/a.js": /* js */ `
const {foo} = require("./shared.js")
console.log(foo)
`,
"/b.js": /* js */ `
const {foo} = require("./shared.js")
console.log(foo)
`,
"/shared.js": `exports.foo = 123`,
},
entryPoints: ["/a.js", "/b.js"],
splitting: true,
run: [
{ file: "/out/a.js", stdout: "123" },
{ file: "/out/b.js", stdout: "123" },
],
assertNotPresent: {
"/out/a.js": "123",
"/out/b.js": "123",
},
});
itBundled("splitting/DynamicES6IntoES6", {
todo: true,
files: {
"/entry.js": `import("./foo.js").then(({bar}) => console.log(bar))`,
"/foo.js": `export let bar = 123`,
},
splitting: true,
outdir: "/out",
assertNotPresent: {
"/out/entry.js": "123",
},
onAfterBundle(api) {
const files = readdirSync(api.outdir);
assert.strictEqual(
files.length,
2,
"should have 2 files: entry.js and foo-[hash].js, found [" + files.join(", ") + "]",
);
assert(files.includes("entry.js"), "has entry.js");
assert(!files.includes("foo.js"), "does not have foo.js");
},
run: {
file: "/out/entry.js",
stdout: "123",
},
});
itBundled("splitting/DynamicCommonJSIntoES6", {
files: {
"/entry.js": `import("./foo.js").then(({default: {bar}}) => console.log(bar))`,
"/foo.js": `exports.bar = 123`,
},
splitting: true,
outdir: "/out",
assertNotPresent: {
"/out/entry.js": "123",
},
run: {
file: "/out/entry.js",
stdout: "123",
},
});
itBundled("splitting/DynamicAndNotDynamicES6IntoES6", {
files: {
"/entry.js": /* js */ `
import {bar as a} from "./foo.js"
import("./foo.js").then(({bar: b}) => console.log(a, b))
`,
"/foo.js": `export let bar = 123`,
},
splitting: true,
outdir: "/out",
});
itBundled("splitting/DynamicAndNotDynamicCommonJSIntoES6", {
skipOnEsbuild: true,
files: {
"/entry.js": /* js */ `
import {bar as a} from "./foo.js"
import("./foo.js").then(({default: {bar: b}}) => console.log(a, b))
`,
"/foo.js": `exports.bar = 123`,
},
outdir: "/out",
splitting: true,
run: {
file: "/out/entry.js",
stdout: "123 123",
},
});
itBundled("splitting/AssignToLocal", {
files: {
"/a.js": /* js */ `
import {foo, setFoo} from "./shared.js"
setFoo(123)
console.log(foo)
`,
"/b.js": /* js */ `
import {foo} from "./shared.js"
console.log(foo)
`,
"/shared.js": /* js */ `
export let foo = 456
export function setFoo(value) {
foo = value
}
`,
},
entryPoints: ["/a.js", "/b.js"],
splitting: true,
runtimeFiles: {
"/test1.js": /* js */ `
await import('./out/a.js')
await import('./out/b.js')
`,
"/test2.js": /* js */ `
await import('./out/b.js')
await import('./out/a.js')
`,
},
run: [
{ file: "/out/a.js", stdout: "123" },
{ file: "/out/b.js", stdout: "456" },
{ file: "/test1.js", stdout: "123\n123" },
{ file: "/test2.js", stdout: "456\n123" },
],
});
itBundled("splitting/SideEffectsWithoutDependencies", {
files: {
"/a.js": /* js */ `
import {a} from "./shared.js"
console.log(a)
`,
"/b.js": /* js */ `
import {b} from "./shared.js"
console.log(b)
`,
"/shared.js": /* js */ `
export let a = 1
export let b = 2
console.log('side effect')
`,
},
entryPoints: ["/a.js", "/b.js"],
splitting: true,
runtimeFiles: {
"/test1.js": /* js */ `
await import('./out/a.js')
await import('./out/b.js')
`,
"/test2.js": /* js */ `
await import('./out/b.js')
await import('./out/a.js')
`,
},
run: [
{ file: "/out/a.js", stdout: "side effect\n1" },
{ file: "/out/b.js", stdout: "side effect\n2" },
{ file: "/test1.js", stdout: "side effect\n1\n2" },
{ file: "/test2.js", stdout: "side effect\n2\n1" },
],
});
itBundled("splitting/NestedDirectories", {
files: {
"/Users/user/project/src/pages/pageA/page.js": /* js */ `
import x from "../shared.js"
console.log(x)
`,
"/Users/user/project/src/pages/pageB/page.js": /* js */ `
import x from "../shared.js"
console.log(-x)
`,
"/Users/user/project/src/pages/shared.js": `export default 123`,
},
entryPoints: ["/Users/user/project/src/pages/pageA/page.js", "/Users/user/project/src/pages/pageB/page.js"],
outputPaths: ["/out/pageA/page.js", "/out/pageB/page.js"],
splitting: true,
run: [
{ file: "/out/pageA/page.js", stdout: "123" },
{ file: "/out/pageB/page.js", stdout: "-123" },
],
});
itBundled("splitting/CircularReferenceESBuildIssue251", {
todo: true,
files: {
"/a.js": /* js */ `
export * from './b.js';
export var p = 5;
`,
"/b.js": /* js */ `
export * from './a.js';
export var q = 6;
export function foo() {
q = 7;
}
`,
},
entryPoints: ["/a.js", "/b.js"],
splitting: true,
runtimeFiles: {
"/test.js": /* js */ `
import { p, q, foo } from './out/a.js';
console.log(p, q)
import { p as p2, q as q2, foo as foo2 } from './out/b.js';
console.log(p2, q2)
console.log(foo === foo2)
foo();
console.log(q, q2)
`,
},
run: [{ file: "/test.js", stdout: "5 6\n5 6\ntrue\n7 7" }],
});
itBundled("splitting/MissingLazyExport", {
files: {
"/a.js": /* js */ `
import {foo} from './common.js'
console.log(JSON.stringify(foo()))
`,
"/b.js": /* js */ `
import {bar} from './common.js'
console.log(JSON.stringify(bar()))
`,
"/common.js": /* js */ `
import * as ns from './empty.js'
export function foo() { return [ns, ns.missing] }
export function bar() { return [ns.missing] }
`,
"/empty.js": /* js */ `
// This forces the module into ES6 mode without importing or exporting anything
import.meta
`,
},
entryPoints: ["/a.js", "/b.js"],
splitting: true,
run: [
{ file: "/out/a.js", stdout: "[{},null]" },
{ file: "/out/b.js", stdout: "[null]" },
],
bundleWarnings: {
"/common.js": [`Import "missing" will always be undefined because there is no matching export in "empty.js"`],
},
});
itBundled("splitting/ReExportESBuildIssue273", {
files: {
"/a.js": `export const a = { value: 1 }`,
"/b.js": `export { a } from './a'`,
},
entryPoints: ["/a.js", "/b.js"],
splitting: true,
runtimeFiles: {
"/test.js": /* js */ `
import { a } from './out/a.js';
import { a as a2 } from './out/b.js';
console.log(a === a2, a.value, a2.value)
`,
},
run: [{ file: "/test.js", stdout: "true 1 1" }],
});
itBundled("splitting/DynamicImportESBuildIssue272", {
files: {
"/a.js": `import('./b')`,
"/b.js": `export default 1; console.log('imported')`,
},
entryPoints: ["/a.js", "/b.js"],
splitting: true,
run: [{ file: "/out/a.js", stdout: "imported" }],
assertNotPresent: {
"/out/a.js": "imported",
},
});
itBundled("splitting/DynamicImportOutsideSourceTreeESBuildIssue264", {
files: {
"/Users/user/project/src/entry1.js": `import('package')`,
"/Users/user/project/src/entry2.js": `import('package')`,
"/Users/user/project/node_modules/package/index.js": `console.log('imported')`,
},
runtimeFiles: {
"/both.js": /* js */ `
import('./out/entry1.js');
import('./out/entry2.js');
`,
},
entryPoints: ["/Users/user/project/src/entry1.js", "/Users/user/project/src/entry2.js"],
splitting: true,
run: [
{ file: "/out/entry1.js", stdout: "imported" },
{ file: "/out/entry2.js", stdout: "imported" },
{ file: "/both.js", stdout: "imported" },
],
});
itBundled("splitting/CrossChunkAssignmentDependencies", {
files: {
"/a.js": /* js */ `
import {setValue} from './shared'
setValue(123)
`,
"/b.js": `import './shared'; console.log('b')`,
"/c.js": /* js */ `
import * as shared from './shared'
globalThis.shared = shared;
`,
"/shared.js": /* js */ `
var observer;
var value;
export function setObserver(cb) {
observer = cb;
}
export function getValue() {
return value;
}
export function setValue(next) {
console.log('setValue', next)
value = next;
if (observer) observer();
}
console.log("side effects!", getValue);
`,
},
entryPoints: ["/a.js", "/b.js", "/c.js"],
splitting: true,
target: "bun",
runtimeFiles: {
"/test.js": /* js */ `
import './out/c.js';
const { getValue, setObserver } = globalThis.shared;
function observer() {
console.log('observer', getValue());
}
setObserver(observer);
await import('./out/a.js');
await import('./out/b.js');
`,
},
run: [
{ file: "/out/a.js", stdout: "side effects! [Function: getValue]\nsetValue 123" },
{ file: "/out/b.js", stdout: "side effects! [Function: getValue]\nb" },
{ file: "/test.js", stdout: "side effects! [Function: getValue]\nsetValue 123\nobserver 123\nb" },
],
});
itBundled("splitting/CrossChunkAssignmentDependenciesRecursive", {
files: {
"/a.js": /* js */ `
import { setX } from './x'
globalThis.a = { setX };
`,
"/b.js": /* js */ `
import { setZ } from './z'
globalThis.b = { setZ };
`,
"/c.js": /* js */ `
import { setX2 } from './x'
import { setY2 } from './y'
import { setZ2 } from './z'
globalThis.c = { setX2, setY2, setZ2 };
`,
"/x.js": /* js */ `
let _x
export function setX(v) { _x = v }
export function setX2(v) { _x = v }
globalThis.x = { setX, setX2 };
`,
"/y.js": /* js */ `
import { setX } from './x'
let _y
export function setY(v) { _y = v }
export function setY2(v) { setX(v); _y = v }
globalThis.y = { setY, setY2 };
`,
"/z.js": /* js */ `
import { setY } from './y'
let _z
export function setZ(v) { _z = v }
export function setZ2(v) { setY(v); _z = v }
globalThis.z = { setZ, setZ2, setY };
`,
},
entryPoints: ["/a.js", "/b.js", "/c.js"],
splitting: true,
runtimeFiles: {
"/test_all.js": /* js */ `
import './out/a.js';
import './out/b.js';
import './out/c.js';
try {
a; b; c; x; y; z; // throw if not defined
} catch (error) {
throw new Error('chunks were not emitted right.')
}
import assert from 'assert';
assert(a.setX === x.setX, 'a.setX');
assert(b.setZ === z.setZ, 'b.setZ');
assert(c.setX2 === x.setX2, 'c.setX2');
assert(c.setY2 === y.setY2, 'c.setY2');
assert(c.setZ2 === z.setZ2, 'c.setZ2');
assert(z.setY === y.setY, 'z.setY');
`,
"/test_a_only.js": /* js */ `
import './out/a.js';
try {
a; x; // throw if not defined
} catch (error) {
throw new Error('chunks were not emitted right.')
}
import assert from 'assert';
assert(a.setX === x.setX, 'a.setX');
assert(globalThis.b === undefined, 'b should not be loaded');
assert(globalThis.c === undefined, 'c should not be loaded');
assert(globalThis.y === undefined, 'y should not be loaded');
assert(globalThis.z === undefined, 'z should not be loaded');
`,
"/test_b_only.js": /* js */ `
import './out/b.js';
try {
b; x; y; z; // throw if not defined
} catch (error) {
throw new Error('chunks were not emitted right.')
}
import assert from 'assert';
assert(globalThis.a === undefined, 'a should not be loaded');
assert(globalThis.c === undefined, 'c should not be loaded');
`,
"/test_c_only.js": /* js */ `
import './out/c.js';
try {
c; x; y; z; // throw if not defined
} catch (error) {
throw new Error('chunks were not emitted right.')
}
import assert from 'assert';
assert(globalThis.a === undefined, 'a should not be loaded');
assert(globalThis.b === undefined, 'b should not be loaded');
`,
},
run: [
{ file: "/test_all.js" },
{ file: "/test_a_only.js" },
{ file: "/test_b_only.js" },
{ file: "/test_c_only.js" },
],
});
itBundled("splitting/DuplicateChunkCollision", {
files: {
"/a.js": `import "./ab"`,
"/b.js": `import "./ab"`,
"/c.js": `import "./cd"`,
"/d.js": `import "./cd"`,
"/ab.js": `console.log(123)`,
"/cd.js": `console.log(123)`,
},
entryPoints: ["/a.js", "/b.js", "/c.js", "/d.js"],
splitting: true,
minifyWhitespace: true,
onAfterBundle(api) {
const files = readdirSync(api.outdir);
expect(files.length).toBe(6);
},
});
itBundled("splitting/MinifyIdentifiersCrashESBuildIssue437", {
files: {
"/a.js": /* js */ `
import {foo} from "./shared"
console.log(foo)
`,
"/b.js": /* js */ `
import {foo} from "./shared"
console.log(foo)
`,
"/c.js": `import "./shared"`,
"/shared.js": `export function foo(bar) {}`,
},
entryPoints: ["/a.js", "/b.js", "/c.js"],
splitting: true,
minifyIdentifiers: true,
run: [
{ file: "/out/a.js", stdout: "[Function: f]" },
{ file: "/out/b.js", stdout: "[Function: f]" },
],
});
itBundled("splitting/HybridESMAndCJSESBuildIssue617", {
files: {
"/a.js": `export let foo = 123`,
"/b.js": `export let bar = require('./a')`,
},
entryPoints: ["/a.js", "/b.js"],
splitting: true,
assertNotPresent: {
"/out/b.js": `123`,
},
runtimeFiles: {
"/test.js": /* js */ `
import { foo } from './out/a.js'
import { bar } from './out/b.js'
console.log(JSON.stringify({ foo, bar }))
`,
},
run: {
file: "/test.js",
stdout: '{"foo":123,"bar":{"foo":123}}',
},
});
itBundled("splitting/PublicPathEntryName", {
files: {
"/a.js": `import("./b")`,
"/b.js": `console.log('b')`,
},
outdir: "/out",
splitting: true,
publicPath: "/www/",
onAfterBundle(api) {
const t = new Bun.Transpiler();
const imports = t.scanImports(api.readFile("/out/a.js"));
expect(imports.length).toBe(1);
expect(imports[0].kind).toBe("dynamic-import");
assert(imports[0].path.startsWith("/www/"), `Expected path to start with "/www/" but got "${imports[0].path}"`);
},
});
itBundled("splitting/ChunkPathDirPlaceholderImplicitOutbase", {
files: {
"/project/entry.js": `console.log(import('./output-path/should-contain/this-text/file'))`,
"/project/output-path/should-contain/this-text/file.js": `console.log('file.js')`,
},
outdir: "/out",
splitting: true,
chunkNaming: "[dir]/[name]-[hash].[ext]",
onAfterBundle(api) {
assert(
readdirSync(api.outdir + "/output-path/should-contain/this-text").length === 1,
"Expected one file in out/output-path/should-contain/this-text/",
);
},
});
const EdgeCaseESBuildIssue2793WithSplitting = itBundled("splitting/EdgeCaseESBuildIssue2793WithSplitting", {
files: {
"/src/a.js": `export const A = 42;`,
"/src/b.js": `export const B = async () => (await import(".")).A`,
"/src/index.js": /* js */ `
export * from "./a"
export * from "./b"
`,
},
outdir: "/out",
entryPoints: ["/src/index.js"],
splitting: true,
target: "browser",
runtimeFiles: {
"/test.js": /* js */ `
import { A, B } from './out/index.js'
console.log(A, B() instanceof Promise, await B())
`,
},
run: {
file: "/test.js",
stdout: "42 true 42",
},
});
itBundled("splitting/EdgeCaseESBuildIssue2793WithoutSplitting", {
...EdgeCaseESBuildIssue2793WithSplitting.options,
splitting: false,
runtimeFiles: {
"/test.js": /* js */ `
import { A, B } from './out/index.js'
console.log(A, B() instanceof Promise, await B())
`,
},
run: {
file: "/test.js",
stdout: "42 true 42",
},
});
// Test that CJS modules with dynamic imports to other CJS entry points work correctly
// when code splitting causes the dynamically imported module to be in a separate chunk.
// The dynamic import should properly unwrap the default export using __toESM.
// Regression test for: dynamic import of CJS chunk returns { default: { __esModule, ... } }
// and needs .then((m)=>__toESM(m.default)) to unwrap correctly.
// Note: __esModule is required because bun optimizes simple CJS to ESM otherwise.
itBundled("splitting/CJSDynamicImportOfCJSChunk", {
files: {
"/main.js": /* js */ `
import("./impl.js").then(mod => console.log(mod.foo()));
`,
"/impl.js": /* js */ `
Object.defineProperty(exports, "__esModule", { value: true });
exports.foo = () => "success";
`,
},
entryPoints: ["/main.js", "/impl.js"],
splitting: true,
outdir: "/out",
run: {
file: "/out/main.js",
stdout: "success",
},
});
});