Skip to content

Commit 134dadf

Browse files
committed
fix #4265: @supports nested inside ::pseudo
1 parent 195e05c commit 134dadf

File tree

3 files changed

+83
-14
lines changed

3 files changed

+83
-14
lines changed

CHANGELOG.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,50 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
* Fix `@supports` nested inside pseudo-element ([#4265](https://github.com/evanw/esbuild/issues/4265))
6+
7+
When transforming nested CSS to non-nested CSS, esbuild is supposed to filter out pseudo-elements such as `::placeholder` for correctness. The [CSS nesting specification](https://www.w3.org/TR/css-nesting-1/) says the following:
8+
9+
> The nesting selector cannot represent pseudo-elements (identical to the behavior of the ':is()' pseudo-class). We’d like to relax this restriction, but need to do so simultaneously for both ':is()' and '&', since they’re intentionally built on the same underlying mechanisms.
10+
11+
However, it seems like this behavior is different for nested at-rules such as `@supports`, which do work with pseudo-elements. So this release modifies esbuild's behavior to now take that into account:
12+
13+
```css
14+
/* Original code */
15+
::placeholder {
16+
color: red;
17+
body & { color: green }
18+
@supports (color: blue) { color: blue }
19+
}
20+
21+
/* Old output (with --supported:nesting=false) */
22+
::placeholder {
23+
color: red;
24+
}
25+
body :is() {
26+
color: green;
27+
}
28+
@supports (color: blue) {
29+
{
30+
color: blue;
31+
}
32+
}
33+
34+
/* New output (with --supported:nesting=false) */
35+
::placeholder {
36+
color: red;
37+
}
38+
body :is() {
39+
color: green;
40+
}
41+
@supports (color: blue) {
42+
::placeholder {
43+
color: blue;
44+
}
45+
}
46+
```
47+
348
## 0.25.9
449

550
* Better support building projects that use Yarn on Windows ([#3131](https://github.com/evanw/esbuild/issues/3131), [#3663](https://github.com/evanw/esbuild/issues/3663))

internal/css_parser/css_nesting.go

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ func (p *parser) lowerNestingInRule(rule css_ast.Rule, results []css_ast.Rule) [
2222
}
2323
}
2424

25-
parentSelectors := make([]css_ast.ComplexSelector, 0, len(r.Selectors))
25+
parentSelectorsWithPseudo := make([]css_ast.ComplexSelector, 0, len(r.Selectors))
26+
parentSelectorsNoPseudo := make([]css_ast.ComplexSelector, 0, len(r.Selectors))
2627
for i, sel := range r.Selectors {
2728
// Top-level "&" should be replaced with ":scope" to avoid recursion.
2829
// From https://www.w3.org/TR/css-nesting-1/#nest-selector:
@@ -48,8 +49,14 @@ func (p *parser) lowerNestingInRule(rule css_ast.Rule, results []css_ast.Rule) [
4849
// substitute "&" within child rules. Do not filter out the pseudo
4950
// element from the top-level selector list.
5051
if !sel.UsesPseudoElement() {
51-
parentSelectors = append(parentSelectors, css_ast.ComplexSelector{Selectors: substituted})
52+
parentSelectorsNoPseudo = append(parentSelectorsNoPseudo, css_ast.ComplexSelector{Selectors: substituted})
5253
}
54+
55+
// This filtering is only done conditionally because it seems to only
56+
// apply sometimes. Specifically it doesn't seem to apply when the
57+
// nested rule is an at-rule. So we use the unfiltered list in that
58+
// case. See: https://github.com/evanw/esbuild/issues/4265
59+
parentSelectorsWithPseudo = append(parentSelectorsWithPseudo, css_ast.ComplexSelector{Selectors: substituted})
5360
}
5461

5562
// Emit this selector before its nested children
@@ -58,8 +65,9 @@ func (p *parser) lowerNestingInRule(rule css_ast.Rule, results []css_ast.Rule) [
5865

5966
// Lower all children and filter out ones that become empty
6067
context := lowerNestingContext{
61-
parentSelectors: parentSelectors,
62-
loweredRules: results,
68+
parentSelectorsWithPseudo: parentSelectorsWithPseudo,
69+
parentSelectorsNoPseudo: parentSelectorsNoPseudo,
70+
loweredRules: results,
6371
}
6472
r.Rules = p.lowerNestingInRulesAndReturnRemaining(r.Rules, &context)
6573

@@ -130,8 +138,9 @@ func (p *parser) addExpansionError(loc logger.Loc, n int) {
130138
}
131139

132140
type lowerNestingContext struct {
133-
parentSelectors []css_ast.ComplexSelector
134-
loweredRules []css_ast.Rule
141+
parentSelectorsWithPseudo []css_ast.ComplexSelector
142+
parentSelectorsNoPseudo []css_ast.ComplexSelector
143+
loweredRules []css_ast.Rule
135144
}
136145

137146
func (p *parser) lowerNestingInRuleWithContext(rule css_ast.Rule, context *lowerNestingContext) css_ast.Rule {
@@ -158,15 +167,15 @@ func (p *parser) lowerNestingInRuleWithContext(rule css_ast.Rule, context *lower
158167
}
159168

160169
// Pass 2: Substitute "&" for the parent selector
161-
if !p.options.unsupportedCSSFeatures.Has(compat.IsPseudoClass) || len(context.parentSelectors) <= 1 {
170+
if !p.options.unsupportedCSSFeatures.Has(compat.IsPseudoClass) || len(context.parentSelectorsNoPseudo) <= 1 {
162171
// If we can use ":is", or we don't have to because there's only one
163172
// parent selector, or we are using ":is()" to match zero parent selectors
164173
// (even if ":is" is unsupported), then substituting "&" for the parent
165174
// selector is easy.
166175
for i := range r.Selectors {
167176
complex := &r.Selectors[i]
168177
results := make([]css_ast.CompoundSelector, 0, len(complex.Selectors))
169-
parent := p.multipleComplexSelectorsToSingleComplexSelector(context.parentSelectors)
178+
parent := p.multipleComplexSelectorsToSingleComplexSelector(context.parentSelectorsNoPseudo)
170179
for _, compound := range complex.Selectors {
171180
results = p.substituteAmpersandsInCompoundSelector(compound, parent, results, keepLeadingCombinator)
172181
}
@@ -225,7 +234,7 @@ func (p *parser) lowerNestingInRuleWithContext(rule css_ast.Rule, context *lower
225234
}
226235
index := indices[offset]
227236
offset++
228-
return context.parentSelectors[index]
237+
return context.parentSelectorsNoPseudo[index]
229238
}
230239

231240
// Do the substitution for this particular combination
@@ -244,7 +253,7 @@ func (p *parser) lowerNestingInRuleWithContext(rule css_ast.Rule, context *lower
244253
carry := len(indices)
245254
for carry > 0 {
246255
index := &indices[carry-1]
247-
if *index+1 < len(context.parentSelectors) {
256+
if *index+1 < len(context.parentSelectorsNoPseudo) {
248257
*index++
249258
break
250259
}
@@ -273,13 +282,16 @@ func (p *parser) lowerNestingInRuleWithContext(rule css_ast.Rule, context *lower
273282
return css_ast.Rule{}
274283

275284
case *css_ast.RKnownAt:
276-
childContext := lowerNestingContext{parentSelectors: context.parentSelectors}
285+
childContext := lowerNestingContext{
286+
parentSelectorsWithPseudo: context.parentSelectorsWithPseudo,
287+
parentSelectorsNoPseudo: context.parentSelectorsNoPseudo,
288+
}
277289
r.Rules = p.lowerNestingInRulesAndReturnRemaining(r.Rules, &childContext)
278290

279291
// "div { @media screen { color: red } }" "@media screen { div { color: red } }"
280292
if len(r.Rules) > 0 {
281293
childContext.loweredRules = append([]css_ast.Rule{{Loc: rule.Loc, Data: &css_ast.RSelector{
282-
Selectors: context.parentSelectors,
294+
Selectors: context.parentSelectorsWithPseudo,
283295
Rules: r.Rules,
284296
}}}, childContext.loweredRules...)
285297
}
@@ -294,13 +306,16 @@ func (p *parser) lowerNestingInRuleWithContext(rule css_ast.Rule, context *lower
294306

295307
case *css_ast.RAtLayer:
296308
// Lower all children and filter out ones that become empty
297-
childContext := lowerNestingContext{parentSelectors: context.parentSelectors}
309+
childContext := lowerNestingContext{
310+
parentSelectorsWithPseudo: context.parentSelectorsWithPseudo,
311+
parentSelectorsNoPseudo: context.parentSelectorsNoPseudo,
312+
}
298313
r.Rules = p.lowerNestingInRulesAndReturnRemaining(r.Rules, &childContext)
299314

300315
// "div { @layer foo { color: red } }" "@layer foo { div { color: red } }"
301316
if len(r.Rules) > 0 {
302317
childContext.loweredRules = append([]css_ast.Rule{{Loc: rule.Loc, Data: &css_ast.RSelector{
303-
Selectors: context.parentSelectors,
318+
Selectors: context.parentSelectorsWithPseudo,
304319
Rules: r.Rules,
305320
}}}, childContext.loweredRules...)
306321
}

internal/css_parser/css_parser_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1345,6 +1345,15 @@ func TestNestedSelector(t *testing.T) {
13451345
// https://github.com/w3c/csswg-drafts/issues/7961#issuecomment-1549874958
13461346
expectPrinted(t, "@media screen { a { x: y } x: y; b { x: y } }", "@media screen {\n a {\n x: y;\n }\n x: y;\n b {\n x: y;\n }\n}\n", "")
13471347
expectPrinted(t, ":root { @media screen { a { x: y } x: y; b { x: y } } }", ":root {\n @media screen {\n a {\n x: y;\n }\n x: y;\n b {\n x: y;\n }\n }\n}\n", "")
1348+
1349+
// Nested at-rules work with pseudo-elements while nested "&" rules do not
1350+
// See: https://github.com/evanw/esbuild/issues/4265
1351+
expectPrintedLower(t, "::placeholder { color: red; body & { color: green } }",
1352+
"::placeholder {\n color: red;\n}\nbody :is() {\n color: green;\n}\n", "")
1353+
expectPrintedLower(t, "::placeholder { color: red; @supports (color: green) { color: green } }",
1354+
"::placeholder {\n color: red;\n}\n@supports (color: green) {\n ::placeholder {\n color: green;\n }\n}\n", "")
1355+
expectPrintedLower(t, "::placeholder { opacity: 0.5; @layer base { color: green } }",
1356+
"::placeholder {\n opacity: 0.5;\n}\n@layer base {\n ::placeholder {\n color: green;\n }\n}\n", "")
13481357
}
13491358

13501359
func TestBadQualifiedRules(t *testing.T) {

0 commit comments

Comments
 (0)