Repository navigation
fix(compiler): recover from a rule lightningcss cannot parse - #493
Open
YevheniiKotyrlo wants to merge 2 commits into
Open
YevheniiKotyrlo wants to merge 2 commits into
YevheniiKotyrlo wants to merge 2 commits into
Conversation
YevheniiKotyrlo
marked this pull request as ready for review
October 16, 2026 13:18
YevheniiKotyrlo
marked this pull request as draft
October 16, 2026 13:47
YevheniiKotyrlo
marked this pull request as ready for review
October 16, 2026 14:40
YevheniiKotyrlo
marked this pull request as draft
October 17, 2026 09:18
YevheniiKotyrlo
force-pushed
the
fix/syntax-error-recovery
branch
from
October 17, 2026 09:25
faae963 to
fb345e3
Compare
YevheniiKotyrlo
marked this pull request as ready for review
October 17, 2026 12:05
Contributor
Author
Snippet —
|
| result | |
|---|---|
main (eda2b286) |
throws Unexpected token Ident("bar"), so the sheet compiles to nothing |
this PR, merged into main |
["before", "after"] |
Run through the repo's jest on Windows with Node 26.
Through a Metro build — main against this PR
.a { color: red } <rule> { .b { color: blue } } .c { color: green } through Expo's CSS worker (@expo/metro-config 57.0.12, lightningcss 1.30.1), then transform() in src/metro/metro-transformer.ts on what it returns, as a native build runs them, and rendered from the injected stylesheet in light and dark, portrait and landscape:
<rule> |
main (2421ea77) |
this PR |
|---|---|---|
@media (foo bar), @container (foo bar), @container scroll-state(stuck: top) |
builds: Expo's worker drops the rule with a warning, and .a and .c apply |
the same |
@media (prefers-color-scheme: 0) |
Expo's worker keeps the rule with a warning, then transform() throws SyntaxError: Invalid media query, so the build fails |
builds: .a and .c apply, and .b never does |
Expo's worker run in Node and transform() in the repo's jest, on Windows with Node 26.
This was referenced Oct 21, 2026
YevheniiKotyrlo
marked this pull request as draft
October 27, 2026 21:19
The compiler records every declaration it could not translate, but nothing read compile().warnings() during a build, so a dropped float or z-index: auto was silent. The Metro transformer now prints one block per CSS file: capped in summary mode, listed in full in verbose mode, and printed again only when the file's warnings change. The warnings option sets the level, and none silences it. lightningcss also warns about input it cannot parse, such as an unknown at-rule or pseudo-element. Those warnings reach a new syntax channel, kept apart from what React Native cannot express, and the block counts them as syntax warnings; this package's own @nativeMapping is not reported. Every recorded value is text, so CompilerWarnings.values is Record<string, string[]>; the one number, a percentage line-height, is recorded as the ratio it resolved to.
lightningcss ran without errorRecovery, so one rule or declaration it could not parse threw and the whole stylesheet compiled to nothing: lightningcss 1.30.1 throws on a media or container query's <general-enclosed> condition, on scroll-state(), on var() with no argument and on a media feature value outside its syntax, such as (prefers-color-scheme: 0), all of which a browser drops or evaluates as unknown while keeping every sibling. Both passes now recover, the second because lightningcss keeps a query whose feature value it refuses, with a warning, and reads it again there. Each warning the first pass gives reaches compile().warnings() as `syntax`. The compiler test asserts which rules each case keeps and that it is reported, and the native test renders each case in light and dark.
YevheniiKotyrlo
force-pushed
the
fix/syntax-error-recovery
branch
from
October 27, 2026 21:47
34704fb to
c606c22
Compare
YevheniiKotyrlo
marked this pull request as ready for review
October 27, 2026 22:44
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
The compiler runs lightningcss without
errorRecovery, so one rule or declaration lightningcss cannot parse throws, and the whole stylesheet compiles to nothing. lightningcss 1.30.1 throws on@media (foo bar)- a<general-enclosed>condition, which a browser evaluates as unknown - on@container scroll-state(stuck: top), onvar()with no argument, and on a value outside a media feature's syntax, such as@media (prefers-color-scheme: 0). A browser keeps every rule beside each of them (css-syntax-3 §5, mediaqueries-5 §3.2).A Metro build compiles what Expo's CSS worker returns, and that worker already runs lightningcss with
errorRecovery: true, so it drops the first three beforecompile()sees them. The fourth it keeps with a warning, andcompile()then throws on it, so the build fails. Called directly, asregisterCSSdoes,compile()throws on all four.Solution
Both passes run with
errorRecovery: true, so lightningcss drops what it cannot parse and the rest compiles. A media query whose feature value it refuses it keeps, with a warning, so the second pass reads that query again and recovers too; what the query compiles to is the media compiler's, and #426 answers such a feature as unknown. Each warning the first pass gives reachescompile().warnings()assyntax, through the channel #430 adds.Tests
src/__tests__/compiler/error-recovery.test.tscompiles each case beside other rules: those rules survive, a media or container query with a<general-enclosed>condition is dropped and reported, the query on(prefers-color-scheme: 0)is reported, and a declaration that fails to parse goes alone.src/__tests__/native/error-recovery.test.tsxrenders the same cases in light and dark: the rules beside each style their elements, and the rule itself styles nothing,(prefers-color-scheme: 0)included. The container case avoidsscroll-state(), which lightningcss parses from 1.31.0. Both files fail on #430 without this change, every case, and the(prefers-color-scheme: 0)case in each also fails without the second pass'serrorRecovery.Verification
On Ubuntu with the workflow's steps - build, lint, typecheck,
yarn test,node --test node/__tests__/*.mjs, both library builds, the web export, the unstaged-changes checks and commitlint - everything passes, and the iOS dev build passes with Xcode 27.1, where the workflow pins 26.6.Not run: the Android example build.
The snippets, run on
mainand here, directly and through Expo's CSS worker: #493 (comment)Merge order
It carries #430's commit, whose warning channel is where a dropped rule is reported, so #430 lands first.
#379 changes lines of
metro/metro-transformer.tsthat #430's commit changes; whichever lands second rebases.#461 compiles the stylesheet before Expo's worker sees it, so with it the first three cases reach
compile()in a Metro build too; this lands first, or they fail that build.#426 answers a feature value outside its syntax as unknown, as Chromium, Firefox and WebKit do. Until it lands, a value this lets through compiles as written, as an unknown keyword already does on
main:(hover: 0)styles every screen, as(hover: foo)does,(orientation: 1)a portrait one, as(orientation: foo)does, andnot (prefers-color-scheme: 0)both schemes.