Skip to content

Commit

Permalink
css: container names are local with local-css
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jul 27, 2023
1 parent 5f5a60e commit be33c09
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 9 deletions.
21 changes: 16 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

## Unreleased

* Support local names in CSS for `@keyframe` and `@counter-style` ([#20](https://github.com/evanw/esbuild/issues/20))
* Support local names in CSS for `@keyframe`, `@counter-style`, and `@container` ([#20](https://github.com/evanw/esbuild/issues/20))

This release extends support for local names in CSS files loaded with the `local-css` loader to cover the `@keyframe` and `@counter-style` rules (and also `animation` and `list-style` declarations). Here's an example:
This release extends support for local names in CSS files loaded with the `local-css` loader to cover the `@keyframe`, `@counter-style`, and `@container` rules (and also `animation`, `list-style`, and `container` declarations). Here's an example:

```css
@keyframes pulse {
Expand All @@ -15,9 +15,13 @@
system: cyclic;
symbols: 🌕 🌖 🌗 🌘 🌑 🌒 🌓 🌔;
}
@container squish {
li { float: left }
}
ul {
animation: 2s ease-in-out infinite pulse;
list-style: inside moon;
container: squish / size;
}
```

Expand All @@ -36,26 +40,33 @@
system: cyclic;
symbols: 🌕 🌖 🌗 🌘 🌑 🌒 🌓 🌔;
}
@container stdin_squish {
li {
float: left;
}
}
ul {
animation: 2s ease-in-out infinite stdin_pulse;
list-style: inside stdin_moon;
container: stdin_squish / size;
}
```

If you want to use a global name within a file loaded with the `local-css` loader, you can use a `:global` selector to do that:

```css
div {
/* All symbols are global inside this scope
* (i.e. "pulse" and "moon" are global below) */
/* All symbols are global inside this scope (i.e.
* "pulse", "moon", and "squish" are global below) */
:global {
animation: 2s ease-in-out infinite pulse;
list-style: inside moon;
container: squish / size;
}
}
```

If you want to use `@keyframes` or `@counter-style` with a global name, make sure it's in a file that uses the `css` or `global-css` loader instead of the `local-css` loader. For example, you can configure `--loader:.module.css=local-css` so that the `local-css` loader only applies to `*.module.css` files.
If you want to use `@keyframes`, `@counter-style`, or `@container` with a global name, make sure it's in a file that uses the `css` or `global-css` loader instead of the `local-css` loader. For example, you can configure `--loader:.module.css=local-css` so that the `local-css` loader only applies to `*.module.css` files.

* Support strings as keyframe animation names in CSS ([#2555](https://github.com/evanw/esbuild/issues/2555))

Expand Down
45 changes: 45 additions & 0 deletions internal/bundler_tests/bundler_css_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,51 @@ func TestImportCSSFromJSLocalAtCounterStyle(t *testing.T) {
})
}

func TestImportCSSFromJSLocalAtContainer(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
import styles from "./styles.css"
console.log(styles)
`,
"/styles.css": `
@container not (max-width: 100px) { div { color: red } }
@container local (max-width: 100px) { div { color: red } }
@container local not (max-width: 100px) { div { color: red } }
@container local (max-width: 100px) or (min-height: 100px) { div { color: red } }
@container local (max-width: 100px) and (min-height: 100px) { div { color: red } }
@container general_enclosed(max-width: 100px) { div { color: red } }
@container local general_enclosed(max-width: 100px) { div { color: red } }
div :global { container-name: NONE initial }
div :local { container-name: none INITIAL }
div :global { container-name: GLOBAL1 GLOBAL2 }
div :local { container-name: local1 local2 }
div :global { container: none }
div :local { container: NONE }
div :global { container: NONE / size }
div :local { container: none / size }
div :global { container: GLOBAL1 GLOBAL2 }
div :local { container: local1 local2 }
div :global { container: GLOBAL1 GLOBAL2 / size }
div :local { container: local1 local2 / size }
`,
},
entryPaths: []string{"/entry.js"},
options: config.Options{
Mode: config.ModeBundle,
AbsOutputDir: "/out",
ExtensionToLoader: map[string]config.Loader{
".js": config.LoaderJS,
".css": config.LoaderLocalCSS,
},
UnsupportedCSSFeatures: compat.Nesting,
},
})
}

func TestImportCSSFromJSNthIndexLocal(t *testing.T) {
css_suite.expectBundled(t, bundled{
files: map[string]string{
Expand Down
87 changes: 87 additions & 0 deletions internal/bundler_tests/snapshots/snapshots_css.txt
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,93 @@ TestIgnoreURLsInAtRulePrelude
}
}

================================================================================
TestImportCSSFromJSLocalAtContainer
---------- /out/entry.js ----------
// styles.css
var styles_default = {
local: "styles_local",
local1: "styles_local1",
local2: "styles_local2"
};

// entry.js
console.log(styles_default);

---------- /out/entry.css ----------
/* styles.css */
@container not (max-width: 100px) {
div {
color: red;
}
}
@container styles_local (max-width: 100px) {
div {
color: red;
}
}
@container styles_local not (max-width: 100px) {
div {
color: red;
}
}
@container styles_local (max-width: 100px) or (min-height: 100px) {
div {
color: red;
}
}
@container styles_local (max-width: 100px) and (min-height: 100px) {
div {
color: red;
}
}
@container general_enclosed(max-width: 100px) {
div {
color: red;
}
}
@container styles_local general_enclosed(max-width: 100px) {
div {
color: red;
}
}
div {
container-name: NONE initial;
}
div {
container-name: none INITIAL;
}
div {
container-name: GLOBAL1 GLOBAL2;
}
div {
container-name: styles_local1 styles_local2;
}
div {
container: none;
}
div {
container: NONE;
}
div {
container: NONE / size;
}
div {
container: none / size;
}
div {
container: GLOBAL1 GLOBAL2;
}
div {
container: styles_local1 styles_local2;
}
div {
container: GLOBAL1 GLOBAL2 / size;
}
div {
container: styles_local1 styles_local2 / size;
}

================================================================================
TestImportCSSFromJSLocalAtCounterStyle
---------- /out/entry.js ----------
Expand Down
6 changes: 6 additions & 0 deletions internal/css_ast/css_decl_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ const (
DColumnSpan
DColumnWidth
DColumns
DContainer
DContainerName
DContainerType
DContent
DCounterIncrement
DCounterReset
Expand Down Expand Up @@ -443,6 +446,9 @@ var KnownDeclarations = map[string]D{
"column-span": DColumnSpan,
"column-width": DColumnWidth,
"columns": DColumns,
"container": DContainer,
"container-name": DContainerName,
"container-type": DContainerType,
"content": DContent,
"counter-increment": DCounterIncrement,
"counter-reset": DCounterReset,
Expand Down
8 changes: 7 additions & 1 deletion internal/css_parser/css_decls.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,13 @@ func (p *parser) processDeclarations(rules []css_ast.Rule) (rewrittenRules []css
decl.Value = p.mangleBoxShadows(decl.Value)
}

// Animation name
// Container name
case css_ast.DContainer:
p.processContainerShorthand(decl.Value)
case css_ast.DContainerName:
p.processContainerName(decl.Value)

// Animation name
case css_ast.DAnimation:
p.processAnimationShorthand(decl.Value)
case css_ast.DAnimationName:
Expand Down
53 changes: 53 additions & 0 deletions internal/css_parser/css_decls_container.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package css_parser

import (
"strings"

"github.com/evanw/esbuild/internal/css_ast"
"github.com/evanw/esbuild/internal/css_lexer"
)

// Scan for container names in the "container" shorthand property
func (p *parser) processContainerShorthand(tokens []css_ast.Token) {
// Validate the syntax
for i, t := range tokens {
if t.Kind == css_lexer.TIdent {
continue
}
if t.Kind == css_lexer.TDelimSlash && i+2 == len(tokens) && tokens[i+1].Kind == css_lexer.TIdent {
break
}
return
}

// Convert any local names
for i, t := range tokens {
if t.Kind != css_lexer.TIdent {
break
}
p.handleSingleContainerName(&tokens[i])
}
}

func (p *parser) processContainerName(tokens []css_ast.Token) {
// Validate the syntax
for _, t := range tokens {
if t.Kind != css_lexer.TIdent {
return
}
}

// Convert any local names
for i := range tokens {
p.handleSingleContainerName(&tokens[i])
}
}

func (p *parser) handleSingleContainerName(token *css_ast.Token) {
if lower := strings.ToLower(token.Text); lower == "none" || cssWideAndReservedKeywords[lower] {
return
}

token.Kind = css_lexer.TSymbol
token.PayloadIndex = p.symbolForName(token.Loc, token.Text).Ref.InnerIndex
}
15 changes: 12 additions & 3 deletions internal/css_parser/css_parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -1455,9 +1455,9 @@ prelude:

// Handle local names for "@counter-style"
if len(prelude) == 1 && atToken == "counter-style" {
if token := &prelude[0]; token.Kind == css_lexer.TIdent {
token.Kind = css_lexer.TSymbol
token.PayloadIndex = p.symbolForName(token.Loc, token.Text).Ref.InnerIndex
if t := &prelude[0]; t.Kind == css_lexer.TIdent {
t.Kind = css_lexer.TSymbol
t.PayloadIndex = p.symbolForName(t.Loc, t.Text).Ref.InnerIndex
}
}

Expand All @@ -1481,6 +1481,15 @@ prelude:
if !p.expectWithMatchingLoc(css_lexer.TCloseBrace, matchingLoc) {
closeBraceLoc = logger.Loc{}
}

// Handle local names for "@container"
if len(prelude) >= 1 && atToken == "container" {
if t := &prelude[0]; t.Kind == css_lexer.TIdent && strings.ToLower(t.Text) != "not" {
t.Kind = css_lexer.TSymbol
t.PayloadIndex = p.symbolForName(t.Loc, t.Text).Ref.InnerIndex
}
}

return css_ast.Rule{Loc: atRange.Loc, Data: &css_ast.RKnownAt{AtToken: atToken, Prelude: prelude, Rules: rules, CloseBraceLoc: closeBraceLoc}}

case atRuleQualifiedOrEmpty:
Expand Down

0 comments on commit be33c09

Please sign in to comment.