Compare commits

...

13 Commits

Author SHA1 Message Date
Dylan Conway
942535f9e3 fix build 2025-06-19 16:36:14 -07:00
Dylan Conway
5b25c2922b Merge branch 'main' into cursor/fix-duplicate-exports-in-bun-s-bundler-9dc5 2025-06-19 15:45:25 -07:00
claude[bot]
d4e6cd6ae2 Fix ban-words test failures: replace std.StringHashMap with bun.StringHashMap and catch unreachable with catch bun.outOfMemory()
Co-authored-by: Jarred Sumner <Jarred-Sumner@users.noreply.github.com>
2025-06-15 21:26:08 +00:00
Dylan Conway
7f08f4638f Merge branch 'main' into cursor/fix-duplicate-exports-in-bun-s-bundler-9dc5 2025-06-10 22:10:42 -07:00
Jarred Sumner
4e1529b851 try 2025-06-11 06:20:50 +02:00
Jarred-Sumner
58b4645ef2 bun run prettier 2025-06-11 03:41:50 +00:00
Jarred-Sumner
4b292cca06 bun run zig-format 2025-06-11 03:40:34 +00:00
Jarred Sumner
9d31a4e7ce A
Fix duplicate exports in code splitting by coordinating cross-chunk and entry point export generation

    This commit resolves issue #5344 where duplicate export statements were generated
    when using code splitting with entry points that are imported by other entry points.

    The root cause was that two separate systems were both generating export statements
    for the same symbols:

    1. Cross-chunk export generation (computeCrossChunkDependencies.zig)
    2. Entry point tail generation (postProcessJSChunk.zig)

    When a file serves as both an entry point AND is imported by another entry point,
    both systems would create exports, resulting in syntax errors like:
    `export { a }; export { a };`

    The fix adds coordination between these systems by:
    - Adding deduplication logic in cross-chunk export generation to prevent internal duplicates
    - Filtering out exports in entry point tail generation that are already handled by cross-chunk exports
    - Only generating export statements when there are actual items to export

    This ensures that each symbol is exported exactly once, preventing duplicate export
    syntax errors while maintaining correct code splitting functionality.

    Test cases updated to verify the fix works correctly and doesn't regress.

    🤖 Generated with [Claude Code](https://claude.ai/code)

    Co-authored-by: Claude <noreply@anthropic.com>
A
A
A
2025-06-11 05:38:14 +02:00
claude[bot]
ca0eff7048 bun run zig-format 2025-06-10 04:26:29 +00:00
claude[bot]
a42d3257b6 Fix duplicate exports in cross-chunk generation for mixed imports
Add deduplication logic to cross-chunk export generation to prevent
duplicate exports when both static and dynamic imports of CommonJS 
modules are used with code splitting.

The issue was that exports could be generated in two separate paths:
1. Cross-chunk export generation (no deduplication) 
2. Entry point tail generation (had deduplication)

This caused duplicate exports like `export { $bar, $bar }` in scenarios
with mixed static/dynamic imports, leading to syntax errors.

Co-authored-by: Jarred-Sumner <Jarred-Sumner@users.noreply.github.com>
2025-06-10 04:24:39 +00:00
claude[bot]
ef634a60c6 bun run zig-format 2025-06-10 01:02:16 +00:00
claude[bot]
2033d2f031 Fix duplicate exports by deduplicating in entry point tail generation
Move deduplication logic from computeCrossChunkDependencies to generateEntryPointTailJS where entry points actually generate their export statements. The previous fix only affected non-entry-point chunks but entry points use a different code path through generateEntryPointTailJS.

- Revert changes to computeCrossChunkDependencies.zig that skipped cross-chunk exports for entry points
- Add deduplication logic in generateEntryPointTailJS to remove duplicate export aliases before creating export clause
- Preserves original order and only processes when multiple export items exist

Fixes test failure where duplicate exports were still present in entry point chunks.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Jarred-Sumner <Jarred-Sumner@users.noreply.github.com>
2025-06-10 01:00:20 +00:00
Cursor Agent
27f77d7944 Fix duplicate exports in code splitting with cross-chunk dependencies 2025-06-10 00:11:35 +00:00
8 changed files with 169 additions and 19 deletions

View File

@@ -23,7 +23,7 @@ pub fn computeCrossChunkDependencies(c: *LinkerContext, chunks: []Chunk) !void {
}
{
const cross_chunk_dependencies = c.allocator.create(CrossChunkDependencies) catch unreachable;
const cross_chunk_dependencies = c.allocator.create(CrossChunkDependencies) catch bun.outOfMemory();
defer c.allocator.destroy(cross_chunk_dependencies);
cross_chunk_dependencies.* = .{
@@ -178,10 +178,18 @@ const CrossChunkDependencies = struct {
for (sorted_and_filtered_export_aliases) |alias| {
const export_ = resolved_exports.get(alias).?;
var target_ref = export_.data.import_ref;
var source_index = export_.data.source_index;
// If this is an import, then target what the import points to
if (deps.imports_to_bind[export_.data.source_index.get()].get(target_ref)) |import_data| {
if (deps.imports_to_bind[source_index.get()].get(target_ref)) |import_data| {
target_ref = import_data.data.import_ref;
source_index = import_data.data.source_index;
}
// Skip exports that are defined locally in this entry point to avoid
// duplicate exports in cross-chunk and entry point tail generation
if (source_index.get() == chunk.entry_point.source_index) {
continue;
}
// If this is an ES6 import from a CommonJS file, it will become a
@@ -310,12 +318,31 @@ fn computeCrossChunkDependenciesWithChunkMetas(c: *LinkerContext, chunks: []Chun
chunk_meta.exports,
&stable_ref_list,
);
var clause_items = BabyList(js_ast.ClauseItem).initCapacity(c.allocator, stable_ref_list.items.len) catch unreachable;
clause_items.len = @as(u32, @truncate(stable_ref_list.items.len));
repr.exports_to_other_chunks.ensureUnusedCapacity(c.allocator, stable_ref_list.items.len) catch unreachable;
// Deduplicate export items to prevent duplicate exports in code splitting
var unique_stable_refs = std.ArrayList(StableRef).init(c.allocator);
defer unique_stable_refs.deinit();
if (stable_ref_list.items.len > 1) {
var seen_refs = std.AutoHashMap(Ref, void).init(c.allocator);
defer seen_refs.deinit();
for (stable_ref_list.items) |stable_ref| {
if (!seen_refs.contains(stable_ref.ref)) {
seen_refs.put(stable_ref.ref, {}) catch bun.outOfMemory();
unique_stable_refs.append(stable_ref) catch bun.outOfMemory();
}
}
} else {
unique_stable_refs.appendSlice(stable_ref_list.items) catch bun.outOfMemory();
}
var clause_items = BabyList(js_ast.ClauseItem).initCapacity(c.allocator, unique_stable_refs.items.len) catch bun.outOfMemory();
clause_items.len = @as(u32, @truncate(unique_stable_refs.items.len));
repr.exports_to_other_chunks.ensureUnusedCapacity(c.allocator, unique_stable_refs.items.len) catch bun.outOfMemory();
r.clearRetainingCapacity();
for (stable_ref_list.items, clause_items.slice()) |stable_ref, *clause_item| {
for (unique_stable_refs.items, clause_items.slice()) |stable_ref, *clause_item| {
const ref = stable_ref.ref;
const alias = if (c.options.minify_identifiers) try r.nextMinifiedName(c.allocator) else r.nextRenamedName(c.graph.symbols.get(ref).?.original_name);
@@ -336,8 +363,8 @@ fn computeCrossChunkDependenciesWithChunkMetas(c: *LinkerContext, chunks: []Chun
}
if (clause_items.len > 0) {
var stmts = BabyList(js_ast.Stmt).initCapacity(c.allocator, 1) catch unreachable;
const export_clause = c.allocator.create(js_ast.S.ExportClause) catch unreachable;
var stmts = BabyList(js_ast.Stmt).initCapacity(c.allocator, 1) catch bun.outOfMemory();
const export_clause = c.allocator.create(js_ast.S.ExportClause) catch bun.outOfMemory();
export_clause.* = .{
.items = clause_items.slice(),
.is_single_line = true,

View File

@@ -95,6 +95,7 @@ pub fn postProcessJSChunk(ctx: GenerateChunkCtx, worker: *ThreadPool.Worker, chu
worker.allocator,
arena.allocator(),
chunk.renamer,
chunk,
);
}
@@ -427,6 +428,7 @@ pub fn generateEntryPointTailJS(
allocator: std.mem.Allocator,
temp_allocator: std.mem.Allocator,
r: renamer.Renamer,
chunk: *const Chunk,
) CompileResult {
const flags: JSMeta.Flags = c.graph.meta.items(.flags)[source_index];
var stmts = std.ArrayList(Stmt).init(temp_allocator);
@@ -655,20 +657,49 @@ pub fn generateEntryPointTailJS(
},
.alias = alias,
.alias_loc = resolved_export.data.name_loc,
}) catch unreachable;
}) catch bun.outOfMemory();
}
}
stmts.append(
Stmt.alloc(
S.ExportClause,
.{
.items = items.items,
.is_single_line = false,
},
Logger.Loc.Empty,
),
) catch unreachable;
// Filter out exports that are already handled by cross-chunk exports
// and deduplicate to prevent duplicate exports in code splitting
var seen_aliases = bun.StringHashMap(void).init(temp_allocator);
defer seen_aliases.deinit();
// First, mark aliases that are already exported in cross-chunk exports
const cross_chunk_exports = &chunk.content.javascript.exports_to_other_chunks;
var iterator = cross_chunk_exports.iterator();
while (iterator.next()) |entry| {
seen_aliases.put(entry.value_ptr.*, {}) catch bun.outOfMemory();
}
var unique_items = std.ArrayList(js_ast.ClauseItem).init(temp_allocator);
defer unique_items.deinit();
for (items.items) |item| {
if (!seen_aliases.contains(item.alias)) {
seen_aliases.put(item.alias, {}) catch unreachable;
unique_items.append(item) catch unreachable;
}
}
// Replace items with filtered and deduplicated ones
items.clearAndFree();
items.appendSlice(unique_items.items) catch unreachable;
// Only generate export statement if we have items to export
if (items.items.len > 0) {
stmts.append(
Stmt.alloc(
S.ExportClause,
.{
.items = items.items,
.is_single_line = false,
},
Logger.Loc.Empty,
),
) catch unreachable;
}
if (flags.needs_synthetic_default_export and !had_default_export) {
var properties = G.Property.List.initCapacity(allocator, items.items.len) catch unreachable;

View File

@@ -0,0 +1 @@
export function a() {}

View File

@@ -0,0 +1 @@
export function a() {} export function onlyInA() {}

View File

@@ -0,0 +1 @@
export { a } from './entry-a.js'

View File

@@ -0,0 +1 @@
export { a } from './entry-a2.js'; export function b() {}

View File

@@ -0,0 +1,21 @@
import { readFileSync } from "fs";
import { join } from "path";
// Read the generated entry-a.js file
const content = readFileSync(join(import.meta.dir, "out/entry-a.js"), "utf8");
// Count occurrences of 'export { a };'
const exportMatches = content.match(/export\s*\{\s*a\s*\};/g) || [];
const exportCount = exportMatches.length;
console.log("File content:");
console.log(content);
console.log("\nExport count:", exportCount);
if (exportCount === 1) {
console.log("✅ SUCCESS: Only one export statement found (duplicate exports fixed!)");
process.exit(0);
} else {
console.log("❌ FAILURE: Found", exportCount, "export statements (duplicate exports still present)");
process.exit(1);
}

View File

@@ -609,4 +609,71 @@ describe("bundler", () => {
stdout: "42 true 42",
},
});
itBundled("splitting/ReExportDuplicateExportsFix", {
// Direct test for the duplicate exports fix
files: {
"/shared.js": /* js */ `
export function shared() {
return "shared";
}
`,
"/a.js": /* js */ `
export { shared } from './shared.js';
`,
"/b.js": /* js */ `
export { shared } from './shared.js';
`,
},
entryPoints: ["/a.js", "/b.js"],
splitting: true,
runtimeFiles: {
"/test.js": /* js */ `
import { shared as s1 } from './out/a.js';
import { shared as s2 } from './out/b.js';
console.log(s1 === s2, s1());
`,
},
run: [{ file: "/test.js", stdout: "true shared" }],
onAfterBundle(api) {
// Check that the output files don't have duplicate export statements
const aContent = api.readFile("/out/a.js");
const bContent = api.readFile("/out/b.js");
// Count occurrences of export statements for 'shared'
const countExports = (content: string) => {
const matches = content.match(/export\s*\{\s*shared\s*\}/g) || [];
return matches.length;
};
assert.strictEqual(countExports(aContent), 1, "File a.js should have exactly one export for 'shared'");
assert.strictEqual(countExports(bContent), 1, "File b.js should have exactly one export for 'shared'");
},
});
itBundled("splitting/DuplicateExportsIssue5106", {
// Test for https://github.com/oven-sh/bun/issues/5106
files: {
"/entry-a.js": /* js */ `
export function a() {}
`,
"/entry-b.js": /* js */ `
export { a } from './entry-a.js'
export function b() {}
`,
},
entryPoints: ["/entry-a.js", "/entry-b.js"],
splitting: true,
runtimeFiles: {
"/test.js": /* js */ `
import { a } from './out/entry-a.js';
import { a as a2, b } from './out/entry-b.js';
console.log(a === a2, typeof a, typeof b);
`,
},
run: [{ file: "/test.js", stdout: "true function function" }],
assertNotPresent: {
// Make sure we don't have duplicate exports in any entry point
"/out/entry-a.js": "export { a };\n\nexport { a };",
"/out/entry-b.js": "export { a };\n\nexport { a };",
},
});
});