Skip to content
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
36 changes: 35 additions & 1 deletion selectors/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ bitflags! {
const AFTER_WEBKIT_SCROLLBAR = 1 << 8;
const AFTER_VIEW_TRANSITION = 1 << 9;
const AFTER_UNKNOWN_PSEUDO_ELEMENT = 1 << 10;

/// Whether we explicitly disallow pure CSS modules.
const IGNORE_CSS_MODULE_PURITY_CHECK = 1 << 11;
}
}

Expand Down Expand Up @@ -207,6 +210,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 +361,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 +457,9 @@ 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() && !state.contains(SelectorParsingState::IGNORE_CSS_MODULE_PURITY_CHECK);
loop {
let selector = input.parse_until_before(Delimiter::Comma, |input| {
let mut selector_state = original_state;
Expand All @@ -461,13 +472,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 Expand Up @@ -2981,6 +3007,10 @@ where
SelectorParseErrorKind::UnexpectedSelectorAfterPseudoElement(Token::IDHash(id)),
));
}

// Mark this selector as already checked for purity.
state.insert(SelectorParsingState::IGNORE_CSS_MODULE_PURITY_CHECK);

let id = Component::ID(id.into());
SimpleSelectorParseResult::SimpleSelector(id)
}
Expand All @@ -2998,6 +3028,10 @@ where
return Err(location.new_custom_error(e));
}
};

// Mark this selector as already checked for purity.
state.insert(SelectorParsingState::IGNORE_CSS_MODULE_PURITY_CHECK);

let class = Component::Class(class.into());
SimpleSelectorParseResult::SimpleSelector(class)
}
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
44 changes: 44 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,41 @@ 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(
".my-class:is(a) {color: red;}",
"._8Z4fiW_my-class:is(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