fix(metro): compile the native stylesheet from the authored CSS, never its web build - #461
YevheniiKotyrlo wants to merge 2 commits into
Conversation
…r its web build The transformer asked Expo's worker for the web transform of every native stylesheet and compiled that. Expo's web transform runs lightningcss against the project's browserslist, so the native compiler received the sheet as lowered for browsers: every `:dir()` a `:lang()` list the compiler drops (nativewind#453), every `oklch()` a `lab()` beside a hex-fallback `:root` that doubles each root variable and defeats variable inlining, and all of it moving with the browserslist. Run the project's PostCSS and Sass through Expo's own modules and compile their output. No browser build is produced for a native stylesheet. Closes nativewind#453
EvidenceMeasured on a real Tailwind v4 entry, through Expo's actual web transform:
All of it moves with the project's browserslist: once a window rolls past Chrome 119 those The test drives the real
Also measured and deliberately unchanged: a stylesheet Expo's web pass would have recovered from with No device frame of its own — this defect and #459's paint the same picture, an |
Both fixture projects authored plain CSS, so `matchSass` answered null on every case and `compileSass` was never reached. Measured: deleting the Sass call outright left all 10 tests green, so a whole arm of the new function was unobservable. A third project authors `.scss` — a `$variable` and a nested `&.nested`, neither of which is CSS — and stands in for Expo's Sass helper, which resolves the optional `sass` package from the project root and throws when it is absent. Three cases: the syntax and filename the transformer hands over, that the compiler receives what Sass produced rather than the authored source, and that the stylesheet's own output is still emptied. Its own file, for the reason the plain project has one: Expo resolves a project's pipeline once per process. The same deletion now reddens all three.
Problem
The Metro transformer compiles the web build of a stylesheet for a device, so a native bundle receives CSS lowered for the project's browserslist — which is why every
rtl:/ltr:rule is dropped (#453), plus two more effects nobody has reported.That transform is
@expo/metro-config'stransformCss: PostCSS, then Sass, then lightningcss withtargets: browserslistToTargets(browserslist(…, { path: projectRoot })). So the native compiler is handed the stylesheet as lowered for the project's browsers — or for browserslist'sdefaultswhen the project declares none. Measured on a real Tailwind v4 entry::dir(rtl)→:is(:lang(ae), :lang(ar), …)rtl:/ltr:rules are dropped — this is #453oklch()→lab(), plus a hex-fallback:rootbeside a@supportscopyinlineVariablesinlines none and 331 colour utilities compile to a runtimevar()lookuptransform-origin: left top 30pxre-serialised["0%", 30, 0]instead of[0, 0, 30]None of it is visible in the authored CSS, and all of it moves with the browserslist: once a project's window rolls past Chrome 119 its
rtl:classes start working on native with no change to anything.Solution
The native path needs two of the web transform's three steps, not its output. The transformer runs the project's PostCSS and Sass through the same Expo modules the web transform calls, and compiles their output — no browser build is produced for a native stylesheet.
transformPostCssModuleresolves and caches the project'spostcss.config.*exactly as the web transform does, so a plugin runs for a device iff it runs for a browser. The third step — lightningcss — also collected@importdependencies, and the transformer already discarded them (it returns the injection module's own transform), so nothing the native path had is lost.compile()itself was never wrong: its own lightningcss pass sets no targets and preserves:dir()verbatim, measured on 1.30.1. It was handed the wrong input — which is whyFeatures.DirSelectorin the compiler'sincludelist does not help.Tests
src/__tests__/metro/native-stylesheet-input.test.tsdrives the realtransformagainst a stand-in for Expo's worker whose web branch does what Expo's does: the project's PostCSS, then lightningcss against one browser without:dir()support (chrome: 100), so the lowering happens for real rather than being imitated. The project is a directory with apostcss.config.jsonnaming@tailwindcss/postcss, a@themecolour declared once inoklch(), and a stylesheet generatingbg-[#00f] rtl:bg-[#f00] bg-brand.Eight cases. Two are red on
mainand are the bug's own shape —rtl:bg-[#f00]carries thedircondition with the worker never asked for a web build, andbg-brandinlines to a literal. The other six pin what must not move: the injected stylesheet is what PostCSS produced, the stylesheet's own output is emptied andskipCached, and a web build, a non-stylesheet module and a stylesheet requested as an asset each reach the worker untouched.native-stylesheet-plain-project.test.tsis a second project with no PostCSS config at all, in its own file because Expo resolves a project's pipeline once per process.native-stylesheet-sass.test.tsis a third, authoring.scss— a$variableand a nested&.nested, neither of which is CSS. It stands in for Expo's Sass helper, which resolves the optionalsasspackage from the project root and throws when it is absent, and asserts the hand-off: which syntax the transformer names, and that the compiler receives what Sass produced rather than the authored source.It exists because the arm was measurably unobservable — deleting the
compileSasscall outright left all 10 earlier cases green. The same deletion now reddens all three.No test in the suite compiles a stylesheet whose authored CSS differs from its browser build — every fixture survives a browser pass unchanged, which is why this has never surfaced here.
Mutation-proved: reverting the
src/diff and re-running these files alone turns 6 of 13 red.Verification
yarn test src/__tests__/metro13 passed ·yarn typecheck0 ·yarn lint0Also measured and deliberately not changed: a stylesheet Expo's web pass would have recovered from with
errorRecovery— a malformed selector, a stray brace, an unknown at-rule — compiles to the same surviving rules, so a device does not become stricter about broken CSS than a browser was.Known limits
Closes #453. That report places the lowering inside
compile()'s own lightningcss call; it happens one hop earlier.Base
Branched off
f70c402.mainhas since taken #451 (a5002c5). 1 of the 9 files this changes also moved there (src/metro/metro-transformer.ts), and it still merges cleanly onto currentmain. Every measurement above was taken onf70c402. Say the word and I will re-apply it onto currentmain.