diff --git a/src/bundler/linker_context/computeChunks.rs b/src/bundler/linker_context/computeChunks.rs index be942b837711..706fb9ae4d43 100644 --- a/src/bundler/linker_context/computeChunks.rs +++ b/src/bundler/linker_context/computeChunks.rs @@ -615,10 +615,22 @@ pub fn compute_chunks(this: &mut LinkerContext, unique_key: u64) -> crate::Resul } } - let pathname = bun_fs::PathName::init( - output_paths[chunk.entry_point.entry_point_id() as usize].slice(), - ); - chunk.template.placeholder.name = pathname.base.to_vec().into_boxed_slice(); + let pathname: Option> = if chunk.entry_point.is_entry_point() { + let p = bun_fs::PathName::init( + output_paths[chunk.entry_point.entry_point_id() as usize].slice(), + ); + chunk.template.placeholder.name = p.base.to_vec().into_boxed_slice(); + Some(p) + } else { + chunk.template.placeholder.name = if chunk.entry_bits.count() == 1 { + let ep = chunk.entry_bits.find_first_set().unwrap(); + let base = bun_fs::PathName::init(output_paths[ep].slice()).base; + [base, b"-chunk"].concat().into_boxed_slice() + } else { + b"shared".to_vec().into_boxed_slice() + }; + None + }; chunk.template.placeholder.ext = chunk.content.ext().to_vec().into_boxed_slice(); if chunk.template.needs(PlaceholderField::Target) { @@ -633,7 +645,9 @@ pub fn compute_chunks(this: &mut LinkerContext, unique_key: u64) -> crate::Resul }; } - if chunk.template.needs(PlaceholderField::Dir) { + if let Some(ref pathname) = pathname + && chunk.template.needs(PlaceholderField::Dir) + { // this if check is a specific fix for `bun build hi.ts --external '*'`, without leading `./` let dir_path: &[u8] = if !pathname.dir.is_empty() { pathname.dir diff --git a/src/bundler/linker_context/generateChunksInParallel.rs b/src/bundler/linker_context/generateChunksInParallel.rs index 74db4afb5bf7..c97602905add 100644 --- a/src/bundler/linker_context/generateChunksInParallel.rs +++ b/src/bundler/linker_context/generateChunksInParallel.rs @@ -402,7 +402,6 @@ pub fn generate_chunks_in_parallel( let mut entry_naming: Option<&[u8]> = None; let mut chunk_naming: Option<&[u8]> = None; - let mut asset_naming: Option<&[u8]> = None; writeln!(&mut msg, "Multiple files share the same output path")?; @@ -415,16 +414,13 @@ pub fn generate_chunks_in_parallel( { writeln!(&mut msg, " {}:", bstr::BStr::new(key))?; for chunk in dup.sources.iter() { - if chunk.entry_point.is_entry_point() { - if kinds[chunk.entry_point.source_index() as usize] + if chunk.entry_point.is_entry_point() + && kinds[chunk.entry_point.source_index() as usize] == EntryPoint::Kind::UserSpecified - { - entry_naming = Some(&chunk.template.data); - } else { - chunk_naming = Some(&chunk.template.data); - } + { + entry_naming = Some(&chunk.template.data); } else { - asset_naming = Some(&chunk.template.data); + chunk_naming = Some(&chunk.template.data); } let source_index = chunk.entry_point.source_index(); @@ -440,11 +436,7 @@ pub fn generate_chunks_in_parallel( c.log_mut().add_error(None, bun_ast::Loc::EMPTY, msg); - for (name, template) in [ - ("entry", entry_naming), - ("chunk", chunk_naming), - ("asset", asset_naming), - ] { + for (name, template) in [("entry", entry_naming), ("chunk", chunk_naming)] { let Some(template) = template else { continue }; let mut text: Vec = Vec::new(); diff --git a/test/bundler/bundler_naming.test.ts b/test/bundler/bundler_naming.test.ts index a51451b19cbe..25847041659f 100644 --- a/test/bundler/bundler_naming.test.ts +++ b/test/bundler/bundler_naming.test.ts @@ -1,4 +1,5 @@ -import { describe } from "bun:test"; +import { describe, expect } from "bun:test"; +import { readdirSync } from "node:fs"; import { ESBUILD, itBundled } from "./expectBundled"; describe("bundler", () => { @@ -302,6 +303,68 @@ describe("bundler", () => { }, ], }); + // A non-entry split chunk's `[name]` is the owning entry point's basename + // plus `-chunk` when exactly one entry reaches it, or `shared` otherwise, + // never the first-listed entrypoint. Dynamic-import chunks are entry points + // and keep their own source basename. + for (const order of ["MainFirst", "ModnameFirst"] as const) { + itBundled(`naming/ChunkNamePlaceholderSharedChunk${order}`, { + files: { + "/src/main.js": /* js */ ` + import { v } from "./sub/modname.js"; + import("./lazy.js").then(m => console.log(v, m.z)); + `, + "/src/sub/modname.js": /* js */ ` + export const v = "V"; + `, + "/src/lazy.js": /* js */ ` + export const z = "Z"; + `, + }, + entryPointsRaw: + order === "MainFirst" ? ["./src/main.js", "./src/sub/modname.js"] : ["./src/sub/modname.js", "./src/main.js"], + outdir: "/out", + splitting: true, + chunkNaming: "chunks/[name]-[hash].[ext]", + onAfterBundle(api) { + const chunks = readdirSync(api.outdir + "/chunks").sort(); + const lazy = chunks.filter(f => f.startsWith("lazy-")); + const shared = chunks.filter(f => !f.startsWith("lazy-")); + expect(lazy.length).toBe(1); + expect(shared.length).toBeGreaterThan(0); + for (const f of shared) { + expect(f).toMatch(/^shared-[a-z0-9]+\.js$/); + } + }, + run: { + file: "/out/main.js", + stdout: "V Z", + }, + }); + } + itBundled("naming/ChunkNamingCollisionHint", { + backend: "api", + files: { + "/src/main.js": /* js */ ` + import { v } from "./sub/modname.js"; + import("./lazy.js").then(m => console.log(v, m.z)); + `, + "/src/sub/modname.js": `export const v = "V";`, + "/src/lazy.js": `export const z = "Z";`, + }, + entryPointsRaw: ["./src/main.js", "./src/sub/modname.js"], + outdir: "/out", + splitting: true, + chunkNaming: "chunks/[name].[ext]", + bundleErrors: { + "": ["Multiple files share the same output path"], + }, + onAfterApiBundle(build) { + const text = build.logs.map(l => l.message).join("\n"); + expect(text).toContain("chunk naming is"); + expect(text).not.toContain("asset naming is"); + }, + }); // A non-ASCII ID_Continue basename char is preserved in the generated // CommonJS wrapper symbol, not replaced per-code-point (nor per-UTF-8-byte, // which once regressed to `require_caf__utils`).