Skip to content
Merged
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
26 changes: 20 additions & 6 deletions src/bundler/bundle_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5720,19 +5720,33 @@ pub mod bv2_impl {
// `graph.ast.items(.import_records)[importer_source_index]` when
// they complete. Without this, the graph entry stays at
// JSAst.empty and the deferred plugin callback index-out-of-
// bounds crashes in BundleV2.onResolve / runResolver. The linker
// never runs because `transpiler.log.errors > 0` aborts the
// build before link time, so saving the AST is safe.
// bounds crashes in BundleV2.onResolve / runResolver. In a
// non-dev build the linker never runs after this error
// (`transpiler.log.errors > 0` aborts before link time). The
// dev server does link with failed files, but it filters them
// out by their empty `parts` list and never reads a failed
// file's import_records, so saving them is safe — unlike the
// `css` slot below.
let result_heap = *result.ast.import_records.allocator();
this.graph.ast.items_import_records_mut()[source_index.0 as usize] =
core::mem::replace(
&mut result.ast.import_records,
bun_alloc::ArenaVec::new_in(result_heap),
);

// Move the CSS stylesheet onto the graph row so teardown can find
// and drop it — the `Success` arm that would normally do this is skipped.
this.graph.ast.items_css_mut()[source_index.0 as usize] = result.ast.css.take();
// Drop the parsed stylesheet now — the `Success` arm that would
// normally move it onto the graph row is skipped. It must not be
// parked on the graph row either: the dev server proceeds with
// failed files and treats a populated `css` slot as a
// successfully parsed CSS file (CSS entry point discovery and
// import ordering in `finish_from_bake_dev_server`), so a parked
// stylesheet would produce a CSS chunk for a failed file while
// `graph.css_file_count` stays 0.
Comment thread
robobun marked this conversation as resolved.
if let Some(css_ref) = result.ast.css.take() {
// SAFETY: live arena pointer, uniquely owned here (the graph
// row for this file stays `None`); dropped exactly once.
unsafe { core::ptr::drop_in_place(css_ref.as_ptr()) };
}
Comment thread
robobun marked this conversation as resolved.

parse_result.value = parse_task::ResultValue::Err(parse_task::ResultError {
err,
Expand Down
47 changes: 47 additions & 0 deletions test/bake/dev/css.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,53 @@ devTest("syntax error crash", {
expect((await dev.fetch("/")).status).toBe(500);
},
});
devTest("css url resolve error on hot reload is recoverable", {
files: {
"styles.css": `
body {
color: red;
}
`,
"index.html": emptyHtmlFile({
styles: ["styles.css"],
body: `hello world`,
}),
},
async test(dev) {
{
await using c = await dev.client("/");
await c.style("body").color.expect.toBe("red");
// A CSS file that parses but fails import resolution must fail the
// rebuild with an error instead of being treated as a valid CSS chunk.
// previously: panic: assertion failed: !chunk.content.is_css()
await dev.write(
"styles.css",
`
body {
background-image: url(./missing.png);
}
`,
{
errors: ['styles.css:2:21: error: Could not resolve: "./missing.png"'],
},
);
expect((await dev.fetch("/")).status).toBe(500);
}
// Recovery is checked without a connected client: when a failed CSS root
// recovers, the patch currently ships the HTML route as a JS module
// without the route-reload flag, which trips a client-side debug assert
// (tracked in https://github.com/oven-sh/bun/issues/31908).
await dev.write(
"styles.css",
`
body {
color: blue;
}
`,
);
expect((await dev.fetch("/")).status).toBe(200);
},
});
devTest("circular css imports handle hot reload", {
files: {
"index.html": emptyHtmlFile({
Expand Down
Loading