Skip to content

Commit 16e0988

Browse files
committed
update css nesting stuff to match the latest spec
1 parent 47e54fe commit 16e0988

File tree

9 files changed

+359
-181
lines changed

9 files changed

+359
-181
lines changed

CHANGELOG.md

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

33
## Unreleased
44

5+
* Update esbuild's handling of CSS nesting to match the latest specification changes ([#1945](https://github.com/evanw/esbuild/issues/1945))
6+
7+
The syntax for the upcoming CSS nesting feature has [recently changed](https://webkit.org/blog/13813/try-css-nesting-today-in-safari-technology-preview/). The `@nest` prefix that was previously required in some cases is now gone, and nested rules no longer have to start with `&` (as long as they don't start with an identifier or function token).
8+
9+
This release updates esbuild's pass-through handling of CSS nesting syntax to match the latest specification changes. So you can now use esbuild to bundle CSS containing nested rules and try them out in a browser that supports CSS nesting (which includes nightly builds of both Chrome and Safari).
10+
11+
However, I'm not implementing lowering of nested CSS to non-nested CSS for older browsers yet. While the syntax has been decided, the semantics are still in flux. In particular, there is still some debate about changing the fundamental way that CSS nesting works. For example, you might think that the following CSS is equivalent to a `.outer .inner button { ... }` rule:
12+
13+
```css
14+
.inner button {
15+
.outer & {
16+
color: red;
17+
}
18+
}
19+
```
20+
21+
But instead it's actually equivalent to a `.outer :is(.inner button) { ... }` rule which unintuitively also matches the following DOM structure:
22+
23+
```html
24+
<div class="inner">
25+
<div class="outer">
26+
<button></button>
27+
</div>
28+
</div>
29+
```
30+
31+
The `:is()` behavior is preferred by browser implementers because it's more memory-efficient, but the straightforward translation into a `.outer .inner button { ... }` rule is preferred by developers used to the existing CSS preprocessing ecosystem (e.g. SASS). It seems premature to commit esbuild to specific semantics for this syntax at this time given the ongoing debate.
32+
533
* Fix cross-file CSS rule deduplication involving `url()` tokens ([#2936](https://github.com/evanw/esbuild/issues/2936))
634

735
Previously cross-file CSS rule deduplication didn't handle `url()` tokens correctly. These tokens contain references to import paths which may be internal (i.e. in the bundle) or external (i.e. not in the bundle). When comparing two `url()` tokens for equality, the underlying import paths should be compared instead of their references. This release of esbuild fixes `url()` token comparisons. One side effect is that `@font-face` rules should now be deduplicated correctly across files:

internal/bundler_tests/bundler_css_test.go

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -700,18 +700,71 @@ func TestCSSExternalQueryAndHashMatchIssue1822(t *testing.T) {
700700
func TestCSSNestingOldBrowser(t *testing.T) {
701701
css_suite.expectBundled(t, bundled{
702702
files: map[string]string{
703-
"/entry.css": `
704-
a { &:hover { color: red; } }
705-
`,
703+
"/[email protected]": `a { @layer base { color: red; } }`,
704+
"/[email protected]": `a { @media screen { color: red; } }`,
705+
"/nested-ampersand.css": `a { &, & { color: red; } }`,
706+
"/nested-attribute.css": `a { [href] { color: red; } }`,
707+
"/nested-colon.css": `a { :hover { color: red; } }`,
708+
"/nested-dot.css": `a { .cls { color: red; } }`,
709+
"/nested-greaterthan.css": `a { > b { color: red; } }`,
710+
"/nested-hash.css": `a { #id { color: red; } }`,
711+
"/nested-plus.css": `a { + b { color: red; } }`,
712+
"/nested-tilde.css": `a { ~ b { color: red; } }`,
713+
714+
"/toplevel-ampersand.css": `a { &, & { color: red; } }`,
715+
"/toplevel-attribute.css": `a { [href] { color: red; } }`,
716+
"/toplevel-colon.css": `a { :hover { color: red; } }`,
717+
"/toplevel-dot.css": `a { .cls { color: red; } }`,
718+
"/toplevel-greaterthan.css": `a { > b { color: red; } }`,
719+
"/toplevel-hash.css": `a { #id { color: red; } }`,
720+
"/toplevel-plus.css": `a { + b { color: red; } }`,
721+
"/toplevel-tilde.css": `a { ~ b { color: red; } }`,
722+
},
723+
entryPaths: []string{
724+
725+
726+
"/nested-ampersand.css",
727+
"/nested-attribute.css",
728+
"/nested-colon.css",
729+
"/nested-dot.css",
730+
"/nested-greaterthan.css",
731+
"/nested-hash.css",
732+
"/nested-plus.css",
733+
"/nested-tilde.css",
734+
735+
"/toplevel-ampersand.css",
736+
"/toplevel-attribute.css",
737+
"/toplevel-colon.css",
738+
"/toplevel-dot.css",
739+
"/toplevel-greaterthan.css",
740+
"/toplevel-hash.css",
741+
"/toplevel-plus.css",
742+
"/toplevel-tilde.css",
706743
},
707-
entryPaths: []string{"/entry.css"},
708744
options: config.Options{
709745
Mode: config.ModeBundle,
710-
AbsOutputFile: "/out.css",
746+
AbsOutputDir: "/out",
711747
UnsupportedCSSFeatures: compat.Nesting,
712748
OriginalTargetEnv: "chrome10",
713749
},
714-
expectedScanLog: `entry.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
750+
expectedScanLog: `[email protected]: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
751+
[email protected]: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
752+
nested-ampersand.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
753+
nested-attribute.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
754+
nested-colon.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
755+
nested-dot.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
756+
nested-greaterthan.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
757+
nested-hash.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
758+
nested-plus.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
759+
nested-tilde.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
760+
toplevel-ampersand.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
761+
toplevel-attribute.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
762+
toplevel-colon.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
763+
toplevel-dot.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
764+
toplevel-greaterthan.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
765+
toplevel-hash.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
766+
toplevel-plus.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
767+
toplevel-tilde.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
715768
`,
716769
})
717770
}
@@ -802,6 +855,5 @@ func TestDeduplicateRules(t *testing.T) {
802855
AbsOutputDir: "/out",
803856
MinifySyntax: true,
804857
},
805-
expectedScanLog: "no0.css: WARNING: CSS nesting syntax cannot be used outside of a style rule\n",
806858
})
807859
}

internal/bundler_tests/snapshots/snapshots_css.txt

Lines changed: 141 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,148 @@ console.log(void 0);
180180

181181
================================================================================
182182
TestCSSNestingOldBrowser
183-
---------- /out.css ----------
184-
/* entry.css */
183+
---------- /out/[email protected] ----------
184+
185+
a {
186+
@layer base {
187+
color: red;
188+
}
189+
}
190+
191+
---------- /out/[email protected] ----------
192+
193+
a {
194+
@media screen {
195+
color: red;
196+
}
197+
}
198+
199+
---------- /out/nested-ampersand.css ----------
200+
/* nested-ampersand.css */
201+
a {
202+
&,
203+
& {
204+
color: red;
205+
}
206+
}
207+
208+
---------- /out/nested-attribute.css ----------
209+
/* nested-attribute.css */
210+
a {
211+
[href] {
212+
color: red;
213+
}
214+
}
215+
216+
---------- /out/nested-colon.css ----------
217+
/* nested-colon.css */
218+
a {
219+
:hover {
220+
color: red;
221+
}
222+
}
223+
224+
---------- /out/nested-dot.css ----------
225+
/* nested-dot.css */
226+
a {
227+
.cls {
228+
color: red;
229+
}
230+
}
231+
232+
---------- /out/nested-greaterthan.css ----------
233+
/* nested-greaterthan.css */
234+
a {
235+
> b {
236+
color: red;
237+
}
238+
}
239+
240+
---------- /out/nested-hash.css ----------
241+
/* nested-hash.css */
242+
a {
243+
#id {
244+
color: red;
245+
}
246+
}
247+
248+
---------- /out/nested-plus.css ----------
249+
/* nested-plus.css */
250+
a {
251+
+ b {
252+
color: red;
253+
}
254+
}
255+
256+
---------- /out/nested-tilde.css ----------
257+
/* nested-tilde.css */
258+
a {
259+
~ b {
260+
color: red;
261+
}
262+
}
263+
264+
---------- /out/toplevel-ampersand.css ----------
265+
/* toplevel-ampersand.css */
266+
a {
267+
&,
268+
& {
269+
color: red;
270+
}
271+
}
272+
273+
---------- /out/toplevel-attribute.css ----------
274+
/* toplevel-attribute.css */
275+
a {
276+
[href] {
277+
color: red;
278+
}
279+
}
280+
281+
---------- /out/toplevel-colon.css ----------
282+
/* toplevel-colon.css */
283+
a {
284+
:hover {
285+
color: red;
286+
}
287+
}
288+
289+
---------- /out/toplevel-dot.css ----------
290+
/* toplevel-dot.css */
291+
a {
292+
.cls {
293+
color: red;
294+
}
295+
}
296+
297+
---------- /out/toplevel-greaterthan.css ----------
298+
/* toplevel-greaterthan.css */
299+
a {
300+
> b {
301+
color: red;
302+
}
303+
}
304+
305+
---------- /out/toplevel-hash.css ----------
306+
/* toplevel-hash.css */
307+
a {
308+
#id {
309+
color: red;
310+
}
311+
}
312+
313+
---------- /out/toplevel-plus.css ----------
314+
/* toplevel-plus.css */
315+
a {
316+
+ b {
317+
color: red;
318+
}
319+
}
320+
321+
---------- /out/toplevel-tilde.css ----------
322+
/* toplevel-tilde.css */
185323
a {
186-
&:hover {
324+
~ b {
187325
color: red;
188326
}
189327
}

internal/css_ast/css_ast.go

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -437,12 +437,11 @@ func (r *RUnknownAt) Hash() (uint32, bool) {
437437
type RSelector struct {
438438
Selectors []ComplexSelector
439439
Rules []Rule
440-
HasAtNest bool
441440
}
442441

443442
func (a *RSelector) Equal(rule R, check *CrossFileEqualityCheck) bool {
444443
b, ok := rule.(*RSelector)
445-
if ok && len(a.Selectors) == len(b.Selectors) && a.HasAtNest == b.HasAtNest {
444+
if ok && len(a.Selectors) == len(b.Selectors) {
446445
for i, ai := range a.Selectors {
447446
if !ai.Equal(b.Selectors[i], check) {
448447
return false
@@ -606,7 +605,7 @@ func (a ComplexSelector) Equal(b ComplexSelector, check *CrossFileEqualityCheck)
606605

607606
for i, ai := range a.Selectors {
608607
bi := b.Selectors[i]
609-
if ai.NestingSelector != bi.NestingSelector || ai.Combinator != bi.Combinator {
608+
if ai.HasNestingSelector != bi.HasNestingSelector || ai.Combinator != bi.Combinator {
610609
return false
611610
}
612611

@@ -629,19 +628,11 @@ func (a ComplexSelector) Equal(b ComplexSelector, check *CrossFileEqualityCheck)
629628
return true
630629
}
631630

632-
type NestingSelector uint8
633-
634-
const (
635-
NestingSelectorNone NestingSelector = iota
636-
NestingSelectorPrefix // "&a {}"
637-
NestingSelectorPresentButNotPrefix // "a& {}"
638-
)
639-
640631
type CompoundSelector struct {
641-
Combinator string // Optional, may be ""
642-
TypeSelector *NamespacedName
643-
SubclassSelectors []SS
644-
NestingSelector NestingSelector // "&"
632+
Combinator string // Optional, may be ""
633+
TypeSelector *NamespacedName
634+
SubclassSelectors []SS
635+
HasNestingSelector bool // "&"
645636
}
646637

647638
type NameToken struct {

0 commit comments

Comments
 (0)