Ensure compiled range media queries are correctly parenthesised (#1114)
Fixes #1105
In the test case added, the range query `(width < 256px)` gets compiled
into `not (min-width: 256px)`. If we combine this with another query,
for example `(width < 256px) or (hover: none)`, the compiled query
should parenthesise the compiled range query. That is, the output should
be `(not (min-width: 256px)) or (hover: none)`. Instead, we see an
output of `not (min-width: 256px) or (hover: none)`, which incorrectly
negates the entire media query, not just the `min-width`.
`QueryFeature::needs_parens` determines if parentheses are need with the
following logic:
```rust
match self {
QueryFeature::Interval { .. } => parent_operator != Some(Operator::And),
QueryFeature::Range { operator, .. } => {
matches!(
operator,
MediaFeatureComparison::GreaterThan | MediaFeatureComparison::LessThan
)
}
_ => false,
}
}
```
This is correct ***if*** both interval and range queries are being
compiled, since parentheses are required when the range is replaced with
a negation.
However, above this return was the block:
```rust
if !should_compile!(targets, MediaIntervalSyntax) {
return false;
}
```
This causes the check to be skipped when `Feature::MediaIntervalSyntax`
isn't enabled.
This leaves an edge case: if `Feature::MediaRangeSyntax` is enabled,
without `Feature::MediaIntervalSyntax`, the correct parentheses check
isn't carried out.
One would think that this is an unreasonable edge case, since targeting
a browser without range syntax support would also be a browser without
interval syntax support. However, in `turbopack-css`, the
`MediaRangeSyntax` feature is always included, without adding
`MediaIntervalSyntax` (see
https://github.com/vercel/next.js/commit/a95f8612bcf08dbc52cc4b06e88b43d48f4429e7#diff-389b0ea768c0dbbca95b4aff5d1237ecddb33677521e3e2eb1f717c43c0d4658).
In Next 16, the default targets were updated (see
https://github.com/vercel/next.js/pull/84401), which caused
`MediaIntervalSyntax` to no longer be included by default, hence opening
up this edge case.
I beleive that, unfortunately, the reason for Next adding
`MediaIntervalSyntax` still holds, and applies to intervals too. I'll
make a PR there to add `MediaIntervalSyntax` which will avoid this edge
case, but it'd be nice to fix it here, too :)
This moves the `MediaIntervalSyntax` check into the `Interval` case in
the switch, and adds a separate `MediaRangeSyntax` check in the `Range`
case. J
Jacob O'Toole committed
c4091ee93d113eedf2e9f8dba88b544728eb0d57
Parent: 28d7794
Committed by GitHub <noreply@github.com>
on 1/3/2026, 5:34:39 PM