diff --git a/CHANGELOG.md b/CHANGELOG.md index c4f42c7fb29..c389156d010 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 { @@ -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; } ``` @@ -36,9 +40,15 @@ 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; } ``` @@ -46,16 +56,17 @@ ```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)) diff --git a/internal/bundler_tests/bundler_css_test.go b/internal/bundler_tests/bundler_css_test.go index 2e6901efa22..e031e458515 100644 --- a/internal/bundler_tests/bundler_css_test.go +++ b/internal/bundler_tests/bundler_css_test.go @@ -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{ diff --git a/internal/bundler_tests/snapshots/snapshots_css.txt b/internal/bundler_tests/snapshots/snapshots_css.txt index 2d7b78cbf9a..cb4d4fffcec 100644 --- a/internal/bundler_tests/snapshots/snapshots_css.txt +++ b/internal/bundler_tests/snapshots/snapshots_css.txt @@ -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 ---------- diff --git a/internal/css_ast/css_decl_table.go b/internal/css_ast/css_decl_table.go index fd6f49bf02f..702f3e2b568 100644 --- a/internal/css_ast/css_decl_table.go +++ b/internal/css_ast/css_decl_table.go @@ -116,6 +116,9 @@ const ( DColumnSpan DColumnWidth DColumns + DContainer + DContainerName + DContainerType DContent DCounterIncrement DCounterReset @@ -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, diff --git a/internal/css_parser/css_decls.go b/internal/css_parser/css_decls.go index 9a7b32f851e..782f54dd66d 100644 --- a/internal/css_parser/css_decls.go +++ b/internal/css_parser/css_decls.go @@ -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: diff --git a/internal/css_parser/css_decls_container.go b/internal/css_parser/css_decls_container.go new file mode 100644 index 00000000000..6c7ca3a6b40 --- /dev/null +++ b/internal/css_parser/css_decls_container.go @@ -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 +} diff --git a/internal/css_parser/css_parser.go b/internal/css_parser/css_parser.go index 41d88e5d9f9..6a29f4340c4 100644 --- a/internal/css_parser/css_parser.go +++ b/internal/css_parser/css_parser.go @@ -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 } } @@ -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: