diff --git a/src/bundler/linker_context/computeCrossChunkDependencies.zig b/src/bundler/linker_context/computeCrossChunkDependencies.zig index 8c4fb4a88f..83b7c7b279 100644 --- a/src/bundler/linker_context/computeCrossChunkDependencies.zig +++ b/src/bundler/linker_context/computeCrossChunkDependencies.zig @@ -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 diff --git a/src/bundler/linker_context/postProcessJSChunk.zig b/src/bundler/linker_context/postProcessJSChunk.zig index 33d1ea30ec..c33369ac9c 100644 --- a/src/bundler/linker_context/postProcessJSChunk.zig +++ b/src/bundler/linker_context/postProcessJSChunk.zig @@ -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); @@ -544,6 +546,12 @@ pub fn generateEntryPointTailJS( resolved_export.data.source_index = import_data.data.source_index; } + // Skip exports that are defined locally in this entry point to avoid duplicates. + // Only generate entry point tail exports for symbols imported from other files. + if (resolved_export.data.source_index.get() == source_index) { + continue; + } + // Exports of imports need EImportIdentifier in case they need to be re- // written to a property access later on if (c.graph.symbols.get(resolved_export.data.import_ref).?.namespace_alias != null) { @@ -659,36 +667,45 @@ pub fn generateEntryPointTailJS( } } - // Deduplicate export items to prevent duplicate exports in code splitting - if (items.items.len > 1) { - var seen_aliases = std.StringHashMap(void).init(temp_allocator); - defer seen_aliases.deinit(); + // Deduplicate export items and filter out items already in cross-chunk exports + // to prevent duplicate exports in code splitting + var seen_aliases = std.StringHashMap(void).init(temp_allocator); + defer seen_aliases.deinit(); - 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 deduplicated ones - items.clearAndFree(); - items.appendSlice(unique_items.items) catch unreachable; + // First, populate seen_aliases with exports that are already 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 unreachable; } - stmts.append( - Stmt.alloc( - S.ExportClause, - .{ - .items = items.items, - .is_single_line = false, - }, - Logger.Loc.Empty, - ), - ) catch unreachable; + 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; diff --git a/test/bundler/esbuild/splitting.test.ts b/test/bundler/esbuild/splitting.test.ts index 3353f19eed..483a06f642 100644 --- a/test/bundler/esbuild/splitting.test.ts +++ b/test/bundler/esbuild/splitting.test.ts @@ -671,8 +671,9 @@ describe("bundler", () => { }, run: [{ file: "/test.js", stdout: "true function function" }], assertNotPresent: { - // Make sure we don't have duplicate exports - "/out/entry-b.js": ["export { a };", "export { a };"], + // 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 };", }, }); });