From 7db12645e0690dffb988890be178a45c169eed62 Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Mon, 17 Jul 2023 21:23:30 -0400 Subject: [PATCH] css: add a `global-css` loader with global symbols --- CHANGELOG.md | 12 +- cmd/esbuild/main.go | 4 +- internal/bundler/bundler.go | 2 +- internal/bundler_tests/bundler_css_test.go | 124 ++++------- .../bundler_tests/snapshots/snapshots_css.txt | 208 +++++++++++++----- internal/cli_helpers/cli_helpers.go | 4 +- internal/config/config.go | 8 +- internal/css_parser/css_parser.go | 44 ++-- internal/css_parser/css_parser_selector.go | 26 ++- pkg/api/api.go | 3 +- pkg/api/api_impl.go | 6 +- 11 files changed, 278 insertions(+), 163 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20c618d476a..bb9c907ecf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,11 @@ * Implement local CSS names ([#20](https://github.com/evanw/esbuild/issues/20)) - This release introduces a new loader called `local-css` and two new pseudo-class selectors `:local()` and `:global()`. This is a partial implementation of the popular [CSS modules](https://github.com/css-modules/css-modules) approach for avoiding unintentional name collisions in CSS. I'm not calling this feature "CSS modules" because although some people in the community call it that, other people in the community have started using "CSS modules" to refer to [something completely different](https://github.com/WICG/webcomponents/blob/60c9f682b63c622bfa0d8222ea6b1f3b659e007c/proposals/css-modules-v1-explainer.md) and now CSS modules is an overloaded term. + This release introduces two new loaders called `global-css` and `local-css` and two new pseudo-class selectors `:local()` and `:global()`. This is a partial implementation of the popular [CSS modules](https://github.com/css-modules/css-modules) approach for avoiding unintentional name collisions in CSS. I'm not calling this feature "CSS modules" because although some people in the community call it that, other people in the community have started using "CSS modules" to refer to [something completely different](https://github.com/WICG/webcomponents/blob/60c9f682b63c622bfa0d8222ea6b1f3b659e007c/proposals/css-modules-v1-explainer.md) and now CSS modules is an overloaded term. Here's how this new local CSS name feature works with esbuild: - * Identifiers that look like `.className` and `#idName` are global with the `css` loader and local with the `local-css` loader. Global identifiers are the same across all files (the way CSS normally works) but local identifiers are different between different files. If two separate CSS files use the same local identifier `.button`, esbuild will automatically rename one of them so that they don't collide. This is analogous to how esbuild automatically renames JS local variables with the same name in separate JS files to avoid name collisions. + * Identifiers that look like `.className` and `#idName` are global with the `global-css` loader and local with the `local-css` loader. Global identifiers are the same across all files (the way CSS normally works) but local identifiers are different between different files. If two separate CSS files use the same local identifier `.button`, esbuild will automatically rename one of them so that they don't collide. This is analogous to how esbuild automatically renames JS local variables with the same name in separate JS files to avoid name collisions. * It only makes sense to use local CSS names with esbuild when you are also using esbuild's bundler to bundle JS files that import CSS files. When you do that, esbuild will generate one export for each local name in the CSS file. The JS code can import these names and use them when constructing HTML DOM. For example: @@ -53,7 +53,7 @@ This feature only makes sense to use when bundling is enabled both because your code needs to `import` the renamed local names so that it can use them, and because esbuild needs to be able to process all CSS files containing local names in a single bundling operation so that it can successfully rename conflicting local names to avoid collisions. - * If you are in a global CSS file (with the `css` loader) you can create a local name using `:local()`, and if you are in a local CSS file (with the `local-css` loader) you can create a global name with `:global()`. So the choice of the `css` loader vs. the `local-css` loader just sets the default behavior for identifiers, but you can override it on a case-by-case basis as necessary. For example: + * If you are in a global CSS file (with the `global-css` loader) you can create a local name using `:local()`, and if you are in a local CSS file (with the `local-css` loader) you can create a global name with `:global()`. So the choice of the `global-css` loader vs. the `local-css` loader just sets the default behavior for identifiers, but you can override it on a case-by-case basis as necessary. For example: ```css :local(.button) { @@ -64,7 +64,7 @@ } ``` - Processing this CSS file with esbuild will result in something like this: + Processing this CSS file with esbuild with either the `global-css` or `local-css` loader will result in something like this: ```css .stdin_button { @@ -77,7 +77,9 @@ * The names that esbuild generates for local CSS names are an implementation detail and are not intended to be hard-coded anywhere. The only way you should be referencing the local CSS names in your JS or HTML is with an `import` statement in JS that is bundled with esbuild, as demonstrated above. For example, when `--minify` is enabled esbuild will use a different name generation algorithm which generates names that are as short as possible (analogous to how esbuild minifies local identifiers in JS). - * You can easily use both global CSS files and local CSS files simultaneously if you give them different file extensions. For example, you could pass `--loader:.module.css=local-css` to esbuild so that `.css` files still use global names by default but `.module.css` files use local names by default. + * You can easily use both global CSS files and local CSS files simultaneously if you give them different file extensions. For example, you could pass `--loader:.css=global-css` and `--loader:.module.css=local-css` to esbuild so that `.css` files still use global names by default but `.module.css` files use local names by default. + + * Keep in mind that the `css` loader is different than the `global-css` loader. The `:local` and `:global` annotations are not enabled with the `css` loader and will be passed through unchanged. This allows you to have the option of using esbuild to process CSS containing while preserving these annotations. It also means that local CSS names are disabled by default for now (since the `css` loader is currently the default for CSS files). The `:local` and `:global` syntax may be enabled by default in a future release. Note that esbuild's implementation does not currently have feature parity with other implementations of modular CSS in similar tools. This is only a preliminary release with a partial implementation that includes some basic behavior to get the process started. Additional behavior may be added in future releases. In particular, this release does not implement: diff --git a/cmd/esbuild/main.go b/cmd/esbuild/main.go index 7806736f0dc..7467e960587 100644 --- a/cmd/esbuild/main.go +++ b/cmd/esbuild/main.go @@ -39,8 +39,8 @@ var helpText = func(colors logger.Colors) string { is browser and cjs when platform is node) --loader:X=L Use loader L to load file extension X, where L is one of: base64 | binary | copy | css | dataurl | - empty | file | js | json | jsx | local-css | text | - ts | tsx + empty | file | global-css | js | json | jsx | + local-css | text | ts | tsx --minify Minify the output (sets all --minify-* flags) --outdir=... The output directory (for multiple entry points) --outfile=... The output file (for one entry point) diff --git a/internal/bundler/bundler.go b/internal/bundler/bundler.go index 045327dc743..569eb361e60 100644 --- a/internal/bundler/bundler.go +++ b/internal/bundler/bundler.go @@ -229,7 +229,7 @@ func parseFile(args parseArgs) { result.file.inputFile.Repr = &graph.JSRepr{AST: ast} result.ok = ok - case config.LoaderCSS, config.LoaderLocalCSS: + case config.LoaderCSS, config.LoaderGlobalCSS, config.LoaderLocalCSS: ast := args.caches.CSSCache.Parse(args.log, source, css_parser.OptionsFromConfig(loader, &args.options)) result.file.inputFile.Repr = &graph.CSSRepr{AST: ast} result.ok = true diff --git a/internal/bundler_tests/bundler_css_test.go b/internal/bundler_tests/bundler_css_test.go index d7902dffe86..76d387d8c6e 100644 --- a/internal/bundler_tests/bundler_css_test.go +++ b/internal/bundler_tests/bundler_css_test.go @@ -301,100 +301,65 @@ func TestImportLocalCSSFromJSMinifyIdentifiersAvoidGlobalNames(t *testing.T) { } func TestImportCSSFromJSLocalVsGlobal(t *testing.T) { - css_suite.expectBundled(t, bundled{ - files: map[string]string{ - "/entry.js": ` - import "./foo.css" - import "./bar.module.css" - `, - "/foo.css": ` - .GLOBAL { color: #000 } - - :global(.GLOBAL) { color: #001 } - :local(.local) { color: #002 } - - div:global(.GLOBAL) { color: #003 } - div:local(.local) { color: #004 } - - .GLOBAL:global(div) { color: #005 } - .GLOBAL:local(div) { color: #006 } - - :global(div.GLOBAL) { color: #007 } - :local(div.local) { color: #008 } - - div:global(span.GLOBAL) { color: #009 } - div:local(span.local) { color: #00A } - - div:global(#GLOBAL0.GLOBAL1.GLOBAL2):local(.local0.local1#local2) { color: #00B } - div:global(#GLOBAL0 .GLOBAL1 .GLOBAL2):local(.local0 .local1 #local2) { color: #00C } - - .nested { - :global(&.GLOBAL) { color: #00D } - :local(&.local) { color: #00E } + css := ` + .top_level { color: #000 } - &:global(.GLOBAL) { color: #00F } - &:local(.local) { color: #010 } - } - - :global(.GLOBAL0, .GLOBAL1) { color: #011 } - :local(.local0, .local1) { color: #012 } - - div:global(.GLOBAL0, .GLOBAL1) { color: #013 } - div:local(.local0, .local1) { color: #014 } + :global(.GLOBAL) { color: #001 } + :local(.local) { color: #002 } - div :global(.GLOBAL0, .GLOBAL1) span { color: #015 } - div :local(.local0, .local1) span { color: #016 } + div:global(.GLOBAL) { color: #003 } + div:local(.local) { color: #004 } - div :global(.GLOBAL0 .GLOBAL1) span { color: #017 } - div :local(.local0 .local1) span { color: #018 } + .top_level:global(div) { color: #005 } + .top_level:local(div) { color: #006 } - div > :global(.GLOBAL0 ~ .GLOBAL1) + span { color: #019 } - div > :local(.local0 ~ .local1) + span { color: #01A } - `, - "/bar.module.css": ` - .local { color: #000 } - - :global(.GLOBAL) { color: #001 } - :local(.local) { color: #002 } + :global(div.GLOBAL) { color: #007 } + :local(div.local) { color: #008 } - div:global(.GLOBAL) { color: #003 } - div:local(.local) { color: #004 } + div:global(span.GLOBAL) { color: #009 } + div:local(span.local) { color: #00A } - .local:global(div) { color: #005 } - .local:local(div) { color: #006 } + div:global(#GLOBAL_A.GLOBAL_B.GLOBAL_C):local(.local_a.local_b#local_c) { color: #00B } + div:global(#GLOBAL_A .GLOBAL_B .GLOBAL_C):local(.local_a .local_b #local_c) { color: #00C } - :global(div.GLOBAL) { color: #007 } - :local(div.local) { color: #008 } + .nested { + :global(&.GLOBAL) { color: #00D } + :local(&.local) { color: #00E } - div:global(span.GLOBAL) { color: #009 } - div:local(span.local) { color: #00A } + &:global(.GLOBAL) { color: #00F } + &:local(.local) { color: #010 } + } - div:global(#GLOBAL0.GLOBAL1.GLOBAL2):local(.local0.local1#local2) { color: #00B } - div:global(#GLOBAL0 .GLOBAL1 .GLOBAL2):local(.local0 .local1 #local2) { color: #00C } + :global(.GLOBAL_A, .GLOBAL_B) { color: #011 } + :local(.local_a, .local_b) { color: #012 } - .nested { - :global(&.GLOBAL) { color: #00D } - :local(&.local) { color: #00E } + div:global(.GLOBAL_A, .GLOBAL_B) { color: #013 } + div:local(.local_a, .local_b) { color: #014 } - &:global(.GLOBAL) { color: #00F } - &:local(.local) { color: #010 } - } + div :global(.GLOBAL_A, .GLOBAL_B) span { color: #015 } + div :local(.local_a, .local_b) span { color: #016 } - :global(.GLOBAL0, .GLOBAL1) { color: #011 } - :local(.local0, .local1) { color: #012 } + div :global(.GLOBAL_A .GLOBAL_B) span { color: #017 } + div :local(.local_a .local_b) span { color: #018 } - div:global(.GLOBAL0, .GLOBAL1) { color: #013 } - div:local(.local0, .local1) { color: #014 } + div > :global(.GLOBAL_A ~ .GLOBAL_B) + span { color: #019 } + div > :local(.local_a ~ .local_b) + span { color: #01A } + ` - div :global(.GLOBAL0, .GLOBAL1) span { color: #015 } - div :local(.local0, .local1) span { color: #016 } - - div :global(.GLOBAL0 .GLOBAL1) span { color: #017 } - div :local(.local0 .local1) span { color: #018 } + css_suite.expectBundled(t, bundled{ + files: map[string]string{ + "/entry.js": ` + import normalStyles from "./normal.css" + import globalStyles from "./LOCAL.global-css" + import localStyles from "./LOCAL.local-css" - div > :global(.GLOBAL0 ~ .GLOBAL1) + span { color: #019 } - div > :local(.local0 ~ .local1) + span { color: #01A } + console.log('should be empty:', normalStyles) + console.log('fewer local names:', globalStyles) + console.log('more local names:', localStyles) `, + "/normal.css": css, + "/LOCAL.global-css": css, + "/LOCAL.local-css": css, }, entryPaths: []string{"/entry.js"}, options: config.Options{ @@ -403,7 +368,8 @@ func TestImportCSSFromJSLocalVsGlobal(t *testing.T) { ExtensionToLoader: map[string]config.Loader{ ".js": config.LoaderJS, ".css": config.LoaderCSS, - ".module.css": config.LoaderLocalCSS, + ".global-css": config.LoaderGlobalCSS, + ".local-css": config.LoaderLocalCSS, }, }, }) diff --git a/internal/bundler_tests/snapshots/snapshots_css.txt b/internal/bundler_tests/snapshots/snapshots_css.txt index e4491e7f70d..ad0c9e7b7e5 100644 --- a/internal/bundler_tests/snapshots/snapshots_css.txt +++ b/internal/bundler_tests/snapshots/snapshots_css.txt @@ -624,179 +624,289 @@ TestIgnoreURLsInAtRulePrelude ================================================================================ TestImportCSSFromJSLocalVsGlobal ---------- /out/entry.js ---------- +// normal.css +var normal_default = {}; + +// LOCAL.global-css +var LOCAL_default = { + local: "LOCAL_local", + local_a: "LOCAL_local_a", + local_b: "LOCAL_local_b", + local_c: "LOCAL_local_c" +}; + +// LOCAL.local-css +var LOCAL_default2 = { + top_level: "LOCAL_top_level", + local: "LOCAL_local2", + local_a: "LOCAL_local_a2", + local_b: "LOCAL_local_b2", + local_c: "LOCAL_local_c2", + nested: "LOCAL_nested" +}; + +// entry.js +console.log("should be empty:", normal_default); +console.log("fewer local names:", LOCAL_default); +console.log("more local names:", LOCAL_default2); ---------- /out/entry.css ---------- -/* foo.css */ -.GLOBAL { +/* normal.css */ +.top_level { + color: #000; +} +:global(.GLOBAL) { + color: #001; +} +:local(.local) { + color: #002; +} +div:global(.GLOBAL) { + color: #003; +} +div:local(.local) { + color: #004; +} +.top_level:global(div) { + color: #005; +} +.top_level:local(div) { + color: #006; +} +:global(div.GLOBAL) { + color: #007; +} +:local(div.local) { + color: #008; +} +div:global(span.GLOBAL) { + color: #009; +} +div:local(span.local) { + color: #00A; +} +div:global(#GLOBAL_A.GLOBAL_B.GLOBAL_C):local(.local_a.local_b#local_c) { + color: #00B; +} +div:global(#GLOBAL_A .GLOBAL_B .GLOBAL_C):local(.local_a .local_b #local_c) { + color: #00C; +} +.nested { + :global(&.GLOBAL) { + color: #00D; + } + :local(&.local) { + color: #00E; + } + &:global(.GLOBAL) { + color: #00F; + } + &:local(.local) { + color: #010; + } +} +:global(.GLOBAL_A, .GLOBAL_B) { + color: #011; +} +:local(.local_a, .local_b) { + color: #012; +} +div:global(.GLOBAL_A, .GLOBAL_B) { + color: #013; +} +div:local(.local_a, .local_b) { + color: #014; +} +div :global(.GLOBAL_A, .GLOBAL_B) span { + color: #015; +} +div :local(.local_a, .local_b) span { + color: #016; +} +div :global(.GLOBAL_A .GLOBAL_B) span { + color: #017; +} +div :local(.local_a .local_b) span { + color: #018; +} +div > :global(.GLOBAL_A ~ .GLOBAL_B) + span { + color: #019; +} +div > :local(.local_a ~ .local_b) + span { + color: #01A; +} + +/* LOCAL.global-css */ +.top_level { color: #000; } .GLOBAL { color: #001; } -.foo_local { +.LOCAL_local { color: #002; } div.GLOBAL { color: #003; } -div.foo_local { +div.LOCAL_local { color: #004; } -div.GLOBAL { +div.top_level { color: #005; } -div.GLOBAL { +div.top_level { color: #006; } div.GLOBAL { color: #007; } -div.foo_local { +div.LOCAL_local { color: #008; } div:is(span.GLOBAL) { color: #009; } -div:is(span.foo_local) { +div:is(span.LOCAL_local) { color: #00A; } -div#GLOBAL0.GLOBAL1.GLOBAL2.foo_local0.foo_local1#foo_local2 { +div#GLOBAL_A.GLOBAL_B.GLOBAL_C.LOCAL_local_a.LOCAL_local_b#LOCAL_local_c { color: #00B; } -div:is(#GLOBAL0 .GLOBAL1 .GLOBAL2):is(.foo_local0 .foo_local1 #foo_local2) { +div:is(#GLOBAL_A .GLOBAL_B .GLOBAL_C):is(.LOCAL_local_a .LOCAL_local_b #LOCAL_local_c) { color: #00C; } .nested { &.GLOBAL { color: #00D; } - &.foo_local { + &.LOCAL_local { color: #00E; } &.GLOBAL { color: #00F; } - &.foo_local { + &.LOCAL_local { color: #010; } } -.GLOBAL0, -.GLOBAL1 { +.GLOBAL_A, +.GLOBAL_B { color: #011; } -.foo_local0, -.foo_local1 { +.LOCAL_local_a, +.LOCAL_local_b { color: #012; } -div:is(.GLOBAL0, .GLOBAL1) { +div:is(.GLOBAL_A, .GLOBAL_B) { color: #013; } -div:is(.foo_local0, .foo_local1) { +div:is(.LOCAL_local_a, .LOCAL_local_b) { color: #014; } -div :is(.GLOBAL0, .GLOBAL1) span { +div :is(.GLOBAL_A, .GLOBAL_B) span { color: #015; } -div :is(.foo_local0, .foo_local1) span { +div :is(.LOCAL_local_a, .LOCAL_local_b) span { color: #016; } -div .GLOBAL0 .GLOBAL1 span { +div .GLOBAL_A .GLOBAL_B span { color: #017; } -div .foo_local0 .foo_local1 span { +div .LOCAL_local_a .LOCAL_local_b span { color: #018; } -div > .GLOBAL0 ~ .GLOBAL1 + span { +div > .GLOBAL_A ~ .GLOBAL_B + span { color: #019; } -div > .foo_local0 ~ .foo_local1 + span { +div > .LOCAL_local_a ~ .LOCAL_local_b + span { color: #01A; } -/* bar.module.css */ -.bar_module_local { +/* LOCAL.local-css */ +.LOCAL_top_level { color: #000; } .GLOBAL { color: #001; } -.bar_module_local { +.LOCAL_local2 { color: #002; } div.GLOBAL { color: #003; } -div.bar_module_local { +div.LOCAL_local2 { color: #004; } -div.bar_module_local { +div.LOCAL_top_level { color: #005; } -div.bar_module_local { +div.LOCAL_top_level { color: #006; } div.GLOBAL { color: #007; } -div.bar_module_local { +div.LOCAL_local2 { color: #008; } div:is(span.GLOBAL) { color: #009; } -div:is(span.bar_module_local) { +div:is(span.LOCAL_local2) { color: #00A; } -div#GLOBAL0.GLOBAL1.GLOBAL2.bar_module_local0.bar_module_local1#bar_module_local2 { +div#GLOBAL_A.GLOBAL_B.GLOBAL_C.LOCAL_local_a2.LOCAL_local_b2#LOCAL_local_c2 { color: #00B; } -div:is(#GLOBAL0 .GLOBAL1 .GLOBAL2):is(.bar_module_local0 .bar_module_local1 #bar_module_local2) { +div:is(#GLOBAL_A .GLOBAL_B .GLOBAL_C):is(.LOCAL_local_a2 .LOCAL_local_b2 #LOCAL_local_c2) { color: #00C; } -.bar_module_nested { +.LOCAL_nested { &.GLOBAL { color: #00D; } - &.bar_module_local { + &.LOCAL_local2 { color: #00E; } &.GLOBAL { color: #00F; } - &.bar_module_local { + &.LOCAL_local2 { color: #010; } } -.GLOBAL0, -.GLOBAL1 { +.GLOBAL_A, +.GLOBAL_B { color: #011; } -.bar_module_local0, -.bar_module_local1 { +.LOCAL_local_a2, +.LOCAL_local_b2 { color: #012; } -div:is(.GLOBAL0, .GLOBAL1) { +div:is(.GLOBAL_A, .GLOBAL_B) { color: #013; } -div:is(.bar_module_local0, .bar_module_local1) { +div:is(.LOCAL_local_a2, .LOCAL_local_b2) { color: #014; } -div :is(.GLOBAL0, .GLOBAL1) span { +div :is(.GLOBAL_A, .GLOBAL_B) span { color: #015; } -div :is(.bar_module_local0, .bar_module_local1) span { +div :is(.LOCAL_local_a2, .LOCAL_local_b2) span { color: #016; } -div .GLOBAL0 .GLOBAL1 span { +div .GLOBAL_A .GLOBAL_B span { color: #017; } -div .bar_module_local0 .bar_module_local1 span { +div .LOCAL_local_a2 .LOCAL_local_b2 span { color: #018; } -div > .GLOBAL0 ~ .GLOBAL1 + span { +div > .GLOBAL_A ~ .GLOBAL_B + span { color: #019; } -div > .bar_module_local0 ~ .bar_module_local1 + span { +div > .LOCAL_local_a2 ~ .LOCAL_local_b2 + span { color: #01A; } diff --git a/internal/cli_helpers/cli_helpers.go b/internal/cli_helpers/cli_helpers.go index 296d8ca1d3b..03eea3a77d9 100644 --- a/internal/cli_helpers/cli_helpers.go +++ b/internal/cli_helpers/cli_helpers.go @@ -39,6 +39,8 @@ func ParseLoader(text string) (api.Loader, *ErrorWithNote) { return api.LoaderEmpty, nil case "file": return api.LoaderFile, nil + case "global-css": + return api.LoaderGlobalCSS, nil case "js": return api.LoaderJS, nil case "json": @@ -56,7 +58,7 @@ func ParseLoader(text string) (api.Loader, *ErrorWithNote) { default: return api.LoaderNone, MakeErrorWithNote( fmt.Sprintf("Invalid loader value: %q", text), - "Valid values are \"base64\", \"binary\", \"copy\", \"css\", \"dataurl\", \"empty\", \"file\", \"js\", \"json\", \"jsx\", \"local-css\", \"text\", \"ts\", or \"tsx\".", + "Valid values are \"base64\", \"binary\", \"copy\", \"css\", \"dataurl\", \"empty\", \"file\", \"global-css\", \"js\", \"json\", \"jsx\", \"local-css\", \"text\", \"ts\", or \"tsx\".", ) } } diff --git a/internal/config/config.go b/internal/config/config.go index 106ff56e499..6bb6c5f5be9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -193,6 +193,7 @@ const ( LoaderDefault LoaderEmpty LoaderFile + LoaderGlobalCSS LoaderJS LoaderJSON LoaderJSX @@ -213,6 +214,7 @@ var LoaderToString = []string{ "default", "empty", "file", + "global-css", "js", "json", "jsx", @@ -234,7 +236,11 @@ func (loader Loader) IsTypeScript() bool { func (loader Loader) CanHaveSourceMap() bool { switch loader { - case LoaderJS, LoaderJSX, LoaderTS, LoaderTSNoAmbiguousLessThan, LoaderTSX, LoaderCSS, LoaderLocalCSS, LoaderJSON, LoaderText: + case + LoaderJS, LoaderJSX, + LoaderTS, LoaderTSNoAmbiguousLessThan, LoaderTSX, + LoaderCSS, LoaderGlobalCSS, LoaderLocalCSS, + LoaderJSON, LoaderText: return true default: return false diff --git a/internal/css_parser/css_parser.go b/internal/css_parser/css_parser.go index 3f886b170dd..e9e688a50b0 100644 --- a/internal/css_parser/css_parser.go +++ b/internal/css_parser/css_parser.go @@ -34,6 +34,7 @@ type parser struct { prevError logger.Loc options Options shouldLowerNesting bool + makeLocalSymbols bool } type Options struct { @@ -45,16 +46,32 @@ type Options struct { optionsThatSupportStructuralEquality } +type symbolMode uint8 + +const ( + symbolModeDisabled symbolMode = iota + symbolModeGlobal + symbolModeLocal +) + type optionsThatSupportStructuralEquality struct { originalTargetEnv string unsupportedCSSFeatures compat.CSSFeature minifySyntax bool minifyWhitespace bool minifyIdentifiers bool - makeLocalSymbols bool + symbolMode symbolMode } func OptionsFromConfig(loader config.Loader, options *config.Options) Options { + var symbolMode symbolMode + switch loader { + case config.LoaderGlobalCSS: + symbolMode = symbolModeGlobal + case config.LoaderLocalCSS: + symbolMode = symbolModeLocal + } + return Options{ cssPrefixData: options.CSSPrefixData, @@ -64,7 +81,7 @@ func OptionsFromConfig(loader config.Loader, options *config.Options) Options { minifyIdentifiers: options.MinifyIdentifiers, unsupportedCSSFeatures: options.UnsupportedCSSFeatures, originalTargetEnv: options.OriginalTargetEnv, - makeLocalSymbols: loader == config.LoaderLocalCSS, + symbolMode: symbolMode, }, } } @@ -99,16 +116,17 @@ func Parse(log logger.Log, source logger.Source, options Options) css_ast.AST { RecordAllComments: options.minifyIdentifiers, }) p := parser{ - log: log, - source: source, - tracker: logger.MakeLineColumnTracker(&source), - options: options, - tokens: result.Tokens, - allComments: result.AllComments, - legalComments: result.LegalComments, - prevError: logger.Loc{Start: -1}, - localSymbolMap: make(map[string]ast.Ref), - globalSymbolMap: make(map[string]ast.Ref), + log: log, + source: source, + tracker: logger.MakeLineColumnTracker(&source), + options: options, + tokens: result.Tokens, + allComments: result.AllComments, + legalComments: result.LegalComments, + prevError: logger.Loc{Start: -1}, + localSymbolMap: make(map[string]ast.Ref), + globalSymbolMap: make(map[string]ast.Ref), + makeLocalSymbols: options.symbolMode == symbolModeLocal, } p.end = len(p.tokens) rules := p.parseListOfRules(ruleContext{ @@ -286,7 +304,7 @@ func (p *parser) symbolForName(name string) ast.Ref { var kind ast.SymbolKind var scope map[string]ast.Ref - if p.options.makeLocalSymbols { + if p.makeLocalSymbols { kind = ast.SymbolLocalCSS scope = p.globalSymbolMap } else { diff --git a/internal/css_parser/css_parser_selector.go b/internal/css_parser/css_parser_selector.go index b024dd070d3..e5ff5013078 100644 --- a/internal/css_parser/css_parser_selector.go +++ b/internal/css_parser/css_parser_selector.go @@ -23,7 +23,7 @@ func (p *parser) parseSelectorList(opts parseSelectorOpts) (list []css_ast.Compl if !good { return } - list = flattenLocalAndGlobalSelectors(list, sel) + list = p.flattenLocalAndGlobalSelectors(list, sel) // Parse the remaining selectors skip: @@ -49,7 +49,7 @@ skip: } } - list = flattenLocalAndGlobalSelectors(list, sel) + list = p.flattenLocalAndGlobalSelectors(list, sel) } if p.options.minifySyntax { @@ -79,7 +79,11 @@ skip: } // This handles the ":local()" and ":global()" annotations from CSS modules -func flattenLocalAndGlobalSelectors(list []css_ast.ComplexSelector, sel css_ast.ComplexSelector) []css_ast.ComplexSelector { +func (p *parser) flattenLocalAndGlobalSelectors(list []css_ast.ComplexSelector, sel css_ast.ComplexSelector) []css_ast.ComplexSelector { + if p.options.symbolMode == symbolModeDisabled { + return append(list, sel) + } + // If this selector consists only of ":local" or ":global" and the // contents can be inlined, then inline it directly. This has to be // done separately from the loop below because inlining may produce @@ -541,19 +545,23 @@ func (p *parser) parsePseudoClassSelector(isElement bool) css_ast.SS { // Potentially parse a pseudo-class with a selector list if !isElement { var kind css_ast.PseudoClassKind - local := p.options.makeLocalSymbols + local := p.makeLocalSymbols ok := true switch text { case "global": kind = css_ast.PseudoClassGlobal - local = false + if p.options.symbolMode != symbolModeDisabled { + local = false + } case "has": kind = css_ast.PseudoClassHas case "is": kind = css_ast.PseudoClassIs case "local": kind = css_ast.PseudoClassLocal - local = true + if p.options.symbolMode != symbolModeDisabled { + local = true + } case "not": kind = css_ast.PseudoClassNot case "where": @@ -565,10 +573,10 @@ func (p *parser) parsePseudoClassSelector(isElement bool) css_ast.SS { old := p.index // ":local" forces local names and ":global" forces global names - oldLocal := p.options.makeLocalSymbols - p.options.makeLocalSymbols = local + oldLocal := p.makeLocalSymbols + p.makeLocalSymbols = local selectors, ok := p.parseSelectorList(parseSelectorOpts{stopOnCloseParen: true}) - p.options.makeLocalSymbols = oldLocal + p.makeLocalSymbols = oldLocal if ok && p.expectWithMatchingLoc(css_lexer.TCloseParen, matchingLoc) { return &css_ast.SSPseudoClassWithSelectorList{Kind: kind, Selectors: selectors} diff --git a/pkg/api/api.go b/pkg/api/api.go index c7329b152b8..ad76552e48e 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -134,7 +134,7 @@ const ( ES2022 ) -type Loader uint8 +type Loader uint16 const ( LoaderNone Loader = iota @@ -146,6 +146,7 @@ const ( LoaderDefault LoaderEmpty LoaderFile + LoaderGlobalCSS LoaderJS LoaderJSON LoaderJSX diff --git a/pkg/api/api_impl.go b/pkg/api/api_impl.go index e813298fb17..151d2642f42 100644 --- a/pkg/api/api_impl.go +++ b/pkg/api/api_impl.go @@ -251,16 +251,18 @@ func validateLoader(value Loader) config.Loader { return config.LoaderEmpty case LoaderFile: return config.LoaderFile + case LoaderGlobalCSS: + return config.LoaderGlobalCSS case LoaderJS: return config.LoaderJS case LoaderJSON: return config.LoaderJSON case LoaderJSX: return config.LoaderJSX - case LoaderNone: - return config.LoaderNone case LoaderLocalCSS: return config.LoaderLocalCSS + case LoaderNone: + return config.LoaderNone case LoaderText: return config.LoaderText case LoaderTS: