From c08b2d7b4ce3577592a4d9b61852af1084b0b0e4 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 16 Aug 2026 04:38:48 +0000 Subject: [PATCH 1/4] bake: report the output directory bun build --app cannot open When dist/ cannot be opened or created, build_with_vm propagated the bun_sys error with ? and the CLI printed it as "An internal error occurred (ENOTDIR)" (or the generic ENOENT line), without the path. Print the errno and the output directory's path like the other user-facing failures in this function, then exit 1. --- src/runtime/bake/production.rs | 12 +++++++++- test/bake/dev/production.test.ts | 41 ++++++++++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/src/runtime/bake/production.rs b/src/runtime/bake/production.rs index de67f02ea5ee..cacd4f072681 100644 --- a/src/runtime/bake/production.rs +++ b/src/runtime/bake/production.rs @@ -646,7 +646,17 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result< Output::flush(); // mkdir -p + open. - let root_dir = bun_sys::Dir::cwd().make_open_path(b"dist", Default::default())?; + let root_dir = match bun_sys::Dir::cwd().make_open_path(b"dist", Default::default()) { + Ok(dir) => dir, + Err(err) => { + Output::err( + err, + "could not open output directory {}", + (bun_core::fmt::quote(&root_dir_path),), + ); + Global::crash(); + } + }; let mut maybe_runtime_file_index: Option = None; diff --git a/test/bake/dev/production.test.ts b/test/bake/dev/production.test.ts index 994f8d412240..7993751a7c6a 100644 --- a/test/bake/dev/production.test.ts +++ b/test/bake/dev/production.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; -import { existsSync } from "fs"; -import { bunEnv, bunExe } from "harness"; +import { existsSync, symlinkSync } from "fs"; +import { bunEnv, bunExe, isWindows } from "harness"; import path from "path"; import { tempDirWithBakeDeps } from "../bake-harness"; @@ -394,6 +394,43 @@ export default function Docs() { } }); + describe.concurrent("output directory that cannot be opened", () => { + const app = { + "src/index.tsx": `export default { app: { framework: "react" } };`, + "pages/index.tsx": `export default function IndexPage() { return

index

; }`, + }; + + async function build(dir: string) { + const { exitCode, stderr } = await Bun.$`${bunExe()} build --app ./src/index.tsx` + .cwd(dir) + .env(bunEnv) + .throws(false); + return { exitCode, stderr: stderr.toString() }; + } + + test("a file at dist is reported with its path", async () => { + const dir = await tempDirWithBakeDeps("bake-production-dist-is-a-file", { + ...app, + "dist": "a file in the way of the output directory", + }); + + const { exitCode, stderr } = await build(dir); + expect(stderr).toContain(`ENOTDIR: Not a directory: could not open output directory "${path.join(dir, "dist")}"`); + expect(stderr).not.toContain("An internal error occurred"); + expect(exitCode).toBe(1); + }); + + test.skipIf(isWindows)("a dangling symlink at dist is reported with its path", async () => { + const dir = await tempDirWithBakeDeps("bake-production-dist-is-a-dangling-symlink", app); + symlinkSync("does-not-exist", path.join(dir, "dist")); + + const { exitCode, stderr } = await build(dir); + expect(stderr).toContain(`could not open output directory "${path.join(dir, "dist")}"`); + expect(stderr).not.toContain("missing a better error"); + expect(exitCode).toBe(1); + }); + }); + test("client-side component with default import should work", async () => { const dir = await tempDirWithBakeDeps("bake-production-client-import", { "src/index.tsx": `export default { app: { framework: "react" } };`, From 0773af5bcc0c77e38f1e65e79922996589fd88e7 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 16 Aug 2026 05:02:24 +0000 Subject: [PATCH 2/4] bake: exit through the build VM when the output directory cannot be opened build_with_vm reports the failure and returns Error::BakeBuildFailed; build_command sets the exit code and leaves through on_exit and global_exit, the same way it already does for a build that throws, instead of exiting in place from build_with_vm. --- src/runtime/bake/production.rs | 37 ++++++++-------- src/runtime/error.rs | 5 +++ test/bake/dev/production.test.ts | 74 +++++++++++++++++++++----------- 3 files changed, 71 insertions(+), 45 deletions(-) diff --git a/src/runtime/bake/production.rs b/src/runtime/bake/production.rs index cacd4f072681..dd54476d238e 100644 --- a/src/runtime/bake/production.rs +++ b/src/runtime/bake/production.rs @@ -116,7 +116,7 @@ pub fn build_command(ctx: Context) -> crate::Result<()> { // Note: pass `vm_ptr` by value into the guard so the drop closure does // not borrow the local (`defer!` would capture `&vm_ptr`, which under // edition-2024 disjoint-capture rules collides with the `&mut *vm_ptr` - // re-borrows on the JSError path). + // re-borrows below). let _vm_guard = scopeguard::guard(vm_ptr, |p| { // SAFETY: p is the unique live VM on this thread; its loop is alive, so // queued work is released here rather than by a thread teardown. @@ -211,28 +211,25 @@ pub fn build_command(ctx: Context) -> crate::Result<()> { // LIFO order — under the API lock, before the VM is destroyed. let mut pt = PerThread::placeholder(vm_ptr); - match build_with_vm(ctx, &cwd, &mut pt) { - Ok(()) => {} + let result = build_with_vm(ctx, &cwd, &mut pt); + // SAFETY: `build_with_vm` has returned, so its reborrows through `pt.vm` are + // dead; the VM is live until `global_exit` (or `_vm_guard`) destroys it. + let vm = unsafe { &mut *vm_ptr }; + match result { + Ok(()) => return Ok(()), Err(crate::Error::JSError) => { - // SAFETY: vm.global is live for VM lifetime. - let global = unsafe { &*(*vm_ptr).global }; - let err_value = global.take_exception(jsc::JsError::Thrown); - // SAFETY: see above. - unsafe { - (*vm_ptr) - .print_error_like_object_to_console(err_value.to_error().unwrap_or(err_value)) - }; - // SAFETY: see above. - let vm = unsafe { &mut *vm_ptr }; - if vm.exit_handler.exit_code == 0 { - vm.exit_handler.exit_code = 1; - } - vm.on_exit(); - vm.global_exit(); + let err_value = vm.global().take_exception(jsc::JsError::Thrown); + vm.print_error_like_object_to_console(err_value.to_error().unwrap_or(err_value)); } + // Already reported by `build_with_vm`. + Err(crate::Error::BakeBuildFailed) => {} Err(e) => return Err(e), } - Ok(()) + if vm.exit_handler.exit_code == 0 { + vm.exit_handler.exit_code = 1; + } + vm.on_exit(); + vm.global_exit() } /// Ported inline from `bun.bun_js.failWithBuildError` to avoid the @@ -654,7 +651,7 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result< "could not open output directory {}", (bun_core::fmt::quote(&root_dir_path),), ); - Global::crash(); + return Err(crate::Error::BakeBuildFailed); } }; diff --git a/src/runtime/error.rs b/src/runtime/error.rs index f68f3158b592..2cd932f1fe4c 100644 --- a/src/runtime/error.rs +++ b/src/runtime/error.rs @@ -218,6 +218,10 @@ pub enum Error { MissingPackageJSON, #[error("InstallFailed")] InstallFailed, + /// `bun build --app` failed and has already printed why; `bake::production::build_command` + /// exits 1 through the build VM. + #[error("BakeBuildFailed")] + BakeBuildFailed, #[error("InvalidPackageJSON")] InvalidPackageJSON, #[error("HTTPForbidden")] @@ -679,6 +683,7 @@ impl Error { Self::FailedToBindPipe => "FailedToBindPipe", Self::MissingPackageJSON => "MissingPackageJSON", Self::InstallFailed => "InstallFailed", + Self::BakeBuildFailed => "BakeBuildFailed", Self::InvalidPackageJSON => "InvalidPackageJSON", Self::HTTPForbidden => "HTTPForbidden", Self::ExampleNotFound => "ExampleNotFound", diff --git a/test/bake/dev/production.test.ts b/test/bake/dev/production.test.ts index 7993751a7c6a..ffdd2eedbc4c 100644 --- a/test/bake/dev/production.test.ts +++ b/test/bake/dev/production.test.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"; import { existsSync, symlinkSync } from "fs"; import { bunEnv, bunExe, isWindows } from "harness"; import path from "path"; -import { tempDirWithBakeDeps } from "../bake-harness"; +import { tempDirWithBakeDeps, WAIT_MULTIPLIER } from "../bake-harness"; const normalizePath = (path: string) => (process.platform === "win32" ? path.replaceAll("\\", "/") : path); const platformPath = (path: string) => (process.platform === "win32" ? path.replaceAll("/", "\\") : path); @@ -394,41 +394,65 @@ export default function Docs() { } }); + // The failure is reported with the directory's path and, like a build whose page throws, the + // process exits through the build VM: the config's 'exit' handler runs and, with + // BUN_DESTRUCT_VM_ON_EXIT set, the VM is torn down before the exit code is returned. describe.concurrent("output directory that cannot be opened", () => { const app = { - "src/index.tsx": `export default { app: { framework: "react" } };`, + "src/index.tsx": ` + process.on("exit", code => console.log("exit event: " + code)); + export default { app: { framework: "react" } }; + `, "pages/index.tsx": `export default function IndexPage() { return

index

; }`, }; async function build(dir: string) { - const { exitCode, stderr } = await Bun.$`${bunExe()} build --app ./src/index.tsx` + const { exitCode, stdout, stderr } = await Bun.$`${bunExe()} build --app ./src/index.tsx` .cwd(dir) - .env(bunEnv) + .env({ ...bunEnv, BUN_DESTRUCT_VM_ON_EXIT: "1" }) + .quiet() .throws(false); - return { exitCode, stderr: stderr.toString() }; + return { exitCode, stdout: stdout.toString(), stderr: stderr.toString() }; } - test("a file at dist is reported with its path", async () => { - const dir = await tempDirWithBakeDeps("bake-production-dist-is-a-file", { - ...app, - "dist": "a file in the way of the output directory", - }); - - const { exitCode, stderr } = await build(dir); - expect(stderr).toContain(`ENOTDIR: Not a directory: could not open output directory "${path.join(dir, "dist")}"`); - expect(stderr).not.toContain("An internal error occurred"); - expect(exitCode).toBe(1); - }); - - test.skipIf(isWindows)("a dangling symlink at dist is reported with its path", async () => { - const dir = await tempDirWithBakeDeps("bake-production-dist-is-a-dangling-symlink", app); - symlinkSync("does-not-exist", path.join(dir, "dist")); + // Each case bundles a react app, which on a debug build takes most of the default timeout; + // this is the budget bake-harness gives its production builds. + const timeout = 30_000 * WAIT_MULTIPLIER; + + test( + "a file at dist", + async () => { + const dir = await tempDirWithBakeDeps("bake-production-dist-is-a-file", { + ...app, + "dist": "a file in the way of the output directory", + }); + + const { exitCode, stdout, stderr } = await build(dir); + expect(stderr).toContain( + `ENOTDIR: Not a directory: could not open output directory "${path.join(dir, "dist")}"`, + ); + expect(stderr).not.toContain("An internal error occurred"); + expect(stdout).toBe("exit event: 1\n"); + expect(exitCode).toBe(1); + }, + timeout, + ); - const { exitCode, stderr } = await build(dir); - expect(stderr).toContain(`could not open output directory "${path.join(dir, "dist")}"`); - expect(stderr).not.toContain("missing a better error"); - expect(exitCode).toBe(1); - }); + test.skipIf(isWindows)( + "a dangling symlink at dist", + async () => { + const dir = await tempDirWithBakeDeps("bake-production-dist-is-a-dangling-symlink", app); + symlinkSync("does-not-exist", path.join(dir, "dist")); + + const { exitCode, stdout, stderr } = await build(dir); + expect(stderr).toContain( + `ENOENT: No such file or directory: could not open output directory "${path.join(dir, "dist")}"`, + ); + expect(stdout).toBe("exit event: 1\n"); + expect(exitCode).toBe(1); + }, + timeout, + ); }); test("client-side component with default import should work", async () => { From 2948cb2359cc44d8a7e76bbd5003704c80e747f9 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 16 Aug 2026 05:03:59 +0000 Subject: [PATCH 3/4] bake: shorten the BakeBuildFailed doc comment --- src/runtime/error.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/runtime/error.rs b/src/runtime/error.rs index 2cd932f1fe4c..a9b9733b0d26 100644 --- a/src/runtime/error.rs +++ b/src/runtime/error.rs @@ -218,8 +218,7 @@ pub enum Error { MissingPackageJSON, #[error("InstallFailed")] InstallFailed, - /// `bun build --app` failed and has already printed why; `bake::production::build_command` - /// exits 1 through the build VM. + /// Already reported; `bake::production::build_command` exits 1 through the build VM. #[error("BakeBuildFailed")] BakeBuildFailed, #[error("InvalidPackageJSON")] From e0627170e8d083435bd686315646e6a42799455b Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sun, 16 Aug 2026 05:55:10 +0000 Subject: [PATCH 4/4] bake: return every failure the production build reports instead of exiting in place Every site in production.rs that printed a message and then exited now returns Error::BakeBuildFailed (or JSError for a stopped microtask drain). build_command turns the ones raised while the build VM is up into an exit through the VM; the ones raised before that reach handle_root_error, which treats BakeBuildFailed like InstallFailed and only exits 1. fail_with_build_error is gone: the configure_defines errors are in ctx.log, which Cli::start prints. --- src/crash_handler/lib.rs | 2 +- src/runtime/bake/production.rs | 49 ++++++++++++------------------- src/runtime/error.rs | 2 +- test/bake/dev/production.test.ts | 50 +++++++++++++++++++++++--------- 4 files changed, 56 insertions(+), 47 deletions(-) diff --git a/src/crash_handler/lib.rs b/src/crash_handler/lib.rs index a5cb8c8966ea..8f2abea8b242 100644 --- a/src/crash_handler/lib.rs +++ b/src/crash_handler/lib.rs @@ -1275,7 +1275,7 @@ mod draft { super::out_of_memory(); } else if matches!( name, - b"InvalidArgument" | b"Invalid Bunfig" | b"InstallFailed" + b"InvalidArgument" | b"Invalid Bunfig" | b"InstallFailed" | b"BakeBuildFailed" ) { if !show_trace { Global::exit(1); diff --git a/src/runtime/bake/production.rs b/src/runtime/bake/production.rs index dd54476d238e..f5ba9b01cc3e 100644 --- a/src/runtime/bake/production.rs +++ b/src/runtime/bake/production.rs @@ -20,8 +20,8 @@ use bun_bundler::options::{self as bundler_options, OutputFile, SourceMapOption} use bun_bundler::output_file::Index as OutputFileIndex; use bun_collections::{AutoBitSet, StringArrayHashMap}; +use bun_core::Output; use bun_core::String as BunString; -use bun_core::{Global, Output}; use bun_dotenv as dotenv; use bun_jsc::js_promise::{UnwrapMode, Unwrapped}; use bun_jsc::virtual_machine::VirtualMachine; @@ -75,12 +75,12 @@ pub fn build_command(ctx: Context) -> crate::Result<()> { if ctx.args.entry_points.len() > 1 { bun_core::err_generic!("bun build --app only accepts one entrypoint"); - Global::crash(); + return Err(crate::Error::BakeBuildFailed); } if ctx.debug.hot_reload != HotReload::None { bun_core::err_generic!("Instead of using --watch, use 'bun run'"); - Global::crash(); + return Err(crate::Error::BakeBuildFailed); } let mut cwd_buf = PathBuffer::uninit(); @@ -88,7 +88,7 @@ pub fn build_command(ctx: Context) -> crate::Result<()> { Ok(cwd) => cwd.as_bytes(), Err(err) => { Output::err(err, "Could not query current working directory", ()); - Global::crash(); + return Err(crate::Error::BakeBuildFailed); } }; // Note: reshaped for borrowck — clone the cwd slice so the PathBuffer @@ -187,9 +187,10 @@ pub fn build_command(ctx: Context) -> crate::Result<()> { } MacroOptions::Unspecified => {} } - if vm.transpiler.configure_defines().is_err() { - fail_with_build_error(vm); - } + // The errors are in `ctx.log`, which `Cli::start` prints. + vm.transpiler + .configure_defines() + .map_err(|_| crate::Error::BakeBuildFailed)?; // `vm.log` was set from `ctx.log` above (non-null, process-lifetime); // `log_mut()` is the safe accessor encapsulating the NonNull deref. bun_http::async_http::load_env(vm.log_mut().unwrap(), vm.env_loader()); @@ -232,20 +233,6 @@ pub fn build_command(ctx: Context) -> crate::Result<()> { vm.global_exit() } -/// Ported inline from `bun.bun_js.failWithBuildError` to avoid the -/// `bun_runtime → bun (binary)` dep cycle (PORTING.md §Forbidden: dep-cycle -/// fixes via fn-ptr hooks — move/port the code instead). -#[cold] -#[inline(never)] -fn fail_with_build_error(vm: &mut VirtualMachine) -> ! { - // `vm.log` is the process-lifetime ctx.log set in build_command; - // `log_ref()` is the safe accessor encapsulating the NonNull deref. - if let Some(log) = vm.log_ref() { - let _ = log.print(std::ptr::from_mut(Output::error_writer())); - } - Global::exit(1); -} - fn write_sourcemap_to_disk( file: &OutputFile, bundled_outputs: &[OutputFile], @@ -316,7 +303,7 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result< TODO: insert a link to `bun.com/docs`", (), ); - Global::crash(); + return Err(crate::Error::BakeBuildFailed); } } @@ -325,7 +312,7 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result< "could not resolve application config file '{}'", (BStr::new(&unresolved_config_entry_point),), ); - Global::crash(); + return Err(crate::Error::BakeBuildFailed); } }; @@ -514,7 +501,7 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result< let _ = server_transpiler .log() .print(std::ptr::from_mut(Output::error_writer())); - Global::crash(); + return Err(crate::Error::BakeBuildFailed); } }; @@ -869,7 +856,7 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result< pt.input_file(server_file).abs_path() )) ); - Global::crash(); + return Err(crate::Error::BakeBuildFailed); }; let server_param_func = if router.dynamic_routes.count() > 0 { @@ -895,7 +882,7 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result< pt.input_file(server_file).abs_path() )) ); - Global::crash(); + return Err(crate::Error::BakeBuildFailed); } } } else { @@ -1204,8 +1191,7 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result< Ok(()) } -/// unsafe function, must be run outside of the event loop -/// quits the process on exception +/// Must be run outside of the event loop; failures are returned as `JSError`. fn load_module( vm: *mut VirtualMachine, global: &JSGlobalObject, @@ -1232,9 +1218,10 @@ fn load_module( // TODO: Specially draining microtasks here because `waitForPromise` has a // bug which forgets to do it, but I don't want to fix it right now as it // could affect a lot of the codebase. This should be removed. - if vm_ref.event_loop_mut().drain_microtasks().is_err() { - Global::crash(); - } + vm_ref + .event_loop_mut() + .drain_microtasks() + .map_err(|stopped| js_err(stopped.throw(vm_ref.global())))?; let jsc_vm = vm_ref.as_mut().jsc_vm_mut(); match jsc::JSInternalPromise::opaque_mut(promise).unwrap(jsc_vm, UnwrapMode::MarkHandled) { Unwrapped::Pending => unreachable!(), diff --git a/src/runtime/error.rs b/src/runtime/error.rs index a9b9733b0d26..4f7bf6bc6d20 100644 --- a/src/runtime/error.rs +++ b/src/runtime/error.rs @@ -218,7 +218,7 @@ pub enum Error { MissingPackageJSON, #[error("InstallFailed")] InstallFailed, - /// Already reported; `bake::production::build_command` exits 1 through the build VM. + /// `bun build --app` already reported the failure; like `InstallFailed`, the handler only exits 1. #[error("BakeBuildFailed")] BakeBuildFailed, #[error("InvalidPackageJSON")] diff --git a/test/bake/dev/production.test.ts b/test/bake/dev/production.test.ts index ffdd2eedbc4c..698f840caa38 100644 --- a/test/bake/dev/production.test.ts +++ b/test/bake/dev/production.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "bun:test"; import { existsSync, symlinkSync } from "fs"; -import { bunEnv, bunExe, isWindows } from "harness"; +import { bunEnv, bunExe, isWindows, tempDir } from "harness"; import path from "path"; import { tempDirWithBakeDeps, WAIT_MULTIPLIER } from "../bake-harness"; @@ -394,20 +394,22 @@ export default function Docs() { } }); - // The failure is reported with the directory's path and, like a build whose page throws, the - // process exits through the build VM: the config's 'exit' handler runs and, with - // BUN_DESTRUCT_VM_ON_EXIT set, the VM is torn down before the exit code is returned. - describe.concurrent("output directory that cannot be opened", () => { + // A failure the build reports itself is returned rather than exited on: once the config has been + // loaded, build_command exits through the build VM like it does for a build that throws (the + // config's 'exit' handler runs; with BUN_DESTRUCT_VM_ON_EXIT set the VM is torn down first), and + // before that the error goes back to the CLI, which must not print it as an internal error. + describe.concurrent("failures reported by the build", () => { + const config = ` + process.on("exit", code => console.log("exit event: " + code)); + export default { app: { framework: "react" } }; + `; const app = { - "src/index.tsx": ` - process.on("exit", code => console.log("exit event: " + code)); - export default { app: { framework: "react" } }; - `, + "src/index.tsx": config, "pages/index.tsx": `export default function IndexPage() { return

index

; }`, }; - async function build(dir: string) { - const { exitCode, stdout, stderr } = await Bun.$`${bunExe()} build --app ./src/index.tsx` + async function build(dir: string, ...entryPoints: string[]) { + const { exitCode, stdout, stderr } = await Bun.$`${bunExe()} build --app ${entryPoints}` .cwd(dir) .env({ ...bunEnv, BUN_DESTRUCT_VM_ON_EXIT: "1" }) .quiet() @@ -415,7 +417,7 @@ export default function Docs() { return { exitCode, stdout: stdout.toString(), stderr: stderr.toString() }; } - // Each case bundles a react app, which on a debug build takes most of the default timeout; + // The dist cases bundle a react app, which on a debug build takes most of the default timeout; // this is the budget bake-harness gives its production builds. const timeout = 30_000 * WAIT_MULTIPLIER; @@ -427,7 +429,7 @@ export default function Docs() { "dist": "a file in the way of the output directory", }); - const { exitCode, stdout, stderr } = await build(dir); + const { exitCode, stdout, stderr } = await build(dir, "./src/index.tsx"); expect(stderr).toContain( `ENOTDIR: Not a directory: could not open output directory "${path.join(dir, "dist")}"`, ); @@ -444,7 +446,7 @@ export default function Docs() { const dir = await tempDirWithBakeDeps("bake-production-dist-is-a-dangling-symlink", app); symlinkSync("does-not-exist", path.join(dir, "dist")); - const { exitCode, stdout, stderr } = await build(dir); + const { exitCode, stdout, stderr } = await build(dir, "./src/index.tsx"); expect(stderr).toContain( `ENOENT: No such file or directory: could not open output directory "${path.join(dir, "dist")}"`, ); @@ -453,6 +455,26 @@ export default function Docs() { }, timeout, ); + + test("framework imports that do not resolve", async () => { + // No react packages are installed here. + using dir = tempDir("bake-production-framework-unresolved", { "app.ts": config }); + + const { exitCode, stdout, stderr } = await build(String(dir), "./app.ts"); + expect(stderr).toContain("error: Failed to resolve all imports required by the framework"); + expect(stdout).toBe("exit event: 1\n"); + expect(exitCode).toBe(1); + }); + + test("more than one entry point", async () => { + using dir = tempDir("bake-production-two-entry-points", { "app.ts": config, "other.ts": config }); + + const { exitCode, stdout, stderr } = await build(String(dir), "./app.ts", "./other.ts"); + expect(stderr).toContain("error: bun build --app only accepts one entrypoint"); + expect(stderr).not.toContain("BakeBuildFailed"); + expect(stdout).toBe(""); + expect(exitCode).toBe(1); + }); }); test("client-side component with default import should work", async () => {