Repository navigation
css: consolidate the open CSS parser fixes - #47025
Merged
Merged
Conversation
…` hint only at the midpoint (#46790)
…back as the same name (#46606)
…ate the vendor prefix table from one caniuse-lite copy (#45945)
…t after ::selection (#45815)
… an unmatched value as written (#45806)
…ix in the border-radius handler (#45665)
…sted declarations rule (#45462)
…sk-box-image value (#45143)
… rules, and never print a bare `:full-screen` (#43339)
robobun
added a commit
that referenced
this pull request
Oct 27, 2026
#47025 prints the vendor prefix on a downleveled `:autofill`, so the vendor prefix section no longer lists it as missing.
This was referenced Oct 27, 2026
Open
robobun
added a commit
that referenced
this pull request
Oct 28, 2026
… names a channel
A relative rgb() or hsl() whose alpha holds var() or env() is printed
with computed channels and without its origin:
rgb(from #336699 r g b / var(--o)) -> rgb(51 102 153/var(--o))
A channel keyword in that alpha has a meaning only after `from`. The
printed value is then invalid at computed-value time once the variable
has a value, and an earlier declaration of the property does not apply:
:root { --m: .3 }
rgb(from teal r g b / calc(alpha * var(--m)))
-> rgb(0 128 128/calc(alpha*var(--m)))
As a background-color, Chrome 153 computes
`color(srgb 0 0.501961 0.501961 / 0.3)` for the first and
`rgba(0, 0, 0, 0)` for the second.
UnresolvedColor::parse now reads the alpha, nested blocks included,
before it buffers it. When an identifier there is a channel keyword of
the function, or `alpha`, the parse fails, and the function stays as
tokens with its origin. The check reads names: a relative color nested
in the alpha has `r g b` of its own, and the outer function then stays
as written too.
This is the third commit of #45362 (4ca1f11), ported to main after
#47025.
This was referenced Oct 28, 2026
Open
robobun
added a commit
that referenced
this pull request
Oct 29, 2026
…efix The default browser targets keep light-dark() as written since #47025, so the polyfill no longer tells a stylesheet built for browsers from one built for the runtime. Nesting and media range syntax show the targets a stylesheet was printed for. The :-webkit-full-screen copy of a :fullscreen rule shows the targets it was minified for. The server copy of a shared stylesheet is now asserted without a condition: the server build minifies and prints it for the runtime.
This was referenced Oct 29, 2026
Open
khromov
added a commit
to khromov/mochi
that referenced
this pull request
Oct 30, 2026
… race) (#375) Fixes the two Bun-canary test failures in `test.yml`. Both are test-only changes; no framework code changes. ## 1. `cssFontImports.test.ts` — consistent canary failure **Root cause: an intentional Bun output change, not a Bun bug or a mochi bug.** oven-sh/bun#47025 ("css: consolidate the open CSS parser fixes", commit `794200868`, between 1.4.4 `38afee6c8` and canary `da867c60b`) now prints: - "The path of a copied asset spliced into `url()` is always quoted, so a file name with a space or a parenthesis cannot end the `url()`" (#41557) - "@font-face format() keywords as strings" (#37979) The test extracted the asset name with `/url\(\.\/([^)"']+\.png)\)/`, which only matches the unquoted form. On canary, every sheet returned `undefined`, so `names` was `{undefined}`. Its size of 1 passed, and `toMatch` threw `Received value must be a string: undefined`. Standalone repro (no mochi): `Bun.build({ entrypoints: ['a.css'], minify: true })` on `.a{background:url("./shared.png")}` with a 200 KB png: ``` 1.4.4 38afee6c8 .a{background:url(./shared-fpg4xa8b.png)}…format(woff2) 1.4.4 da867c60b .a{background:url("./shared-fpg4xa8b.png")}…format("woff2") ``` Production code is unaffected, because `cssFontAssets.ts` reads `url()` and `format()` through the css-tree AST (`parseCss` in `cssAst.ts`), which decodes quoting. **Fix (after review): the tests now read bundled CSS the same way.** `cssFontImports.test.ts` gains three test-local helpers: - `parsed()` wraps `parseCss` and throws a clear error on a `null` result. - `urlValues()` returns the decoded `url()` values. - `relativePng()` returns the first non-`data:` `.png` url containing a given stem. Converted from regex extraction to the AST, each kept at least as strict: - `:98` checks the served woff2 URL and its `format()` on the **same** `src` entry, where the old regex only required them to be adjacent text. - `:105` checks that no font source has `format` `woff`, guarded against an empty source list. - `:302` checks that no relative (non-`/_mochi/fonts/`) `.woff2` url is left. - `:388`, `:401`, `:407`, `:460`, `:487` read the relative image urls. - `:550` is the canonical-name test. Each sheet must contribute exactly one `./`-relative `.png` url, otherwise it adds `undefined` to the set. There must be exactly one name, and it must match `^shared-[a-z0-9]{8}\.png$`. Left as text checks on purpose: - `toContain`/`not.toContain` presence checks on URLs and `data:` payloads (`:108`, `:113`, `:192`, `:202`, `:233`, `:299-301`, `:312-313`, `:323-324`, `:400`, `:459`, `:543`). They don't extract a value, they don't depend on quoting, and most match mochi's own substituted text. - `cssImports.test.ts:57-59` asserts the raw minified text. - `cssImports.test.ts:123-124` asserts the serialized quoting of `format('woff2-variations')` itself. The AST's `format` lowercases both the string and the identifier form, so it cannot tell them apart. - All of `cssFontAssets.test.ts` checks mochi's own `substituteFontUrls`/`classifyFontAssets` output on hand-written input, with no Bun involved. No production change: `parseCss` already exposes everything the tests needed. Mutation checks on the converted file, with production code broken temporarily and then restored: | Mutation | Result | |---|---| | Keep the per-build suffix (`canonical = assetPath`) | 10 fail, including the canonical-name test | | Stop dropping legacy woff | `drops the legacy woff source entirely` fails | | Make `substituteFontUrls` write a wrong URL | `rewrites the large woff2…` fails | | | before | after | |---|---|---| | stable 1.4.4 (38afee6c8) | 22/22 | 22/22 | | canary 1.4.4-canary.1 (da867c60b) | 21 pass, 1 fail | 22/22 | ## 2. `cache-storage-file.test.ts` — probable flake **I could not reproduce it.** I ran it 0/320 failures: 100 runs each on stable and canary at idle, and 60 runs each with 24 busy loops on 12 cores plus sync `dd` I/O. Reading the test does show a real timing assumption, though. `waitForMarker` polled for at most 250 × (getItem + 2 ms), which is about 530 ms of wall time. After `release()`, A must do a durable `setItem` (fsync + rename) and then remove the marker. Locally that takes about 2 ms. This file's own `LEASE` comment records CI runs of 400–1300 ms per test. Injecting an 800 ms delay into A's `setItem` reproduces the exact CI error and stack: `marker for "k" never became absent` from `waitForMarker`, called from the line after `release()`. The fix replaces the fixed-budget polling with deterministic synchronization: - **Marker present:** wait until `fn` is entered (`run()` awaits `writeMarker` before calling `fn`), then assert that the marker exists. - **Marker absent:** `await cacheA.whenIdle()` (it settles only after the run's `finally { await removeMarker }`), or `await second` in the superseded-run test. Then assert once that the marker is `null`. These checks are at least as strict as before: the marker must be gone the moment the run settles, with no grace window. With the 800 ms injection, the new test passes. With `removeMarker` mutated out, the cross-process tests still fail. `waitForMarker` is deleted because nothing uses it now. ## Verification - `bun run checks` (stable 1.4.4): pass. `mochi-framework` passed 204/204 test files. - `bun run build` (stable 1.4.4): pass. - `cssFontImports.test.ts` + `cssImports.test.ts`: 31/31 on stable and on canary. - After the AST conversion: `cssFontImports.test.ts` 22/22 on stable 1.4.4 and on canary 1.4.4-canary.1 (da867c60b). `bun run checks` (stable): pass. - `cache-storage-file.test.ts`: 37/37 on stable and on canary. The cross-process block also passed 40/40 on each version under CPU load. No upstream issue is needed: the Bun change is intentional and valid CSS.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Consolidates the open pull requests on the CSS parser (
src/css: parsing, minifying, lowering, printing, CSS modules) into one.179 open pull requests were in scope. Each was judged by one rule: the output must mean to a browser what the input meant. Every claim was run through a build of
main, a build with the change, lightningcss 1.33 and Chromium 156 (CSSOM, computed values, painted pixels for colors and gradients), and counted against 10,116 real stylesheets: the published CSS of about 190 popular npm packages, plus stylesheets from open source repositories. 107 land here, whole or in part, most of them cut down. The source diff is net negative.Of the 10,116, 9,509 are stylesheets that someone ships. The rest are test fixtures of other tools (521), Tailwind v4 source that needs its plugin (59) and SCSS, PostCSS or template source (27).
mainmainmain, fail heremain, wrong here (minified, and at the default targets)A syntax error is a build error, with one exception: the IE star hack
*zoom: 1is skipped, because tachyons and handsontable ship it. The 35 shipped stylesheets that fail are invalid CSS: 28 have a selector that Chromium drops (semantic-ui-css, swiper, @ionic/core, Adobe Spectrum, xp.css), 2 an@importafter other rules, and one each@supports not X and Y, a nested@keyframes, a stray comma, a selector list in::slotted(),@media screen\0(govuk-frontend). The 2 that read differently fold acolor-mix()inside a gradient (see the known gaps).The growth of the minified output is rules and declarations that
maindeleted by mistake: 87 KB of rules, 45 KB of prefixed declarations that only browser targets can rule out.Popular CSS that
mainbreaks todayfont-family: monospace, monospaceis collapsed tomonospace, which changes the font size from 16px to 13px (css: keep repeated font families and dedupe only the inserted system-ui fallbacks #46662).PropertyIdequality and importance in duplicate-rule detection #33684).var()size loses to its own fallback (css: keep an unparsed width/height/color after the declarations written before it #42101).<general-enclosed>media and container conditions as written #46756, css: keep scanning an @supports condition after a nested block #38552, css: only reject bare :global / :local pseudo-classes in CSS modules #38534). tachyons, handsontable: it fails on the IE star hack (css: parse the*propertyhack as a declaration #46751).@applyor@mixinin them is moved out of the rule (css: keep unknown at-rules inside their style rule when nesting is compiled away #46759).z-index: 1000001and1000000become the same number (css: print integers below 2^24 in full and long integer tokens as written #46715).--version: 5.0.1prints as5.1.align-items: center(css: print align-items:center without a stray space when no safe/unsafe keyword is given #41993).Behavior changes to look at first
light-dark()is kept as written at the default browser targets (css: keep light-dark() as written under the default browser targets #42009). The lowered formvar(--buncss-light, a) var(--buncss-dark, b)is invalid unless a compiledcolor-schemecovers the element: 184 of the 215 real stylesheets that get it never define the variable, so every browser drops the declaration. The docs section on the polyfill is removed.borderis not built from longhands (css: only synthesizeborder/transitionwhen the rule sets every longhand they reset #42072):border-width:0;border-style:solid;border-color:redstays three declarations, becauseborderalso resetsborder-image. 305 real stylesheets grow by 30 KB in total;bootstrap.min.cssby 113 bytes, 5 gzipped. No real stylesheet was found where the old merge shows, so this is the one to drop if size matters more. 14 existing expectations change.:global(...)(css modules: decide local vs global at parse, fixing :global() exports and&inside :global() #45487). 73 real modules lose 202 export keys, all of them names that no rule has.import { g }orcomposes: gof such a name becomes a build error.@counter-stylenames stay global in a CSS module (fix(css): keep container names global in CSS modules #45247 by @mikamikasuki).mainhashes the@containername but notcontainer-name, so a named query never matches. postcss-modules (Vite, css-loader, Next.js) keeps them global; lightningcss and esbuild hash both. Global keeps a container that is queried from another file or from JS working.mainlets through: a media query that stops short (@media screen and {,@media not {,@import "a.css" screen and;) and@media (width <) foo {. No stylesheet of the 10,116 has either. A condition that is valid by the grammar though no browser knows it,( ... )orfn( ... ), is kept as written (css: keep<general-enclosed>media and container conditions as written #46756):(min-width:0\0),(not(hover)).mainwas wrong in 1,056 and the pull request's code, before it was trimmed here, in none.Bun.color:color()inputs are converted instead of returned as a string (Bun.color("color(srgb 1 0 0)", "hex")was"color(srgb 1 0 0)");currentColor, system colors andlight-dark()givenullin the converting formats instead of the keyword as a string; a relative color orcolor-mix()outside sRGB is clipped, not gamut-mapped (hsl(from oklch(100% .399 336.3) h s l)is#ff5bff, was#fff).:-moz-any()and:-webkit-any()copies of rules with:is()are gone at the default targets (css: stop printing the:-moz-any()copy for Firefox 78, and regenerate the vendor prefix table from one caniuse-lite copy #45945, css: stop emitting :-webkit-any() copies of :is() that outrank the original rule #42029). The:-webkit-any()copy outranked the original rule:.title{color:blue} :is(h1,h2,h3){color:red}painted red. 5.8% of all default-target output was the:-moz-any()copies.Propertygrows from 216 to 224 bytes:Urlstores its text as a slice so that twourl()values can be compared (css: compareurl()values by their text #46895). The pull request packed a pointer and au32behindunsafeto avoid that.Not in this PR, needs a decision
cssTargetoption (Bun.build: add cssTarget option to set CSS browser targets #40368). Today: Chrome 80, Edge 80, Firefox 78, Safari 14, Opera 67, and no public way to change them. At Vite 8's floor the output of the 9,924 stylesheets is 15.3% smaller (3.9% gzipped). About a dozen fixes here only matter at today's floor, and code for older targets is unreachable without the option.user-select,appearance,hyphensand five more (css: write the vendor prefixes for user-select, appearance, hyphens and five more properties #45814): 1,116 real stylesheets lack a prefix they need at the default targets. And forbackdrop-filter(css: parse filter and backdrop-filter, and lower colors inside drop-shadow() #44648): 173.Changes
Rules: duplicates, merging, nesting, fallbacks
:-moz-any()copy of every rule with:is()or:not(a, b)is gone: Firefox 78 has:is()(css: stop printing the:-moz-any()copy for Firefox 78, and regenerate the vendor prefix table from one caniuse-lite copy #45945). 1,164 real stylesheets.wrapper{@apply flex}printed a bare@apply flex;and the rule was gone, so a CSS module exported a class that no rule defines. It now stays in a block under the compiled selector. 72 real modules with Tailwind's@apply, 25 with Mantine's@mixin(css: keep unknown at-rules inside their style rule when nesting is compiled away #46759). 316 real stylesheetsPropertyIdequality and importance in duplicate-rule detection (css: compare custom property names inPropertyIdequality and importance in duplicate-rule detection #33684). 40 real stylesheets:full-screen(css: keep target-added vendor prefix passes on cloned and re-minified rules, and never print a bare:full-screen#43339). 12 real stylesheets@supportscolor copies of the earlier one, which printed after the rule and won (css: drop @supports color fallbacks that a later declaration of the same property replaces #45560). 2 real stylesheetsrevert-rule(css: do not merge style rules in a stylesheet that holdsrevert-rule#46953). 1 real stylesheets@import ... supports(-webkit-mask: none)kept its prefix (css: parse (property: value) @supports conditions as declarations and merge same-condition rules #38589)Properties and shorthands
50%hint only at the midpoint (css: keep the stops of an upward gradient as written, and drop a50%hint only at the midpoint #46790). 104 real stylesheetswidth/height/min-*/max-*withvar()orenv()was printed before the plain fallback written above it, so the fallback won (css: keep an unparsed width/height/color after the declarations written before it #42101). 26 real stylesheetsnoneinstead of an empty border-image, mask-border or mask-box-image value (css: printnoneinstead of an empty border-image, mask-border or mask-box-image value #45143). 2 real stylesheetsfont:12px Arial;font-family:inheritprintedfont:12px inherit,color-scheme:inheritandcolor-scheme:autoprintednormal,transition-property:unsetwas folded intotransition(css: keep css-wide keywords and percentage stretch out of the font and transition shorthands #42022). 2 real stylesheetsfontwhen the rule declared afont(css: only fold font longhands intofontwhen the rule declared afont#45459)borderis no longer built from longhands unless the rule also sets whatborderresets (border-image), and an author'sborder-imagereset is no longer dropped.transitionis likewise only written where the rule wrote one, because it resetstransition-behavior(css: only synthesizeborder/transitionwhen the rule sets every longhand they reset #42072)border; border-image: <gradient>; border-image-slice: 1lost the gradient in every default build (css: keep border-image andborderin source cascade order within a rule #42018)linear-gradientkeeps its direction, written or generated: the prefixed syntax names the starting side and measures angles differently (css: read a prefixed linear-gradient direction in the 2011 syntax #46697)At-rules
notconditions without a leading space (css: print @supportsnotconditions without a leading space #38559). 88 real stylesheets<general-enclosed>media and container conditions as written (css: keep<general-enclosed>media and container conditions as written #46756). 24 real stylesheets(width < 768px)lowered for old targets still matched at exactly 768px. The step is now.02px. Thenot all andform for other units is not taken (css: lower strict media range bounds withnot all andor a .02px step #46806). 18 real stylesheets@counter-style nameprinted as@counter-stylename, which browsers drop (also css: print the space after@counter-style, and hash the counter style name inlist-stylein CSS modules #46692). In a CSS module the name now stays global, like thelist-stylethat refers to it (css: fix @counter-style and @scope printer round-trip #33679). 9 real stylesheets@media {}isall. A media query that stops afterand,orornot(@media screen and {) is a build error;mainprintednot allor dropped the condition, silently. The@layerand type selector parts are not taken (css: make a media query,@layername list or type selector that stops short a parse error #46869). 2 real stylesheets@containerand astyle()feature with no value (css: accept a name-only@containerand astyle()feature with no value #46689). 1 real stylesheets<timeline-range-name> <percentage>keyframe selectors (css: parse<timeline-range-name> <percentage>keyframe selectors #45652)orinside parentheses after a media type (css: alloworinside parentheses after a media type #45088)=comparison in media and container queries as=(css: print the=comparison in media and container queries as=#45071)Selectors
.s *::-ms-backdrop,.s{flex:auto}applied everywhere. Rewritten; the list is printed as written (css: keep a selector list whole when a member is vendor prefixed #42100). 42 real stylesheets&that does not lead its selector (css: fix nesting lowering of a&that does not lead its selector #43047). 8 real stylesheets@scope:&and implicit selectors compile to:where(:scope), relative selectors and bare declarations parse (css: fix @scope nesting, relative selectors, and declarations in @scope #43165). 7 real stylesheets@scope to (...)and stop replacing&in the scope-end selector (css: stop truncating@scope to (...)and stop replacing&in the scope-end selector #43146). 2 real stylesheets[xl|href$=".pdf"]lost its namespace prefix (css: keep the namespace prefix on attribute selectors that have an operator #45159). 2 real stylesheets+as the leading sign (css: match An+B keywords exactly and take only+as the leading sign #42953). 1 real stylesheetsli::before::markerfailed the build (css: accept::markerdirectly after::beforeand::after#45810)&inside:nth-child(... of S)is resolved when nesting is compiled away. The second selector serializer (326 lines) is deleted (css: resolve&inside :nth-child(... of S), :host(), ::slotted() and ::cue() #43024)Colors and
Bun.colorlab(),lch(),oklab(),oklch(),hsl()andhwb(); the hue oflch/oklchis no longer scaled by alpha (css: parse number and percentage channels in lab(), lch(), oklab(), oklch(), hsl() and hwb() #45084). 130 real stylesheetsBun.colorreturnsnullforcurrentColor, system colors andlight-dark()in the converting formats, instead of the CSS keyword as a string (Bun.color: return null for unconvertible colors, fix hsl/lab output, lab() and named-color parsing #33047). 130 real stylesheetsrgb(from #369 calc(r + 10) g b)), the origin's alpha is kept, a relative color kept as written gets no@supportscopies, and a result outside sRGB is not folded (css: fold relative colors on the scale of their function, and keep them as written when the fold is not exact #45362). 31 real stylesheetscalc(l - .08)in a percentage channel of a relative color became NaN and printed#000(also css: fail a percentage calc that cannot be reduced instead of folding it to NaN #45970) (css: read a NaN calculation in a color channel as 0, notnone#45971)color-mix(in srgb, ...)result outside sRGB printscolor(srgb ...)instead of a gamut-mapped hex (css: clip color-mix() results outside the sRGB gamut instead of gamut mapping them #39269)calc()and numbersz-index:2147483647became2147480000,max-width:9999999pxbecame10000000px,--x:1234567.5became1234570. It now only cuts a fraction short: a value kept as tokens prints its source text, a typed value prints every integer digit and at least one digit of the fraction (css: print integers below 2^24 in full and long integer tokens as written #46715). 43 real stylesheetspx(height:1.5715is dropped by a standards-mode browser, so it prints as written),calc(-1 * 0)is not a length, and on a property of the range [0,∞] (sizes, border radii, padding, border and outline widths, gap,flex-basis,font-size) acalc()that folds below zero prints0instead of an invalid negative literal, and a negative literal no longer replaces the valid declaration before it (width:10px;width:-5pxprintedwidth:-5px) (css: treat out-of-range and unit-less numeric values as invalid instead of merging or repairing them #42104). 24 real stylesheetsmin()/max()around a sum printed without any function (width:100% - 20px) (css: wrap a one-argument min()/max() sum in calc() and give hypot() one fold for every argument count #46145)round()breaks ties upward and handles a negative step as browsers do (css: fold round() by choosing between the lower and upper multiple of |B| #45249)CSS modules
@keyframesnames (styles.spin). Hashing is unchanged (css modules: make@keyframesand animation names symbols and export them #46720). 145 real stylesheets:global(...)prints plain but was exported asname_<hash>, a class no rule has (also css modules: keep names inside :global(...) out of the exports object #38573) (css modules: decide local vs global at parse, fixing :global() exports and&inside :global() #45487). 90 real stylesheetsanimationvalue falls back to raw tokens (css modules: tag the keyframes name when ananimationvalue falls back to raw tokens #46034). 6 real stylesheetscomposes: a from "./b.css"was treated as an import: the file was inlined and an absolute path printed (css: parsecomposesas CSS modules syntax only in a CSS module #45961). 4 real stylesheetsIdentOrRef::hashread 2 of 16 bytes, so in release builds every class of a module hashed alike (css: hash full Ref identity in IdentOrRef::hash #30784)Tokens,
url(), diagnosticsurl()values by their text (css: compareurl()values by their text #46895). 26 real stylesheetsattr(x type(<number>))printedtype(< number> ), which browsers drop. Minified output still drops the blanks that cannot matter (css: keep whitespace and comments in token lists #45577). 19 real stylesheets*zoom:1) failed the whole build. The declaration is skipped, as every browser does. It is the one syntax error that builds: tachyons, handsontable and jasmine ship it (css: parse the*propertyhack as a declaration #46751). 8 real stylesheetsurl()is always quoted, so a file name with a space or a parenthesis cannot end theurl()(css: quote and escape the asset path spliced into url() #41557). 7 real stylesheetsimpl Default for Property(Remove dead code from bun_css: the unusedimpl Default for Property#45618)Speed
Four inputs that took quadratic or exponential time, or produced output out of proportion, are now linear, with no new limit and no extra pass (#31639, #42243, #38531, #43421).
Tests only
css-fuzz.test.tsnever ran in CI and timed out on debug builds; it is a deterministic table now (test(css): replace the random css-fuzz loops with a deterministic table #38500)nth-anplusb-ident.test.tscounts heaps in a child process (test: count the heaps of the An+B selector test in a child process #46331)color.test.tschecks the ansi-16 sweep with oneexpect()(test: check the ansi-16 color sweep without an expect() per color #42824)maintoo:ws-proxy.test.tson Windows, "a message sent during a TLS renegotiation arrives after it". The helper server now renegotiates from the callback of the frame's write (test(ws-proxy): renegotiate only after the "renegotiating" frame is written #46170)No pull request covered these
--v: 5.0.1printed as5.1,--w: 1.0.0as10,--x: 1+2as12,2n+1as2n1: a number token that touches another keeps its source text.color-scheme: autoprinted asnormal, andcolor-scheme: dark lightaslight dark.:globalor:localin a CSS module sets the scope of the names after it, up to the end of the selector, as in css-loader. It was a build error.a:horizontal,::-webkit-scrollbar{&:horizontal{}}) are valid in a plain compound, as in WebKit and Blink.-webkit-box-align,-ms-flex-alignand the like were dropped next to their standard form (flatpickr, foundation-sites, materialize-css: 28 stylesheets), and-webkit-fill-availablebefore-moz-available(beercss).(-o-min-device-pixel-ratio: 2/1)printed as2.sin(),cos()andtan()are exact at multiples of 90deg:calc(100px * cos(90deg))printed-.00000437114px.display-p3color at targets that have P3 but not the othercolor()spaces (Safari 10.1 to 14) got itself as a fallback.width: rem(-18px, 5px)folds to a negative literal oncerem()is right; it prints0.@mediablocks in one rule merge, are folded into one and count against the existing selector expansion limit as the&{}rules they replace did.Known and not fixed here
transitionthe author did not write where the targets add a prefixed copy of a property (mask-imageat Chrome 80): only the shorthand gives the copy its own times.background,box-shadoworborder-*declaration still loses to the@supportscopy of an earliervar()value of the same property with a wide-gamut color. Same onmain; css: drop @supports color fallbacks that a later declaration of the same property replaces #45560 is taken for the properties of the fallback handler and for unparsed values only..d::-webkit-input-placeholder{}.d::-moz-placeholder{}prints::placeholderfor the second (css: print explicitly prefixed selectors as written in every vendor prefix pass #42106, not taken: it breaks the merge of prefix-equivalent rules).border,outlineandfontshorthands,line-height,scroll-padding,background-sizeand a few more still print acalc()that folds below zero as a negative literal. No real stylesheet has one.allis not a property Bun knows, soa{margin-top:1px;all:unset}printsallfirst. Same onmain.*when minified:--sel: .dark *prints.dark*. Same onmain.width:calc(0% + 12px)folds to12px, which lays out differently in a table cell. Same onmain.:dir()/:lang()rules of a lowered logical property print before the nested rules of their rule, so a logical declaration written after a nested rule of equal specificity still loses to it. Same onmain.12345.678prints12345.7,33.3333333333%prints33.3333%). Keeping every digit would add up to 282 KB over the corpus.@keyframes "quoted"is hashed in a module but not exported.var()fallback is not hashed:animation: var(--enter, fade 1s). Same onmain.color-mix()or relative color folded to a hex inside a gradient changes the interpolation from Oklab to sRGB (VS Code, clawhub). Printingcolor(srgb ..)instead needs a fallback formask-image,list-style-imageand custom properties at the default targets, which have none.@containerstill fails the build.:global, names that are hashed at print time (::view-transition-old(foo)) are still hashed; inside:global(...)they are not.Pull requests in scope that do not land here
&inside :global() #45487), test(css): build every css-fuzz generator input instead of mis-indexing the tables #38640 (of test(css): replace the random css-fuzz loops with a deterministic table #38500), css: scope container and container-name to the @container hash in CSS modules #45525 (of fix(css): keep container names global in CSS modules #45247), css: parse scroll-state() container queries #45589 (of css: keep<general-enclosed>media and container conditions as written #46756), css: fail a percentage calc that cannot be reduced instead of folding it to NaN #45970 (of css: read a NaN calculation in a color channel as 0, notnone#45971)main: test(css): run the selector-list recovery cases through one child process #38674allshorthand reset the declarations before it #42042, css: recognize :target-current, :target-before and :target-after #42484, css: use :is() for a nesting parent with a pseudo-element when more follows theꡆ, css: keep a url() in an at-rule prelude as written, and never print the resolved path of a url() #45604, css: keep an earlier transform or translate declaration when a target does not support the later one #45908, css: keep the legacy two-value-webkit-border-radiusas its own declaration and print other-webkit-radii with three values #46086, css: fold hwb() above 100% to the gray W / (W + B) #46221, css: print a calc() product as a multiplication when the reciprocal of its factor is not finite #46224, css: read the font-family names of @font-palette-values without the generic-family table #46273, css: keep an earlier gap or flex-basis declaration when a target lacks the later one #46694, css: keep the slice and width of mask-border and -webkit-mask-box-image as written #46707, css: print :lang("") as a string #46737, css: decide the vendor prefixes of a background list per layer #46755, css: require at least one component in the background, transition, animation, mask, flex and flex-flow shorthands #46816, css: keep a prefixed gradient that uses the standard syntax as written #46870, css: keep the earlier copy of a file with a conditional layer in every import order #46934resolutionmedia features #45946, css: keep the prelude and block of an unknown at-rule as raw tokens #46061, css: read the float channels of an hsl(), hwb() or relative color origin #46127, css: parse transform-origin, perspective-origin and mask-position with their own grammars #46206Out of scope: CSS linking and chunking in the bundler, the dev server, docs-only pull requests, and repository-wide sweeps that touch
src/cssin passing.Thanks to @mikamikasuki (#45247), @alii (#39520, #31999) and @oddharsh, whose measurements on #40369 and #41990 were used.
Fixes #16727.
Fixes #24770.
Fixes #30772.
Fixes #44704.
Fixes #45089.
Fixes #45237.
Fixes #46872.
How did you verify your code works?
FORCE_COLOR=1. 600+ of the tests in this branch fail on a build ofmain. An earlier head of this branch passed all 179 CI jobs that ran.main(wrong) and on this branch (right).mainagainst this branch, minified without targets and throughBun.buildat the default targets: no stylesheet that builds onmainfails; Chromium 156 compares the output with the input rule by rule (CSSOM, then computed values on a sized element in a standards-mode document, colors as painted pixels in sRGB and P3). The comparison has 195 control pairs that must read equal or different. For stylesheets that read differently on both builds, mostly because of lowering, the sets of differences were compared; the three new ones are two minifier fixtures that are wrong on both and one comparison artifact.Bun.color: 59 inputs in 13 formats on both builds; every changed cell was read.