diff --git a/src/bundler/LinkerContext.rs b/src/bundler/LinkerContext.rs index e8a16aae5a91..08a842bbe303 100644 --- a/src/bundler/LinkerContext.rs +++ b/src/bundler/LinkerContext.rs @@ -1774,10 +1774,12 @@ impl<'a> LinkerContext<'a> { .path_with_pretty_initialized(&source.path, arena) .expect("OOM"); } - // Note: `Path::assert_pretty_is_valid` lives on the - // resolver-side `Path<'a>`; the logger `Path` has no - // such debug hook yet. - debug_assert!(source.path.text.as_ptr() != source.path.pretty.as_ptr()); + // FileMap keys may be relative; when the computed pretty + // equals text, `dupe_alloc` aliases them and that's fine. + debug_assert!( + source.path.text.as_ptr() != source.path.pretty.as_ptr() + || !bun_paths::is_absolute(source.path.text) + ); break 'brk source.path.pretty; } else { diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index 16de9421663f..3ae0f92c0c8a 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -2583,7 +2583,13 @@ pub mod bv2_impl { None => return Ok(None), }; - path.assert_file_path_is_absolute(); + // FileMap keys are user-supplied and may be relative; they are lookup + // identities, not real fs paths, so the absolute invariant does not apply. + if bun_core::Environment::CI_ASSERT + && !self.file_map.is_some_and(|fm| fm.contains(path.text)) + { + path.assert_file_path_is_absolute(); + } // borrowck: get-then-put instead of a single get-or-put. if self .path_to_source_index_map(target) diff --git a/test/bundler/bundler_files.test.ts b/test/bundler/bundler_files.test.ts index 81a5d904576b..eea2dd3a65ff 100644 --- a/test/bundler/bundler_files.test.ts +++ b/test/bundler/bundler_files.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { tempDir } from "harness"; +import { bunEnv, bunExe, tempDir } from "harness"; describe("bundler files option", () => { test("basic in-memory file bundling", async () => { @@ -582,4 +582,66 @@ describe("bundler files option", () => { const output = await result.outputs[0].text(); expect(output).toContain("injected by plugin"); }); + + // FileMap keys are user-supplied identities and may be relative; run in a + // subprocess so an assertion panic fails the test instead of killing the runner. + test.concurrent.each(["./e.js", "e.js", "./src/e.js"])( + "relative key %j as entry point does not trip the absolute-path assertion", + async key => { + const script = ` + const r = await Bun.build({ + entrypoints: [${JSON.stringify(key)}], + files: { ${JSON.stringify(key)}: 'console.log("from relative key")' }, + target: "bun", + throw: false, + }); + if (!r.success) { + for (const l of r.logs) console.error(l.message ?? l); + process.exit(1); + } + const out = await r.outputs[0].text(); + if (!out.includes("from relative key")) { + console.error("missing content:", out); + process.exit(1); + } + console.log("ok"); + `; + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", script], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout: stdout.trim(), stderr, exitCode }).toEqual({ + stdout: "ok", + stderr: "", + exitCode: 0, + }); + }, + ); + + test.concurrent("relative key as entry point surfaces parse errors without crashing", async () => { + const script = ` + const r = await Bun.build({ + entrypoints: ["./e.js"], + files: { "./e.js": ")" }, + target: "bun", + throw: false, + }); + console.log(JSON.stringify({ success: r.success, logs: r.logs.length })); + `; + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", script], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect({ stdout: stdout.trim(), stderr, exitCode }).toEqual({ + stdout: JSON.stringify({ success: false, logs: 1 }), + stderr: "", + exitCode: 0, + }); + }); });