Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 30 additions & 25 deletions src/runtime/bake/production.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,25 @@ fn write_sourcemap_to_disk(
Ok(())
}

struct OutputDir {
dir: bun_sys::Dir,
failed_writes: usize,
}

impl OutputDir {
fn write(&mut self, file: &OutputFile) {
if let Err(err) = file.write_to_disk(self.dir.fd(), b".") {
bun_core::handle_error_return_trace(err);
Output::err(
err,
"Failed to write {} to output directory",
(bun_core::fmt::quote(&file.dest_path),),
);
self.failed_writes += 1;
}
}
}

fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result<()> {
// `pt.vm` is the live per-thread VM's BackRef set in `build_command`;
// `as_ptr()` is `Copy` and does not borrow `pt`.
Expand Down Expand Up @@ -641,6 +660,10 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result<
return Err(crate::Error::BakeBuildFailed);
}
};
let mut output_dir = OutputDir {
dir: root_dir,
failed_writes: 0,
};

let mut maybe_runtime_file_index: Option<u32> = None;

Expand Down Expand Up @@ -697,25 +720,11 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result<
match side {
bun_bundler::options::Side::Client => {
// Client-side resources will be written to disk for usage on the client side
if let Err(err) = file.write_to_disk(root_dir.fd(), b".") {
bun_core::handle_error_return_trace(err);
Output::err(
err,
"Failed to write {} to output directory",
(bun_core::fmt::quote(&file.dest_path),),
);
}
output_dir.write(file);
}
bun_bundler::options::Side::Server => {
if ctx.bundler_options.bake_debug_dump_server {
if let Err(err) = file.write_to_disk(root_dir.fd(), b".") {
bun_core::handle_error_return_trace(err);
Output::err(
err,
"Failed to write {} to output directory",
(bun_core::fmt::quote(&file.dest_path),),
);
}
output_dir.write(file);
}

// If the file has a sourcemap, store it so we can put it on
Expand Down Expand Up @@ -787,17 +796,13 @@ fn build_with_vm(ctx: Context, cwd: &[u8], pt: &mut PerThread) -> crate::Result<
&& file.src_path.text != b"bun-framework-react/client.tsx"
});
if any_client_chunks {
let runtime_file: &OutputFile = &bundled_outputs_list[runtime_file_index as usize];
if let Err(err) = runtime_file.write_to_disk(root_dir.fd(), b".") {
bun_core::handle_error_return_trace(err);
Output::err(
err,
"Failed to write {} to output directory",
(bun_core::fmt::quote(&runtime_file.dest_path),),
);
}
output_dir.write(&bundled_outputs_list[runtime_file_index as usize]);
}
}
if output_dir.failed_writes > 0 {
// Every failed write has been reported; the pages would only reference the missing files.
return Err(crate::Error::BakeBuildFailed);
}

*pt = PerThread::init(
vm_ptr,
Expand Down
119 changes: 118 additions & 1 deletion test/bake/dev/production.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
import { existsSync, symlinkSync } from "fs";
import { existsSync, mkdirSync, readdirSync, rmSync, symlinkSync } from "fs";
import { bunEnv, bunExe, isWindows, tempDir } from "harness";
import path from "path";
import { tempDirWithBakeDeps, WAIT_MULTIPLIER } from "../bake-harness";
Expand Down Expand Up @@ -742,4 +742,121 @@ export default function IndexPage() {
// Verify NO JavaScript imports are included in the HTML
expect(htmlContent).not.toContain('<script type="module"');
});

describe.concurrent("output files that cannot be written", () => {
const app = {
"src/index.tsx": `
process.on("exit", code => console.log("exit event: " + code));
export default { app: { framework: "react" } };
`,
"pages/index.tsx": `import Client from "../components/Client";

export default function IndexPage() {
return <Client />;
}`,
"components/Client.tsx": `"use client";

export default function Client() {
return "client";
}`,
};

// With --debug-no-minify the files are named after their sources. A build of `app` writes the
// client entry point, the client component and the runtime chunk both of them import into
// dist/_bun; the server files only with --debug-dump-server-files. The page becomes dist/index.html.
const clientEntry = expect.stringMatching(/^bun-framework-react\/client\.\w+\.js$/);
const runtimeChunk = expect.stringMatching(/^bun-framework-react\/server\.\w+\.chunk\.js$/);
const clientComponent = expect.stringMatching(/^components\/Client\.\w+\.js$/);
const serverPage = expect.stringMatching(/^pages\/index\.\w+\.js$/);

async function build(dir: string, ...flags: string[]) {
const { exitCode, stdout, stderr } =
await Bun.$`${bunExe()} build --app ./src/index.tsx --debug-no-minify ${flags}`
.cwd(dir)
.env({ ...bunEnv, BUN_DESTRUCT_VM_ON_EXIT: "1" })
.quiet()
.throws(false);
return {
exitCode,
stdout: stdout.toString(),
// Paths relative to dist/_bun, sorted.
failedWrites: Array.from(
normalizePath(stderr.toString()).matchAll(/Failed to write "_bun\/([^"]+)" to output directory/g),
match => match[1],
).sort(),
prerendered: existsSync(path.join(dir, "dist", "index.html")),
stderr: stderr.toString(),
};
}

// The failure is returned to build_command, which exits through the build VM: the config's
// 'exit' handler runs, and nothing is prerendered.
const failed = { exitCode: 1, stdout: "exit event: 1\n", prerendered: false };

// Every test bundles a react app once or twice; see "failures reported by the build" above.
const timeout = 30_000 * WAIT_MULTIPLIER;

test(
"every failed write is reported",
async () => {
// Files in the way of both directories the outputs go into. (Not a single file at dist/_bun:
// on Windows, creating a directory underneath a file currently never returns.)
const dir = await tempDirWithBakeDeps("bake-production-unwritable-output-dirs", {
...app,
"dist/_bun/bun-framework-react": "a file in the way of the directory",
"dist/_bun/components": "a file in the way of the directory",
});

expect(await build(dir)).toMatchObject({
...failed,
failedWrites: [clientEntry, runtimeChunk, clientComponent],
});
},
timeout,
);

test(
"a client chunk that cannot be written fails the build",
async () => {
const dir = await tempDirWithBakeDeps("bake-production-unwritable-client-chunk", {
...app,
"dist/_bun/components": "a file in the way of the directory",
});

expect(await build(dir)).toMatchObject({ ...failed, failedWrites: [clientComponent] });
},
timeout,
);

test(
"a runtime chunk that cannot be written fails the build",
async () => {
const dir = await tempDirWithBakeDeps("bake-production-unwritable-runtime-chunk", app);
expect(await build(dir)).toMatchObject({ exitCode: 0, failedWrites: [], prerendered: true });

// The hash in the chunk's name is only known from a build that wrote it.
const frameworkDir = path.join(dir, "dist", "_bun", "bun-framework-react");
const chunks = readdirSync(frameworkDir).filter(name => name.endsWith(".chunk.js"));
expect(chunks).toHaveLength(1);
rmSync(path.join(dir, "dist"), { recursive: true });
mkdirSync(path.join(frameworkDir, chunks[0]), { recursive: true });

expect(await build(dir)).toMatchObject({ ...failed, failedWrites: [`bun-framework-react/${chunks[0]}`] });
},
2 * timeout,
);

test(
"a dumped server file that cannot be written fails the build",
async () => {
const dir = await tempDirWithBakeDeps("bake-production-unwritable-server-file", {
...app,
"dist/_bun/pages": "a file in the way of the directory",
});

expect(await build(dir, "--debug-dump-server-files")).toMatchObject({ ...failed, failedWrites: [serverPage] });
},
timeout,
);
});
});
Loading