Repository navigation
fix(compiler): scope single-definition variable inlining to the declaring block - #413
Open
YevheniiKotyrlo wants to merge 1 commit into
Open
YevheniiKotyrlo wants to merge 1 commit into
YevheniiKotyrlo wants to merge 1 commit into
Conversation
This was referenced Aug 14, 2026
YevheniiKotyrlo
marked this pull request as ready for review
August 15, 2026 19:13
Contributor
Author
YevheniiKotyrlo
force-pushed
the
fix/single-definition-inliner-scope
branch
from
October 11, 2026 12:35
b77fafb to
dcef09c
Compare
This was referenced Oct 11, 2026
YevheniiKotyrlo
marked this pull request as draft
October 23, 2026 15:52
YevheniiKotyrlo
force-pushed
the
fix/single-definition-inliner-scope
branch
from
October 23, 2026 16:38
077d5c6 to
aeb29d6
Compare
…ring block The inliner folded a custom property declared once into every var() in the stylesheet, whether or not the reference's element can hold the declaration. It now folds a value only into the block that declares it, or from a declaration outside @media and @container that reaches every element: `*`, or `:root`, `:host` and `html` for an inheriting property. An @supports block counts as extractRule decides it, through one rulesTheCompilerKeeps, so a dropped block is emptied before the census; a registered `*` property that nothing declares folds to its initial value; and a declaration is removed once no rule reads it. Values the fold no longer normalises reach the token path, which now reads `0` and `false` as values and gives a <ratio> the descriptor parseAspectRatio gives it. A compiled box-shadow drops a transparent shadow, as the runtime does.
YevheniiKotyrlo
force-pushed
the
fix/single-definition-inliner-scope
branch
from
October 23, 2026 18:25
aeb29d6 to
2dbd607
Compare
YevheniiKotyrlo
marked this pull request as ready for review
October 23, 2026 18:48
This branch has not been deployed
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.


Problem
inlineVariablesfolds a custom property declared once into everyvar()that reads it, then deletes the declaration. It counts declarations with a visitor that cannot see the rule around them, so the fold ignores where the declaration applies. Onmain:.parent { --x: 10px } .child { width: var(--x) }compiles.childtowidth: 10, so an element carrying only.childgets a width. Chromium, Firefox and WebKit give it none (css-variables-1 §2). Whether.childfolds also depends on unrelated rules: a second--xanywhere in the sheet turns the fold off.@supportsblock still declares. Tailwind registers its utility variables with@propertyand repeats each initial value in an@supportsfallback for browsers without@property. The compiler drops that block (supportsConditionValid), but the visitor counts the declaration inside it, and that declaration is the value that folds. With@property --x { syntax: "*"; inherits: false; initial-value: 10px }, a fallback* { --x: 20px }and.b { width: var(--x) },mainfolds20. The same sheet renders10withinlineVariables: false, and in Chromium and Firefox.inherits: falseis ignored.:root { --x: 20px }folds20into.balthough the registration says other elements hold the initial value..a { --c: 0.5; color: rgb(255 0 0 / var(--c)) }loses--c, because the fold does not reach inside an unresolved colour, and the alpha comes out empty.Folding also decides which parser a value meets, and three parsers disagree with the runtime:
0renders nothing unfolded, becauseparseUnparsedread0as an absence;<ratio>keeps the text16 / 9where the property parser gives16/9;Solution
The inliner takes its census from its own walk over the stylesheet. It folds a value into a
var()only when every element the reference's rule matches holds that value:*, or:root,:hostandhtmlfor a property that inherits, outside@mediaand@container.An
@supportsblock is decided by onerulesTheCompilerKeeps, whichextractRulenow uses too, so a kept block is unconditional and a dropped one is emptied before the census. A registered*-syntax property that nothing declares folds to its initial value (css-properties-values-api-1 §3.3). A declaration is removed once no rule reads it, so one another rule reads at runtime stays.inlineVariables.excludestill keeps a variable a separately compiled stylesheet orvars()needs.The three parsers now agree with the runtime:
0andfalseare values, a<ratio>gets the descriptorparseAspectRatiogives it, and both paths drop a transparent shadow through oneisTransparentColor.Existing expectations change in three places:
transparent shadow is preservedpinned the compile-time path keeping it. Both paths drop it now.scale-0andscale-50fold now, so they get the compiler'sscaleXandscaleYinstead of the runtime'sscale. Both render the same scale.variables.test.tsxread"green"and"red"where they read"#008000"and"#f00". The colour now reaches the element by runtime inheritance instead of a fold into a rule that does not declare it, and the runtime keeps a named colour as written. React Native reads both.Tests
src/__tests__/compiler/inline-variables.test.ts: where a value folds and where it must not, by selector, conditional rule,@supports, registration, nesting, keyframe and!important, and when a declaration is removed or kept.src/__tests__/native/variable-inlining.test.tsx: what the runtime paints for the same stylesheets.src/__tests__/native/variable-fold-parity.test.tsx: every value kind lightningcss can put in a custom property, rendered as written, through a folded variable and through a kept one. Folding renders the written value for every kind. A kept variable does too, except where a row names the runtime's open defect: a named colour stays a name,scaleandrotatekeep their own keys until fix(transforms): resolve and cascade transform properties as browsers do #389, a1px 2pxshorthand stays a list, and a lonetransition-durationstays unanimated.src/__tests__/native/box-shadow.test.tsx: transparent shadows dropped on both paths, and a transparent list still replacing an earlier one.On
main, 45 of the 209 cases in the six files fail; all pass here. Twenty-five mutations, one per rule, each fail at least one case.The scope cases agree with Chromium 156, Firefox 157 and WebKit 27.2:
*withinherits: falsereads20px.:rootwithinherits: falsereads the initial10px.10px.initial, read directly or through another variable, gives way to the fallback10px.--x: 20px, the declaring rule's ownvar(--x)reads20px.In the Tailwind-shaped fallback, Chromium and Firefox drop the block as the compiler does. Playwright's WebKit keeps it.
Verification
On Ubuntu with Node 22.18.0, each job in
ci.ymlbut the iOS build, on this branch merged intomain(2421ea7):yarn lintandyarn typecheckcleanyarn test --maxWorkers=2 --coverage: 1472 passednode --test node/__tests__/*.mjspassedyarn buildandyarn example expo export --platform web, with nothing unstaged after either buildyarn commitlintcleanThe iOS dev build ran with my Mac's Xcode 27.1, where
ci.ymlpins 26.6. It fails compiling Expo's prebuiltExpoModulesJSIbefore any of this package's code, and fails the same way onmain.Compiling a Tailwind v4 stylesheet of 1,708 classes costs the same as on
main: about 480 ms on Node 26.12.1, with this branch 1.7% faster per round over 360 compiles a side, inside the 2.1% two runs of the same tree differ by.No existing issue tracks this. I searched for
inlineVariables,variable inlining,custom property scope,@property initial-valueand@supports.Device evidence, before and after: #413 (comment)
Known limits
.bunder:root { --x: 20px }withinherits: falsestill resolves20. The compiler now leaves that reference to the runtime instead of folding it, which is the prerequisite for fix(native): honourinherits: falseon registered custom properties #412.vars()write cannot reach a reference folded in its own block.excluderestores it, as doesinlineVariables: false.<length>or<color>, is left to the runtime, which reads the same value from the registration.@mediaor@containerquery the compiler drops, and a selector it drops, still count: a declaration there keeps a variable from folding, and a read there keeps its declaration. Neither changes what renders.var()left in the sheet makescompile()throw (compile()fails with "failed to deserialize; expected an object-like struct named Specifier, found ()" on lightningcss >= 1.31 (any unresolvedvar()); the 1.30.2 guard doesn't cover it #470). This leaves unfolded the cross-block referencesmainfolded, so more stylesheets meet that untilcompile()fails with "failed to deserialize; expected an object-like struct named Specifier, found ()" on lightningcss >= 1.31 (any unresolvedvar()); the 1.30.2 guard doesn't cover it #470 is fixed.variable-fold-parity.test.tsxnames row by row. Two lose what the written value renders:margin: var(--v)over1px 2pxreaches React Native as one list, and atransition-*longhand read through a variable leaves its element unanimated. Both already happen onmainto a variable declared twice or excluded.Merge order
This conflicts with #379, #389, #391, #411, #412, #417, #420, #489, #491, #494, #495, #497, #501 and #503; I'll re-cut whichever lands after the others.