From 83839a98dbe0000acbdf039d968f33e1e8c50277 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Wed, 15 May 2024 07:25:52 +0200 Subject: [PATCH] Add granular CSS Modules options (#739) --- Cargo.lock | 23 ++++++ Cargo.toml | 1 + napi/src/lib.rs | 9 +++ src/css_modules.rs | 23 +++++- src/lib.rs | 132 +++++++++++++++++++++++++++++++++++ src/printer.rs | 44 ++++++------ src/properties/animation.rs | 19 +++-- src/properties/grid.rs | 27 +++---- src/rules/keyframes.rs | 7 +- src/selector.rs | 6 +- src/values/ident.rs | 21 +++++- website/pages/css-modules.md | 51 +++++++++----- 12 files changed, 299 insertions(+), 64 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1aba540b..eed17d06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -444,6 +444,12 @@ dependencies = [ "matches", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "difflib" version = "0.4.0" @@ -786,6 +792,7 @@ dependencies = [ "paste", "pathdiff", "predicates 2.1.5", + "pretty_assertions", "rayon", "schemars", "serde", @@ -1197,6 +1204,16 @@ dependencies = [ "termtree", ] +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -1947,6 +1964,12 @@ dependencies = [ "tap", ] +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + [[package]] name = "zerocopy" version = "0.7.32" diff --git a/Cargo.toml b/Cargo.toml index 73ac94fe..a298e0b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -86,6 +86,7 @@ assert_cmd = "2.0" assert_fs = "1.0" predicates = "2.1" serde_json = "1" +pretty_assertions = "1.4.0" [[test]] name = "cli_integration_tests" diff --git a/napi/src/lib.rs b/napi/src/lib.rs index cc3ce78e..9edfb1ce 100644 --- a/napi/src/lib.rs +++ b/napi/src/lib.rs @@ -605,6 +605,9 @@ enum CssModulesOption { struct CssModulesConfig { pattern: Option, dashed_idents: Option, + animation: Option, + grid: Option, + custom_idents: Option, } #[cfg(feature = "bundler")] @@ -713,6 +716,9 @@ fn compile<'i>( Default::default() }, dashed_idents: c.dashed_idents.unwrap_or_default(), + animation: c.animation.unwrap_or(true), + grid: c.grid.unwrap_or(true), + custom_idents: c.custom_idents.unwrap_or(true), }), } } else { @@ -840,6 +846,9 @@ fn compile_bundle< Default::default() }, dashed_idents: c.dashed_idents.unwrap_or_default(), + animation: c.animation.unwrap_or(true), + grid: c.grid.unwrap_or(true), + custom_idents: c.custom_idents.unwrap_or(true), }), } } else { diff --git a/src/css_modules.rs b/src/css_modules.rs index b794bb43..cfe33d71 100644 --- a/src/css_modules.rs +++ b/src/css_modules.rs @@ -25,13 +25,34 @@ use std::hash::{Hash, Hasher}; use std::path::Path; /// Configuration for CSS modules. -#[derive(Default, Clone, Debug)] +#[derive(Clone, Debug)] pub struct Config<'i> { /// The name pattern to use when renaming class names and other identifiers. /// Default is `[hash]_[local]`. pub pattern: Pattern<'i>, /// Whether to rename dashed identifiers, e.g. custom properties. pub dashed_idents: bool, + /// Whether to scope animation names. + /// Default is `true`. + pub animation: bool, + /// Whether to scope grid names. + /// Default is `true`. + pub grid: bool, + /// Whether to scope custom identifiers + /// Default is `true`. + pub custom_idents: bool, +} + +impl<'i> Default for Config<'i> { + fn default() -> Self { + Config { + pattern: Default::default(), + dashed_idents: Default::default(), + animation: true, + grid: true, + custom_idents: true, + } + } } /// A CSS modules class name pattern. diff --git a/src/lib.rs b/src/lib.rs index 7d8aff33..d0ea1982 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -64,6 +64,7 @@ mod tests { use crate::vendor_prefix::VendorPrefix; use cssparser::SourceLocation; use indoc::indoc; + use pretty_assertions::assert_eq; use std::collections::HashMap; fn test(source: &str, expected: &str) { @@ -23053,6 +23054,97 @@ mod tests { Default::default(), ); + css_modules_test( + r#" + .foo { + color: red; + } + + #id { + animation: 2s test; + } + + @keyframes test { + from { color: red } + to { color: yellow } + } + "#, + indoc! {r#" + .EgL3uq_foo { + color: red; + } + + #EgL3uq_id { + animation: 2s test; + } + + @keyframes test { + from { + color: red; + } + + to { + color: #ff0; + } + } + "#}, + map! { + "foo" => "EgL3uq_foo", + "id" => "EgL3uq_id" + }, + HashMap::new(), + crate::css_modules::Config { + animation: false, + // custom_idents: false, + ..Default::default() + }, + ); + + css_modules_test( + r#" + @counter-style circles { + symbols: Ⓐ Ⓑ Ⓒ; + } + + ul { + list-style: circles; + } + + ol { + list-style-type: none; + } + + li { + list-style-type: disc; + } + "#, + indoc! {r#" + @counter-style circles { + symbols: Ⓐ Ⓑ Ⓒ; + } + + ul { + list-style: circles; + } + + ol { + list-style-type: none; + } + + li { + list-style-type: disc; + } + "#}, + map! { + "circles" => "EgL3uq_circles" referenced: true + }, + HashMap::new(), + crate::css_modules::Config { + custom_idents: false, + ..Default::default() + }, + ); + #[cfg(feature = "grid")] css_modules_test( r#" @@ -23135,6 +23227,46 @@ mod tests { Default::default(), ); + #[cfg(feature = "grid")] + css_modules_test( + r#" + .grid { + grid-template-areas: "foo"; + } + + .foo { + grid-area: foo; + } + + .bar { + grid-column-start: foo-start; + } + "#, + indoc! {r#" + .EgL3uq_grid { + grid-template-areas: "foo"; + } + + .EgL3uq_foo { + grid-area: foo; + } + + .EgL3uq_bar { + grid-column-start: foo-start; + } + "#}, + map! { + "foo" => "EgL3uq_foo", + "grid" => "EgL3uq_grid", + "bar" => "EgL3uq_bar" + }, + HashMap::new(), + crate::css_modules::Config { + grid: false, + ..Default::default() + }, + ); + css_modules_test( r#" test { diff --git a/src/printer.rs b/src/printer.rs index 12fe09e3..d8c08e2d 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -267,30 +267,32 @@ impl<'a, 'b, 'c, W: std::fmt::Write + Sized> Printer<'a, 'b, 'c, W> { /// Writes a CSS identifier to the underlying destination, escaping it /// as appropriate. If the `css_modules` option was enabled, then a hash /// is added, and the mapping is added to the CSS module. - pub fn write_ident(&mut self, ident: &str) -> Result<(), PrinterError> { - if let Some(css_module) = &mut self.css_module { - let dest = &mut self.dest; - let mut first = true; - css_module.config.pattern.write( - &css_module.hashes[self.loc.source_index as usize], - &css_module.sources[self.loc.source_index as usize], - ident, - |s| { - self.col += s.len() as u32; - if first { - first = false; - serialize_identifier(s, dest) - } else { - serialize_name(s, dest) - } - }, - )?; + pub fn write_ident(&mut self, ident: &str, handle_css_module: bool) -> Result<(), PrinterError> { + if handle_css_module { + if let Some(css_module) = &mut self.css_module { + let dest = &mut self.dest; + let mut first = true; + css_module.config.pattern.write( + &css_module.hashes[self.loc.source_index as usize], + &css_module.sources[self.loc.source_index as usize], + ident, + |s| { + self.col += s.len() as u32; + if first { + first = false; + serialize_identifier(s, dest) + } else { + serialize_name(s, dest) + } + }, + )?; - css_module.add_local(&ident, &ident, self.loc.source_index); - } else { - serialize_identifier(ident, self)?; + css_module.add_local(&ident, &ident, self.loc.source_index); + return Ok(()); + } } + serialize_identifier(ident, self)?; Ok(()) } diff --git a/src/properties/animation.rs b/src/properties/animation.rs index c995c222..d5d73f83 100644 --- a/src/properties/animation.rs +++ b/src/properties/animation.rs @@ -58,17 +58,24 @@ impl<'i> ToCss for AnimationName<'i> { where W: std::fmt::Write, { + let css_module_animation_enabled = + dest.css_module.as_ref().map_or(false, |css_module| css_module.config.animation); + match self { AnimationName::None => dest.write_str("none"), AnimationName::Ident(s) => { - if let Some(css_module) = &mut dest.css_module { - css_module.reference(&s.0, dest.loc.source_index) + if css_module_animation_enabled { + if let Some(css_module) = &mut dest.css_module { + css_module.reference(&s.0, dest.loc.source_index) + } } - s.to_css(dest) + s.to_css_with_options(dest, css_module_animation_enabled) } AnimationName::String(s) => { - if let Some(css_module) = &mut dest.css_module { - css_module.reference(&s, dest.loc.source_index) + if css_module_animation_enabled { + if let Some(css_module) = &mut dest.css_module { + css_module.reference(&s, dest.loc.source_index) + } } // CSS-wide keywords and `none` cannot remove quotes. @@ -78,7 +85,7 @@ impl<'i> ToCss for AnimationName<'i> { Ok(()) }, _ => { - dest.write_ident(s.as_ref()) + dest.write_ident(s.as_ref(), css_module_animation_enabled) } } } diff --git a/src/properties/grid.rs b/src/properties/grid.rs index fa0e5a64..912f563d 100644 --- a/src/properties/grid.rs +++ b/src/properties/grid.rs @@ -430,21 +430,24 @@ fn write_ident(name: &str, dest: &mut Printer) -> Result<(), PrinterError> where W: std::fmt::Write, { - if let Some(css_module) = &mut dest.css_module { - if let Some(last) = css_module.config.pattern.segments.last() { - if !matches!(last, crate::css_modules::Segment::Local) { - return Err(Error { - kind: PrinterErrorKind::InvalidCssModulesPatternInGrid, - loc: Some(ErrorLocation { - filename: dest.filename().into(), - line: dest.loc.line, - column: dest.loc.column, - }), - }); + let css_module_grid_enabled = dest.css_module.as_ref().map_or(false, |css_module| css_module.config.grid); + if css_module_grid_enabled { + if let Some(css_module) = &mut dest.css_module { + if let Some(last) = css_module.config.pattern.segments.last() { + if !matches!(last, crate::css_modules::Segment::Local) { + return Err(Error { + kind: PrinterErrorKind::InvalidCssModulesPatternInGrid, + loc: Some(ErrorLocation { + filename: dest.filename().into(), + line: dest.loc.line, + column: dest.loc.column, + }), + }); + } } } } - dest.write_ident(name)?; + dest.write_ident(name, css_module_grid_enabled)?; Ok(()) } diff --git a/src/rules/keyframes.rs b/src/rules/keyframes.rs index a9489f30..511a3172 100644 --- a/src/rules/keyframes.rs +++ b/src/rules/keyframes.rs @@ -92,9 +92,12 @@ impl<'i> ToCss for KeyframesName<'i> { where W: std::fmt::Write, { + let css_module_animation_enabled = + dest.css_module.as_ref().map_or(false, |css_module| css_module.config.animation); + match self { KeyframesName::Ident(ident) => { - dest.write_ident(ident.0.as_ref())?; + dest.write_ident(ident.0.as_ref(), css_module_animation_enabled)?; } KeyframesName::Custom(s) => { // CSS-wide keywords and `none` cannot remove quotes. @@ -103,7 +106,7 @@ impl<'i> ToCss for KeyframesName<'i> { serialize_string(&s, dest)?; }, _ => { - dest.write_ident(s.as_ref())?; + dest.write_ident(s.as_ref(), css_module_animation_enabled)?; } } } diff --git a/src/selector.rs b/src/selector.rs index 9cc04848..62550198 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -672,7 +672,7 @@ where if let Some(class) = class { dest.write_char('.')?; - dest.write_ident(class) + dest.write_ident(class, true) } else { dest.write_str($s) } @@ -1551,11 +1551,11 @@ where Component::Nesting => serialize_nesting(dest, context, false), Component::Class(ref class) => { dest.write_char('.')?; - dest.write_ident(&class.0) + dest.write_ident(&class.0, true) } Component::ID(ref id) => { dest.write_char('#')?; - dest.write_ident(&id.0) + dest.write_ident(&id.0, true) } Component::Host(selector) => { dest.write_str(":host")?; diff --git a/src/values/ident.rs b/src/values/ident.rs index 9d244965..173d3a08 100644 --- a/src/values/ident.rs +++ b/src/values/ident.rs @@ -49,7 +49,26 @@ impl<'i> ToCss for CustomIdent<'i> { where W: std::fmt::Write, { - dest.write_ident(&self.0) + self.to_css_with_options(dest, true) + } +} + +impl<'i> CustomIdent<'i> { + /// Write the custom ident to CSS. + pub(crate) fn to_css_with_options( + &self, + dest: &mut Printer, + enabled_css_modules: bool, + ) -> Result<(), PrinterError> + where + W: std::fmt::Write, + { + let css_module_custom_idents_enabled = enabled_css_modules + && dest + .css_module + .as_mut() + .map_or(false, |css_module| css_module.config.custom_idents); + dest.write_ident(&self.0, css_module_custom_idents_enabled) } } diff --git a/website/pages/css-modules.md b/website/pages/css-modules.md index d66016c3..732f83cf 100644 --- a/website/pages/css-modules.md +++ b/website/pages/css-modules.md @@ -13,16 +13,16 @@ CSS modules treat the classes defined in each file as unique. Each class name or To enable CSS modules, provide the `cssModules` option when calling the Lightning CSS API. When using the CLI, enable the `--css-modules` flag. ```js -import { transform } from 'lightningcss'; +import {transform} from 'lightningcss'; -let { code, map, exports } = transform({ +let {code, map, exports} = transform({ // ... cssModules: true, code: Buffer.from(` .logo { background: skyblue; } - `) + `), }); ``` @@ -89,7 +89,7 @@ You can also reference class names defined in a different CSS file using the `fr ```css .logo { - composes: bg-indigo from "./colors.module.css"; + composes: bg-indigo from './colors.module.css'; } ``` @@ -150,10 +150,10 @@ compiles to: By default, class names, id selectors, and the names of `@keyframes`, `@counter-style`, and CSS grid lines and areas are scoped to the module they are defined in. Scoping for CSS variables and other [``](https://www.w3.org/TR/css-values-4/#dashed-idents) names can also be enabled using the `dashedIdents` option when calling the Lightning CSS API. When using the CLI, enable the `--css-modules-dashed-idents` flag. ```js -let { code, map, exports } = transform({ +let {code, map, exports} = transform({ // ... cssModules: { - dashedIdents: true + dashedIdents: true, }, }); ``` @@ -186,7 +186,7 @@ You can also reference variables defined in other files using the `from` keyword ```css .button { - background: var(--accent-color from "./vars.module.css"); + background: var(--accent-color from './vars.module.css'); } ``` @@ -207,19 +207,19 @@ By default, Lightning CSS prepends the hash of the filename to each class name a A pattern is a string with placeholders that will be filled in by Lightning CSS. This allows you to add custom prefixes or adjust the naming convention for scoped classes. ```js -let { code, map, exports } = transform({ +let {code, map, exports} = transform({ // ... cssModules: { - pattern: 'my-company-[name]-[hash]-[local]' - } + pattern: 'my-company-[name]-[hash]-[local]', + }, }); ``` The following placeholders are currently supported: -* `[name]` - The base name of the file, without the extension. -* `[hash]` - A hash of the full file path. -* `[local]` - The original class name or identifier. +- `[name]` - The base name of the file, without the extension. +- `[hash]` - A hash of the full file path. +- `[local]` - The original class name or identifier.
@@ -231,7 +231,7 @@ The following placeholders are currently supported: let { code, map, exports } = transform({ // ... cssModules: { - // ❌ [local] must be at the end so that + // ❌ [local] must be at the end so that // auto-generated grid line names work pattern: '[local]-[hash]' // ✅ do this instead @@ -242,7 +242,7 @@ let { code, map, exports } = transform({ ```css .grid { - grid-template-areas: "nav main"; + grid-template-areas: 'nav main'; } .nav { @@ -252,10 +252,25 @@ let { code, map, exports } = transform({
+## Turning off feature scoping + +Scoping of grid, animations, and custom identifiers can be turned off. By default all of these are scoped. + +```js +let {code, map, exports} = transform({ + // ... + cssModules: { + animation: true, + grid: true, + customIdents: true, + }, +}); +``` + ## Unsupported features Lightning CSS does not currently implement all CSS modules features available in other implementations. Some of these may be added in the future. -* Non-function syntax for the `:local` and `:global` pseudo classes. -* The `@value` rule – superseded by standard CSS variables. -* The `:import` and `:export` ICSS rules. +- Non-function syntax for the `:local` and `:global` pseudo classes. +- The `@value` rule – superseded by standard CSS variables. +- The `:import` and `:export` ICSS rules.