fix(bundler): prevent duplicate export statements with code splitting

When code splitting is enabled and a file is both an entry point and
imported by another entry point, the bundler was generating duplicate
export statements. This happened because:

1. generateEntryPointTailJS generates exports for entry point's named exports
2. computeCrossChunkDependencies generates cross_chunk_suffix_stmts for
   symbols that other chunks need to import

Both paths were adding export clauses to the output, resulting in invalid
JavaScript with duplicate `export { symbol }` statements.

The fix skips generating cross_chunk_suffix_stmts for entry point chunks
since generateEntryPointTailJS already handles their exports. The
exports_to_other_chunks map is still populated for the import side.

Fixes #10631

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Claude Bot
2026-01-27 07:14:30 +00:00
parent bfe40e8760
commit 1e816992e6
2 changed files with 174 additions and 1 deletions

View File

@@ -337,7 +337,11 @@ fn computeCrossChunkDependenciesWithChunkMetas(c: *LinkerContext, chunks: []Chun
);
}
if (clause_items.len > 0) {
// Don't generate cross-chunk export statements for entry point chunks.
// Entry points already have their exports generated by generateEntryPointTailJS,
// so adding cross_chunk_suffix_stmts would create duplicate export clauses.
// We still need exports_to_other_chunks populated above for the import side.
if (clause_items.len > 0 and !chunk.isEntryPoint()) {
var stmts = BabyList(js_ast.Stmt).initCapacity(c.allocator(), 1) catch unreachable;
const export_clause = c.allocator().create(js_ast.S.ExportClause) catch unreachable;
export_clause.* = .{

View File

@@ -0,0 +1,169 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
// https://github.com/oven-sh/bun/issues/10631
// When code splitting is enabled and a file is both an entry point and imported
// by another entry point, the bundler was generating duplicate export statements.
test("code splitting does not produce duplicate exports when entry point is also imported", async () => {
using dir = tempDir("issue-10631", {
"index.ts": `import { logStuff } from "./other";
logStuff();`,
"other.ts": `export function logStuff() {
console.log("Logging Stuff");
};`,
});
// Bundle with splitting enabled
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "index.ts", "other.ts", "--outdir=dist/", "--target=bun", "--splitting"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(exitCode).toBe(0);
// Read the generated other.js file
const otherJs = await Bun.file(`${dir}/dist/other.js`).text();
// Count how many times "export {" appears - should be exactly once
const exportMatches = otherJs.match(/export\s*\{/g);
expect(exportMatches?.length).toBe(1);
// Verify the file can be executed without errors
await using execProc = Bun.spawn({
cmd: [bunExe(), `${dir}/dist/index.js`],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [execStdout, execStderr, execExitCode] = await Promise.all([
execProc.stdout.text(),
execProc.stderr.text(),
execProc.exited,
]);
expect(execStdout).toBe("Logging Stuff\n");
expect(execStderr).toBe("");
expect(execExitCode).toBe(0);
});
test("code splitting works correctly with multiple cross-chunk imports", async () => {
using dir = tempDir("issue-10631-multi", {
"entry1.ts": `import { shared } from "./shared";
console.log("entry1:", shared());`,
"entry2.ts": `import { shared } from "./shared";
console.log("entry2:", shared());`,
"shared.ts": `export function shared() {
return "shared value";
}`,
});
// Bundle with splitting enabled
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "entry1.ts", "entry2.ts", "--outdir=dist/", "--target=bun", "--splitting"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(exitCode).toBe(0);
// Verify entry1 works
await using exec1 = Bun.spawn({
cmd: [bunExe(), `${dir}/dist/entry1.js`],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [exec1Stdout, exec1Stderr, exec1ExitCode] = await Promise.all([
exec1.stdout.text(),
exec1.stderr.text(),
exec1.exited,
]);
expect(exec1Stdout).toBe("entry1: shared value\n");
expect(exec1Stderr).toBe("");
expect(exec1ExitCode).toBe(0);
// Verify entry2 works
await using exec2 = Bun.spawn({
cmd: [bunExe(), `${dir}/dist/entry2.js`],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [exec2Stdout, exec2Stderr, exec2ExitCode] = await Promise.all([
exec2.stdout.text(),
exec2.stderr.text(),
exec2.exited,
]);
expect(exec2Stdout).toBe("entry2: shared value\n");
expect(exec2Stderr).toBe("");
expect(exec2ExitCode).toBe(0);
});
test("code splitting with entry point as both exporter and importer", async () => {
using dir = tempDir("issue-10631-complex", {
"index.ts": `import { logStuff } from "./other";
logStuff();
export const fromIndex = "index value";`,
"other.ts": `export function logStuff() {
console.log("Logging Stuff");
};
export const fromOther = "other value";`,
});
// Bundle with splitting enabled, both files as entry points
await using proc = Bun.spawn({
cmd: [bunExe(), "build", "index.ts", "other.ts", "--outdir=dist/", "--target=bun", "--splitting"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(exitCode).toBe(0);
// Read the generated other.js file and verify no duplicate exports
const otherJs = await Bun.file(`${dir}/dist/other.js`).text();
const exportMatches = otherJs.match(/export\s*\{/g);
expect(exportMatches?.length).toBe(1);
// Verify both exports are present in other.js
expect(otherJs).toContain("logStuff");
expect(otherJs).toContain("fromOther");
// Verify index.js can be executed
await using execProc = Bun.spawn({
cmd: [bunExe(), `${dir}/dist/index.js`],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [execStdout, execStderr, execExitCode] = await Promise.all([
execProc.stdout.text(),
execProc.stderr.text(),
execProc.exited,
]);
expect(execStdout).toBe("Logging Stuff\n");
expect(execStderr).toBe("");
expect(execExitCode).toBe(0);
});