From 69503b9fa46ed72af00709fd0956494ba3c1eb14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EB=8F=99=EC=9C=A4=20=28Donny=29?= Date: Wed, 14 Aug 2024 15:49:30 +0900 Subject: [PATCH 01/11] Add a test --- src/error.rs | 4 ++++ src/lib.rs | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/src/error.rs b/src/error.rs index c7f62e4f..f27b28d3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -235,6 +235,9 @@ pub enum SelectorError<'i> { /// Ambiguous CSS module class. AmbiguousCssModuleClass(CowArcStr<'i>), + + /// CSS module selector without any class or ID selector. + PureCssModuleClass, } impl<'i> fmt::Display for SelectorError<'i> { @@ -261,6 +264,7 @@ impl<'i> fmt::Display for SelectorError<'i> { UnexpectedTokenInAttributeSelector(token) => write!(f, "Unexpected token in attribute selector: {:?}", token), UnsupportedPseudoClassOrElement(name) => write!(f, "Unsupported pseudo class or element: {}", name), AmbiguousCssModuleClass(_) => write!(f, "Ambiguous CSS module class not supported"), + PureCssModuleClass => write!(f, "A selector in CSS modules should contain at least one class or ID selector"), } } } diff --git a/src/lib.rs b/src/lib.rs index 02bf9fcf..c0539fff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6890,6 +6890,12 @@ mod tests { ".foo /deep/ .bar {width: 20px}", ParserError::SelectorError(SelectorError::DanglingCombinator), ); + + error_test_with_options( + "div {width: 20px}", + ParserError::SelectorError(SelectorError::PureCssModuleClass), + ); + minify_test_with_options( ".foo >>> .bar {width: 20px}", ".foo>>>.bar{width:20px}", From 4b71d49352eea1e561b170663532440875b706a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EB=8F=99=EC=9C=A4=20=28Donny=29?= Date: Wed, 14 Aug 2024 16:05:11 +0900 Subject: [PATCH 02/11] fix build --- src/lib.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index c0539fff..3fa3d68b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -217,6 +217,14 @@ mod tests { } } + fn error_test_with_options<'i, 'o>(source: &'i str, error: ParserError, options: ParserOptions<'o, 'i>) { + let res = StyleSheet::parse(&source, options); + match res { + Ok(_) => unreachable!(), + Err(e) => assert_eq!(e.kind, error), + } + } + macro_rules! map( { $($key:expr => $name:literal $(referenced: $referenced: literal)? $($value:literal $(global: $global: literal)? $(from $from:literal)?)*),* } => { { @@ -6891,9 +6899,15 @@ mod tests { ParserError::SelectorError(SelectorError::DanglingCombinator), ); + let css_module_options = ParserOptions { + css_modules: Some(crate::css_modules::Config::default()), + ..ParserOptions::default() + }; + error_test_with_options( "div {width: 20px}", ParserError::SelectorError(SelectorError::PureCssModuleClass), + css_module_options.clone(), ); minify_test_with_options( From 8aa4ba072e820291314e3c24d0b11109f233295f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EB=8F=99=EC=9C=A4=20=28Donny=29?= Date: Wed, 14 Aug 2024 16:30:02 +0900 Subject: [PATCH 03/11] Implement --- selectors/parser.rs | 24 +++++++++++++++++++++++- src/css_modules.rs | 3 +++ src/error.rs | 1 + src/lib.rs | 9 ++++++--- src/selector.rs | 4 ++++ 5 files changed, 37 insertions(+), 4 deletions(-) diff --git a/selectors/parser.rs b/selectors/parser.rs index c0e27a9a..05ec6413 100644 --- a/selectors/parser.rs +++ b/selectors/parser.rs @@ -206,6 +206,7 @@ pub enum SelectorParseErrorKind<'i> { InvalidQualNameInAttr(Token<'i>), ExplicitNamespaceUnexpectedToken(Token<'i>), ClassNeedsIdent(Token<'i>), + PureCssModuleClass, } macro_rules! with_all_bounds { @@ -356,6 +357,10 @@ pub trait Parser<'i> { fn deep_combinator_enabled(&self) -> bool { false } + + fn check_for_pure_css_modules(&self) -> bool { + false + } } #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -448,6 +453,8 @@ impl<'i, Impl: SelectorImpl<'i>> SelectorList<'i, Impl> { { let original_state = *state; let mut values = SmallVec::new(); + let mut had_class_or_id = false; + let need_to_check_for_purity = parser.check_for_pure_css_modules(); loop { let selector = input.parse_until_before(Delimiter::Comma, |input| { let mut selector_state = original_state; @@ -460,13 +467,28 @@ impl<'i, Impl: SelectorImpl<'i>> SelectorList<'i, Impl> { let was_ok = selector.is_ok(); match selector { - Ok(selector) => values.push(selector), + Ok(selector) => { + if need_to_check_for_purity + && had_class_or_id + && selector + .iter() + .all(|component| matches!(component, Component::Class(..) | Component::ID(..))) + { + had_class_or_id = true; + } + + values.push(selector) + } Err(err) => match recovery { ParseErrorRecovery::DiscardList => return Err(err), ParseErrorRecovery::IgnoreInvalidSelector => {} }, } + if need_to_check_for_purity && !had_class_or_id { + return Err(input.new_custom_error(SelectorParseErrorKind::PureCssModuleClass)); + } + loop { match input.next() { Err(_) => return Ok(SelectorList(values)), diff --git a/src/css_modules.rs b/src/css_modules.rs index cfe33d71..7663964f 100644 --- a/src/css_modules.rs +++ b/src/css_modules.rs @@ -41,6 +41,8 @@ pub struct Config<'i> { /// Whether to scope custom identifiers /// Default is `true`. pub custom_idents: bool, + /// Whether to check for pure CSS modules. + pub pure: bool, } impl<'i> Default for Config<'i> { @@ -51,6 +53,7 @@ impl<'i> Default for Config<'i> { animation: true, grid: true, custom_idents: true, + pure: false, } } } diff --git a/src/error.rs b/src/error.rs index f27b28d3..7bc2dd33 100644 --- a/src/error.rs +++ b/src/error.rs @@ -306,6 +306,7 @@ impl<'i> From> for SelectorError<'i> { } SelectorParseErrorKind::ClassNeedsIdent(t) => SelectorError::ClassNeedsIdent(t.into()), SelectorParseErrorKind::AmbiguousCssModuleClass(name) => SelectorError::AmbiguousCssModuleClass(name.into()), + SelectorParseErrorKind::PureCssModuleClass => SelectorError::PureCssModuleClass, } } } diff --git a/src/lib.rs b/src/lib.rs index 3fa3d68b..2eb9babe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6899,15 +6899,18 @@ mod tests { ParserError::SelectorError(SelectorError::DanglingCombinator), ); - let css_module_options = ParserOptions { - css_modules: Some(crate::css_modules::Config::default()), + let pure_css_module_options = ParserOptions { + css_modules: Some(crate::css_modules::Config { + pure: true, + ..Default::default() + }), ..ParserOptions::default() }; error_test_with_options( "div {width: 20px}", ParserError::SelectorError(SelectorError::PureCssModuleClass), - css_module_options.clone(), + pure_css_module_options.clone(), ); minify_test_with_options( diff --git a/src/selector.rs b/src/selector.rs index 2c7d4340..8450a9f1 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -356,6 +356,10 @@ impl<'a, 'o, 'i> parcel_selectors::parser::Parser<'i> for SelectorParser<'a, 'o, fn deep_combinator_enabled(&self) -> bool { self.options.flags.contains(ParserFlags::DEEP_SELECTOR_COMBINATOR) } + + fn check_for_pure_css_modules(&self) -> bool { + self.options.css_modules.as_ref().map_or(false, |v| v.pure) + } } enum_property! { From be726a67ca7a9ddcd2da2bc2668c563d016ee51b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EB=8F=99=EC=9C=A4=20=28Donny=29?= Date: Fri, 16 Aug 2024 09:09:59 +0900 Subject: [PATCH 04/11] Add tests --- src/lib.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 2eb9babe..3cc06933 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6912,6 +6912,21 @@ mod tests { ParserError::SelectorError(SelectorError::PureCssModuleClass), pure_css_module_options.clone(), ); + minify_test_with_options( + "div.my-class {color: red;}", + "div.my-class{color:red}", + pure_css_module_options.clone(), + ); + minify_test_with_options( + "a .my-class{color: red;}", + "a .my-class{color:red}", + pure_css_module_options.clone(), + ); + minify_test_with_options( + ".my-class a {color: red;}", + ".my-class a{color:red}", + pure_css_module_options.clone(), + ); minify_test_with_options( ".foo >>> .bar {width: 20px}", From 808f2aca54133de89f18b2c499f7e5b9eff07412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EB=8F=99=EC=9C=A4=20=28Donny=29?= Date: Fri, 16 Aug 2024 09:11:36 +0900 Subject: [PATCH 05/11] fixup --- src/lib.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3cc06933..f271e69b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -71,6 +71,7 @@ mod tests { test_with_options(source, expected, ParserOptions::default()) } + #[track_caller] fn test_with_options<'i, 'o>(source: &'i str, expected: &'i str, options: ParserOptions<'o, 'i>) { let mut stylesheet = StyleSheet::parse(&source, options).unwrap(); stylesheet.minify(MinifyOptions::default()).unwrap(); @@ -6912,17 +6913,17 @@ mod tests { ParserError::SelectorError(SelectorError::PureCssModuleClass), pure_css_module_options.clone(), ); - minify_test_with_options( + test_with_options( "div.my-class {color: red;}", - "div.my-class{color:red}", + "div.my-class {color: red}", pure_css_module_options.clone(), ); - minify_test_with_options( + test_with_options( "a .my-class{color: red;}", "a .my-class{color:red}", pure_css_module_options.clone(), ); - minify_test_with_options( + test_with_options( ".my-class a {color: red;}", ".my-class a{color:red}", pure_css_module_options.clone(), From 092a9e5f4c14118d7e4a5725503ec4dd0ab96d69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EB=8F=99=EC=9C=A4=20=28Donny=29?= Date: Fri, 16 Aug 2024 09:12:17 +0900 Subject: [PATCH 06/11] fix --- selectors/parser.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selectors/parser.rs b/selectors/parser.rs index 05ec6413..41af72e6 100644 --- a/selectors/parser.rs +++ b/selectors/parser.rs @@ -469,7 +469,7 @@ impl<'i, Impl: SelectorImpl<'i>> SelectorList<'i, Impl> { match selector { Ok(selector) => { if need_to_check_for_purity - && had_class_or_id + && !had_class_or_id && selector .iter() .all(|component| matches!(component, Component::Class(..) | Component::ID(..))) From 430ab3fb69303181e1d52664a75633a73e5714c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EB=8F=99=EC=9C=A4=20=28Donny=29?= Date: Fri, 16 Aug 2024 09:25:44 +0900 Subject: [PATCH 07/11] fix --- selectors/parser.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selectors/parser.rs b/selectors/parser.rs index 41af72e6..2c5e5a74 100644 --- a/selectors/parser.rs +++ b/selectors/parser.rs @@ -472,7 +472,7 @@ impl<'i, Impl: SelectorImpl<'i>> SelectorList<'i, Impl> { && !had_class_or_id && selector .iter() - .all(|component| matches!(component, Component::Class(..) | Component::ID(..))) + .any(|component| matches!(component, Component::Class(..) | Component::ID(..))) { had_class_or_id = true; } From 1ae67a2b2a77a5b2bcaae115fb149cd37228dcd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EB=8F=99=EC=9C=A4=20=28Donny=29?= Date: Fri, 16 Aug 2024 09:34:57 +0900 Subject: [PATCH 08/11] Fix --- selectors/parser.rs | 2 +- src/lib.rs | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/selectors/parser.rs b/selectors/parser.rs index 2c5e5a74..c7e2973c 100644 --- a/selectors/parser.rs +++ b/selectors/parser.rs @@ -471,7 +471,7 @@ impl<'i, Impl: SelectorImpl<'i>> SelectorList<'i, Impl> { if need_to_check_for_purity && !had_class_or_id && selector - .iter() + .iter_raw_match_order() .any(|component| matches!(component, Component::Class(..) | Component::ID(..))) { had_class_or_id = true; diff --git a/src/lib.rs b/src/lib.rs index f271e69b..7bc09aff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -71,7 +71,6 @@ mod tests { test_with_options(source, expected, ParserOptions::default()) } - #[track_caller] fn test_with_options<'i, 'o>(source: &'i str, expected: &'i str, options: ParserOptions<'o, 'i>) { let mut stylesheet = StyleSheet::parse(&source, options).unwrap(); stylesheet.minify(MinifyOptions::default()).unwrap(); @@ -83,6 +82,7 @@ mod tests { minify_test_with_options(source, expected, ParserOptions::default()) } + #[track_caller] fn minify_test_with_options<'i, 'o>(source: &'i str, expected: &'i str, options: ParserOptions<'o, 'i>) { let mut stylesheet = StyleSheet::parse(&source, options.clone()).unwrap(); stylesheet.minify(MinifyOptions::default()).unwrap(); @@ -6913,19 +6913,19 @@ mod tests { ParserError::SelectorError(SelectorError::PureCssModuleClass), pure_css_module_options.clone(), ); - test_with_options( + minify_test_with_options( "div.my-class {color: red;}", - "div.my-class {color: red}", + "div._8Z4fiW_my-class{color:red}", pure_css_module_options.clone(), ); - test_with_options( + minify_test_with_options( "a .my-class{color: red;}", - "a .my-class{color:red}", + "a ._8Z4fiW_my-class{color:red}", pure_css_module_options.clone(), ); - test_with_options( + minify_test_with_options( ".my-class a {color: red;}", - ".my-class a{color:red}", + "._8Z4fiW_my-class a{color:red}", pure_css_module_options.clone(), ); From 01528342392d7127d1475eba6c8e981467ab2e2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EB=8F=99=EC=9C=A4=20=28Donny=29?= Date: Mon, 19 Aug 2024 08:31:51 +0900 Subject: [PATCH 09/11] Fix napi --- napi/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/napi/src/lib.rs b/napi/src/lib.rs index 9edfb1ce..e5952fa5 100644 --- a/napi/src/lib.rs +++ b/napi/src/lib.rs @@ -608,6 +608,7 @@ struct CssModulesConfig { animation: Option, grid: Option, custom_idents: Option, + pure: Option, } #[cfg(feature = "bundler")] @@ -719,6 +720,7 @@ fn compile<'i>( animation: c.animation.unwrap_or(true), grid: c.grid.unwrap_or(true), custom_idents: c.custom_idents.unwrap_or(true), + pure: c.pure.unwrap_or_default(), }), } } else { From 57c2d49efe3b0e45a4c87abd70d71fbaaf299396 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EB=8F=99=EC=9C=A4=20=28Donny=29?= Date: Mon, 19 Aug 2024 08:35:44 +0900 Subject: [PATCH 10/11] napi --- napi/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/napi/src/lib.rs b/napi/src/lib.rs index e5952fa5..18bbd7e4 100644 --- a/napi/src/lib.rs +++ b/napi/src/lib.rs @@ -851,6 +851,7 @@ fn compile_bundle< animation: c.animation.unwrap_or(true), grid: c.grid.unwrap_or(true), custom_idents: c.custom_idents.unwrap_or(true), + pure: c.pure.unwrap_or_default(), }), } } else { From 4d4320d87975d3f0762aa3149de611ca19041f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EB=8F=99=EC=9C=A4=20=28Donny=29?= Date: Mon, 19 Aug 2024 19:38:27 +0900 Subject: [PATCH 11/11] Document --- website/pages/css-modules.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/website/pages/css-modules.md b/website/pages/css-modules.md index 732f83cf..721bfedf 100644 --- a/website/pages/css-modules.md +++ b/website/pages/css-modules.md @@ -252,6 +252,26 @@ let { code, map, exports } = transform({ + +### Pure mode + +Just like the `pure` option of the `css-loader` for webpack, Lightning CSS also has a `pure` option that enforces usage of one or more id or class selectors for each rule. + + +```js +let {code, map, exports} = transform({ + // ... + cssModules: { + pure: true, + }, +}); +``` + +If you enable this option, Lightning CSS will throw an error for CSS rules that don't have at least one id or class selector, like `div`. +This is useful because selectors like `div` are not scoped and affects all elements on the page. + + + ## Turning off feature scoping Scoping of grid, animations, and custom identifiers can be turned off. By default all of these are scoped.