Skip to content

Commit

Permalink
Merge branch 'lint-pure' of github.com:kdy1/lightningcss into kdy1-li…
Browse files Browse the repository at this point in the history
…nt-pure

# Conflicts:
#	selectors/parser.rs
#	src/error.rs
  • Loading branch information
devongovett committed Aug 28, 2024
2 parents 186435f + 4d4320d commit cdd12a7
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 1 deletion.
3 changes: 3 additions & 0 deletions napi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,7 @@ struct CssModulesConfig {
animation: Option<bool>,
grid: Option<bool>,
custom_idents: Option<bool>,
pure: Option<bool>,
}

#[cfg(feature = "bundler")]
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -849,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 {
Expand Down
24 changes: 23 additions & 1 deletion selectors/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ pub enum SelectorParseErrorKind<'i> {
ExplicitNamespaceUnexpectedToken(Token<'i>),
ClassNeedsIdent(Token<'i>),
UnexpectedSelectorAfterPseudoElement(Token<'i>),
PureCssModuleClass,
}

macro_rules! with_all_bounds {
Expand Down Expand Up @@ -357,6 +358,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)]
Expand Down Expand Up @@ -449,6 +454,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;
Expand All @@ -461,13 +468,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_raw_match_order()
.any(|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)),
Expand Down
3 changes: 3 additions & 0 deletions src/css_modules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand All @@ -51,6 +53,7 @@ impl<'i> Default for Config<'i> {
animation: true,
grid: true,
custom_idents: true,
pure: false,
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,9 @@ pub enum SelectorError<'i> {
UnexpectedSelectorAfterPseudoElement(
#[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(skip))] Token<'i>,
),

/// CSS module selector without any class or ID selector.
PureCssModuleClass,
}

impl<'i> fmt::Display for SelectorError<'i> {
Expand Down Expand Up @@ -272,6 +275,7 @@ impl<'i> fmt::Display for SelectorError<'i> {
"Pseudo-elements like '::before' or '::after' can't be followed by selectors like '{token:?}'"
)
},
PureCssModuleClass => write!(f, "A selector in CSS modules should contain at least one class or ID selector"),
}
}
}
Expand Down Expand Up @@ -316,6 +320,7 @@ impl<'i> From<SelectorParseErrorKind<'i>> for SelectorError<'i> {
SelectorParseErrorKind::UnexpectedSelectorAfterPseudoElement(t) => {
SelectorError::UnexpectedSelectorAfterPseudoElement(t.into())
}
SelectorParseErrorKind::PureCssModuleClass => SelectorError::PureCssModuleClass,
}
}
}
Expand Down
39 changes: 39 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,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();
Expand Down Expand Up @@ -217,6 +218,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)?)*),* } => {
{
Expand Down Expand Up @@ -6890,6 +6899,36 @@ mod tests {
".foo /deep/ .bar {width: 20px}",
ParserError::SelectorError(SelectorError::DanglingCombinator),
);

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),
pure_css_module_options.clone(),
);
minify_test_with_options(
"div.my-class {color: red;}",
"div._8Z4fiW_my-class{color:red}",
pure_css_module_options.clone(),
);
minify_test_with_options(
"a .my-class{color: red;}",
"a ._8Z4fiW_my-class{color:red}",
pure_css_module_options.clone(),
);
minify_test_with_options(
".my-class a {color: red;}",
"._8Z4fiW_my-class a{color:red}",
pure_css_module_options.clone(),
);

minify_test_with_options(
".foo >>> .bar {width: 20px}",
".foo>>>.bar{width:20px}",
Expand Down
4 changes: 4 additions & 0 deletions src/selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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! {
Expand Down
20 changes: 20 additions & 0 deletions website/pages/css-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,26 @@ let { code, map, exports } = transform({

</div>


### 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.
Expand Down

0 comments on commit cdd12a7

Please sign in to comment.