1use std::sync::{Arc, RwLock};
2
3use anyhow::{Context, Result, bail};
4use lightningcss::{
5 css_modules::{CssModuleExport, CssModuleExports, Pattern, Segment},
6 stylesheet::{ParserOptions, PrinterOptions, StyleSheet, ToCssResult},
7 targets::{Features, Targets},
8 values::url::Url,
9 visit_types,
10 visitor::Visit,
11};
12use rustc_hash::FxHashMap;
13use smallvec::smallvec;
14use swc_core::base::sourcemap::SourceMapBuilder;
15use tracing::Instrument;
16use turbo_rcstr::RcStr;
17use turbo_tasks::{FxIndexMap, ResolvedVc, ValueToString, Vc};
18use turbo_tasks_fs::{FileContent, FileSystemPath, rope::Rope};
19use turbopack_core::{
20 SOURCE_URL_PROTOCOL,
21 asset::{Asset, AssetContent},
22 chunk::{ChunkingContext, MinifyType},
23 issue::{
24 Issue, IssueExt, IssueSource, IssueStage, OptionIssueSource, OptionStyledString,
25 StyledString,
26 },
27 reference::ModuleReferences,
28 reference_type::ImportContext,
29 resolve::origin::ResolveOrigin,
30 source::Source,
31 source_map::{OptionStringifiedSourceMap, utils::add_default_ignore_list},
32 source_pos::SourcePos,
33};
34
35use crate::{
36 CssModuleAssetType,
37 lifetime_util::stylesheet_into_static,
38 references::{
39 analyze_references,
40 url::{UrlAssetReference, replace_url_references, resolve_url_reference},
41 },
42};
43
44#[derive(Debug)]
45pub struct StyleSheetLike<'i, 'o>(pub(crate) StyleSheet<'i, 'o>);
46
47impl PartialEq for StyleSheetLike<'_, '_> {
48 fn eq(&self, _: &Self) -> bool {
49 false
50 }
51}
52
53pub type CssOutput = (ToCssResult, Option<Rope>);
54
55impl StyleSheetLike<'_, '_> {
56 pub fn to_static(
57 &self,
58 options: ParserOptions<'static, 'static>,
59 ) -> StyleSheetLike<'static, 'static> {
60 StyleSheetLike(stylesheet_into_static(&self.0, options))
61 }
62
63 pub fn to_css(
64 &self,
65 code: &str,
66 minify_type: MinifyType,
67 enable_srcmap: bool,
68 handle_nesting: bool,
69 ) -> Result<CssOutput> {
70 let ss = &self.0;
71 let mut srcmap = if enable_srcmap {
72 Some(parcel_sourcemap::SourceMap::new(""))
73 } else {
74 None
75 };
76
77 let targets = if handle_nesting {
78 Targets {
79 include: Features::Nesting,
80 ..Default::default()
81 }
82 } else {
83 Default::default()
84 };
85
86 let result = ss.to_css(PrinterOptions {
87 minify: matches!(minify_type, MinifyType::Minify { .. }),
88 source_map: srcmap.as_mut(),
89 targets,
90 analyze_dependencies: None,
91 ..Default::default()
92 })?;
93
94 if let Some(srcmap) = &mut srcmap {
95 debug_assert_eq!(ss.sources.len(), 1);
96
97 srcmap.add_sources(ss.sources.clone());
98 srcmap.set_source_content(0, code)?;
99 }
100
101 let srcmap = match srcmap {
102 Some(srcmap) => Some(generate_css_source_map(&srcmap)?),
103 None => None,
104 };
105
106 Ok((result, srcmap))
107 }
108}
109
110#[turbo_tasks::value(transparent)]
112pub struct UnresolvedUrlReferences(pub Vec<(String, ResolvedVc<UrlAssetReference>)>);
113
114#[turbo_tasks::value(shared, serialization = "none", eq = "manual", cell = "new")]
115#[allow(clippy::large_enum_variant)] pub enum ParseCssResult {
117 Ok {
118 code: ResolvedVc<FileContent>,
119
120 #[turbo_tasks(trace_ignore)]
121 stylesheet: StyleSheetLike<'static, 'static>,
122
123 references: ResolvedVc<ModuleReferences>,
124
125 url_references: ResolvedVc<UnresolvedUrlReferences>,
126
127 #[turbo_tasks(trace_ignore)]
128 options: ParserOptions<'static, 'static>,
129 },
130 Unparseable,
131 NotFound,
132}
133
134#[turbo_tasks::value(shared, serialization = "none", eq = "manual", cell = "new")]
135pub enum CssWithPlaceholderResult {
136 Ok {
137 parse_result: ResolvedVc<ParseCssResult>,
138
139 references: ResolvedVc<ModuleReferences>,
140
141 url_references: ResolvedVc<UnresolvedUrlReferences>,
142
143 #[turbo_tasks(trace_ignore)]
144 exports: Option<FxIndexMap<String, CssModuleExport>>,
145
146 #[turbo_tasks(trace_ignore)]
147 placeholders: FxHashMap<String, Url<'static>>,
148 },
149 Unparseable,
150 NotFound,
151}
152
153#[turbo_tasks::value(shared, serialization = "none", eq = "manual")]
154pub enum FinalCssResult {
155 Ok {
156 #[turbo_tasks(trace_ignore)]
157 output_code: String,
158
159 #[turbo_tasks(trace_ignore)]
160 exports: Option<CssModuleExports>,
161
162 source_map: ResolvedVc<OptionStringifiedSourceMap>,
163 },
164 Unparseable,
165 NotFound,
166}
167
168impl PartialEq for FinalCssResult {
169 fn eq(&self, _: &Self) -> bool {
170 false
171 }
172}
173
174#[turbo_tasks::function]
175pub async fn process_css_with_placeholder(
176 parse_result: ResolvedVc<ParseCssResult>,
177) -> Result<Vc<CssWithPlaceholderResult>> {
178 let result = parse_result.await?;
179
180 match &*result {
181 ParseCssResult::Ok {
182 stylesheet,
183 references,
184 url_references,
185 code,
186 ..
187 } => {
188 let code = code.await?;
189 let code = match &*code {
190 FileContent::Content(v) => v.content().to_str()?,
191 _ => bail!("this case should be filtered out while parsing"),
192 };
193
194 let (result, _) = stylesheet.to_css(&code, MinifyType::NoMinify, false, false)?;
197
198 let exports = result.exports.map(|exports| {
199 let mut exports = exports.into_iter().collect::<FxIndexMap<_, _>>();
200
201 exports.sort_keys();
202
203 exports
204 });
205
206 Ok(CssWithPlaceholderResult::Ok {
207 parse_result,
208 exports,
209 references: *references,
210 url_references: *url_references,
211 placeholders: FxHashMap::default(),
212 }
213 .cell())
214 }
215 ParseCssResult::Unparseable => Ok(CssWithPlaceholderResult::Unparseable.cell()),
216 ParseCssResult::NotFound => Ok(CssWithPlaceholderResult::NotFound.cell()),
217 }
218}
219
220#[turbo_tasks::function]
221pub async fn finalize_css(
222 result: Vc<CssWithPlaceholderResult>,
223 chunking_context: Vc<Box<dyn ChunkingContext>>,
224 minify_type: MinifyType,
225) -> Result<Vc<FinalCssResult>> {
226 let result = result.await?;
227 match &*result {
228 CssWithPlaceholderResult::Ok {
229 parse_result,
230 url_references,
231 ..
232 } => {
233 let (mut stylesheet, code) = match &*parse_result.await? {
234 ParseCssResult::Ok {
235 stylesheet,
236 options,
237 code,
238 ..
239 } => (stylesheet.to_static(options.clone()), *code),
240 ParseCssResult::Unparseable => return Ok(FinalCssResult::Unparseable.into()),
241 ParseCssResult::NotFound => return Ok(FinalCssResult::NotFound.into()),
242 };
243
244 let url_references = *url_references;
245
246 let mut url_map = FxHashMap::default();
247
248 for (src, reference) in (*url_references.await?).iter() {
249 let resolved = resolve_url_reference(**reference, chunking_context).await?;
250 if let Some(v) = resolved.as_ref().cloned() {
251 url_map.insert(RcStr::from(src.as_str()), v);
252 }
253 }
254
255 replace_url_references(&mut stylesheet, &url_map);
256
257 let code = code.await?;
258 let code = match &*code {
259 FileContent::Content(v) => v.content().to_str()?,
260 _ => bail!("this case should be filtered out while parsing"),
261 };
262 let (result, srcmap) = stylesheet.to_css(&code, minify_type, true, true)?;
263
264 Ok(FinalCssResult::Ok {
265 output_code: result.code,
266 exports: result.exports,
267 source_map: ResolvedVc::cell(srcmap),
268 }
269 .into())
270 }
271 CssWithPlaceholderResult::Unparseable => Ok(FinalCssResult::Unparseable.into()),
272 CssWithPlaceholderResult::NotFound => Ok(FinalCssResult::NotFound.into()),
273 }
274}
275
276#[turbo_tasks::value_trait]
277pub trait ParseCss {
278 async fn parse_css(self: Vc<Self>) -> Result<Vc<ParseCssResult>>;
279}
280
281#[turbo_tasks::value_trait]
282pub trait ProcessCss: ParseCss {
283 async fn get_css_with_placeholder(self: Vc<Self>) -> Result<Vc<CssWithPlaceholderResult>>;
284
285 async fn finalize_css(
286 self: Vc<Self>,
287 chunking_context: Vc<Box<dyn ChunkingContext>>,
288 minify_type: MinifyType,
289 ) -> Result<Vc<FinalCssResult>>;
290}
291
292#[turbo_tasks::function]
293pub async fn parse_css(
294 source: ResolvedVc<Box<dyn Source>>,
295 origin: ResolvedVc<Box<dyn ResolveOrigin>>,
296 import_context: Option<ResolvedVc<ImportContext>>,
297 ty: CssModuleAssetType,
298) -> Result<Vc<ParseCssResult>> {
299 let span = {
300 let name = source.ident().to_string().await?.to_string();
301 tracing::info_span!("parse css", name = name)
302 };
303 async move {
304 let content = source.content();
305 let fs_path = source.ident().path();
306 let ident_str = &*source.ident().to_string().await?;
307 Ok(match &*content.await? {
308 AssetContent::Redirect { .. } => ParseCssResult::Unparseable.cell(),
309 AssetContent::File(file_content) => match &*file_content.await? {
310 FileContent::NotFound => ParseCssResult::NotFound.cell(),
311 FileContent::Content(file) => match file.content().to_str() {
312 Err(_err) => ParseCssResult::Unparseable.cell(),
313 Ok(string) => {
314 process_content(
315 *file_content,
316 string.into_owned(),
317 fs_path.to_resolved().await?,
318 ident_str,
319 source,
320 origin,
321 import_context,
322 ty,
323 )
324 .await?
325 }
326 },
327 },
328 })
329 }
330 .instrument(span)
331 .await
332}
333
334async fn process_content(
335 content_vc: ResolvedVc<FileContent>,
336 code: String,
337 fs_path_vc: ResolvedVc<FileSystemPath>,
338 filename: &str,
339 source: ResolvedVc<Box<dyn Source>>,
340 origin: ResolvedVc<Box<dyn ResolveOrigin>>,
341 import_context: Option<ResolvedVc<ImportContext>>,
342 ty: CssModuleAssetType,
343) -> Result<Vc<ParseCssResult>> {
344 #[allow(clippy::needless_lifetimes)]
345 fn without_warnings<'o, 'i>(config: ParserOptions<'o, 'i>) -> ParserOptions<'o, 'static> {
346 ParserOptions {
347 filename: config.filename,
348 css_modules: config.css_modules,
349 source_index: config.source_index,
350 error_recovery: config.error_recovery,
351 warnings: None,
352 flags: config.flags,
353 }
354 }
355
356 let config = ParserOptions {
357 css_modules: match ty {
358 CssModuleAssetType::Module => Some(lightningcss::css_modules::Config {
359 pattern: Pattern {
360 segments: smallvec![
361 Segment::Name,
362 Segment::Literal("__"),
363 Segment::Hash,
364 Segment::Literal("__"),
365 Segment::Local,
366 ],
367 },
368 dashed_idents: false,
369 grid: false,
370 container: false,
371 ..Default::default()
372 }),
373
374 _ => None,
375 },
376 filename: filename.to_string(),
377 error_recovery: true,
378 ..Default::default()
379 };
380
381 let stylesheet = StyleSheetLike({
382 let warnings: Arc<RwLock<_>> = Default::default();
383
384 match StyleSheet::parse(
385 &code,
386 ParserOptions {
387 warnings: Some(warnings.clone()),
388 ..config.clone()
389 },
390 ) {
391 Ok(mut ss) => {
392 if matches!(ty, CssModuleAssetType::Module) {
393 let mut validator = CssValidator { errors: Vec::new() };
394 ss.visit(&mut validator).unwrap();
395
396 for err in validator.errors {
397 err.report(fs_path_vc);
398 }
399 }
400
401 let warngins = warnings.read().unwrap().iter().cloned().collect::<Vec<_>>();
404 for err in warngins.iter() {
405 match err.kind {
406 lightningcss::error::ParserError::UnexpectedToken(_)
407 | lightningcss::error::ParserError::UnexpectedImportRule
408 | lightningcss::error::ParserError::SelectorError(..)
409 | lightningcss::error::ParserError::EndOfInput => {
410 let source = match &err.loc {
411 Some(loc) => {
412 let pos = SourcePos {
413 line: loc.line as _,
414 column: loc.column as _,
415 };
416 Some(IssueSource::from_line_col(source, pos, pos))
417 }
418 None => None,
419 };
420
421 ParsingIssue {
422 file: fs_path_vc,
423 msg: ResolvedVc::cell(err.to_string().into()),
424 source,
425 }
426 .resolved_cell()
427 .emit();
428 return Ok(ParseCssResult::Unparseable.cell());
429 }
430
431 _ => {
432 }
434 }
435 }
436
437 ss.minify(Default::default())
443 .context("failed to transform css")?;
444
445 stylesheet_into_static(&ss, without_warnings(config.clone()))
446 }
447 Err(e) => {
448 let source = match &e.loc {
449 Some(loc) => {
450 let pos = SourcePos {
451 line: loc.line as _,
452 column: loc.column as _,
453 };
454 Some(IssueSource::from_line_col(source, pos, pos))
455 }
456 None => None,
457 };
458 ParsingIssue {
459 file: fs_path_vc,
460 msg: ResolvedVc::cell(e.to_string().into()),
461 source,
462 }
463 .resolved_cell()
464 .emit();
465 return Ok(ParseCssResult::Unparseable.cell());
466 }
467 }
468 });
469
470 let config = without_warnings(config);
471 let mut stylesheet = stylesheet.to_static(config.clone());
472
473 let (references, url_references) =
474 analyze_references(&mut stylesheet, source, origin, import_context).await?;
475
476 Ok(ParseCssResult::Ok {
477 code: content_vc,
478 stylesheet,
479 references: ResolvedVc::cell(references),
480 url_references: ResolvedVc::cell(url_references),
481 options: config,
482 }
483 .cell())
484}
485
486struct CssValidator {
495 errors: Vec<CssError>,
496}
497
498#[derive(Debug, PartialEq, Eq)]
499enum CssError {
500 LightningCssSelectorInModuleNotPure { selector: String },
501}
502
503impl CssError {
504 fn report(self, file: ResolvedVc<FileSystemPath>) {
505 match self {
506 CssError::LightningCssSelectorInModuleNotPure { selector } => {
507 ParsingIssue {
508 file,
509 msg: ResolvedVc::cell(
510 format!("{CSS_MODULE_ERROR}, (lightningcss, {selector})").into(),
511 ),
512 source: None,
513 }
514 .resolved_cell()
515 .emit();
516 }
517 }
518 }
519}
520
521const CSS_MODULE_ERROR: &str =
522 "Selector is not pure (pure selectors must contain at least one local class or id)";
523
524impl lightningcss::visitor::Visitor<'_> for CssValidator {
526 type Error = ();
527
528 fn visit_types(&self) -> lightningcss::visitor::VisitTypes {
529 visit_types!(SELECTORS)
530 }
531
532 fn visit_selector(
533 &mut self,
534 selector: &mut lightningcss::selector::Selector<'_>,
535 ) -> Result<(), Self::Error> {
536 fn is_selector_problematic(sel: &lightningcss::selector::Selector) -> bool {
537 sel.iter_raw_parse_order_from(0).all(is_problematic)
538 }
539
540 fn is_problematic(c: &lightningcss::selector::Component) -> bool {
541 match c {
542 parcel_selectors::parser::Component::ID(..)
543 | parcel_selectors::parser::Component::Class(..) => false,
544
545 parcel_selectors::parser::Component::Combinator(..)
546 | parcel_selectors::parser::Component::AttributeOther(..)
547 | parcel_selectors::parser::Component::AttributeInNoNamespaceExists { .. }
548 | parcel_selectors::parser::Component::AttributeInNoNamespace { .. }
549 | parcel_selectors::parser::Component::ExplicitUniversalType
550 | parcel_selectors::parser::Component::Negation(..) => true,
551
552 parcel_selectors::parser::Component::Where(sel) => {
553 sel.iter().all(is_selector_problematic)
554 }
555
556 parcel_selectors::parser::Component::LocalName(local) => {
557 !matches!(&*local.name.0, "html" | "body")
559 }
560 _ => false,
561 }
562 }
563
564 if is_selector_problematic(selector) {
565 self.errors
566 .push(CssError::LightningCssSelectorInModuleNotPure {
567 selector: format!("{selector:?}"),
568 });
569 }
570
571 Ok(())
572 }
573}
574
575fn generate_css_source_map(source_map: &parcel_sourcemap::SourceMap) -> Result<Rope> {
576 let mut builder = SourceMapBuilder::new(None);
577
578 for src in source_map.get_sources() {
579 builder.add_source(&format!("{SOURCE_URL_PROTOCOL}///{src}"));
580 }
581
582 for (idx, content) in source_map.get_sources_content().iter().enumerate() {
583 builder.set_source_contents(idx as _, Some(content));
584 }
585
586 for m in source_map.get_mappings() {
587 builder.add_raw(
588 m.generated_line,
589 m.generated_column,
590 m.original.map(|v| v.original_line).unwrap_or_default(),
591 m.original.map(|v| v.original_column).unwrap_or_default(),
592 Some(0),
593 None,
594 false,
595 );
596 }
597
598 let mut map = builder.into_sourcemap();
599 add_default_ignore_list(&mut map);
600 let mut result = vec![];
601 map.to_writer(&mut result)?;
602 Ok(Rope::from(result))
603}
604
605#[turbo_tasks::value]
606struct ParsingIssue {
607 msg: ResolvedVc<RcStr>,
608 file: ResolvedVc<FileSystemPath>,
609 source: Option<IssueSource>,
610}
611
612#[turbo_tasks::value_impl]
613impl Issue for ParsingIssue {
614 #[turbo_tasks::function]
615 fn file_path(&self) -> Vc<FileSystemPath> {
616 *self.file
617 }
618
619 #[turbo_tasks::function]
620 fn stage(&self) -> Vc<IssueStage> {
621 IssueStage::Parse.cell()
622 }
623
624 #[turbo_tasks::function]
625 fn title(&self) -> Vc<StyledString> {
626 StyledString::Text("Parsing css source code failed".into()).cell()
627 }
628
629 #[turbo_tasks::function]
630 async fn source(&self) -> Result<Vc<OptionIssueSource>> {
631 Ok(Vc::cell(match &self.source {
632 Some(s) => Some(s.resolve_source_map().await?.into_owned()),
633 None => None,
634 }))
635 }
636
637 #[turbo_tasks::function]
638 async fn description(&self) -> Result<Vc<OptionStyledString>> {
639 Ok(Vc::cell(Some(
640 StyledString::Text(self.msg.await?.as_str().into()).resolved_cell(),
641 )))
642 }
643}
644
645#[cfg(test)]
646mod tests {
647 use lightningcss::{
648 css_modules::Pattern,
649 stylesheet::{ParserOptions, StyleSheet},
650 visitor::Visit,
651 };
652
653 use super::{CssError, CssValidator};
654
655 fn lint_lightningcss(code: &str) -> Vec<CssError> {
656 let mut ss = StyleSheet::parse(
657 code,
658 ParserOptions {
659 css_modules: Some(lightningcss::css_modules::Config {
660 pattern: Pattern::default(),
661 dashed_idents: false,
662 grid: false,
663 container: false,
664 ..Default::default()
665 }),
666 ..Default::default()
667 },
668 )
669 .unwrap();
670
671 let mut validator = CssValidator { errors: Vec::new() };
672 ss.visit(&mut validator).unwrap();
673
674 validator.errors
675 }
676
677 #[track_caller]
678 fn assert_lint_success(code: &str) {
679 assert_eq!(lint_lightningcss(code), vec![], "lightningcss: {code}");
680 }
681
682 #[track_caller]
683 fn assert_lint_failure(code: &str) {
684 assert_ne!(lint_lightningcss(code), vec![], "lightningcss: {code}");
685 }
686
687 #[test]
688 fn css_module_pure_lint() {
689 assert_lint_success(
690 "html {
691 --foo: 1;
692 }",
693 );
694
695 assert_lint_success(
696 "#id {
697 color: red;
698 }",
699 );
700
701 assert_lint_success(
702 ".class {
703 color: red;
704 }",
705 );
706
707 assert_lint_success(
708 "html.class {
709 color: red;
710 }",
711 );
712
713 assert_lint_success(
714 ".class > * {
715 color: red;
716 }",
717 );
718
719 assert_lint_success(
720 ".class * {
721 color: red;
722 }",
723 );
724
725 assert_lint_success(
726 ":where(.main > *) {
727 color: red;
728 }",
729 );
730
731 assert_lint_success(
732 ":where(.main > *, .root > *) {
733 color: red;
734 }",
735 );
736 assert_lint_success(
737 ".style {
738 background-image: var(--foo);
739 }",
740 );
741
742 assert_lint_failure(
743 "div {
744 color: red;
745 }",
746 );
747
748 assert_lint_failure(
749 "div > span {
750 color: red;
751 }",
752 );
753
754 assert_lint_failure(
755 "div span {
756 color: red;
757 }",
758 );
759
760 assert_lint_failure(
761 "div[data-foo] {
762 color: red;
763 }",
764 );
765
766 assert_lint_failure(
767 "div[data-foo=\"bar\"] {
768 color: red;
769 }",
770 );
771
772 assert_lint_failure(
773 "div[data-foo=\"bar\"] span {
774 color: red;
775 }",
776 );
777
778 assert_lint_failure(
779 "* {
780 --foo: 1;
781 }",
782 );
783
784 assert_lint_failure(
785 "[data-foo] {
786 --foo: 1;
787 }",
788 );
789
790 assert_lint_failure(
791 ":not(.class) {
792 --foo: 1;
793 }",
794 );
795
796 assert_lint_failure(
797 ":not(div) {
798 --foo: 1;
799 }",
800 );
801
802 assert_lint_failure(
803 ":where(div > *) {
804 color: red;
805 }",
806 );
807
808 assert_lint_failure(
809 ":where(div) {
810 color: red;
811 }",
812 );
813 }
814}