Repository navigation
Conversation
…nd five more properties Port the lightningcss `PrefixHandler` as the 16th entry of the handler chain, with typed rows for the nine properties it needs: tab-size, hyphens, text-align-last, text-decoration-skip-ink, text-size-adjust, box-decoration-break, user-select, appearance and print-color-adjust. Unlike lightningcss, the handler keeps a prefix that the author wrote, does not add a prefix that the block already declares, merges only equal values, and also prefixes a value that did not parse (`var()`, `initial`). The new rows need three more changes: the `@import ... supports()` condition prints the prefix it is written with, duplicate-rule detection compares the names of unknown properties, and the prefix table no longer gives `print-color-adjust` a `-moz-` form that no Firefox reads.
|
Updated 4:42 PM PT - Oct 27th, 2026
✅ @robobun, your commit 6de8f2836400febefa4419faf5f47a7599407c5c passed in 🧪 To try this PR locally: bunx bun-pr 45814That installs a local version of the PR into your bun-45814 --bun |
|
Reproduced on 1.4.3 and on main with .modal { user-select: none; }
.button { appearance: none; }
.title { hyphens: auto; }
.card { backface-visibility: hidden; box-decoration-break: clone; }
@media print { .page { print-color-adjust: exact; } }The output has no
The one open point is the question at the top of the description: keep a written prefix, or replace it as lightningcss does. |
|
No actionable comments were generated in the recent review. ℹ️ Recent review info
WalkthroughThe change adds CSS property models and target-based vendor-prefix processing. It integrates prefix handling into declaration processing, updates bundler behavior for prefixed declarations and ChangesCSS Vendor Prefixing
Suggested reviewers: Priority: Merge Risk:
|
There was a problem hiding this comment.
Beyond the inline findings, I also checked the index bookkeeping in PrefixHandler (last[slot], start, floor) for out-of-bounds access: dest only grows during a block's handle_property calls (no handler shrinks it), last[slot] is only read when start is set, and finalize resets the state between blocks, so no panic path. The same_property change in src/css/rules/style.rs only adds a name comparison on top of the existing PropertyId equality, so it can only reject merges the base branch allowed, never introduce new ones.
Extended reasoning...
The PR adds a PrefixHandler to the CSS minifier handler chain and nine new typed property rows in src/css/, plus merge and @ supports printing changes that affect all prefixed properties; it touches no auth, crypto, or injection surface. Four verified findings are posted inline (rule-merge re-minify divergence for Safari, a CSS-modules composes regression, a docs claim about print-color-adjust on Firefox, and a missing exit-code check in cli.test.ts), so a human review is already signalled; this note records only the two additional concerns examined and ruled out.
Findings marked 🟡 are optional suggestions and need no follow-up push.
The `!important` declarations of a block have their own handler. Each `PrefixHandler` now gets the prefixes that the author wrote in the other list, so `-webkit-user-select: text; user-select: none !important` gets no generated `-webkit-` copy. `StyleRule::is_duplicate` compared the combined length of the two lists and only the shorter part of each. A rule with `!important` declarations was a duplicate of a later rule with the same properties and no `!important`. It now compares each list in full.
There was a problem hiding this comment.
The latest push addresses the two threads from the last round: see_other_list seeds each handler with the other list's written prefixes (pinned by the !important cases in css.test.ts), and is_duplicate now compares both declaration lists length-by-length. Beyond the inline findings, I also checked that close() on all keeps elsewhere while resetting written — correct, since the other list's declarations are not reset by all in this one — and that the see_other_list guard on an empty important list loses nothing, because an empty list seeds nothing either way.
Extended reasoning...
Third review of this PR; the two confirmed inline findings (prefix stripped from later @ supports conditions, and a std eq_ignore_ascii_case where the crate uses the in-tree helper) remain for the author. The prior-round objections on the important-list prefix and the vacuous duplicate-rule compare are addressed in commit e0fc9b1 with tests. No security-sensitive surface; the PR still carries an open maintainer design question on keeping versus replacing author-written prefixes.
Findings marked 🟡 are optional suggestions and need no follow-up push.
Additional findings (outside the current diff — GitHub can't attach inline comments there):
-
🔴
src/css/rules/supports.rs— Safari and Chrome 80-83 users lose any@ supportsblock whose second or later condition names a prefixed property such as-webkit-user-selector-webkit-appearance. At src/css/rules/supports.rs:287 the id is rewritten withwith_prefix(NONE), and that stripped id is what :292 adds and :299 stores, so(display: grid) and (-webkit-user-select: none)prints(user-select: none), which no Safari supports. On the base branch these names were unknown and kept their spelling. Fix: key the dedup on the stripped id but add and store the originalproperty_id_prefix for every later condition, inand/orchains and@ import … supports()alike.Why this was flagged
Input
@ supports (display: grid) and (-webkit-user-select: none) { .x { color: red } }built with default targets. SupportsCondition::parse (src/css/rules/supports.rs:234-307) keeps the first condition asin_parenswith its prefix, but for every later Declaration it computesproperty_id = property_id_.with_prefix(NONE)at :287, then either callsd.property_id.add_prefix(property_id.prefix())at :292 (adds NONE, not WEBKIT) or pushesDeclaration { property_id, value }at :296-299 with the stripped id. The diff's new row makes-webkit-user-selectparse toPropertyId::UserSelect(WEBKIT)(properties_generated.rs:1601) and the new printer at :205 prints exactly the stored flags, so the output is@ supports ((display: grid) and (user-select: none)). Safari (every version) and Chrome 80-83 do not support the unprefixed name, so the block stops applying there. On the base branch-webkit-user-selectwasPropertyId::Custom("-webkit-user-select"),with_prefixwas a no-op, and the output kept(-webkit-user-select: none). No test covers a prefixed name in a non-first@ supportscondition.Verification: src/css/rules/supports.rs line 287
let property_id = property_id_.with_prefix(css::VendorPrefix::NONE);replaces the prefix slot; line 292d.property_id.add_prefix(property_id.prefix())uses the stripped id; line 296-299 pushesDeclaration { property_id, value }with the stripped id, so-webkit-user-select: noneas a second condition prints as(user-select: none).
…II compare A `(property: value)` group in an `@supports` rule or in an `and`/`or` chain is raw text, so it prints as written. The new cases pin that for the names that now have a typed row. `PrefixHandler::see_other` compares `all` with `strings::eql_case_insensitive_ascii_check_length`, like the rest of `src/css`.
|
On the
Output of this branch under the default browsers: /* in */
@supports (display: grid) and (-webkit-user-select: none) { .x { color: red } }
@import "./c.css" supports((display: grid) and (-webkit-user-select: none));
/* out */
@supports (display: grid) and (-webkit-user-select: none) { .x { color: red; } }
@supports (display: grid) and (-webkit-user-select: none) { .c { color: red; } }427318b adds these cases: six conditions in |
There was a problem hiding this comment.
Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.
Beyond the two inline nits, I re-checked the points raised on earlier pushes against the current code: StyleRule::is_duplicate (src/css/rules/style.rs) now requires equal lengths for the normal and !important lists separately before the zip, and the all-important-vs-normal case is pinned at test/js/bun/css/css.test.ts:8603; DeclarationBlock::minify (src/css/declaration.rs) seeds each list's PrefixHandler with the other list's written prefixes; and the all compare in prefix_handler.rs uses the in-tree strings::eql_case_insensitive_ascii_check_length. The new handler still diverges from lightningcss on purpose (written prefixes kept, no re-add), so a maintainer should weigh that policy question the author asks in the description.
Extended reasoning...
The change adds a PrefixHandler to the CSS declaration handler chain, nine new typed prefixed Property variants, a PrintColorAdjust split in prefixes.rs, a rewritten is_duplicate, and a supports() serialization fix, with ~1300 lines of tests; it touches no auth, crypto or injection surface. Earlier red findings (cross-list !important prefixing, vacuous is_duplicate zip) are addressed in commits e0fc9b1 and 427318b as verified from the code, leaving only the two inline nits. The PR is large, intentionally diverges from the lightningcss reference in merge semantics, and the author explicitly asks a maintainer to decide the keep-vs-replace prefix policy, so a human decision is still needed.
Found in review, not reported by a user. The previous head printed these blocks as main does. The handler of the `!important` list adds no copy for a prefixed name that the normal list of the block writes. That ignored `all`. In `-webkit-user-select: text; all: unset; user-select: none !important` the written prefixed value is reset, so a browser that reads only `-webkit-user-select` ended on `unset` and the others on `none`. Now that handler takes only the prefixes that the normal list writes after its last `all`, and drops them at an `all !important`, which beats every normal declaration. The handler of the normal list still takes every prefix that the `!important` list writes: that declaration, or an `all !important` after it, beats a normal copy. Two adjacent rules of one selector are merged and minified again. The merged rule is one rule for `all`, so it can get a copy that the two rules do not get when another rule is between them. Two cases pin that. The three winner tests share one oracle in `util.ts`. The new test runs 584 blocks with `!important` and `all`. 12 of them are wrong on the previous head. Every declaration named `all` counts as a reset, also one with an invalid value. The comment on the handler list said that `transform-origin` and `perspective-origin` have no typed value. They have a row, but no valid value parses into it.
### 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). | 9,509 shipped stylesheets | `main` | this branch | |---|---|---| | fail to build | 71 | 35 | | ... of those, valid CSS that Bun rejects | | 0 | | minified output reads differently in Chromium than the input | 552 | 2 | | CSS modules with a name that is referenced but not defined, or exported but not defined | 166 | 0 | | all 10,116 | `main` | this branch | |---|---|---| | fail to build | 192 | 141 | | minified output reads differently in Chromium than the input | 593 | 15 | | build on `main`, fail here | | 0 | | right on `main`, wrong here (minified, and at the default targets) | | 0 | | size of the minified output, no targets | 162.32 MB | 162.47 MB (+0.093%) | | size of the output at the default targets | 224.1 MB | 210.2 MB (-6.2%) | A syntax error is a build error, with one exception: the IE star hack `*zoom: 1` is 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 `@import` after 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 a `color-mix()` inside a gradient (see the known gaps). The growth of the minified output is rules and declarations that `main` deleted by mistake: 87 KB of rules, 45 KB of prefixed declarations that only browser targets can rule out. #### Popular CSS that `main` breaks today - normalize.css, modern-normalize, sanitize.css: `font-family: monospace, monospace` is collapsed to `monospace`, which changes the font size from 16px to 13px (#46662). - CKEditor 5, Tabler, Carbon, VitePress, Ionic, PatternFly, Excalidraw: a rule of custom properties is deleted as a "duplicate" of a later rule with the same selector and other custom properties. 37 stylesheets lose 1,681 custom properties (#33684). - Material Components, daisyUI, DocSearch, plyr: a `var()` size loses to its own fallback (#42101). - nextra, daisyUI, CKEditor: declarations written after a nested rule are moved above it (#45462). - antd 4, purecss, govuk-frontend: browser hacks in selector lists start to apply everywhere (#42100). - foundation-sites, flatpickr, 98.css, blueprintjs, scalar, react-datepicker: the build fails on valid CSS (#46756, #38552, #38534). tachyons, handsontable: it fails on the IE star hack (#46751). - nodejs.org, Mantine: a CSS module exports classes that no rule defines, because the `@apply` or `@mixin` in them is moved out of the rule (#46759). - hint.css and 42 more: `z-index: 1000001` and `1000000` become the same number (#46715). - Adobe Spectrum: `--version: 5.0.1` prints as `5.1`. - A quarter of all stylesheets: minified output has a stray space in `align-items: center` (#41993). #### Behavior changes to look at first 1. **`light-dark()` is kept as written at the default browser targets** (#42009). The lowered form `var(--buncss-light, a) var(--buncss-dark, b)` is invalid unless a compiled `color-scheme` covers 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. 2. **`border` is not built from longhands** (#42072): `border-width:0;border-style:solid;border-color:red` stays three declarations, because `border` also resets `border-image`. 305 real stylesheets grow by 30 KB in total; `bootstrap.min.css` by 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. 3. **A CSS module no longer exports names that only occur inside `:global(...)`** (#45487). 73 real modules lose 202 export keys, all of them names that no rule has. `import { g }` or `composes: g` of such a name becomes a build error. 4. **Container names and `@counter-style` names stay global in a CSS module** (#45247 by @mikamikasuki). `main` hashes the `@container` name but not `container-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. 5. **Syntax errors are build errors, also two that `main` lets 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, `( ... )` or `fn( ... )`, is kept as written (#46756): `(min-width:0\0)`, `(not(hover))`. 6. **Declarations after a nested rule stay where they were written**, as a nested declarations rule (#45462). Changes the output of 199 real stylesheets; in a fuzz of 2,850 nested programs compared by computed style, `main` was wrong in 1,056 and the pull request's code, before it was trimmed here, in none. 7. **`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 and `light-dark()` give `null` in the converting formats instead of the keyword as a string; a relative color or `color-mix()` outside sRGB is clipped, not gamut-mapped (`hsl(from oklch(100% .399 336.3) h s l)` is `#ff5bff`, was `#fff`). 8. The `:-moz-any()` and `:-webkit-any()` copies of rules with `:is()` are gone at the default targets (#45945, #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. 9. `Property` grows from 216 to 224 bytes: `Url` stores its text as a slice so that two `url()` values can be compared (#46895). The pull request packed a pointer and a `u32` behind `unsafe` to avoid that. #### Not in this PR, needs a decision - **Raise the default browser targets (#40369) and add a `cssTarget` option (#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. - Vendor prefixes for `user-select`, `appearance`, `hyphens` and five more (#45814): 1,116 real stylesheets lack a prefix they need at the default targets. And for `backdrop-filter` (#44648): 173. - Clip instead of gamut-map in the sRGB fallback of a plain wide-gamut color: deletes about 90 lines, changes every pinned fallback hex. If the mapping stays, #44647 by @oddharsh makes it closer to what browsers paint (colors off by 2/255 or more: 168 to 110 of 3,170 real ones). #### Changes **Rules: duplicates, merging, nesting, fallbacks** - The `:-moz-any()` copy of every rule with `:is()` or `:not(a, b)` is gone: Firefox 78 has `:is()` (#45945). 1,164 real stylesheets - Keep declarations written after a nested rule in place with a nested declarations rule (#45462). 379 real stylesheets - With nesting compiled away, an unknown at-rule inside a style rule was moved out of it: `.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` (#46759). 316 real stylesheets - Keep !important in the rules generated for logical properties and color-scheme (#38597). 311 real stylesheets - With browser targets, remove an earlier duplicate rule only when its declaration block is equal (#46861). 171 real stylesheets - Compare custom property names in `PropertyId` equality and importance in duplicate-rule detection (#33684). 40 real stylesheets - Keep same-condition @supports rules and empty named @layer blocks (#46733). 31 real stylesheets - Keep target-added vendor prefix passes on cloned and re-minified rules, and never print a bare `:full-screen` (#43339). 12 real stylesheets - Keep the selectors on rules added for color fallbacks and logical properties (#43240). 3 real stylesheets - A later declaration of a property drops the `@supports` color copies of the earlier one, which printed after the rule and won (#45560). 2 real stylesheets - Do not merge style rules in a stylesheet that holds `revert-rule` (#46953). 1 real stylesheets - `@import ... supports(-webkit-mask: none)` kept its prefix (#38589) **Properties and shorthands** - Print align-items:center without a stray space when no safe/unsafe keyword is given (#41993). 3,317 real stylesheets - Emit the physical longhands instead of the inset shorthand for targets without it (#38583). 1,452 real stylesheets - Keep the stops of an upward gradient as written, and drop a `50%` hint only at the midpoint (#46790). 104 real stylesheets - Emit unparsed border-block-* declarations when compiling logical borders (#38582). 89 real stylesheets - Keep repeated font families and dedupe only the inserted system-ui fallbacks (#46662). 88 real stylesheets - A `width` / `height` / `min-*` / `max-*` with `var()` or `env()` was printed before the plain fallback written above it, so the fallback won (#42101). 26 real stylesheets - Keep box-orient and box-direction when flex-direction or flex-flow follows (#45672). 16 real stylesheets - Write held prefixed background declarations at every flush (#46882). 6 real stylesheets - Derive equality for Size2D and BorderRadius, and keep every prefix in the border-radius handler (#45665). 2 real stylesheets - Print `none` instead of an empty border-image, mask-border or mask-box-image value (#45143). 2 real stylesheets - Keep font/transition sub-properties on the side of the shorthand they were written on (#41990). 2 real stylesheets - CSS-wide keywords are not names: `font:12px Arial;font-family:inherit` printed `font:12px inherit`, `color-scheme:inherit` and `color-scheme:auto` printed `normal`, `transition-property:unset` was folded into `transition` (#42022). 2 real stylesheets - Only fold font longhands into `font` when the rule declared a `font` (#45459) - Keep a font family name quoted when its identifiers do not read back as the same name (#46606) - Stop rewriting valid positions into ones browsers reject (#45197) - `border` is no longer built from longhands unless the rule also sets what `border` resets (`border-image`), and an author's `border-image` reset is no longer dropped. `transition` is likewise only written where the rule wrote one, because it resets `transition-behavior` (#42072) - `border; border-image: <gradient>; border-image-slice: 1` lost the gradient in every default build (#42018) - A prefixed `linear-gradient` keeps its direction, written or generated: the prefixed syntax names the starting side and measures angles differently (#46697) **At-rules** - Print @font-face format() keywords as strings (#37979). 439 real stylesheets - Print @supports `not` conditions without a leading space (#38559). 88 real stylesheets - Reset after the number token of a unicode-range (#45835). 33 real stylesheets - Keep `<general-enclosed>` media and container conditions as written (#46756). 24 real stylesheets - A strict range such as `(width < 768px)` lowered for old targets still matched at exactly 768px. The step is now `.02px`. The `not all and` form for other units is not taken (#46806). 18 real stylesheets - Keep scanning an @supports condition after a nested block (#38552). 9 real stylesheets - Every `@counter-style name` printed as `@counter-stylename`, which browsers drop (also #46692). In a CSS module the name now stays global, like the `list-style` that refers to it (#33679). 9 real stylesheets - Keep a keyframe whose block has an invalid item (#46288). 2 real stylesheets - Read an @Property initial-value on the stylesheet's parser, keep an unmatched value as written (#45806). 2 real stylesheets - `@media {}` is `all`. A media query that stops after `and`, `or` or `not` (`@media screen and {`) is a build error; `main` printed `not all` or dropped the condition, silently. The `@layer` and type selector parts are not taken (#46869). 2 real stylesheets - Accept a name-only `@container` and a `style()` feature with no value (#46689). 1 real stylesheets - Parse `<timeline-range-name> <percentage>` keyframe selectors (#45652) - Allow `or` inside parentheses after a media type (#45088) - Print the `=` comparison in media and container queries as `=` (#45071) - Parse the @font-palette-values font-family descriptor as a list of family names (#45484) **Selectors** - Never wrap a selector list in :is() (#46787). 414 real stylesheets - Stop emitting :-webkit-any() copies of :is() that outrank the original rule (#42029). 373 real stylesheets - With browser targets, a selector list with a vendor-prefixed member was split, which revives hacks that rely on other engines dropping the whole list: `.s *::-ms-backdrop,.s{flex:auto}` applied everywhere. Rewritten; the list is printed as written (#42100). 42 real stylesheets - Print the vendor prefix on downleveled :any-link, :read-only, :read-write, :placeholder-shown and :autofill (#42131). 12 real stylesheets - 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 (#43165). 7 real stylesheets - Only reject bare :global / :local pseudo-classes in CSS modules (#38534). 5 real stylesheets - Stop truncating `@scope to (...)` and stop replacing `&` in the scope-end selector (#43146). 2 real stylesheets - `[xl|href$=".pdf"]` lost its namespace prefix (#45159). 2 real stylesheets - Match An+B keywords exactly and take only `+` as the leading sign (#42953). 1 real stylesheets - Parse ::details-content and ::picker() like ::part() (#45504). 1 real stylesheets - Parse :window-inactive as an ordinary pseudo-class, and accept it after ::selection (#45815) - Print :nth-col(1) and :nth-last-col(1) in functional form (#41575) - `li::before::marker` failed the build (#45810) - `&` inside `:nth-child(... of S)` is resolved when nesting is compiled away. The second selector serializer (326 lines) is deleted (#43024) **Colors and `Bun.color`** - Keep light-dark() as written under the default browser targets (#42009). 227 real stylesheets - Number and percentage channels parse in `lab()`, `lch()`, `oklab()`, `oklch()`, `hsl()` and `hwb()`; the hue of `lch` / `oklch` is no longer scaled by alpha (#45084). 130 real stylesheets - `Bun.color` returns `null` for `currentColor`, system colors and `light-dark()` in the converting formats, instead of the CSS keyword as a string (#33047). 130 real stylesheets - Relative colors: channel keywords are on the scale of their function (`rgb(from #369 calc(r + 10) g b)`), the origin's alpha is kept, a relative color kept as written gets no `@supports` copies, and a result outside sRGB is not folded (#45362). 31 real stylesheets - Keep rotate: 0deg out of none, and keep the origin alpha in relative colors (#38486). 13 real stylesheets - Compute the D50 reference white in f32 so greys convert to lab(l 0 0) like lightningcss (#38535). 7 real stylesheets - Return a missing hue for a grey from conversions into lch, oklch, hsl and hwb (#45201). 5 real stylesheets - Fix color(a98-rgb), srgb-linear relative colors and color-mix(in xyz-d50) (#38487). 4 real stylesheets - Match named colors without regard to ASCII case (#45092). 3 real stylesheets - Fold min(), max() and clamp() over plain numbers (#39515) - `calc(l - .08)` in a percentage channel of a relative color became NaN and printed `#000` (also #45970) (#45971) - Bun.color: convert color() inputs instead of returning them as a CSS string (#38488) - A `color-mix(in srgb, ...)` result outside sRGB prints `color(srgb ...)` instead of a gamut-mapped hex (#39269) **`calc()` and numbers** - Rounding to 6 significant digits ate into the integer part: `z-index:2147483647` became `2147480000`, `max-width:9999999px` became `10000000px`, `--x:1234567.5` became `1234570`. 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 (#46715). 43 real stylesheets - A number without a unit is no longer repaired into `px` (`height:1.5715` is 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`) a `calc()` that folds below zero prints `0` instead of an invalid negative literal, and a negative literal no longer replaces the valid declaration before it (`width:10px;width:-5px` printed `width:-5px`) (#42104). 24 real stylesheets - Print numbers from the f32's shortest digits, not its f64 expansion (#38565). 17 real stylesheets - Print negative numbers below 1 with a single minus sign (#38516) - Keep math functions over two different length units as written (#38489) - Fold rem() with a truncated quotient, so it keeps the sign of the dividend (#32286) - A one-argument `min()` / `max()` around a sum printed without any function (`width:100% - 20px`) (#46145) - `round()` breaks ties upward and handles a negative step as browsers do (#45249) **CSS modules** - A CSS module exports its `@keyframes` names (`styles.spin`). Hashing is unchanged (#46720). 145 real stylesheets - A name inside `:global(...)` prints plain but was exported as `name_<hash>`, a class no rule has (also #38573) (#45487). 90 real stylesheets - Keep container names global in CSS modules (#45247). 6 real stylesheets - Tag the keyframes name when an `animation` value falls back to raw tokens (#46034). 6 real stylesheets - In a plain stylesheet `composes: a from "./b.css"` was treated as an import: the file was inlined and an absolute path printed (#45961). 4 real stylesheets - Drop a rejected composes instead of failing the file at print time (#38538). 1 real stylesheets - Keep every rule's custom properties for the composes conflict check (#38523) - Keep composes working after a nested rule in the same block (#38520) - `IdentOrRef::hash` read 2 of 16 bytes, so in release builds every class of a module hashed alike (#30784) **Tokens, `url()`, diagnostics** - The block skip stack no longer allocates on every block exit (#39533) - Report the diagnostic's line without its line breaks in position.lineText (#38342). 192 real stylesheets - Compare `url()` values by their text (#46895). 26 real stylesheets - Values kept as tokens keep the blanks they were written with instead of having them re-invented at print time: `attr(x type(<number>))` printed `type(< number> )`, which browsers drop. Minified output still drops the blanks that cannot matter (#45577). 19 real stylesheets - The IE star hack (`*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 (#46751). 8 real stylesheets - 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). 7 real stylesheets - Move the skip loop of parse_until_before out of the generic body (#39520) - Remove dead code from bun_css: the unused `impl Default for Property` (#45618) - Default the at-rule and qualified-rule parser traits, drop the url_to_css shim (#31999) **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.ts` never ran in CI and timed out on debug builds; it is a deterministic table now (#38500) - `nth-anplusb-ident.test.ts` counts heaps in a child process (#46331) - `color.test.ts` checks the ansi-16 sweep with one `expect()` (#42824) - Not CSS, but it failed this PR's CI and fails on `main` too: `ws-proxy.test.ts` on Windows, "a message sent during a TLS renegotiation arrives after it". The helper server now renegotiates from the callback of the frame's write (#46170) **No pull request covered these** - `--v: 5.0.1` printed as `5.1`, `--w: 1.0.0` as `10`, `--x: 1+2` as `12`, `2n+1` as `2n1`: a number token that touches another keeps its source text. - `color-scheme: auto` printed as `normal`, and `color-scheme: dark light` as `light dark`. - A bare `:global` or `:local` in 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. - The scrollbar states (`a:horizontal`, `::-webkit-scrollbar{&:horizontal{}}`) are valid in a plain compound, as in WebKit and Blink. - Without browser targets, `-webkit-box-align`, `-ms-flex-align` and the like were dropped next to their standard form (flatpickr, foundation-sites, materialize-css: 28 stylesheets), and `-webkit-fill-available` before `-moz-available` (beercss). - `(-o-min-device-pixel-ratio: 2/1)` printed as `2`. - `sin()`, `cos()` and `tan()` are exact at multiples of 90deg: `calc(100px * cos(90deg))` printed `-.00000437114px`. - A `display-p3` color at targets that have P3 but not the other `color()` spaces (Safari 10.1 to 14) got itself as a fallback. - `width: rem(-18px, 5px)` folds to a negative literal once `rem()` is right; it prints `0`. - Consecutive nested declarations rules, which arise when same-query `@media` blocks 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 - Four longhands are still folded into a `transition` the author did not write where the targets add a prefixed copy of a property (`mask-image` at Chrome 80): only the shorthand gives the copy its own times. - A later `background`, `box-shadow` or `border-*` declaration still loses to the `@supports` copy of an earlier `var()` value of the same property with a wide-gamut color. Same on `main`; #45560 is taken for the properties of the fallback handler and for unparsed values only. - A merged rule prints the unprefixed name in the passes of the rule it absorbed: `.d::-webkit-input-placeholder{}.d::-moz-placeholder{}` prints `::placeholder` for the second (#42106, not taken: it breaks the merge of prefix-equivalent rules). - The `border`, `outline` and `font` shorthands, `line-height`, `scroll-padding`, `background-size` and a few more still print a `calc()` that folds below zero as a negative literal. No real stylesheet has one. - `all` is not a property Bun knows, so `a{margin-top:1px;all:unset}` prints `all` first. Same on `main`. - A selector kept as raw tokens loses the blank before `*` when minified: `--sel: .dark *` prints `.dark*`. Same on `main`. - `width:calc(0% + 12px)` folds to `12px`, which lays out differently in a table cell. Same on `main`. - The `: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 on `main`. - A fraction is still rounded to 6 significant digits (`12345.678` prints `12345.7`, `33.3333333333%` prints `33.3333%`). Keeping every digit would add up to 282 KB over the corpus. - `@keyframes "quoted"` is hashed in a module but not exported. - In a module, a keyframes name inside a `var()` fallback is not hashed: `animation: var(--enter, fade 1s)`. Same on `main`. - A `color-mix()` or relative color folded to a hex inside a gradient changes the interpolation from Oklab to sRGB (VS Code, clawhub). Printing `color(srgb ..)` instead needs a fallback for `mask-image`, `list-style-image` and custom properties at the default targets, which have none. - A comma list in `@container` still fails the build. - After a bare `: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 - Duplicates of a pull request that lands here: #36961 (of #37979), #38478 (of #46662), #38480 (of #46733), #38482 (of #46715), #38491 (of #38500), #38508 (of #45201), #38513 (of #45362), #38573 (of #45487), #38640 (of #38500), #45525 (of #45247), #45589 (of #46756), #45970 (of #45971) - Already fixed on `main`: #38674 - Right, but for input nobody writes (0 of 10,116 stylesheets, and not new syntax): #31916, #38526, #38592, #39349, #41139, #41801, #42042, #42484, #43078, #45604, #45908, #46086, #46221, #46224, #46273, #46694, #46707, #46737, #46755, #46816, #46870, #46934 - Right or harmless, but not worth their code, their bigger output or the behavior they change: #30905, #31924, #33332, #34426, #36225, #38006, #38512, #38547, #38551, #38561, #38572, #38576, #38607, #38623, #38667, #38692, #42106, #42912, #42918, #43407, #43434, #44700, #45442, #45546, #45747, #45759, #45825, #45946, #46061, #46127, #46206 - Need a decision (see above), stay open: #40368, #40369, #44647, #44648, #45814 - Stays open: #45787 Out of scope: CSS linking and chunking in the bundler, the dev server, docs-only pull requests, and repository-wide sweeps that touch `src/css` in 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? - The 35 CSS test files pass on a debug build, also with `FORCE_COLOR=1`. 600+ of the tests in this branch fail on a build of `main`. An earlier head of this branch passed all 179 CI jobs that ran. - The reproduction of each issue listed above was run on `main` (wrong) and on this branch (right). - Differential run over the 10,116 stylesheets, `main` against this branch, minified without targets and through `Bun.build` at the default targets: no stylesheet that builds on `main` fails; 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. - CSS modules are checked for consistency instead, since hashed names defeat the comparison: every reference points at a name the output defines, every export exists. - `Bun.color`: 59 inputs in 13 formats on both builds; every changed cell was read.
…prefix-handler # Conflicts: # src/css/properties/properties_generated.rs # src/css/rules/style.rs
#47025 prints the vendor prefix on a downleveled `:autofill`, so the vendor prefix section no longer lists it as missing.
Question for a maintainer: keep a prefix that the author wrote (this PR, autoprefixer, esbuild), or replace it (lightningcss)? Keeping needs state across the
!importantlist. Details in Notes.Problem
bun buildwrites no vendor prefix foruser-select,appearance,hyphensand five more properties under its default browsers. The docs say prefixing is on.PrefixHandlerwas never ported (src/css/properties/prefix_handler.rs). Port backlog (MVP of CSS parser #14167), no user report. css: consolidate the open CSS parser fixes #47025 counted 1,116 real stylesheets that lack a needed prefix.Fix
PrefixHandleras the 16th entry of the handler chain, as in lightningcss, and the nine rows it needs. It keeps written prefixes, merges only equal values, and also prefixesvar()andinitial.print-color-adjustgets no-moz-form. css: consolidate the open CSS parser fixes #47025 landed the two other changes that the rows need.css.test.ts(124 new cases, 56 fail before),vendor-prefix.test.ts, the other css suites.Background
src/css/prefixes.rssays which browsers need which prefix. A typed property stores a value and its prefixes, and prints one line for each.transitionlists prefixes.Downsides
* { user-select: none }ships-webkit-user-select: none..textgrows by 18,432 bytes.bun buildprints acomposesconflict error for these names in two more cases. The exit code stays 0.Notes
After #47025. This branch has main at 7942008. #47025 lists this PR with #40368 and #40369 (default browsers,
cssTarget) as needing a decision. It landed two changes that this PR carried: the duplicate-rule compare of names and importance (from #33684) and the prefix print of@import … supports()(from #38589). So this PR no longer touchessrc/css/rules/style.rsandsrc/css/rules/supports.rs, and it is 14 files. #47025 did not take #42042 (all), #45747 (owner dispatch), #46206 (origin rows) and #38692 (transition list). Where a note below names one of them, that change is not coming.The question. For
-webkit-text-size-adjust: 100%; text-size-adjust: 100%under the default browsers, lightningcss 1.32.0 prints-moz-text-size-adjust: 100%; text-size-adjust: 100%: the written-webkit-line is gone, and the default list has no iOS Safari, which needs it. Bun's flex, transform, transition, border-radius and box-shadow handlers follow lightningcss. This handler keeps the prefixes that the author wrote. It can remove a dead rule that main prints. An equal prefixed and plain pair is now one declaration. The duplicate check then matches an earlier rule with the same names. Under the default browsers.btn{user-select:none} .q{top:0} .btn{-webkit-user-select:text;user-select:text}loses its first rule, which the last rule overrides. Main prints all three. The rule of this PR is the keep rule of esbuild for a whole block, with one exception: afterall, Bun adds the copy again, as autoprefixer and lightningcss do, also across the!importantlist. The keep rule needed state across the two importance lists twice in review (e0fc9b1, f52e2a1). If you want the lightningcss rule here,PrefixHandlerloseswritten,importantand the twosee_*_listcalls inDeclarationBlock::minify,handle_propertytakes the prefixes of the targets alone, and the cases that pin a kept prefix get new expected output.Where the handler differs from lightningcss 1.32.0, default browsers.
-webkit-text-size-adjust: 100%; text-size-adjust: 100%-moz-text-size-adjust: 100%; text-size-adjust: 100%-webkit-text-size-adjusttoo-webkit-user-select: text; user-select: none-webkit-user-select: noneafter the written line (parcel-bundler/lightningcss#977)-webkit-user-select: none; cursor: pointer; user-select: noneuser-select: nonein front ofcursorappearance: none; appearance: base-selectbase-select, three timesappearance: none; -moz-appearance: textfieldtextfieldfor every prefixnonefor every prefix, then-moz-appearance: textfielduser-select: initial,user-select: var(--x)-webkit-copyprint-color-adjust: exact-moz-print-color-adjust: exact-moz-lineThe rules behind the rows:
-webkit-value stays the value for Safari.-khtml-user-select).allresets everything..a{-webkit-user-select:none} .a{user-select:text}are two rules, so the second one gets its-webkit-copy, also after Bun merges them. autoprefixer, esbuild and lightningcss print the same for two rules. One exception: a merged rule is one rule forall..a{-webkit-user-select:text;user-select:none!important} .a{all:unset}gets-webkit-user-select:none!importantwhen the two rules are adjacent, and not when another rule is between them. Over 1,764 pairs of rules withalland!important, 28 differ in this way. In each of the 28 the merged rule gives the browser that reads only the prefixed name the value that the other browsers get.!importantdeclarations of a rule are a list of their own. A prefix that the author wrote in one list is not added in the other:-webkit-user-select: text; user-select: none !importantstays as written.allresets a written prefix here too.-webkit-user-select: text; all: unset; user-select: none !importantgets-webkit-user-select: none !important, as autoprefixer 10.6.1 and lightningcss 1.32.0 print it. esbuild 0.28.2 prints that block as written. Anall !importantalso resets what the normal list wrote. Every declaration namedallcounts, also one with an invalid value such asall: none, which a browser drops..a{user-select:none} .b{user-select:initial}gives Safari thenoneand not the reset.color-adjust, and no Firefox reads-moz-print-color-adjust. The table entry insrc/css/prefixes.rsis corrected by hand, with the same condition inbuild-prefixes.js. This PR does not writecolor-adjust.Three tests in
css.test.tscheck the declaration that three kinds of browser apply: one that reads onlyuser-select, one that reads both names, and one that reads only-webkit-user-select. They share one oracle,changed_winnersintest/js/bun/css/util.ts. For the last kind of browser it says: a written prefixed declaration that wins in a rule stays the winner, and without one the browser gets the declaration that wins for the unprefixed name. "the prefixes change no winning declaration" runs every block of up to four declarations over six declarations (1,554 blocks). "!importantandallchange no winning declaration" runs every block of up to three over eight declarations (584 blocks). "two rules of one selector keep the winning declaration" runs 468 pairs of rules with and without!important.Repro.
bun buildof a sheet withbackdrop-filter,user-select,appearance,hyphens,mask-image,backface-visibility,box-decoration-breakandprint-color-adjustgoes from 0 of 8 prefixed to 6 of 8.mask-imageneeds the mask handler.backdrop-filteris #44648. The five more properties of the first bullet arebackface-visibility,box-decoration-break,print-color-adjust,tab-sizeandtext-size-adjust. Bun's own sheets have the gap:src/runtime/bake/client/overlay.cssand thebun initReact templates write bareuser-select: noneandappearance: none.Against lightningcss 1.32.0. Measured on the base before #47025. 306 one-declaration sheets cover every place where lightningcss can write a prefix.
The 37 default places that still differ:
mask*andmask-border*(18),text-decorationandtext-emphasis*(7),backdrop-filter(4, #44648),resolutionmedia features (4),image-set()inlist-style-image,:autofill,@supports (user-select: none), and the-moz-print-color-adjustline above. #47025 has since added the:autofillprefix.Fixture sheets. The 15 sheets in
test/js/bun/css/files, target browser, against main at 7942008: 6 outputs change. 58 prefixed declarations added (-webkit-appearance18,-moz-appearance18,-webkit-user-select16,-webkit-backface-visibility4,-moz-text-size-adjust2), 0 removed, output 3,067,457 to 3,069,093 bytes. Bun keeps 40 written declarations that lightningcss removes there (-moz-user-select14,-webkit-box-sizing14,-ms-user-select9, three others).--target=bunoutput changes in 1 of 15: in one rule ofpico.css,appearance: nonejoins its prefixed copies in front ofdisplay: inline-block. lightningcss does the same.#47025 measured the gap on 10,116 real stylesheets: 1,116 lack a prefix that they need at the default targets.
Duplicate rules. With typed rows and merged prefix pairs, more rules reach the duplicate-rule check:
.a{-webkit-user-select:none!important;user-select:none!important} .b{top:0} .a{user-select:text}must keep its first rule. The check has to compare names and each importance list. #47025 landed that, and the cases incss.test.tsstay as pins.@import … supports(). #47025 landed the print of the written prefix. With typed rows,supports(-webkit-user-select: none)prints((-webkit-user-select: none)).@supports (user-select: none)itself gets no prefixed alternative: a(property: value)group does not parse as a declaration on main, and the lightningcss stepSupportsCondition::set_prefixes_for_targetsis not ported. For the same reason a group of an@supportsrule or of anand/orchain is raw text and prints as written, also for the names that now have a row. Six conditions incss.test.tsand two imports invendor-prefix.test.tspin the written names.Second pass. Bun minifies a merged rule again. A rule that is minified twice prints the same. A merged rule can get one more copy after an
all(the exception above).transition: user-select 1snow gets-webkit-user-select 1sin front, and when its rule is merged with a neighbour the prefixed entry is there twice. That is an existing bug of the transition handler (main prints-webkit-background-clip 1stwice for.a{transition:background-clip 1s} .a{color:red}). #38692 had the fix, and #47025 did not take it.CSS modules.
composesfrom another file reports "The value of user-select in the class foo is undefined" in two new cases: one class sets-webkit-user-selectand the otheruser-select, and a class whose second rule (for example in@media) does not set the property. Both are how properties with a row already behave (-webkit-transitionandtransition).bun buildprints the error and still exits 0, as for everycomposesconflict on main.Cost. Measured on the base before #47025, with the method of #45747:
qemu-x86_64 -d exectrace ofStyleSheet::minifyon release builds of main (5866b5f) and this branch at 09c03b1, one rule with 5,000 and 10,000 declarations. The two later!importantcommits (e0fc9b1, f52e2a1) are not in these numbers. They add one check for each rule that the!importantlist is empty, and one pass over each list of a rule that has!importantdeclarations.zoom: 1--x: 1display: blockbox-sizing: border-boxuser-select: noneappearance: nonecolor: redmargin: 0background: redanimate.csstailwind.cssbootstrap-4.cssbulma.cssThe handler is the last of 16 chain entries, so a declaration that no handler claims pays for one more call. A whole
bun build <sheet>runs 0.2 to 0.7% more user instructions (perf_event_open, 40 interleaved pairs, six fixture sheets), and the wall-clock difference is inside the noise. Allocator calls for each declaration are 0 before and after. 4,000 merged rules cost 2.0 times what 2,000 cost.Sizes.
size -A, also at 09c03b1:.text81,071,045 to 81,089,477,.rodata22,532,844 to 22,536,940.DeclarationHandler6,848 to 7,000 bytes (two for each stylesheet).PropertyBitset32 to 64 bytes: 263 tags no longer fit in 256 bits.Propertystays 216 bytes,PropertyId24.allwith!important(f52e2a1). Found in review, with no user report. The head before it printed these blocks as main and 1.4.3 do. The 15 fixture sheets give the same 60 outputs before and after (two targets, plain and minified).Other open PRs. #40369 raises the default browsers and #40368 adds a
cssTargetoption. Both are open. With the list of #40369,appearance,backface-visibilityandtab-sizeneed no prefix. The bundler test and the docs example only use properties that need one under both lists. #44648 (open) addsbackdrop-filter. Three things that earlier notes planned for a second lander do not apply, because #47025 did not take the other PR:is_allkeeps its arm for the unknown nameall(#42042), the twosee_*_listcalls need no owner mark (#45747), andtransform-originandperspective-originstay out ofdefine_prefixes!until their rows parse a valid value (#46206).Not taken from the self-review.
transform-origin,perspective-origin,mask-position,all). That fix is separate work, and this handler leaves the first two out until they parse.@supportsconditions. They need a(property: value)group that parses as a declaration first.allin f52e2a1: only a CSS-wide keyword or a value withvar()resets. That needs a row forall, which main does not have.allis what lightningcss prints too, so it stays under both answers.no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/bundler/cli.test.ts