Skip to content

fix: six small findings, plus a windows CI lane - #428

Open
YevheniiKotyrlo wants to merge 6 commits into
nativewind:mainfrom
YevheniiKotyrlo:fix/small-findings
Open

YevheniiKotyrlo wants to merge 6 commits into
nativewind:mainfrom
YevheniiKotyrlo:fix/small-findings

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Aug 15, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Six small independent findings, each separable — split any of them out if you would rather take them apart.

  1. prefers-color-scheme resolved to nothing when the OS reported no preference. MQ5 §12.5 gives the feature light | dark and no third value — unlike prefers-reduced-motion (§12.1), which does have no-preference — so resolving to nothing puts it in a state the feature has no value for, and (prefers-color-scheme: dark) and (prefers-color-scheme: light) are false at once. It now resolves to light.
  2. src/web/api.tsx called a method that does not exist. react-native-web@0.21.1's Appearance exports only getColorScheme and addChangeListener — there is no setColorScheme, so colorScheme.set(…) was a hard TypeError on web. It is invisible to tsc because the file imports Appearance from "react-native", so TypeScript resolves React Native's own .d.ts while the bundler substitutes react-native-web. It now reports that the scheme cannot be set on web rather than throwing.

Solution

Six small findings, each independent.

Web reports that the colour scheme cannot be set there rather than failing quietly, and prefers-color-scheme resolves to light when the host states no preference. Which of the two MQ5 §12.5 values absence resolves to is not normative — css-color-adjust-1 §2 routes it through the UA default color scheme, which that spec leaves to the UA — so light is a choice, matching the web platform's. The rest is test and CI coverage: the platform media queries are revived, the globalClassNamePolyfill gate gets a metro test, dynamicRootVariables is added so a :root test reaches the runtime, and the unit suite runs on Windows in CI.

Tests

  1. ci: run the unit suite on windows. ci.yml was ubuntu-latest plus one macos-15, with no Windows runner — which is why a three-test failure sat in main unnoticed and why a previous cross-platform test of mine was inert on the only runner that existed. This is the smallest change here and probably the most valuable.
  2. prefers-color-scheme had no compiler test. media-query.test.ts had no case for it — its platform block is describe.skip, leaving only hover. The single existing assertion of that condition reached it via light-dark(), where the compiler synthesises the condition, so the @media parse path was unproven. The platform media queries are revived in the same commit.
  3. src/metro/ had no tests at all. metro/resolver.ts is the only import rewriter when globalClassNamePolyfill is false, and the babel plugin the only one when it is true — they are alternatives, not layers, so one of the two rewriting paths was entirely unexercised. This covers the gate.
  4. dynamicRootVariables so a :root test reaches the runtime rather than asserting only the compiled output.

Mutation-proved: reverting the src/ diff and re-running these files alone turns 6 of 27 red. Six findings, six reds — each one is covered by a case that fails without it.

Verification

tsc --noEmit 0 errors · eslint 0 problems · jest 1074 passed, 3 failed, 19 skipped.

The 3 are the Windows-only babel-plugin-tester baseline. The CI lane this adds is what would surface them upstream rather than only on a contributor's machine.

Known limits

Item 2 is a behaviour change on web for anyone currently calling colorScheme.set there — today that call throws, so nothing can be depending on it working, but it is the one item here that is not purely additive. Happy to split it out if you would rather take it separately.

yarn typecheck and yarn lint exit 0. The 3 remaining failures are the known Windows babel-plugin-tester baseline, present on main.

Base

Branched off f70c402. main has since taken #451 (a5002c5). 5 of the 11 files this changes also moved there, and 2 genuinely conflict — src/__tests__/compiler/media-query.test.ts, src/web/api.tsx. Every measurement above was taken on f70c402. Say the word and I will re-apply it onto current main.

`colorScheme.set()` called `Appearance.setColorScheme`, which does not exist.
The web plane imports `Appearance` from "react-native" and the bundler
substitutes react-native-web, whose Appearance reads through to
`matchMedia("(prefers-color-scheme: dark)")` and exposes only `getColorScheme`
and `addChangeListener`. Every call was a TypeError.

TypeScript cannot see this: it resolves react-native's `.d.ts` for that import
either way, so the substitution is invisible to `yarn typecheck`.

The browser owns the color scheme on web and evaluates
`@media (prefers-color-scheme)` itself, so there is no observable for an
override to drive the way there is on native. Throwing names that constraint;
returning silently would leave an in-app theme toggle broken with nothing to
find.

The test runs the web module against react-native-web, which is the first
coverage the web plane has had.
The compiler had no `@media (prefers-color-scheme: ...)` case. The one
assertion of that condition reaches it through `light-dark()`, where the
compiler synthesises the condition, so the parse path was unproven. The runtime
half had no direct coverage either — `testMediaQuery` was only ever reached
through a rendered component, which also exercises the collection and the
resolver, so a condition evaluated wrongly could still produce the right style.

The platform block was `describe.skip`. It is stale expectations, not a gap:
the conditions it asserts are produced correctly today, and the block fails
only on drift the compiler has since accumulated — `#ff0000` now shortens to
`#f00`, specificity is `[2, 1]`, and a `v` entry carries the inherited color.
The ios case also asserted one nesting level too few on `m`, which is an
authoring slip rather than drift. Corrected and unskipped.
`withReactNativeCSS` installs the `resolveRequest` that decides whether the
native and web resolvers run at all, and nothing exercised it. The resolvers
themselves are covered; the dispatch into them was not, so the gate could be
inverted, defaulted the other way, or dropped without a test noticing.

Covers both settings on both platforms, the metro-override short-circuit, and
the preference for a config's existing `resolveRequest` over the context's.
`inlineVariables` inlines a custom property that has exactly one declaration,
so `:root { --my-var: red }` compiles to a literal with no root variable entry
at all. A test written that way asserts the inliner and keeps passing with the
runtime variable registry deleted, which makes it silently worthless. Use count
does not save it: the pass counts declarations, so one declaration read from
ten rules is still inlined.

Rendering cannot tell the two apart either, because the inlined literal and the
resolved variable produce the same style. Only the compiled stylesheet shows
the difference.

`dynamicRootVariables` emits a second declaration behind a guard that never
matches, so the property stays dynamic. Both declarations carry the same value,
so the resolved value does not depend on the guard staying unmatched.

The tests pin the inliner's behaviour as well as the helper, so if it stops
inlining or starts keying on use count the helper can be retired.
Every job runs on ubuntu except one macos builder, so nothing in CI executes
this codebase on Windows. The babel plugin, the metro resolver and the compiler
all join and compare file paths, which is exactly the class of code a POSIX-only
matrix cannot vet.

That gap is not theoretical. At this commit, on a Windows host, three of the
repo's own tests fail: "7. import View from '../View/View'" in
src/__tests__/babel/react-native.test.ts, and "6. import View from '../View'"
and "17. const View = _interopRequireDefault(require('../View'))" in
src/__tests__/babel/react-native-web.test.ts. Relative imports are not
rewritten because the separator comparison assumes forward slashes.

Coverage stays on the ubuntu job; this one only needs to be able to fail.

Ordering: the fix for those three lives on fix/babel-windows-posix-paths. Merge
that branch first, or this job lands red.
… is set

`Appearance.getColorScheme()` answers null whenever the OS reports
`unspecified` or the native module is absent, so a null scheme is a reachable
production state rather than a test-harness artifact. Comparing the queried
value straight against it made `@media (prefers-color-scheme: light)` match
nothing in that state, so an explicit light rule silently never applied.

MQ5 resolves the absence of a preference to `light`, and the rest of the
library already assumes that. `light-dark(red, blue)` compiles to a light base
rule plus the dark value behind `["=", "prefers-color-scheme", "dark"]`;
`colorScheme.get()` on native ends in `?? "light"`; and react-native-web's
`getColorScheme()` reads the dark media query and answers "light" when it does
not match. Only this comparison disagreed, so a light rule behaved differently
on the two platforms.

Resolving the scheme before the comparison rather than branching on it keeps an
unrecognised value false, which MQ5 also requires.
@YevheniiKotyrlo

YevheniiKotyrlo commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor Author

Why no device screenshot

Five of the six findings are test and CI coverage, and the sixth changes which rule applies rather than how a frame is painted.

The one worth measuring is the web TypeError: src/web/api.tsx called Appearance.setColorScheme, which react-native-web@0.21.1 does not export. tsc cannot see it — the file imports Appearance from "react-native", so TypeScript resolves React Native's .d.ts while the bundler substitutes react-native-web.

tsc 0 errors · eslint 0 problems · jest 1074 passed, 3 failed, 19 skipped   (Windows)

The three are the Windows-only babel-plugin-tester baseline. The CI lane this adds is what would surface them upstream — ci.yml had no Windows runner, which is why they sat in main unnoticed.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant