From 9bec72c087257ed28aea5bc713eb62b28e920f6a Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 1 Aug 2026 10:43:49 +0000 Subject: [PATCH 1/5] bundler: fail the build when --bytecode cannot be generated When JSC's bytecode generator rejects a bundled chunk, bun build --bytecode would log 'error: Failed to generate bytecode for ...' but then proceed to write the executable and exit 0, so CI could not detect the failure. The --outdir (non --compile) path did not even log the error. Return Err(BuildFailed) from both chunk-generation paths after logging. This also avoids a debug-build assertion in OutputFileList::take() that fires when the pre-sized bytecode slot is left unfilled. Fixes #15528 --- .../generateChunksInParallel.rs | 8 ++-- .../linker_context/writeOutputFilesToDisk.rs | 11 +++++ test/bundler/bundler_compile.test.ts | 40 +++++++++++++++++++ 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/bundler/linker_context/generateChunksInParallel.rs b/src/bundler/linker_context/generateChunksInParallel.rs index 6d44301fcbe1..26d4da061bf6 100644 --- a/src/bundler/linker_context/generateChunksInParallel.rs +++ b/src/bundler/linker_context/generateChunksInParallel.rs @@ -1076,13 +1076,11 @@ pub(crate) fn generate_chunks_in_parallel( ..Default::default() })); } else { - // an error - // logger OOM-only // Split-borrow — `static_route_visitor.c` holds a // detached `&LinkerContext`; `log_disjoint` returns the // disjoint `Transpiler.log` backref so no `&mut c` is // materialized. - let _ = c.log_disjoint().add_error_fmt( + c.log_disjoint().add_error_fmt( None, bun_ast::Loc::EMPTY, format_args!( @@ -1090,6 +1088,10 @@ pub(crate) fn generate_chunks_in_parallel( bstr::BStr::new(&chunk.final_rel_path) ), ); + // `output_files` was pre-sized assuming this chunk + // would produce a bytecode entry; bailing here keeps + // the `take()` insertion-count invariant intact. + return Err(crate::Error::BuildFailed); } } } diff --git a/src/bundler/linker_context/writeOutputFilesToDisk.rs b/src/bundler/linker_context/writeOutputFilesToDisk.rs index 4e3d991dde76..4b2c9144c245 100644 --- a/src/bundler/linker_context/writeOutputFilesToDisk.rs +++ b/src/bundler/linker_context/writeOutputFilesToDisk.rs @@ -493,6 +493,17 @@ pub(crate) fn write_output_files_to_disk( source_index: IndexOptional::NONE, bake_extra: BakeExtra::default(), })); + } else { + // `log_disjoint`: split-borrow with `parse_graph` above. + c.log_disjoint().add_error_fmt( + None, + Loc::EMPTY, + format_args!( + "Failed to generate bytecode for {}", + bstr::BStr::new(&chunk.final_rel_path) + ), + ); + return Err(crate::Error::BuildFailed); } } } diff --git a/test/bundler/bundler_compile.test.ts b/test/bundler/bundler_compile.test.ts index cdc71b4a9eeb..cb3ff10302ab 100644 --- a/test/bundler/bundler_compile.test.ts +++ b/test/bundler/bundler_compile.test.ts @@ -137,6 +137,46 @@ describe("bundler", () => { }, }, }); + // https://github.com/oven-sh/bun/issues/15528 + // When JSC rejects the bundled output (forced here by prepending a banner + // that JSC's parser cannot parse; bun's own parser never sees banner text), + // the build must exit non-zero instead of printing the error and then + // reporting success. Covers both the --compile path and the --outdir path + // (the latter previously swallowed the failure entirely). + for (const [name, extraArgs] of [ + ["compile/BytecodeFailureIsAnError", ["--compile", "--outfile", "./out"]], + ["bytecode/OutdirBytecodeFailureIsAnError", ["--format=cjs", "--outdir", "./out"]], + ] as const) { + test(name, async () => { + using dir = tempDir("bytecode-failure", { + "entry.ts": `console.log("Hello, world!");`, + }); + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "build", + "./entry.ts", + "--bytecode", + "--target=bun", + "--banner", + "this is not valid js !!!", + ...extraArgs, + ], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([ + proc.stdout.text(), + proc.stderr.text(), + proc.exited, + ]); + expect(stderr).toContain("error: Failed to generate bytecode"); + expect(stdout).not.toContain("compile"); + expect(exitCode).toBe(1); + }); + } // `import defer * as ns from "..."` must not break bytecode generation. // The bundler inlines the deferred module into the entry chunk (documented From cfc2f197ad58390c1e123df35eac079a6914e6c9 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 1 Aug 2026 10:48:35 +0000 Subject: [PATCH 2/5] [autofix.ci] apply automated fixes --- test/bundler/bundler_compile.test.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/bundler/bundler_compile.test.ts b/test/bundler/bundler_compile.test.ts index cb3ff10302ab..469057d21626 100644 --- a/test/bundler/bundler_compile.test.ts +++ b/test/bundler/bundler_compile.test.ts @@ -167,11 +167,7 @@ describe("bundler", () => { stdout: "pipe", stderr: "pipe", }); - const [stdout, stderr, exitCode] = await Promise.all([ - proc.stdout.text(), - proc.stderr.text(), - proc.exited, - ]); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); expect(stderr).toContain("error: Failed to generate bytecode"); expect(stdout).not.toContain("compile"); expect(exitCode).toBe(1); From 35056f271b1db85dcc2ad73bab8c86e90f7b1e28 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 1 Aug 2026 10:50:29 +0000 Subject: [PATCH 3/5] drop redundant comment on bytecode-fail return --- src/bundler/linker_context/generateChunksInParallel.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/bundler/linker_context/generateChunksInParallel.rs b/src/bundler/linker_context/generateChunksInParallel.rs index 26d4da061bf6..ef989a53bd1c 100644 --- a/src/bundler/linker_context/generateChunksInParallel.rs +++ b/src/bundler/linker_context/generateChunksInParallel.rs @@ -1088,9 +1088,6 @@ pub(crate) fn generate_chunks_in_parallel( bstr::BStr::new(&chunk.final_rel_path) ), ); - // `output_files` was pre-sized assuming this chunk - // would produce a bytecode entry; bailing here keeps - // the `take()` insertion-count invariant intact. return Err(crate::Error::BuildFailed); } } From 2d79296fc994585d027e0768b8b688ed9e21e312 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 1 Aug 2026 11:07:45 +0000 Subject: [PATCH 4/5] guard --outdir bytecode on chunk.content to skip CSS chunks A CSS chunk created from a JS entry has a JS-like loader, so the --outdir bytecode path was attempting (and, after the previous commit, failing) bytecode generation on CSS text. Match the sibling guard used by the --compile path and the OutputFileList pre-sizing. --- .../linker_context/writeOutputFilesToDisk.rs | 2 +- test/bundler/bundler_compile.test.ts | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/bundler/linker_context/writeOutputFilesToDisk.rs b/src/bundler/linker_context/writeOutputFilesToDisk.rs index 4b2c9144c245..2a932ab7bb3d 100644 --- a/src/bundler/linker_context/writeOutputFilesToDisk.rs +++ b/src/bundler/linker_context/writeOutputFilesToDisk.rs @@ -398,7 +398,7 @@ pub(crate) fn write_output_files_to_disk( Loader::Js }; - if loader.is_javascript_like() { + if matches!(chunk.content, Content::Javascript(_)) && loader.is_javascript_like() { let mut fdpath = PathBuffer::uninit(); let source_provider_url = BunString::create_format(format_args!( "{}{}", diff --git a/test/bundler/bundler_compile.test.ts b/test/bundler/bundler_compile.test.ts index 469057d21626..0da54ca9e795 100644 --- a/test/bundler/bundler_compile.test.ts +++ b/test/bundler/bundler_compile.test.ts @@ -173,6 +173,26 @@ describe("bundler", () => { expect(exitCode).toBe(1); }); } + // A CSS chunk created from a JS entry point has a JS-like loader. Bytecode + // must be skipped for it, not attempted and reported as a failure. + test("bytecode/OutdirBytecodeSkipsCssChunk", async () => { + using dir = tempDir("bytecode-css", { + "entry.ts": `import "./styles.css"; console.log(1);`, + "styles.css": `.foo { color: red }`, + }); + await using proc = Bun.spawn({ + cmd: [bunExe(), "build", "./entry.ts", "--bytecode", "--target=bun", "--format=cjs", "--outdir", "./out"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).not.toContain("error"); + expect(stdout).toContain("entry.js.jsc"); + expect(stdout).toContain("entry.css"); + expect(exitCode).toBe(0); + }); // `import defer * as ns from "..."` must not break bytecode generation. // The bundler inlines the deferred module into the entry chunk (documented From b06839f7fc5419eb94a871c51a291eb108b2930f Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 1 Aug 2026 12:32:15 +0000 Subject: [PATCH 5/5] ci: retrigger