Skip to content

Commit

Permalink
fix #3119: avoid :is() with pseudo-elements
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed May 16, 2023
1 parent e7481cd commit 905959f
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 8 deletions.
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,30 @@

This change should hopefully not affect correct code. It should hopefully introduce type errors only for incorrect code.

* Fix CSS nesting transform for pseudo-elements ([#3119](https://github.com/evanw/esbuild/issues/3119))

This release fixes esbuild's CSS nesting transform for pseudo-elements (e.g. `::before` and `::after`). The CSS nesting specification says that [the nesting selector does not work with pseudo-elements](https://www.w3.org/TR/css-nesting-1/#ref-for-matches-pseudo%E2%91%A0). This can be seen in the example below: esbuild does not carry the parent pseudo-element `::before` through the nesting selector `&`. However, that doesn't apply to pseudo-elements that are within the same selector. Previously esbuild had a bug where it considered pseudo-elements in both locations as invalid. This release changes esbuild to only consider those from the parent selector invalid, which should align with the specification:

```css
/* Original code */
a, b::before {
&.c, &::after {
content: 'd';
}
}
/* Old output (with --target=chrome90) */
a:is(.c, ::after) {
content: "d";
}
/* New output (with --target=chrome90) */
a.c,
a::after {
content: "d";
}
```

## 0.17.19

* Fix CSS transform bugs with nested selectors that start with a combinator ([#3096](https://github.com/evanw/esbuild/issues/3096))
Expand Down
8 changes: 8 additions & 0 deletions internal/css_parser/css_nesting.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,14 @@ func lowerNestingInRuleWithContext(rule css_ast.Rule, context *lowerNestingConte
sel.Selectors = append([]css_ast.CompoundSelector{{HasNestingSelector: true}}, sel.Selectors...)
}

// Pseudo-elements aren't supported by ":is" (i.e. ":is(div, div::before)"
// is the same as ":is(div)") so we need to avoid generating ":is" if a
// pseudo-element is present.
if sel.UsesPseudoElement() {
canUseGroupDescendantCombinator = false
canUseGroupSubSelector = false
}

// Are all children of the form "& «something»"?
if len(sel.Selectors) < 2 || !sel.Selectors[0].IsSingleAmpersand() {
canUseGroupDescendantCombinator = false
Expand Down
21 changes: 13 additions & 8 deletions internal/css_parser/css_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,19 @@ func TestNestedSelector(t *testing.T) {
expectPrintedLower(t, "~ .a { > &.b, + &.c { color: red } }", "~ .a > .a.b,\n~ .a + .a.c {\n color: red;\n}\n")
expectPrintedLower(t, ".foo .bar { > &.a, > &.b { color: red } }", ".foo .bar > :is(.foo .bar.a, .foo .bar.b) {\n color: red;\n}\n")
expectPrintedLower(t, ".foo .bar { > &.a, + &.b { color: red } }", ".foo .bar > :is(.foo .bar).a,\n.foo .bar + :is(.foo .bar).b {\n color: red;\n}\n")
expectPrintedLower(t, ".demo { .lg { &.triangle, &.circle { color: red } } }", ".demo .lg:is(.triangle, .circle) {\n color: red;\n}\n")
expectPrintedLower(t, ".demo { .lg { .triangle, .circle { color: red } } }", ".demo .lg :is(.triangle, .circle) {\n color: red;\n}\n")
expectPrintedLower(t, ".card { .featured & & & { color: red } }", ".featured .card .card .card {\n color: red;\n}\n")
expectPrintedLower(t, ".card { &--header { color: red } }", "--header.card {\n color: red;\n}\n")

// Check pseudo-elements (those coming through "&" must be dropped)
expectPrintedLower(t, "a, b::before { &.foo { color: red } }", "a.foo {\n color: red;\n}\n")
expectPrintedLower(t, "a, b::before { & .foo { color: red } }", "a .foo {\n color: red;\n}\n")
expectPrintedLower(t, "a { &.foo, &::before { color: red } }", "a.foo,\na::before {\n color: red;\n}\n")
expectPrintedLower(t, "a { & .foo, & ::before { color: red } }", "a .foo,\na ::before {\n color: red;\n}\n")
expectPrintedLower(t, "a, b::before { color: blue; &.foo, &::after { color: red } }", "a,\nb::before {\n color: blue;\n}\na.foo,\na::after {\n color: red;\n}\n")

// Test at-rules
expectPrintedLower(t, ".foo { @media screen {} }", "")
expectPrintedLower(t, ".foo { @media screen { color: red } }", "@media screen {\n .foo {\n color: red;\n }\n}\n")
expectPrintedLower(t, ".foo { @media screen { &:hover { color: red } } }", "@media screen {\n .foo:hover {\n color: red;\n }\n}\n")
Expand All @@ -943,14 +956,6 @@ func TestNestedSelector(t *testing.T) {
"@supports (display: flex) {\n @supports selector(h2 > p) {\n :is(a, b):hover {\n color: red;\n }\n }\n}\n")
expectPrintedLower(t, "@layer foo { @layer bar { a, b { &:hover { color: red } } } }",
"@layer foo {\n @layer bar {\n :is(a, b):hover {\n color: red;\n }\n }\n}\n")
expectPrintedLower(t, ".demo { .lg { &.triangle, &.circle { color: red } } }",
".demo .lg:is(.triangle, .circle) {\n color: red;\n}\n")
expectPrintedLower(t, ".demo { .lg { .triangle, .circle { color: red } } }",
".demo .lg :is(.triangle, .circle) {\n color: red;\n}\n")
expectPrintedLower(t, ".card { .featured & & & { color: red } }",
".featured .card .card .card {\n color: red;\n}\n")
expectPrintedLower(t, ".card { &--header { color: red } }",
"--header.card {\n color: red;\n}\n")
expectPrintedLower(t, ".card { @supports (selector(&)) { &:hover { color: red } } }",
"@supports (selector(&)) {\n .card:hover {\n color: red;\n }\n}\n")
expectPrintedLower(t, "html { @layer base { color: blue; @layer support { & body { color: red } } } }",
Expand Down

0 comments on commit 905959f

Please sign in to comment.