From a66692761fa2db26acbfc7d48a67cbaa9159a9f9 Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Wed, 23 Jul 2025 21:58:31 +0000 Subject: [PATCH] Fix duplicate exports when entry points re-export from other entry points (#5344) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When code splitting is enabled and one entry point re-exports from another entry point, the bundler was generating duplicate export statements. This happened because both the cross-chunk export generation logic and the entry point export generation logic were creating export statements for the same symbols. The fix prevents cross-chunk export statement generation for entry points that have their own sorted export aliases, since these entry points will generate their own exports in generateEntryPointTailJS(). Test cases added to verify: - Basic re-export scenario between two entry points - Multiple re-exports through intermediate modules 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .../computeCrossChunkDependencies.zig | 8 ++- test/bundler/bundler_regressions.test.ts | 61 +++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/bundler/linker_context/computeCrossChunkDependencies.zig b/src/bundler/linker_context/computeCrossChunkDependencies.zig index 25e2b5bcf7..465283b6d2 100644 --- a/src/bundler/linker_context/computeCrossChunkDependencies.zig +++ b/src/bundler/linker_context/computeCrossChunkDependencies.zig @@ -334,7 +334,13 @@ fn computeCrossChunkDependenciesWithChunkMetas(c: *LinkerContext, chunks: []Chun ); } - if (clause_items.len > 0) { + // Only generate cross-chunk export statements if there are items to export + // and if this chunk is not an entry point with sorted export aliases + // (which indicates it will generate its own exports in generateEntryPointTailJS) + const sorted_and_filtered_export_aliases = c.graph.meta.items(.sorted_and_filtered_export_aliases)[chunk.entry_point.source_index]; + const should_skip_cross_chunk_exports = chunk.entry_point.is_entry_point and sorted_and_filtered_export_aliases.len > 0; + + if (clause_items.len > 0 and !should_skip_cross_chunk_exports) { 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.* = .{ diff --git a/test/bundler/bundler_regressions.test.ts b/test/bundler/bundler_regressions.test.ts index e4efcf36f8..98514be815 100644 --- a/test/bundler/bundler_regressions.test.ts +++ b/test/bundler/bundler_regressions.test.ts @@ -294,4 +294,65 @@ describe("bundler", () => { run: true, capture: ["1 /* Value */", "1 /* Value */", "1 /* Value */"], }); + + // https://github.com/oven-sh/bun/issues/5344 + itBundled("regression/DuplicateExports#5344", { + files: { + "/entry-b.ts": ` + export function b() {} + `, + "/entry-a.ts": ` + export { b } from "./entry-b.ts"; + export function a() {} + `, + }, + entryPoints: ["/entry-a.ts", "/entry-b.ts"], + outdir: "/out", + format: "esm", + splitting: true, + onAfterBundle(api) { + // Check entry-b.js output for duplicate exports + const entryB = api.readFile("/out/entry-b.js"); + + // Count all export statements (both single line and multiline) + const exportMatches = entryB.match(/export\s*\{[^}]*\}/g); + + if (exportMatches && exportMatches.length > 1) { + throw new Error(`Found ${exportMatches.length} export statements (expected 1) in entry-b.js:\n${exportMatches.join('\n')}\n\nFull output:\n${entryB}`); + } + }, + }); + + // Additional test case for multiple re-exports + itBundled("regression/DuplicateExports#5344-MultipleReExports", { + files: { + "/lib.ts": ` + export const value = 42; + export function helper() { return "helper"; } + `, + "/intermediate.ts": ` + export { value, helper } from "./lib.ts"; + `, + "/entry.ts": ` + export { value, helper } from "./intermediate.ts"; + export const main = "main"; + `, + }, + entryPoints: ["/entry.ts", "/intermediate.ts", "/lib.ts"], + outdir: "/out", + format: "esm", + splitting: true, + onAfterBundle(api) { + // Check each file for duplicate exports + const files = ["entry.js", "intermediate.js", "lib.js"]; + for (const file of files) { + const content = api.readFile(`/out/${file}`); + const exportMatches = content.match(/export\s*\{[^}]*\}/g); + + if (exportMatches && exportMatches.length > 1) { + throw new Error(`Found ${exportMatches.length} export statements (expected max 1) in ${file}:\n${exportMatches.join('\n')}\n\nFull output:\n${content}`); + } + } + }, + }); });