fix(native): evaluate container and media conditions correctly on both axes and every operator - #425
Conversation
…ctly The container query evaluator's comparison switch returned `left > right` for `>=`, `<` and `<=` as well as for `>`. Only `>` and `=` were correct, so `@container (min-width: 400px)` and `@container (max-width: 400px)` both behaved as a strict greater-than: a 400px container matched neither, and a `max-width` query matched every container wider than its threshold. The compiler is not at fault. lightningcss normalises `min-`/`max-` into range conditions and the compiler emits `>=` / `<=` faithfully; the operator was discarded one hop later, at evaluation. The media query evaluator carried a correct copy of the same five-armed switch, so this was drift between two hand-written copies of one decision. Both now call `compareMediaFeature`, a single exhaustive primitive, and the media query side is narrowed to `MediaCondition`'s comparison arm (derived with `Extract`, not restated) so it can call the primitive without a cast — which also removes the unreachable `default` arm that let the wide parameter type hide the duplication. Tests: a 15-case cross product of the five operators against left<right, left===right and left>right over the primitive; a 14-case runtime table against a 400x200 container covering both false negatives and false positives; and a compiler-plane table pinning the emitted condition IR so a future normalisation change cannot silently reintroduce the same symptom.
`containerHeightFamily` read `layout.width`, so every container reported its width as its height. Three container query features are derived from it and all three were wrong: - `height` / `min-height` / `max-height` answered with the container's width. - `orientation` could never be `landscape`, because it compares width against height and both sides were the same number. - `aspect-ratio` was always 1. The compiler emits `height` and `orientation` faithfully; the measurement was substituted one hop later, when the layout rectangle was projected onto the two axis observables. Tests: a 13-case height table and a 6-case orientation table, both against a 400x200 container so the two axes hold different values and reading the wrong one cannot pass by coincidence — square and portrait containers are included as controls, since those are the shapes under which the defect is invisible. The compiler-plane assertion is differential: identical syntax on the two axes has to compile to two different conditions.
`compiler.types.ts` declares nothing but types, yet imported `VAR_SYMBOL`
with a value import. `verbatimModuleSyntax` is on, so the declaration is not
elided and the emitted module is one unused side-effectful require of the
native runtime:
// dist/commonjs/compiler/compiler.types.js
"use strict";
var _reactivity = require("../native/reactivity.js");
Evaluating that module registers a `Dimensions` and an `Appearance` listener,
neither of which a build-time compiler has any use for. Nothing loads it
today — every other reference to `compiler.types` is `import type`, so the
emitted file is an orphan rather than a live cost — but the compiler entry is
one ordinary re-export away from pulling the whole native runtime into Metro,
and nothing in the source says so.
`VAR_SYMBOL` is used as a computed property key in a type declaration, which
`import type` supports; the emitted declaration file is unchanged.
The guard is the invariant, not the line: a scan of every file under
`src/compiler/` for a module reference that survives emit — `import type` and
`export type` elided, `import { type X }` and `import defer` not, since
neither of those is. The detector is unit-tested against all ten declaration
shapes, and two vacuity guards fail if the scan stops reaching files or stops
recognising imports, so the invariant cannot pass by finding nothing.
A condition the compiler could not compile was discarded and the block was
emitted anyway, with no condition on it — so its declarations applied to every
element carrying the class, at every size. That is worse than the block being a
no-op: the rule fires where the author said it must not.
@container style(--foo: bar) cq: [{ m: undefined }]
@container (width > env(safe-area-inset-top)) cq: [{ m: undefined }]
@media (width > env(safe-area-inset-top)) no `m` at all
`@container` reached it one way and `@media` the other. `extractContainer`
built `{ m: parseContainerCondition(...) }` and never looked at the `undefined`;
`parseMediaQuery` returned early without adding anything, which `extractMedia`
could not tell apart from `@media all` — the case where there is genuinely no
condition and the block really does always apply. Both extractors then walked
the block's rules regardless.
So the fault is one missing distinction, not two bugs: "there is no condition"
and "the condition did not compile" were the same value. `CompiledCondition`
separates them into `always` / `never` / `condition`, and the container half is
`Exclude`d from it rather than restated, because a container prelude is always
a condition. `parseMediaQuery` now returns that verdict instead of mutating the
builder, which is what lets `extractMedia` aggregate over a comma-separated
list before deciding: the list is a union, so one uncompilable branch still
contributes nothing while the others apply, and only a list where no branch can
match drops the block. `extractMedia` already dropped a block no query could
match — that is what the `@media print` filter does — so this extends an
existing decision rather than adding a new one.
Behaviour change, deliberately: styles under an uncompilable condition used to
apply everywhere and now apply nowhere. Nowhere is what the runtime already
does with an unsupported feature it can see — `getContainerFeatureValue`
returns `undefined` and the comparison is false — so the two planes now agree.
`@media not print and (width > 400px)` is the one case that must stay
unconditional: it reads `not (print and ...)`, which is true on every non-print
device. It is in the table, and inverting it is one of the mutations below.
Tests: a compiler-plane table of four uncompilable preludes across both
at-rules asserting nothing is emitted, against six controls asserting that a
compilable one still is — without the controls a compiler that emitted nothing
at all would pass. Runtime tables on both at-rules confirm the styles do not
apply. Each uncompilable case is also a vacuity guard on the others: if support
for `env()` or `style()` lands, that row starts emitting and fails, which is the
signal to move the case rather than delete it.
Mutation-proved. Deleting the container guard fails exactly the four container
cases; deleting the media guard fails exactly the four media cases; returning
`never` for print fails `@media not print`; returning `never` for the
no-condition case fails `@media all` and `@media screen`.
The native container-query helper now takes the condition in full, parentheses
included. It used to add them, which made `style(--foo: bar)` and a leading
container name inexpressible — both are `<container-condition>` forms — and it
disagreed with the compiler-plane helper, which already took the full prelude.
`parseMediaFeatureValue` had no `ratio` arm, so every `<ratio>` compiled to `undefined` and took its whole condition with it. `@container (aspect-ratio > 1)` and `@media (min-aspect-ratio: 16/9)` reached the runtime as nothing at all. The runtime was ready for the container half and had been all along: `getContainerFeatureValue` already answers `aspect-ratio` with the container's width over its height. No IR ever named the feature, so that arm could not run — the compiler is where the support was missing, not the evaluator. A `<ratio>` is a pair of numbers standing for their quotient, which is the same number both runtimes derive from their two axes, so the pair is compiled to the quotient and every comparison operator then works on it unchanged. A bare number is a ratio too, so `1` arrives as `[1, 1]`. The media evaluator needed the feature itself, which it did not have: viewport aspect ratio is `vw / vh`, read off the two observables it already uses for `width` and `height`, and it joins them in the same numeric-feature switch. Tests: compiler tables on both at-rules pinning the emitted condition, with `min-`/`max-` prefixed forms included because lightningcss normalises those into `>=`/`<=` range conditions the same way it does for lengths. Runtime tables measure against containers and viewports whose ratio is exactly 2, 0.5 and 1, so a value read off the wrong axis cannot pass by coincidence. Mutation-proved. Removing the `ratio` arm fails all 17 cases across both planes and both at-rules; removing the media evaluator's `aspect-ratio` arm fails only the 3 media runtime cases, the compiler ones staying green; inverting the container evaluator to `height / width` fails 7 of the 11 container cases, including two that flip from false to true.
`@container (400px < width < 800px)` matched nothing, at any size. Both evaluators carried `case "[]": return false`, so every range pair was a no-op on both at-rules. The compiler was never at fault. It emits the pair faithfully, in source order — `["[]", "width", 400, "<", 800, "<"]` — including the descending form `(800px > width > 400px)`. Only the evaluation was missing, which is why the compiler-plane tests here are pins rather than fixes. An interval is two comparisons, and the shared `compareMediaFeature` already had the operator semantics, so the addition is which operand goes on which side: the start bound is on the left of its operator and the measured value on the right, the end bound the other way round. Getting that backwards produces a well-formed interval that means something else, so both evaluators call one `testMediaFeatureInterval` rather than each writing the destructuring out. `MediaInterval` is `Extract`ed from `MediaCondition`, matching how the comparison arm is derived. The media evaluator had no way to answer a feature outside a comparison tuple — its numeric features were resolved by a `let left` switch inside `testComparison` — so that resolution is now `getMediaFeatureValue`, which the comparison arm and the interval arm share. `testComparison` reads better for it: the value it compares is fetched, not accumulated through `break`s. Two `Boolean` return annotations became `boolean` on the way past. They are the boxed object type, and both are on functions this change restructures. Tests: a nine-case table over the primitive varying which side of each bound the value falls on, with strict and non-strict operators paired so the two are never interchangeable, plus four unanswerable cases — an unmeasurable feature and an unresolved bound are no answer, not no bound. Runtime tables on both at-rules against a 600x200 box, with the measured value placed exactly on each bound open and closed. The compiler-plane table pins the tuple layout the evaluation depends on. Mutation-proved. Restoring `return false` in the container evaluator fails 5 cases and in the media evaluator 4, disjointly; assembling the two halves the other way round fails 13 across both planes, including four of the primitive's own cases; swapping start and end in the compiler's interval emit fails the pins and the container runtime together. This also dissolves the last identical-bodied switch arms under `src/native/conditions`: an AST sweep for clauses with textually identical bodies now reports none there.
An audit of what this branch actually observes, done by reintroducing each
defect and counting the cases that went red — first against the branch head,
then against this commit. Three code paths were reddened by nothing, and a
fourth by a single case.
reintroduced defect head now
a `height` range compiled as `width` (@media) 0 7
an interval's bounds swapped in the emit (@media) 0 5
the runtime drops its Appearance subscription - 1
the viewport `height` feature answered with `vw` 1 12
`>=`, `<` and `<=` all `left > right` 38 65
`>=` compiled as `>` 8 13
a `height` range compiled as `width` (@container) 4 8
`containerHeightFamily` reads the width axis 16 19
a value import of VAR_SYMBOL 1 2
…plus a value re-export from the compiler entry 1 3
`@media all` compiled as "did not compile" 2 6
a comma list dropped for one refused branch 1 2
an interval's bounds swapped (@container) 5 5
the media evaluator's interval arm returns false 4 4
the `<ratio>` arm removed 17 17
The first four rows are the finding. `@media` had no compiler-plane table for
either the comparison operators or the interval layout, so the whole at-rule
compiled unobserved while `@container` was pinned twice over; and one
primitive now decides a range comparison for both, which is precisely the
place a divergence between them would land unseen. The viewport `height`
feature was reachable only through an interval case, on an axis this branch
rewrote. And the compiler-plane isolation scan reads source shape, so nothing
asserted the fact it rests on — that evaluating the runtime installs two
listeners on the host.
`src/__tests__/native/runtime-boot.test.ts` is the native half of that
isolation invariant: load a module into a fresh registry and count what it
attached to `Dimensions` and `Appearance`. It reaches further than the scan,
which only sees direct specifiers out of `src/compiler/` — a path into the
runtime through a third directory is invisible there and caught here.
Measured before writing it, because the jest transform decides whether the
plane can carry the defect at all: `babel-preset-expo` does NOT elide an
import whose only specifier is used in a type position, so the pre-fix source
emits `require("../native/reactivity")` under jest exactly as it does in
`dist`. Had it elided, the test would have passed for a reason unrelated to
the fix.
The operator tables are generated from one census in
`src/__tests__/_media-features.ts` rather than listed four times: the five
operators, the two size features, the `min-`/`max-` spellings, and what each
operator MEANS. The meaning is written out, never computed — deriving it from
the code under test would make every table agree with a wrong operator — and
it is shared so the primitive's own table and the four rendered ones cannot
disagree. Each consumer asserts the census reached it, since an empty one
generates no cases and leaves every suite green. The file is underscore-
prefixed, which is what `testPathIgnorePatterns` already excludes.
`@container` has no counterpart to the `@media all` cases, and that is a type
rather than a gap: `CompiledContainerCondition` `Exclude`s the `always` state,
so a container prelude with no condition is unrepresentable.
Tests: 1325, up from 1225. The three failures are the pre-existing Windows
`babel-plugin-tester` output mismatches, unchanged.
Reintroducing each defect and counting the reds surfaced four places where the branch guarded less than it claimed, and two sibling defects of the same class as the ones it fixes. Counts here are whole-suite, and exclude the three pre-existing Windows babel-plugin-tester failures. The isolation scan had a hole it could not see ---------------------------------------------- native-runtime-isolation restated its own plane census as ["native", "native-internal"], and the ways into the native plane are not directory names. src/index.ts re-exports runtime, and runtime is runtime.native on the native platform, so `import "react-native-css"` in a compiler source resolves to `index` — not under either directory, and pulling all of both. Measured against the census form: that import, `import "../runtime.native"` and `import "react-native-css/runtime"` each reddened zero cases while producing exactly the dist defect the VAR_SYMBOL fix exists to prevent. So the census is gone and the question is reachability: can the module a specifier resolves to reach native/reactivity, through any number of hops and on any platform. That is the fact the invariant actually rests on — the listeners — rather than a second name for it. All three specifiers now redden the scan, and so does a path through a third directory, which no scan of src/compiler/ alone could see: a value import of the runtime added to src/utilities reddens four cases, the scan among them. Two guards go with it. The detector walks the whole tree rather than the top-level statements, so a require() or a dynamic import() inside a function body is a module reference it can find; and a reachability table states what the check discriminates, so a renamed native/reactivity fails loudly instead of making every answer false. compiler/inheritance.test.ts stays in the scan. It sits in that directory rather than under __tests__, so bob compiles it into dist and the package ships it, which makes it a compiler source like any other. One comparison, not one and a half ---------------------------------- compare.ts claimed an operator has exactly one meaning at runtime, and `=` had two: the container evaluator short-circuited it before reaching the primitive, because a keyword feature like orientation answers a string. Measured: deciding `=` wrongly inside the primitive reddened 8 cases and none of them under @container, whose table holds (width = 400px) rows. The primitive takes StyleDescriptor now and answers `=` itself, since narrowing at the call site is how the second copy got written — an evaluator that must reject a keyword before it can call the primitive ends up deciding equality on its own. Both evaluators hand it whatever the feature answered and nothing else. The same mutation now reddens 14, six under @container, including the orientation rows. The numeric guard travels with it, so testMediaFeatureInterval holds none of its own: an unmeasured value or an unresolved bound fails whichever comparison it is an operand of. That guard was observed by nothing — dropping it reddened zero — because the cases that looked like they pinned it return false either way. "landscape" < 800 is NaN < 800; only a string that COERCES tells the two apart, and 400 < "500" is 400 < 500. Three rows cover it now, one per slot, each mutation-proven. Two sibling defects, same class ------------------------------- inline-size and block-size compile cleanly and answered undefined, so @container (min-inline-size: 400px) matched nothing at any size — on the axis container-type: inline-size names, which is the one most container queries are written against. React Native lays out in one writing mode, so inline is the width axis and block the height axis. Reverting reddens 5. A <ratio> with a zero denominator has no finite quotient, and (min-aspect-ratio: 1/0) emitted Infinity. The native injection path serialises through JSON.stringify, which writes Infinity and NaN as null, so the condition meant one thing under jest and another on a device. A degenerate ratio is refused instead, which routes it into the same "did not compile" path as every other unresolvable value. Reverting reddens 3. Assertions that could not fail ------------------------------ Five expect(cases).toHaveLength(census.length * ORDERINGS.length) compared a generated table against the generator that made it, so the product held for any census. Deleting "<" from the operator census removes 19 cases across four files and left all five green; only compare.test.ts's comparison against COMPARISON_MATCHES — a total Record over MediaFeatureComparison, whose keys are the union itself — noticed. Every table asserts coverage against that record now, and the same deletion reddens 5 tests in 5 suites. conditional-group-rules's control table asserted only that one rule came out, never which condition it carried, so a block emitted with the wrong condition — or with none — passed it. Each case names the condition now, including the two preludes that legitimately carry none. And the aspect-ratio and interval tables get the label they were missing: a matches: true row and a matches: false row fail under opposite defects, one for a block that stops being emitted and one for a block emitted with nothing to check. Neither half observes the other's direction, which is why both are there and why the size of a table is not its coverage. Corrections to what was claimed ------------------------------- - The audit table's "an interval's bounds swapped in the emit (@media)" row reads 0 -> 5. Measured whole-suite it is 9, of which 5 are the compiler-plane pins and 4 the runtime rows that already existed one commit earlier — so the head column is 4, not 0. Zero is the figure against the base, where an unevaluated interval cannot observe its own layout. Both readings of "swapped bounds" give the same numbers. - That table's counting rule is not constant across its rows: the 5 there counts the compiler plane only, while other rows count the whole suite. The counts above use one rule throughout. - "import VAR_SYMBOL for its type only" says the emitted declaration file is unchanged. It is not: compiler.types.d.ts carries `import type { VAR_SYMBOL }` where it carried `import { VAR_SYMBOL }`, and a declaration emit diffs in exactly that one line and nowhere else. Inert for consumers, but not nothing. - "evaluate interval (range pair) conditions" says it dissolves the last identical-bodied switch arms under src/native/conditions. That holds within each switch and not across the two files: `case "!!": return false` is the same body in both, and it is a gap rather than a decision. Known limits, now stated where the decision is made --------------------------------------------------- A comma-separated media query list is intersected, not unioned. rule.m is a flat array fed from two places with opposite meanings — one entry per comma branch, which CSS unions, and one per enclosing @media block or media-carrying selector, which CSS intersects — and testMediaQuery intersects the whole array. Measured: @media (min-width: 400px), (min-height: 300px) at 600x200 does not match while (min-width: 400px) alone does, and switching the evaluator to .some(...) fixes the list and breaks nesting — @media (min-width: 400px) { @media (min-height: 900px) { ... } } starts matching at 600x200. The existing suite catches neither direction, so the substitution passes it. Two entries of the same shape mean two different things; the emit has to say which, which is a change to what is produced rather than to how it is read. It is left standing and written down at the every(never) decision it sits beside, and at the evaluator a reader would otherwise reach for. The boolean context is unimplemented on both planes: (width) compiles to ["!!", name] and both evaluators answer false, so the query reads as valid and can never match. Answering it needs a truthiness rule per feature. Suite: 1361, up from 1325. Two runs with identical totals, zero suite-load failures. yarn typecheck and yarn lint exit 0.
|
Cross-referencing #425 and #426, which conflict — flagging it now rather than at merge time. They are independent siblings ( The hazard is in the
Taking #426's arm wholesale therefore un-fixes #425's range container queries. Taking #425's wholesale loses #426's three-valued handling, where The resolution that is correct in both directions evaluates the interval and returns One more line worth keeping whichever way it lands: #426's boolean-context arm uses A two-case test pins both halves, and neither branch passes it alone:
|
|
Correction to my note above — I undercounted the conflict set, and the omission was the load-bearing file.
A second resolution needs the same care, in Taking #426's arm onto #425's types does not compile — ["!", query.type === "condition" ? query.condition : (["?"] as MediaCondition)]And one more, in |
…unanswerable value `parseMediaFeatureOperand`'s doc comment lists what has no compile-time answer as `env()`, a ratio, an unsupported `calc()`. On this branch that is true, and it stops being true the moment nativewind#425 lands: there a reducible ratio compiles to its quotient and only a degenerate one stays unanswerable. Naming the ratio more precisely does not fix it. A restrictive qualifier — "a ratio the compiler cannot reduce to a finite quotient" — implies a non-empty complement, and on this branch the complement is empty: every ratio, `16/9` and bare `1` included, answers `undefined`, because `case "ratio"` falls through to `case "env"`. So the qualified form is truth-conditionally fine and tells a reader of THIS branch something false about it. There is no wording that names ratios here and is right both before and after nativewind#425. Dropping the ratio is not a narrower list, it is an honest one, because the enumeration was never the case table it reads as. `parseLength` refuses 43 units by name — every physical unit, every font-relative unit but `em`/`rem`, every viewport and container variant but bare `vw`/`vh` — so `(min-width: 1lh)` already answers `null` here with no `calc()` anywhere in it, unlisted. The two members kept are the two verified stable: `env()` and an unsupported `calc()` answer `null` on this branch and after nativewind#425 alike. `such as` is what stops the next reader trusting the list as exhaustive, which is the failure this comment already had. The ratio's own explanation belongs at the hop that owns the fact, and nativewind#425 puts it there, in `case "ratio"`. Comments only.
Eight defects in the container-query and media-query condition evaluators. Every fix is mutation-proven — the defect is reintroduced, the red cases counted, then reverted — and each commit carries its own figures.
The defects
min-width/max-widthcontainer query was a strict greater-than.>=,<and<=all returnedleft > rightinnative/conditions/container-query.ts; only>and=were correct. So@container (max-width: 400px)matched exactly when it should not.containerHeightFamilyread?.width, so@container (orientation: …)answeredportraitfor every real container —width > heightwas unsatisfiable — and every height-based query answered on the wrong axis.inline-sizeandblock-sizenever matched. Both compile cleanly, andgetContainerFeatureValueansweredundefinedfor each, so@container (min-inline-size: 400px)matched nothing at any size. That is the axiscontainer-type: inline-sizenames, which makes it the feature most container queries are written against. React Native lays out in one writing mode, so inline is the width axis and block the height axis.case "[]": return false, so(400px < width < 800px)was a no-op on both at-rules. The compiler emitted the pair faithfully throughout; only the evaluation was missing.aspect-ratioqueries were dropped instead of evaluated —parseMediaFeatureValuehad noratioarm, so the whole condition was unresolvable.<ratio>with no finite quotient compiled to a value the bundle cannot carry.@media (min-aspect-ratio: 1/0)produced[">=", "aspect-ratio", Infinity]. The native injection path serialises throughJSON.stringify, which writesInfinityandNaNasnull, so the condition meant one thing under jest and another on a device. A degenerate ratio is refused instead, which routes it into the same "did not compile" path as every other unresolvable value.react-native-css/compilerbooted the native runtime.compiler/compiler.types.tshad a value import ofVAR_SYMBOL, andverbatimModuleSyntaxmeans it is not elided, so the emitteddist/commonjs/compiler/compiler.types.jsrequired native reactivity and registeredDimensionsandAppearancelisteners in a bundler process.One comparison, shared
@mediaand@containerspeak oneMediaFeatureComparisonvocabulary, and defect 1 is what a second hand-written copy of it costs.native/conditions/compare.tsnow holds the only implementation, and both evaluators call it with whatever the feature answered rather than narrowing first — narrowing at the call site is exactly how the second copy gets written, since an evaluator that has to reject a keyword before it can call the primitive ends up deciding=itself.Measured: making
=wrong inside that primitive reddens 14 cases, six of them under@container, including theorientationkeyword rows. Split across two implementations the same mutation reddened 8, and none of them under@container.Evidence
The figures below are the ones re-measured for the final pass, all under one rule: whole-suite, excluding the three pre-existing Windows
babel-plugin-testerfailures that are present onmain.=decided wrongly in the shared comparison<ratio>arm removed@mediaemitinline-size/block-sizeback to unansweredTwo of those need a word.
@mediainterval table did not exist yet; against the base it is 0, because an unevaluated interval cannot observe its own layout.@mediahad no compiler-plane table at all for either the comparison operators or the interval layout, while@containerwas pinned twice — and one primitive now decides a range comparison for both, which is precisely where a divergence between their emits would land unseen.src/__tests__/_media-features.tsis one census (five operators, two axes, themin-/max-spellings) feeding five tables, and an incomplete census silently generates fewer cases while every suite stays green — deleting"<"removes 19 cases. Coverage is therefore asserted againstCOMPARISON_MATCHES, a totalRecordoverMediaFeatureComparisonwhose keys are the union itself, not against the length of the generator each table was built from: that length is the census's length by construction and agrees with a census of any size, zero included.Defect 8 is pinned from two sides.
src/__tests__/compiler/native-runtime-isolation.test.tswalks the module graph and refuses any specifier out ofsrc/compiler/that can reachnative/reactivityon any platform — asking about reachability rather than about a list of runtime directory names, because the ways in are the package's own entry points:react-native-cssre-exportsruntime, which isruntime.nativeon native, and none of those three ids sits under a runtime directory.src/__tests__/native/runtime-boot.test.tsis the other side, loading a module into a fresh registry and counting what it attached toDimensionsandAppearance. Before writing it I measured thatbabel-preset-expodoes not elide an import whose only specifier sits in a type position — had it elided, the test would have passed for an unrelated reason.Suite: 1072 → 1361 tests, two runs with identical totals and zero suite-load failures.
yarn typecheckandyarn lintexit 0. The 3 failures are the known Windowsbabel-plugin-testerbaseline (an unrewritten relativerequire("../View")), present onmain.Known limits, deliberately left standing
rule.mis a flat array fed from two places with opposite meanings — one entry per comma branch, which CSS unions, and one per enclosing@mediablock or media-carrying selector, which CSS intersects — andtestMediaQueryintersects the whole array. Nesting is therefore right and a comma list is not:@media (min-width: 400px), (min-height: 300px)at 600x200 does not match, where CSS asks for either. Switching the evaluator to.some(...)fixes the list and breaks nesting, measured both ways; the emit has to say which kind of entry it is producing. That is a change to what is emitted rather than to how a condition is evaluated, so it is out of scope here and noted where the decision is made.@media (width)and@container (width)compile to["!!", name]and both evaluators answerfalse, so the query reads as valid and can never match. Answering it needs a truthiness rule per feature.@containerhas no counterpart to the@media allcases:CompiledContainerConditionExcludes thealwaysstate, so it is unrepresentable rather than untested.Relationship to the media-condition-semantics branch
These two branches overlap and I would rather say so than have it surface at merge. They touch 10 files in common, and
git merge-treeagainstfix/media-condition-semanticsreports 7 conflicted files with 17 conflict hunks. No commit is duplicated — patch-id overlap is zero — but defects 1 and 2 are each fixed on both branches by different implementations.I am happy to rebase this onto whichever lands first, or to drop the overlapping commits from one side. Say which you prefer and I will restructure rather than ask you to resolve it.