Skip to content

bake: print CSS chunks with browser targets - #37056

Merged
Jarred-Sumner merged 1 commit into
farm/c2ac6d7c/bake-css-server-import-targetsfrom
farm/77f32ffa/bake-css-print-targets
Aug 6, 2026
Merged

Jarred-Sumner merged 1 commit into
farm/c2ac6d7c/bake-css-server-import-targetsfrom
farm/77f32ffa/bake-css-print-targets

Conversation

@robobun

@robobun robobun commented Aug 6, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #37051 (the diff below is relative to that branch; merge that PR first). The two fix the two halves of the same divergence, and the light-dark() polyfill is only correct with both halves in place, so this one must not land alone.

Problem

Every stylesheet served by the bake dev server or emitted by bun build --app skips the CSS downleveling that a plain bun build index.html applies for the default browser targets.

Repro (index.html linking styles.css):

.a {
  .b {
    color: light-dark(white, black);
  }
}

bun build index.html emits the downleveled form:

.a .b {
  color: var(--buncss-light, #fff) var(--buncss-dark, #000);
}

while bun index.html (dev server) serves the stylesheet raw:

.a {
  & .b {
    color: light-dark(#fff, #000);
  }
}

The default browser targets (chrome87/safari14/firefox78/edge88) support neither CSS nesting nor light-dark(), so the served stylesheet is broken in the browsers those targets claim to support. The same applies to media range syntax: a preserved @import url(...) screen and (width >= 500px) keeps the range syntax in bake builds but is rewritten to (min-width: 500px) in plain browser builds.

Cause

Several CSS transforms are decided at print time against PrinterOptions.targets: nesting compilation (StyleRule::to_css_base), light-dark() reference rewriting and hex-alpha color fallbacks, and media interval syntax downleveling. The linker's four CSS print sites (three arms in generateCompileResultForCssChunk.rs, plus the nested-condition data: URL wrapper in prepareCssAstsForChunk.rs) built those options from Targets::for_bundler_target(c.options.target), and LinkerOptions.target is copied from the bundle's primary transpiler. In bake builds the primary transpiler is the server one, whose target maps to runtime targets (browsers: None), under which every feature counts as supported and none of the print-gated transforms fire.

This is the print-time half of the divergence fixed at minify time by #37051 (vendor prefixing, selector downleveling, and the injected half of the light-dark polyfill), whose description explicitly defers this print-side inconsistency.

Why this is stacked on #37051

The light-dark() polyfill spans both phases: StyleSheet::minify injects the --buncss-light/--buncss-dark definitions and the @media (prefers-color-scheme: dark) toggle rule (gated on the minify targets), and printing rewrites light-dark(a, b) to var(--buncss-light, a) var(--buncss-dark, b) (gated on the print targets). If the phases disagree, the output is a half-polyfill: on this change alone, a server-imported stylesheet with color-scheme: light dark would emit references with no definitions, and var(--buncss-light, #fff) var(--buncss-dark, #000) substitutes to the invalid #fff #000, dropping the declaration in every browser (worse than the raw light-dark(), which at least works in browsers that support it). With #37051 underneath, minify and print resolve targets through the same BundleOptions::css_target() and the full polyfill is emitted. The new polyfill test pins exactly this: it fails on current releases, on #37051 alone, and on this change alone.

Fix

  • LinkerOptions.css_target, populated from BundleOptions::css_target() (from bake: apply browser CSS targets to stylesheets imported on the server #37051) at the existing option-copy site, used by the four CSS print sites. LinkerOptions.target is untouched: JS decisions (postProcessJSChunk, HMR runtime selection) still key off it.
  • Doc comments on css_target() and the new field state the minify/print agreement requirement.

Plain bun build is unchanged: without a framework or dev server, css_target() returns the bundle target, so --target=browser prints as before and --target=bun/--target=node keep runtime targets for both minify and print.

Verification

New tests, one per print site plus the cross-phase polyfill, all failing on the current release and passing with this branch:

  • test/bake/dev/css.test.ts "css chunks are printed with browser targets": dev-server-served CSS has nesting flattened and no light-dark() (the per-source print site).
  • test/bake/dev/css.test.ts "external css imports are printed with browser targets": media range syntax in a preserved external @import condition is downleveled (the external-path arm).
  • test/bake/dev/css.test.ts "nested external css import conditions are printed with browser targets": an external import reached through a conditional internal import is wrapped in a data: URL stylesheet, and the condition inside the wrapper is downleveled (the prepareCssAstsForChunk wrapper site).
  • test/bake/dev/css.test.ts "css layer statements are printed with browser targets": a duplicated conditional layer import leaves a bare @layer foo; statement whose wrapping media condition is downleveled (the layer-statement arm).
  • test/bake/dev/css.test.ts "light-dark() polyfill spans minify and print for server-imported css": with color-scheme: light dark declared, the served CSS contains the variable definitions, the prefers-color-scheme toggle, and the rewritten references. This is the test that requires bake: apply browser CSS targets to stylesheets imported on the server #37051 and this change together.
  • test/bake/dev/production.test.ts "css is printed with browser targets": bun build --app output has nesting flattened and the full light-dark polyfill.

test/bake/dev/css.test.ts (21), test/bake/dev/production.test.ts (13), test/bake/dev/html.test.ts, test/bake/dev/bundle.test.ts, test/bake/dev/sourcemap.test.ts, test/bake/dev-and-prod.test.ts, and the test/bundler/css suite pass with a debug build of this branch.


no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/bake/dev/production.test.ts

@github-actions github-actions Bot added the claude label Aug 6, 2026
@coderabbitai

coderabbitai Bot commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

The bundler now selects a separate CSS target. Bake builds use Target::Browser, and CSS printers receive this target during development and production builds. Tests cover nesting, light-dark(), external imports, and media queries.

Bake CSS target

Layer / File(s) Summary
CSS target selection
src/bundler/options.rs, src/bundler/LinkerContext.rs
Bake builds select Target::Browser. LinkerOptions stores the dedicated CSS target.
CSS target propagation and printing
src/bundler/bundle_v2.rs, src/bundler/linker_context/...
BundleV2 passes the CSS target to linker options. CSS chunk and import printers use it instead of the general target.
Bake CSS behavior validation
test/bake/dev/css.test.ts, test/bake/dev/production.test.ts
Tests verify browser-target transformations for development and production CSS output.

Possibly related PRs

  • oven-sh/bun#37051: Both changes centralize bake-build detection and apply browser-target CSS transformations.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main change: printing bake CSS chunks with browser targets.
Description check ✅ Passed The description explains the problem, cause, fix, dependency on #37051, and verification results in sufficient detail.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/bake/dev/css.test.ts`:
- Around line 741-746: Update the CSS test around the existing css assertions to
verify that the fetched output still contains both the `@import` directive and
https://example.com/x.css before checking media-range conversion. Keep the
current min-width and >= assertions unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c7be8c61-3a0c-4c96-885f-49d06d383cac

📥 Commits

Reviewing files that changed from the base of the PR and between 0ffabf6 and f1aae90.

📒 Files selected for processing (7)
  • src/bundler/LinkerContext.rs
  • src/bundler/bundle_v2.rs
  • src/bundler/linker_context/generateCompileResultForCssChunk.rs
  • src/bundler/linker_context/prepareCssAstsForChunk.rs
  • src/bundler/options.rs
  • test/bake/dev/css.test.ts
  • test/bake/dev/production.test.ts

Comment thread test/bake/dev/css.test.ts
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. bake: apply browser CSS targets to stylesheets imported on the server #37051 - Fixes the same bug (bake CSS compiling against the server target instead of browser targets), adds the identical is_bake_build()/css_target() helpers at the same spot in src/bundler/options.rs, and extends test/bake/dev/css.test.ts + test/bake/dev/production.test.ts at the same anchors — split across the minify (ParseTask.rs) and print (linker) halves.

🤖 Generated with Claude Code

@robobun

robobun commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

Not a duplicate, though the two PRs are deliberately adjacent. #37051 fixes the minify-time half of this divergence: vendor prefixing and selector downleveling are assigned during StyleSheet::minify in ParseTask, and that PR makes the minify targets importer-independent. Its description explicitly defers the print-time half, which is what this PR fixes: nesting compilation, light-dark() and hex-alpha fallbacks, and media range downleveling are decided at print time in the linker's CSS print sites, which read LinkerOptions.target and never saw browser targets in bake builds regardless of #37051's change.

The is_bake_build()/css_target() helpers are intentionally identical to the ones in #37051 and added at the same spot, so whichever PR lands first, the other rebases cleanly (identical hunks merge as one). The linker plumbing, the print-site changes, and the tests (print-gated transforms rather than minify-gated ones) do not overlap. Each PR fixes its half independently; both are needed for bake CSS output to match a plain browser build.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No bugs found. This is a focused, well-tested fix, but it introduces a new is_bake_build() predicate and a css_target split from target — a small design decision (bake CSS always prints for Target::Browser) that a maintainer should sign off on, especially given the stated interaction with #37051's minify-time half.

What was reviewed:

  • Verified css_target() returns self.target for non-bake builds, so plain bun build --target=bun|node CSS output is unchanged.
  • Confirmed all bake transpiler setup paths (DevServer.rs, bake/mod.rs, bake_body.rs) set options.framework, so the predicate fires for both dev server and --app production.
  • Checked the four for_bundler_target print sites are the complete set in the linker; the two remaining call sites (transpiler.rs, ParseTask.rs) are parse/minify-time and intentionally deferred to #37051.
  • The external-@import test's https://example.com URL is preserved as text in the output CSS and never fetched.
Extended reasoning...

Overview

Adds a css_target: Target field to LinkerOptions, populated from a new BundleOptions::css_target() helper that returns Target::Browser when is_bake_build() (framework set or dev server present) and self.target otherwise. The four CSS print sites in generateCompileResultForCssChunk.rs and prepareCssAstsForChunk.rs switch from c.options.target to c.options.css_target. Three new tests cover the dev-server-served CSS chunk, an external @import with a media range condition, and bun build --app output.

Security risks

None. No untrusted-input parsing, no auth/crypto/permission surface. The change only alters which browser feature-set the CSS printer downlevels for.

Level of scrutiny

Medium. It's a bundler output-behavior change, but scoped narrowly: non-bake builds are provably unaffected (css_target() falls through to self.target), and within bake builds the effect is strictly more downleveling — which is the fix. Targets::for_bundler_target already maps Target::Browser → browser_default() and Target::Bun/Node → runtime_default(), so the mechanism is well-understood.

Other factors

  • The is_bake_build() predicate is new reusable surface. It's simple and its two inputs (framework, dev_server) are only set from bake code paths (verified by grep), but a maintainer should confirm this is the right long-term shape versus, say, threading an explicit CSS target from the bake caller.
  • The PR description positions this as the print-time complement to #37051's minify-time fix; a reviewer with context on that PR should confirm the two compose as described.
  • Tests look solid: they assert both the positive (.a .b, min-width) and negative (light-dark(, >=) invariants, and per the description fail on the current release.

@robobun
robobun force-pushed the farm/77f32ffa/bake-css-print-targets branch from f1aae90 to 0d84808 Compare August 6, 2026 13:23
@robobun
robobun changed the base branch from main to farm/c2ac6d7c/bake-css-server-import-targets August 6, 2026 13:23
Comment thread src/bundler/LinkerContext.rs Outdated
Comment thread src/bundler/options.rs Outdated
In bake builds (dev server and bun build --app) the primary transpiler
targets the server, and the linker's CSS print sites built PrinterOptions
from that target. With no browser targets, print-gated CSS transforms
(nesting compilation, light-dark() reference rewriting, hex-alpha
fallbacks, media range syntax downleveling) never fired, so every bake
stylesheet was emitted raw while the same file built via plain
'bun build index.html' was downleveled for the default browser targets.

Add LinkerOptions.css_target, populated from BundleOptions::css_target()
(browser for bake builds, the bundle target otherwise), and use it at the
four CSS print sites. JS decisions keep using LinkerOptions.target.

Built on top of the branch for #37051: the light-dark() polyfill injects
its variable definitions at minify time and rewrites references at print
time, so the minify targets (fixed there) and the print targets (fixed
here) must agree or the output is a half-polyfill that drops the
declaration in every browser.
@robobun
robobun force-pushed the farm/77f32ffa/bake-css-print-targets branch from 0d84808 to 25c6314 Compare August 6, 2026 13:24
Comment thread src/bundler/LinkerContext.rs
Comment thread src/bundler/options.rs
@robobun

robobun commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

Restructured after a deeper review pass found that this change is not independent of #37051 after all: the light-dark() polyfill injects its variable definitions at minify time and rewrites references at print time. Fixing only the print targets produced a half-polyfill for server-imported stylesheets that declare color-scheme (references with no definitions, which computes to an invalid value and drops the declaration in every browser), worse than the raw light-dark() it replaced.

This PR is now based on the branch for #37051, so minify and print resolve CSS targets through the same css_target() helper and the diff here is only the print-side plumbing plus tests. Merge #37051 first.

Also added tests for the two print sites the original three tests did not reach (the data: URL wrapper for nested import conditions and the bare layer-statement path), and a polyfill test with color-scheme declared that fails on either PR alone and passes with both. The PR description has the details.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — small, mechanical print-target plumbing with per-site test coverage.

What was reviewed

  • The four linker CSS print sites (three CssImportOrderKind arms + the nested-condition data: wrapper) now read c.options.css_target; verified no other for_bundler_target(c.options.target) call remains in the linker.
  • LinkerOptions has one Default impl and one population site (bundle_v2.rs) — both updated; non-bake builds are unaffected since css_target() returns self.target.
  • The transpiler.rs:3070 print site was checked and left alone deliberately — it's the single-file Transpiler transform path (minifies with default targets too), not a bake/linker chunk path.
  • The external-@import tests reference https://example.com/x.css but the URL is preserved as an @import in the output and never fetched (hermetic).
Extended reasoning...

Overview

Adds a css_target: Target field to LinkerOptions, populated from BundleOptions::css_target() (introduced in the base PR #37051) at the existing option-copy site in bundle_v2.rs. The four CSS print sites in the linker (generateCompileResultForCssChunk.rs × 3 arms, prepareCssAstsForChunk.rs × 1) switch from c.options.target to c.options.css_target. The css_target() doc comment is extended to note the minify/print agreement invariant. Six new tests exercise each print site plus the cross-phase light-dark() polyfill, in both dev-server and bun build --app modes.

Security risks

None. No parsing of untrusted input, no auth/crypto/permission surface. The change threads an existing enum value through an existing options struct.

Level of scrutiny

Low-to-medium. The native diff is one struct field (Copy enum, defaulted to Target::Browser matching the neighboring target default), one assignment, and four one-token substitutions. The behavioral change is scoped to bake builds only: for every non-bake bundle css_target() returns self.target, so css_target == target and the print sites see the same value they did before. I grepped for all for_bundler_target and LinkerOptions construction/population sites to confirm nothing was missed; the only remaining .options.target-fed CSS printer is in transpiler.rs on the single-file transform path, which is not a bake code path and pairs with MinifyOptions::default() anyway.

Other factors

  • Test coverage is unusually thorough: one test per print-site arm (SourceIndex, ExternalPath, Layers, and the prepareCssAstsForChunk data:-URL wrapper), plus a polyfill test that pins the minify+print agreement, plus a production-build variant. Each asserts the specific downleveled output shape.
  • The external-URL tests do not violate hermeticity — the URL is emitted as a preserved @import string, not fetched.
  • All prior bot comments (CodeRabbit assertion strengthening, comment-cop) are resolved; the retained doc comments are field disambiguation / invariant notes, not workaround excuses.
  • The PR is stacked on #37051 and states it must not land alone; that's a merge-order constraint the author has documented, not a code defect in this diff.

@robobun

robobun commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

CI on 25c6314: 195 of 196 jobs passed. The one red job (debian 13 x64-asan) fails only on test/js/node/worker_threads/worker-transfer-terminate-stress.test.ts, which is failing on main as well (JSC !exception() assertion, SIGABRT) and is unrelated to this diff; it has been reported separately. The remaining annotations are per-lane flakes that passed on retry or alone. All bake and bundler CSS lanes are green.

Ready for review. Merge order: #37051 first, then this.

@Jarred-Sumner
Jarred-Sumner merged commit 775e3da into farm/c2ac6d7c/bake-css-server-import-targets Aug 6, 2026
53 of 54 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/77f32ffa/bake-css-print-targets branch August 6, 2026 22:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants