diff --git a/crates/turbopack-css/src/process.rs b/crates/turbopack-css/src/process.rs index 55dee84728a22..ce415017b7f54 100644 --- a/crates/turbopack-css/src/process.rs +++ b/crates/turbopack-css/src/process.rs @@ -9,6 +9,8 @@ use lightningcss::{ stylesheet::{ParserOptions, PrinterOptions, StyleSheet, ToCssResult}, targets::{Features, Targets}, values::url::Url, + visit_types, + visitor::Visit, }; use once_cell::sync::Lazy; use regex::Regex; @@ -18,10 +20,10 @@ use swc_core::{ base::sourcemap::SourceMapBuilder, common::{BytePos, FileName, LineCol}, css::{ - ast::UrlValue, + ast::{TypeSelector, UrlValue}, codegen::{writer::basic::BasicCssWriter, CodeGenerator}, modules::{CssClassName, TransformConfig}, - visit::{VisitMut, VisitMutWith}, + visit::{VisitMut, VisitMutWith, VisitWith}, }, }; use tracing::Instrument; @@ -30,12 +32,13 @@ use turbo_tasks_fs::{FileContent, FileSystemPath}; use turbopack_core::{ asset::{Asset, AssetContent}, chunk::ChunkingContext, - issue::{Issue, IssueExt, OptionStyledString, StyledString}, + issue::{Issue, IssueExt, IssueSource, OptionIssueSource, OptionStyledString, StyledString}, reference::ModuleReferences, reference_type::ImportContext, resolve::origin::ResolveOrigin, source::Source, source_map::{GenerateSourceMap, OptionSourceMap}, + source_pos::SourcePos, }; use turbopack_swc_utils::emitter::IssueEmitter; @@ -470,7 +473,7 @@ pub async fn parse_css( async fn process_content( code: String, - fs_path: Vc, + fs_path_vc: Vc, ident_str: &str, source: Vc>, origin: Vc>, @@ -516,11 +519,28 @@ async fn process_content( let stylesheet = if use_lightningcss { StyleSheetLike::LightningCss(match StyleSheet::parse(&code, config.clone()) { - Ok(stylesheet) => stylesheet_into_static(&stylesheet, without_warnings(config.clone())), + Ok(mut ss) => { + ss.visit(&mut CssModuleValidator { + source, + file: fs_path_vc, + }) + .unwrap(); + + stylesheet_into_static(&ss, without_warnings(config.clone())) + } Err(e) => { + let source = e.loc.as_ref().map(|loc| { + let pos = SourcePos { + line: loc.line as _, + column: loc.column as _, + }; + IssueSource::from_line_col(source, pos, pos) + }); + ParsingIssue { - file: fs_path, + file: fs_path_vc, msg: Vc::cell(e.to_string()), + source: Vc::cell(source), } .cell() .emit(); @@ -528,7 +548,7 @@ async fn process_content( } }) } else { - let fs_path = &*fs_path.await?; + let fs_path = &*fs_path_vc.await?; let handler = swc_core::common::errors::Handler::with_emitter( true, @@ -558,7 +578,7 @@ async fn process_content( err.to_diagnostics(&handler).emit(); } - let ss = match ss { + let ss: swc_core::css::ast::Stylesheet = match ss { Ok(v) => v, Err(err) => { err.to_diagnostics(&handler).emit(); @@ -570,6 +590,13 @@ async fn process_content( return Ok(ParseCssResult::Unparseable.into()); } + if matches!(ty, CssModuleAssetType::Module) { + ss.visit_with(&mut CssModuleValidator { + source, + file: fs_path_vc, + }); + } + StyleSheetLike::Swc { stylesheet: ss, css_modules: if matches!(ty, CssModuleAssetType::Module) { @@ -608,6 +635,91 @@ async fn process_content( .into()) } +/// Visitor that lints wrong css module usage. +/// +/// ```css +/// button { +/// } +/// ``` +/// +/// is wrong for a css module because it doesn't have a class name. +struct CssModuleValidator { + source: Vc>, + file: Vc, +} + +const CSS_MODULE_ERROR: &str = + "Selector is not pure (pure selectors must contain at least one local class or id)"; + +/// We only vist top-level selectors. +impl swc_core::css::visit::Visit for CssModuleValidator { + // TODO: SKip some + fn visit_complex_selector(&mut self, n: &swc_core::css::ast::ComplexSelector) { + if n.children.iter().all(|sel| match sel { + swc_core::css::ast::ComplexSelectorChildren::CompoundSelector(sel) => { + sel.subclass_selectors.is_empty() + && match &sel.type_selector.as_deref() { + Some(TypeSelector::TagName(tag)) => { + !matches!(&*tag.name.value.value, "html" | "body") + } + Some(..) => true, + None => false, + } + } + swc_core::css::ast::ComplexSelectorChildren::Combinator(_) => true, + }) { + ParsingIssue { + file: self.file, + msg: Vc::cell(CSS_MODULE_ERROR.to_string()), + source: Vc::cell(Some(IssueSource::from_swc_offsets( + self.source, + n.span.lo.0 as _, + n.span.hi.0 as _, + ))), + } + .cell() + .emit(); + } + } + + fn visit_simple_block(&mut self, _: &swc_core::css::ast::SimpleBlock) {} +} + +/// We only vist top-level selectors. +impl lightningcss::visitor::Visitor<'_> for CssModuleValidator { + type Error = (); + + // TODO: Skip some + + fn visit_types(&self) -> lightningcss::visitor::VisitTypes { + visit_types!(SELECTORS) + } + + fn visit_selector( + &mut self, + selector: &mut lightningcss::selector::Selector<'_>, + ) -> Result<(), Self::Error> { + if selector.iter().all(|component| !match component { + parcel_selectors::parser::Component::ID(_) + | parcel_selectors::parser::Component::Class(_) => true, + parcel_selectors::parser::Component::LocalName(local) => { + !matches!(&*local.name.0, "html" | "body") + } + _ => false, + }) { + ParsingIssue { + file: self.file, + msg: Vc::cell(CSS_MODULE_ERROR.to_string()), + source: Vc::cell(None), + } + .cell() + .emit(); + } + + Ok(()) + } +} + #[turbo_tasks::value(shared, serialization = "none", eq = "manual")] pub enum ParseCssResultSourceMap { Parcel { @@ -762,6 +874,7 @@ impl TransformConfig for ModuleTransformConfig { struct ParsingIssue { msg: Vc, file: Vc, + source: Vc, } #[turbo_tasks::value_impl] @@ -776,6 +889,11 @@ impl Issue for ParsingIssue { StyledString::Text("Parsing css source code failed".to_string()).cell() } + #[turbo_tasks::function] + fn source(&self) -> Vc { + self.source + } + #[turbo_tasks::function] async fn description(&self) -> Result> { Ok(Vc::cell(Some(