Skip to content

Commit ad3d8c6

Browse files
committed
fix #3648: copy selectors before checking children
1 parent a08f30d commit ad3d8c6

File tree

3 files changed

+55
-24
lines changed

3 files changed

+55
-24
lines changed

CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,32 @@
22

33
## Unreleased
44

5+
* Fix a bug with the CSS nesting transform ([#3648](https://github.com/evanw/esbuild/issues/3648))
6+
7+
This release fixes a bug with the CSS nesting transform for older browsers where the generated CSS could be incorrect if a selector list contained a pseudo element followed by another selector. The bug was caused by incorrectly mutating the parent rule's selector list when filtering out pseudo elements for the child rules:
8+
9+
```css
10+
/* Original code */
11+
.foo {
12+
&:after,
13+
& .bar {
14+
color: red;
15+
}
16+
}
17+
18+
/* Old output (with --supported:nesting=false) */
19+
.foo .bar,
20+
.foo .bar {
21+
color: red;
22+
}
23+
24+
/* New output (with --supported:nesting=false) */
25+
.foo:after,
26+
.foo .bar {
27+
color: red;
28+
}
29+
```
30+
531
* Fix a crash when resolving a path from a directory that doesn't exist ([#3634](https://github.com/evanw/esbuild/issues/3634))
632

733
This release fixes a regression where esbuild could crash when resolving an absolute path if the source directory for the path resolution operation doesn't exist. While this situation doesn't normally come up, it could come up when running esbuild concurrently with another operation that mutates the file system as esbuild is doing a build (such as using `git` to switch branches). The underlying problem was a regression that was introduced in version 0.18.0.

internal/css_parser/css_nesting.go

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -23,40 +23,43 @@ func (p *parser) lowerNestingInRule(rule css_ast.Rule, results []css_ast.Rule) [
2323
}
2424
}
2525

26-
// Filter out pseudo elements because they are ignored by nested style
27-
// rules. This is because pseudo-elements are not valid within :is():
28-
// https://www.w3.org/TR/selectors-4/#matches-pseudo. This restriction
29-
// may be relaxed in the future, but this restriction hash shipped so
30-
// we're stuck with it: https://github.com/w3c/csswg-drafts/issues/7433.
31-
selectors := r.Selectors
32-
n := 0
33-
for _, sel := range selectors {
26+
parentSelectors := make([]css_ast.ComplexSelector, 0, len(r.Selectors))
27+
for i, sel := range r.Selectors {
28+
// Top-level "&" should be replaced with ":scope" to avoid recursion.
29+
// From https://www.w3.org/TR/css-nesting-1/#nest-selector:
30+
//
31+
// "When used in the selector of a nested style rule, the nesting
32+
// selector represents the elements matched by the parent rule. When
33+
// used in any other context, it represents the same elements as
34+
// :scope in that context (unless otherwise defined)."
35+
//
36+
substituted := make([]css_ast.CompoundSelector, 0, len(sel.Selectors))
37+
for _, x := range sel.Selectors {
38+
substituted = p.substituteAmpersandsInCompoundSelector(x, scope, substituted, keepLeadingCombinator)
39+
}
40+
r.Selectors[i] = css_ast.ComplexSelector{Selectors: substituted}
41+
42+
// Filter out pseudo elements because they are ignored by nested style
43+
// rules. This is because pseudo-elements are not valid within :is():
44+
// https://www.w3.org/TR/selectors-4/#matches-pseudo. This restriction
45+
// may be relaxed in the future, but this restriction hash shipped so
46+
// we're stuck with it: https://github.com/w3c/csswg-drafts/issues/7433.
47+
//
48+
// Note: This is only for the parent selector list that is used to
49+
// substitute "&" within child rules. Do not filter out the pseudo
50+
// element from the top-level selector list.
3451
if !sel.UsesPseudoElement() {
35-
// Top-level "&" should be replaced with ":scope" to avoid recursion.
36-
// From https://www.w3.org/TR/css-nesting-1/#nest-selector:
37-
//
38-
// "When used in the selector of a nested style rule, the nesting
39-
// selector represents the elements matched by the parent rule. When
40-
// used in any other context, it represents the same elements as
41-
// :scope in that context (unless otherwise defined)."
42-
//
43-
substituted := make([]css_ast.CompoundSelector, 0, len(sel.Selectors))
44-
for _, x := range sel.Selectors {
45-
substituted = p.substituteAmpersandsInCompoundSelector(x, scope, substituted, keepLeadingCombinator)
46-
}
47-
selectors[n] = css_ast.ComplexSelector{Selectors: substituted}
48-
n++
52+
parentSelectors = append(parentSelectors, css_ast.ComplexSelector{Selectors: substituted})
4953
}
5054
}
51-
selectors = selectors[:n]
5255

5356
// Emit this selector before its nested children
5457
start := len(results)
5558
results = append(results, rule)
5659

5760
// Lower all children and filter out ones that become empty
5861
context := lowerNestingContext{
59-
parentSelectors: selectors,
62+
parentSelectors: parentSelectors,
6063
loweredRules: results,
6164
}
6265
r.Rules = p.lowerNestingInRulesAndReturnRemaining(r.Rules, &context)

internal/css_parser/css_parser_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1213,6 +1213,8 @@ func TestNestedSelector(t *testing.T) {
12131213
expectPrintedLowerUnsupported(t, nesting, ".foo, .bar:before { :hover & { color: red } }", ":hover .foo {\n color: red;\n}\n", "")
12141214
expectPrintedLowerUnsupported(t, nesting, ".bar:before { &:hover { color: red } }", ":is():hover {\n color: red;\n}\n", "")
12151215
expectPrintedLowerUnsupported(t, nesting, ".bar:before { :hover & { color: red } }", ":hover :is() {\n color: red;\n}\n", "")
1216+
expectPrintedLowerUnsupported(t, nesting, ".foo { &:after, & .bar { color: red } }", ".foo:after,\n.foo .bar {\n color: red;\n}\n", "")
1217+
expectPrintedLowerUnsupported(t, nesting, ".foo { & .bar, &:after { color: red } }", ".foo .bar,\n.foo:after {\n color: red;\n}\n", "")
12161218
expectPrintedLowerUnsupported(t, nesting, ".xy { :where(&.foo) { color: red } }", ":where(.xy.foo) {\n color: red;\n}\n", "")
12171219
expectPrintedLowerUnsupported(t, nesting, "div { :where(&.foo) { color: red } }", ":where(div.foo) {\n color: red;\n}\n", "")
12181220
expectPrintedLowerUnsupported(t, nesting, ".xy { :where(.foo&) { color: red } }", ":where(.xy.foo) {\n color: red;\n}\n", "")

0 commit comments

Comments
 (0)