Skip to content

Commit adac05d

Browse files
committed
fix #3997, fix #4038: && should add specificity
1 parent 31e7b4d commit adac05d

File tree

8 files changed

+79
-40
lines changed

8 files changed

+79
-40
lines changed

CHANGELOG.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
It has been requested for esbuild to delete files when a build fails in watch mode. Previously esbuild left the old files in place, which could cause people to not immediately realize that the most recent build failed. With this release, esbuild will now delete all output files if a rebuild fails. Fixing the build error and triggering another rebuild will restore all output files again.
88

9-
* Fix correctness issues with the CSS nesting transform ([#3620](https://github.com/evanw/esbuild/issues/3620), [#4037](https://github.com/evanw/esbuild/pull/4037))
9+
* Fix correctness issues with the CSS nesting transform ([#3620](https://github.com/evanw/esbuild/issues/3620), [#3997](https://github.com/evanw/esbuild/issues/3997), [#4037](https://github.com/evanw/esbuild/pull/4037), [#4038](https://github.com/evanw/esbuild/pull/4038))
1010

1111
This release fixes the following problems:
1212

@@ -35,6 +35,34 @@
3535

3636
Thanks to [@tim-we](https://github.com/tim-we) for working on a fix.
3737

38+
* The `&` CSS nesting selector can be repeated multiple times to increase CSS specificity. Previously esbuild ignored this possibility and incorrectly considered `&&` to have the same specificity as `&`. With this release, this should now work correctly:
39+
40+
```css
41+
/* Original code (color should be red) */
42+
div {
43+
&& { color: red }
44+
& { color: blue }
45+
}
46+
47+
/* Old output (with --supported:nesting=false) */
48+
div {
49+
color: red;
50+
}
51+
div {
52+
color: blue;
53+
}
54+
55+
/* New output (with --supported:nesting=false) */
56+
div:is(div) {
57+
color: red;
58+
}
59+
div {
60+
color: blue;
61+
}
62+
```
63+
64+
Thanks to [@CPunisher](https://github.com/CPunisher) for working on a fix.
65+
3866
* Fix incorrect package for `@esbuild/netbsd-arm64` ([#4018](https://github.com/evanw/esbuild/issues/4018))
3967

4068
Due to a copy+paste typo, the binary published to `@esbuild/netbsd-arm64` was not actually for `arm64`, and didn't run in that environment. This release should fix running esbuild in that environment (NetBSD on 64-bit ARM). Sorry about the mistake.

internal/css_ast/css_ast.go

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -802,7 +802,7 @@ func (s ComplexSelector) CloneWithoutLeadingCombinator() ComplexSelector {
802802
func (sel ComplexSelector) IsRelative() bool {
803803
if sel.Selectors[0].Combinator.Byte == 0 {
804804
for _, inner := range sel.Selectors {
805-
if inner.HasNestingSelector() {
805+
if len(inner.NestingSelectorLocs) > 0 {
806806
return false
807807
}
808808
for _, ss := range inner.SubclassSelectors {
@@ -861,7 +861,7 @@ func (a ComplexSelector) Equal(b ComplexSelector, check *CrossFileEqualityCheck)
861861

862862
for i, ai := range a.Selectors {
863863
bi := b.Selectors[i]
864-
if ai.HasNestingSelector() != bi.HasNestingSelector() || ai.Combinator.Byte != bi.Combinator.Byte {
864+
if len(ai.NestingSelectorLocs) != len(bi.NestingSelectorLocs) || ai.Combinator.Byte != bi.Combinator.Byte {
865865
return false
866866
}
867867

@@ -890,25 +890,21 @@ type Combinator struct {
890890
}
891891

892892
type CompoundSelector struct {
893-
TypeSelector *NamespacedName
894-
SubclassSelectors []SubclassSelector
895-
NestingSelectorLoc ast.Index32 // "&"
896-
Combinator Combinator // Optional, may be 0
893+
TypeSelector *NamespacedName
894+
SubclassSelectors []SubclassSelector
895+
NestingSelectorLocs []logger.Loc // "&" vs. "&&" is different specificity
896+
Combinator Combinator // Optional, may be 0
897897

898898
// If this is true, this is a "&" that was generated by a bare ":local" or ":global"
899899
WasEmptyFromLocalOrGlobal bool
900900
}
901901

902-
func (sel *CompoundSelector) HasNestingSelector() bool {
903-
return sel.NestingSelectorLoc.IsValid()
904-
}
905-
906902
func (sel CompoundSelector) IsSingleAmpersand() bool {
907-
return sel.HasNestingSelector() && sel.Combinator.Byte == 0 && sel.TypeSelector == nil && len(sel.SubclassSelectors) == 0
903+
return len(sel.NestingSelectorLocs) == 1 && sel.Combinator.Byte == 0 && sel.TypeSelector == nil && len(sel.SubclassSelectors) == 0
908904
}
909905

910906
func (sel CompoundSelector) IsInvalidBecauseEmpty() bool {
911-
return !sel.HasNestingSelector() && sel.TypeSelector == nil && len(sel.SubclassSelectors) == 0
907+
return len(sel.NestingSelectorLocs) == 0 && sel.TypeSelector == nil && len(sel.SubclassSelectors) == 0
912908
}
913909

914910
func (sel CompoundSelector) Range() (r logger.Range) {
@@ -918,8 +914,8 @@ func (sel CompoundSelector) Range() (r logger.Range) {
918914
if sel.TypeSelector != nil {
919915
r.ExpandBy(sel.TypeSelector.Range())
920916
}
921-
if sel.HasNestingSelector() {
922-
r.ExpandBy(logger.Range{Loc: logger.Loc{Start: int32(sel.NestingSelectorLoc.GetIndex())}, Len: 1})
917+
for _, loc := range sel.NestingSelectorLocs {
918+
r.ExpandBy(logger.Range{Loc: loc, Len: 1})
923919
}
924920
if len(sel.SubclassSelectors) > 0 {
925921
for _, ss := range sel.SubclassSelectors {

internal/css_parser/css_nesting.go

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package css_parser
33
import (
44
"fmt"
55

6-
"github.com/evanw/esbuild/internal/ast"
76
"github.com/evanw/esbuild/internal/compat"
87
"github.com/evanw/esbuild/internal/css_ast"
98
"github.com/evanw/esbuild/internal/logger"
@@ -123,7 +122,7 @@ func (p *parser) lowerNestingInRuleWithContext(rule css_ast.Rule, context *lower
123122

124123
// Inject the implicit "&" now for simplicity later on
125124
if sel.IsRelative() {
126-
sel.Selectors = append([]css_ast.CompoundSelector{{NestingSelectorLoc: ast.MakeIndex32(uint32(rule.Loc.Start))}}, sel.Selectors...)
125+
sel.Selectors = append([]css_ast.CompoundSelector{{NestingSelectorLocs: []logger.Loc{rule.Loc}}}, sel.Selectors...)
127126
}
128127
}
129128

@@ -288,9 +287,7 @@ func (p *parser) substituteAmpersandsInCompoundSelector(
288287
results []css_ast.CompoundSelector,
289288
strip leadingCombinatorStrip,
290289
) []css_ast.CompoundSelector {
291-
if sel.HasNestingSelector() {
292-
nestingSelectorLoc := logger.Loc{Start: int32(sel.NestingSelectorLoc.GetIndex())}
293-
sel.NestingSelectorLoc = ast.Index32{}
290+
for _, nestingSelectorLoc := range sel.NestingSelectorLocs {
294291
replacement := replacementFn(nestingSelectorLoc)
295292

296293
// Convert the replacement to a single compound selector
@@ -351,6 +348,7 @@ func (p *parser) substituteAmpersandsInCompoundSelector(
351348
sel.SubclassSelectors = append(subclassSelectorPrefix, sel.SubclassSelectors...)
352349
}
353350
}
351+
sel.NestingSelectorLocs = nil
354352

355353
// "div { :is(&.foo) {} }" => ":is(div.foo) {}"
356354
for _, ss := range sel.SubclassSelectors {

internal/css_parser/css_parser.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -898,7 +898,7 @@ var nonDeprecatedElementsSupportedByIE7 = map[string]bool{
898898
func isSafeSelectors(complexSelectors []css_ast.ComplexSelector) bool {
899899
for _, complex := range complexSelectors {
900900
for _, compound := range complex.Selectors {
901-
if compound.HasNestingSelector() {
901+
if len(compound.NestingSelectorLocs) > 0 {
902902
// Bail because this is an extension: https://drafts.csswg.org/css-nesting-1/
903903
return false
904904
}
@@ -2088,8 +2088,8 @@ func (p *parser) parseSelectorRule(isTopLevel bool, opts parseSelectorOpts) css_
20882088
composesContext.problemRange = logger.Range{Loc: first.Combinator.Loc, Len: 1}
20892089
} else if first.TypeSelector != nil {
20902090
composesContext.problemRange = first.TypeSelector.Range()
2091-
} else if first.HasNestingSelector() {
2092-
composesContext.problemRange = logger.Range{Loc: logger.Loc{Start: int32(first.NestingSelectorLoc.GetIndex())}, Len: 1}
2091+
} else if len(first.NestingSelectorLocs) > 0 {
2092+
composesContext.problemRange = logger.Range{Loc: first.NestingSelectorLocs[0], Len: 1}
20932093
} else {
20942094
for i, ss := range first.SubclassSelectors {
20952095
class, ok := ss.Data.(*css_ast.SSClass)

internal/css_parser/css_parser_selector.go

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"fmt"
55
"strings"
66

7-
"github.com/evanw/esbuild/internal/ast"
87
"github.com/evanw/esbuild/internal/css_ast"
98
"github.com/evanw/esbuild/internal/css_lexer"
109
"github.com/evanw/esbuild/internal/logger"
@@ -82,7 +81,7 @@ func (p *parser) parseSelectorList(opts parseSelectorOpts) (list []css_ast.Compl
8281

8382
case canRemoveLeadingAmpersandIfNotFirst:
8483
for i := 1; i < len(list); i++ {
85-
if sel := list[i].Selectors[0]; !sel.HasNestingSelector() && (sel.Combinator.Byte != 0 || sel.TypeSelector == nil) {
84+
if sel := list[i].Selectors[0]; len(sel.NestingSelectorLocs) == 0 && (sel.Combinator.Byte != 0 || sel.TypeSelector == nil) {
8685
list[0].Selectors = list[0].Selectors[1:]
8786
list[0], list[i] = list[i], list[0]
8887
break
@@ -97,8 +96,8 @@ func (p *parser) parseSelectorList(opts parseSelectorOpts) (list []css_ast.Compl
9796

9897
func mergeCompoundSelectors(target *css_ast.CompoundSelector, source css_ast.CompoundSelector) {
9998
// ".foo:local(&)" => "&.foo"
100-
if source.HasNestingSelector() && !target.HasNestingSelector() {
101-
target.NestingSelectorLoc = source.NestingSelectorLoc
99+
if len(source.NestingSelectorLocs) > 0 && len(target.NestingSelectorLocs) == 0 {
100+
target.NestingSelectorLocs = source.NestingSelectorLocs
102101
}
103102

104103
if source.TypeSelector != nil {
@@ -210,7 +209,7 @@ func (p *parser) flattenLocalAndGlobalSelectors(list []css_ast.ComplexSelector,
210209
if len(selectors) == 0 {
211210
// Treat a bare ":global" or ":local" as a bare "&" nesting selector
212211
selectors = append(selectors, css_ast.CompoundSelector{
213-
NestingSelectorLoc: ast.MakeIndex32(uint32(sel.Selectors[0].Range().Loc.Start)),
212+
NestingSelectorLocs: []logger.Loc{sel.Selectors[0].Range().Loc},
214213
WasEmptyFromLocalOrGlobal: true,
215214
})
216215

@@ -235,7 +234,7 @@ const (
235234
func analyzeLeadingAmpersand(sel css_ast.ComplexSelector, isDeclarationContext bool) leadingAmpersand {
236235
if len(sel.Selectors) > 1 {
237236
if first := sel.Selectors[0]; first.IsSingleAmpersand() {
238-
if second := sel.Selectors[1]; second.Combinator.Byte == 0 && second.HasNestingSelector() {
237+
if second := sel.Selectors[1]; second.Combinator.Byte == 0 && len(second.NestingSelectorLocs) > 0 {
239238
// ".foo { & &.bar {} }" => ".foo { & &.bar {} }"
240239
} else if second.Combinator.Byte != 0 || second.TypeSelector == nil || !isDeclarationContext {
241240
// "& + div {}" => "+ div {}"
@@ -330,7 +329,7 @@ func (p *parser) parseCompoundSelector(opts parseComplexSelectorOpts) (sel css_a
330329
hasLeadingNestingSelector := p.peek(css_lexer.TDelimAmpersand)
331330
if hasLeadingNestingSelector {
332331
p.nestingIsPresent = true
333-
sel.NestingSelectorLoc = ast.MakeIndex32(uint32(startLoc.Start))
332+
sel.NestingSelectorLocs = append(sel.NestingSelectorLocs, startLoc)
334333
p.advance()
335334
}
336335

@@ -445,7 +444,7 @@ subclassSelectors:
445444
case css_lexer.TDelimAmpersand:
446445
// This is an extension: https://drafts.csswg.org/css-nesting-1/
447446
p.nestingIsPresent = true
448-
sel.NestingSelectorLoc = ast.MakeIndex32(uint32(subclassToken.Range.Loc.Start))
447+
sel.NestingSelectorLocs = append(sel.NestingSelectorLocs, subclassToken.Range.Loc)
449448
p.advance()
450449

451450
default:

internal/css_parser/css_parser_test.go

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1049,7 +1049,7 @@ func TestNestedSelector(t *testing.T) {
10491049
expectPrinted(t, "a { &a|b {} }", "a {\n &a|b {\n }\n}\n", "<stdin>: WARNING: Cannot use type selector \"a|b\" directly after nesting selector \"&\"\n"+sassWarningWrap)
10501050
expectPrinted(t, "a { &[b] {} }", "a {\n &[b] {\n }\n}\n", "")
10511051

1052-
expectPrinted(t, "a { && {} }", "a {\n & {\n }\n}\n", "")
1052+
expectPrinted(t, "a { && {} }", "a {\n && {\n }\n}\n", "")
10531053
expectPrinted(t, "a { & + & {} }", "a {\n & + & {\n }\n}\n", "")
10541054
expectPrinted(t, "a { & > & {} }", "a {\n & > & {\n }\n}\n", "")
10551055
expectPrinted(t, "a { & ~ & {} }", "a {\n & ~ & {\n }\n}\n", "")
@@ -1141,12 +1141,13 @@ func TestNestedSelector(t *testing.T) {
11411141

11421142
// Inline no-op nesting
11431143
expectPrintedMangle(t, "div { & { color: red } }", "div {\n color: red;\n}\n", "")
1144-
expectPrintedMangle(t, "div { && { color: red } }", "div {\n color: red;\n}\n", "")
1144+
expectPrintedMangle(t, "div { && { color: red } }", "div {\n && {\n color: red;\n }\n}\n", "")
11451145
expectPrintedMangle(t, "div { zoom: 2; & { color: red } }", "div {\n zoom: 2;\n color: red;\n}\n", "")
1146-
expectPrintedMangle(t, "div { zoom: 2; && { color: red } }", "div {\n zoom: 2;\n color: red;\n}\n", "")
1147-
expectPrintedMangle(t, "div { &, && { color: red } zoom: 2 }", "div {\n zoom: 2;\n color: red;\n}\n", "")
1148-
expectPrintedMangle(t, "div { &&, & { color: red } zoom: 2 }", "div {\n zoom: 2;\n color: red;\n}\n", "")
1149-
expectPrintedMangle(t, "div { a: 1; & { b: 4 } b: 2; && { c: 5 } c: 3 }", "div {\n a: 1;\n b: 2;\n c: 3;\n b: 4;\n c: 5;\n}\n", "")
1146+
expectPrintedMangle(t, "div { zoom: 2; && { color: red } }", "div {\n zoom: 2;\n && {\n color: red;\n }\n}\n", "")
1147+
expectPrintedMangle(t, "div { &, & { color: red } zoom: 2 }", "div {\n zoom: 2;\n color: red;\n}\n", "")
1148+
expectPrintedMangle(t, "div { &, && { color: red } zoom: 2 }", "div {\n &,\n && {\n color: red;\n }\n zoom: 2;\n}\n", "")
1149+
expectPrintedMangle(t, "div { &&, & { color: red } zoom: 2 }", "div {\n &&,\n & {\n color: red;\n }\n zoom: 2;\n}\n", "")
1150+
expectPrintedMangle(t, "div { a: 1; & { b: 4 } b: 2; && { c: 5 } c: 3 }", "div {\n a: 1;\n b: 2;\n && {\n c: 5;\n }\n c: 3;\n b: 4;\n}\n", "")
11501151
expectPrintedMangle(t, "div { .b { x: 1 } & { x: 2 } }", "div {\n .b {\n x: 1;\n }\n x: 2;\n}\n", "")
11511152
expectPrintedMangle(t, "div { & { & { & { color: red } } & { & { zoom: 2 } } } }", "div {\n color: red;\n zoom: 2;\n}\n", "")
11521153

@@ -1262,6 +1263,24 @@ func TestNestedSelector(t *testing.T) {
12621263
expectPrintedLowerUnsupported(t, nesting, ".demo { .lg { .triangle, .circle { color: red } } }", ".demo .lg .triangle,\n.demo .lg .circle {\n color: red;\n}\n", "")
12631264
expectPrintedLowerUnsupported(t, nesting, ".card { .featured & & & { color: red } }", ".featured .card .card .card {\n color: red;\n}\n", "")
12641265

1266+
// Duplicate "&" may be used to increase specificity
1267+
expectPrintedLowerUnsupported(t, nesting, ".foo { &&&.bar { color: red } }", ".foo.foo.foo.bar {\n color: red;\n}\n", "")
1268+
expectPrintedLowerUnsupported(t, nesting, ".foo { &&& .bar { color: red } }", ".foo.foo.foo .bar {\n color: red;\n}\n", "")
1269+
expectPrintedLowerUnsupported(t, nesting, ".foo { .bar&&& { color: red } }", ".foo.foo.foo.bar {\n color: red;\n}\n", "")
1270+
expectPrintedLowerUnsupported(t, nesting, ".foo { .bar &&& { color: red } }", ".bar .foo.foo.foo {\n color: red;\n}\n", "")
1271+
expectPrintedLowerUnsupported(t, nesting, ".foo { &.bar&.baz& { color: red } }", ".foo.foo.foo.bar.baz {\n color: red;\n}\n", "")
1272+
expectPrintedLowerUnsupported(t, nesting, "a { &&&.bar { color: red } }", "a:is(a):is(a).bar {\n color: red;\n}\n", "")
1273+
expectPrintedLowerUnsupported(t, nesting, "a { &&& .bar { color: red } }", "a:is(a):is(a) .bar {\n color: red;\n}\n", "")
1274+
expectPrintedLowerUnsupported(t, nesting, "a { .bar&&& { color: red } }", "a:is(a):is(a).bar {\n color: red;\n}\n", "")
1275+
expectPrintedLowerUnsupported(t, nesting, "a { .bar &&& { color: red } }", ".bar a:is(a):is(a) {\n color: red;\n}\n", "")
1276+
expectPrintedLowerUnsupported(t, nesting, "a { &.bar&.baz& { color: red } }", "a:is(a):is(a).bar.baz {\n color: red;\n}\n", "")
1277+
expectPrintedLowerUnsupported(t, nesting, "a, b { &&&.bar { color: red } }", ":is(a, b):is(a, b):is(a, b).bar {\n color: red;\n}\n", "")
1278+
expectPrintedLowerUnsupported(t, nesting, "a, b { &&& .bar { color: red } }", ":is(a, b):is(a, b):is(a, b) .bar {\n color: red;\n}\n", "")
1279+
expectPrintedLowerUnsupported(t, nesting, "a, b { .bar&&& { color: red } }", ":is(a, b):is(a, b):is(a, b).bar {\n color: red;\n}\n", "")
1280+
expectPrintedLowerUnsupported(t, nesting, "a, b { .bar &&& { color: red } }", ".bar :is(a, b):is(a, b):is(a, b) {\n color: red;\n}\n", "")
1281+
expectPrintedLowerUnsupported(t, nesting, "a, b { &.bar&.baz& { color: red } }", ":is(a, b):is(a, b):is(a, b).bar.baz {\n color: red;\n}\n", "")
1282+
expectPrintedLowerUnsupported(t, nesting, ".foo { &, &&.bar, &&& .baz { color: red } }", ".foo,\n.foo.foo.bar,\n.foo.foo.foo .baz {\n color: red;\n}\n", "")
1283+
12651284
// These are invalid SASS-style nested suffixes
12661285
expectPrintedLower(t, ".card { &--header { color: red } }", ".card {\n &--header {\n color: red;\n }\n}\n",
12671286
"<stdin>: WARNING: Cannot use type selector \"--header\" directly after nesting selector \"&\"\n"+sassWarningWrap)

internal/css_printer/css_printer.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -459,9 +459,9 @@ func (p *printer) printCompoundSelector(sel css_ast.CompoundSelector, isFirst bo
459459
p.printNamespacedName(*sel.TypeSelector, whitespace)
460460
}
461461

462-
if sel.HasNestingSelector() {
462+
for _, loc := range sel.NestingSelectorLocs {
463463
if p.options.AddSourceMappings {
464-
p.builder.AddSourceMapping(logger.Loc{Start: int32(sel.NestingSelectorLoc.GetIndex())}, "", p.css)
464+
p.builder.AddSourceMapping(loc, "", p.css)
465465
}
466466

467467
p.print("&")

scripts/browser/index.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,6 @@
234234

235235
// See: https://github.com/evanw/esbuild/issues/3997
236236
async cssNestingIssue3997() {
237-
return // TODO: Remove this
238237
await assertSameColorsWithNestingTransform(esbuild, {
239238
css: `
240239
.foo {

0 commit comments

Comments
 (0)