Compare commits

...

3 Commits

Author SHA1 Message Date
Ciro Spaciari
6feb465020 Merge branch 'main' into claude/fix-duplicate-exports-5344 2025-07-29 14:36:38 -07:00
autofix-ci[bot]
33849ea3a5 [autofix.ci] apply automated fixes 2025-07-23 22:01:13 +00:00
Claude Bot
a66692761f Fix duplicate exports when entry points re-export from other entry points (#5344)
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 <noreply@anthropic.com>
2025-07-23 21:58:31 +00:00
2 changed files with 72 additions and 1 deletions

View File

@@ -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.* = .{

View File

@@ -294,4 +294,69 @@ 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}`,
);
}
}
},
});
});