rspack_loader_lightningcss/
lib.rs

1use std::{
2  borrow::Cow,
3  sync::{Arc, RwLock},
4};
5
6use config::Config;
7use derive_more::Debug;
8pub use lightningcss;
9use lightningcss::{
10  printer::{PrinterOptions, PseudoClasses},
11  stylesheet::{MinifyOptions, ParserFlags, ParserOptions, StyleSheet},
12  targets::{Features, Targets},
13  traits::IntoOwned,
14};
15use rspack_cacheable::{cacheable, cacheable_dyn, with::Skip};
16use rspack_core::{
17  Loader, LoaderContext, RunnerContext,
18  rspack_sources::{Mapping, OriginalLocation, SourceMap, encode_mappings},
19};
20use rspack_error::{Result, ToStringResultToRspackResultExt};
21use rspack_loader_runner::Identifier;
22use tokio::sync::Mutex;
23
24pub mod config;
25mod plugin;
26
27pub use plugin::LightningcssLoaderPlugin;
28
29pub const LIGHTNINGCSS_LOADER_IDENTIFIER: &str = "builtin:lightningcss-loader";
30
31pub type LightningcssLoaderVisitor = Box<dyn Send + Fn(&mut StyleSheet<'static, 'static>)>;
32
33#[cacheable]
34#[derive(Debug)]
35pub struct LightningCssLoader {
36  id: Identifier,
37  #[debug(skip)]
38  #[cacheable(with=Skip)]
39  visitors: Option<Mutex<Vec<LightningcssLoaderVisitor>>>,
40  config: Config,
41}
42
43impl LightningCssLoader {
44  pub fn new(
45    visitors: Option<Vec<LightningcssLoaderVisitor>>,
46    config: Config,
47    ident: &str,
48  ) -> Self {
49    Self {
50      id: ident.into(),
51      visitors: visitors.map(|v| Mutex::new(v)),
52      config,
53    }
54  }
55
56  async fn loader_impl(&self, loader_context: &mut LoaderContext<RunnerContext>) -> Result<()> {
57    let Some(resource_path) = loader_context.resource_path() else {
58      return Ok(());
59    };
60
61    let filename = resource_path.as_str().to_string();
62
63    let Some(content) = loader_context.take_content() else {
64      return Ok(());
65    };
66
67    let content_str = match &content {
68      rspack_core::Content::String(s) => Cow::Borrowed(s.as_str()),
69      rspack_core::Content::Buffer(buf) => String::from_utf8_lossy(buf),
70    };
71
72    let mut parser_flags = ParserFlags::empty();
73    parser_flags.set(
74      ParserFlags::CUSTOM_MEDIA,
75      matches!(&self.config.draft, Some(draft) if draft.custom_media),
76    );
77    parser_flags.set(
78      ParserFlags::DEEP_SELECTOR_COMBINATOR,
79      matches!(&self.config.non_standard, Some(non_standard) if non_standard.deep_selector_combinator),
80    );
81
82    let error_recovery = self.config.error_recovery.unwrap_or(true);
83    let warnings = if error_recovery {
84      Some(Arc::new(RwLock::new(Vec::new())))
85    } else {
86      None
87    };
88
89    let option = ParserOptions {
90      filename: filename.clone(),
91      css_modules: None,
92      source_index: 0,
93      error_recovery,
94      warnings: warnings.clone(),
95      flags: parser_flags,
96    };
97    let stylesheet = StyleSheet::parse(&content_str, option.clone()).to_rspack_result()?;
98    // FIXME: Disable the warnings for now, cause it cause too much positive-negative warnings,
99    // enable when we have a better way to handle it.
100
101    // if let Some(warnings) = warnings {
102    //   #[allow(clippy::unwrap_used)]
103    //   let warnings = warnings.read().unwrap();
104    //   for warning in warnings.iter() {
105    //     if matches!(
106    //       warning.kind,
107    //       lightningcss::error::ParserError::SelectorError(
108    //         lightningcss::error::SelectorError::UnsupportedPseudoClass(_)
109    //       ) | lightningcss::error::ParserError::SelectorError(
110    //         lightningcss::error::SelectorError::UnsupportedPseudoElement(_)
111    //       )
112    //     ) {
113    //       // ignore parsing errors on pseudo class from lightningcss-loader
114    //       // to allow pseudo class in CSS modules and Vue.
115    //       continue;
116    //     }
117    //     loader_context.emit_diagnostic(Diagnostic::warn(
118    //       "builtin:lightningcss-loader".to_string(),
119    //       format!("LightningCSS parse warning: {}", warning),
120    //     ));
121    //   }
122    // }
123
124    let mut stylesheet = to_static(
125      stylesheet,
126      ParserOptions {
127        filename: filename.clone(),
128        css_modules: None,
129        source_index: 0,
130        error_recovery: true,
131        warnings: None,
132        flags: ParserFlags::empty(),
133      },
134    );
135
136    if let Some(visitors) = &self.visitors {
137      let visitors = visitors.lock().await;
138      for v in visitors.iter() {
139        v(&mut stylesheet);
140      }
141    }
142
143    let targets = Targets {
144      browsers: self.config.targets,
145      include: self
146        .config
147        .include
148        .as_ref()
149        .map(|include| Features::from_bits_truncate(*include))
150        .unwrap_or(Features::empty()),
151      exclude: self
152        .config
153        .exclude
154        .as_ref()
155        .map(|exclude| Features::from_bits_truncate(*exclude))
156        .unwrap_or(Features::empty()),
157    };
158
159    let unused_symbols = self
160      .config
161      .unused_symbols
162      .clone()
163      .map(|unused_symbols| unused_symbols.into_iter().collect())
164      .unwrap_or_default();
165
166    stylesheet
167      .minify(MinifyOptions {
168        targets,
169        unused_symbols,
170      })
171      .to_rspack_result()?;
172
173    let enable_sourcemap = loader_context.context.module_source_map_kind.enabled();
174
175    let mut source_map = loader_context
176      .source_map()
177      .map(|input_source_map| -> Result<_> {
178        let mut sm = parcel_sourcemap::SourceMap::new(
179          input_source_map
180            .source_root()
181            .unwrap_or(&loader_context.context.options.context),
182        );
183        sm.add_source(&filename);
184        sm.set_source_content(0, &content_str).to_rspack_result()?;
185        Ok(sm)
186      })
187      .transpose()?
188      .unwrap_or_else(|| {
189        let mut source_map =
190          parcel_sourcemap::SourceMap::new(&loader_context.context.options.context);
191        let source_idx = source_map.add_source(&filename);
192        source_map
193          .set_source_content(source_idx as usize, &content_str)
194          .expect("should set source content");
195        source_map
196      });
197
198    let content = stylesheet
199      .to_css(PrinterOptions {
200        minify: self.config.minify.unwrap_or(false),
201        source_map: if enable_sourcemap {
202          Some(&mut source_map)
203        } else {
204          None
205        },
206        project_root: None,
207        targets,
208        analyze_dependencies: None,
209        pseudo_classes: self
210          .config
211          .pseudo_classes
212          .as_ref()
213          .map(|pseudo_classes| PseudoClasses {
214            hover: pseudo_classes.hover.as_deref(),
215            active: pseudo_classes.active.as_deref(),
216            focus: pseudo_classes.focus.as_deref(),
217            focus_visible: pseudo_classes.focus_visible.as_deref(),
218            focus_within: pseudo_classes.focus_within.as_deref(),
219          }),
220      })
221      .to_rspack_result_with_message(|e| format!("failed to generate css: {e}"))?;
222
223    if enable_sourcemap {
224      let mappings = encode_mappings(source_map.get_mappings().iter().map(|mapping| Mapping {
225        generated_line: mapping.generated_line,
226        generated_column: mapping.generated_column,
227        original: mapping.original.map(|original| OriginalLocation {
228          source_index: original.source,
229          original_line: original.original_line,
230          original_column: original.original_column,
231          name_index: original.name,
232        }),
233      }));
234      let rspack_source_map = SourceMap::new(
235        mappings,
236        source_map
237          .get_sources()
238          .iter()
239          .map(ToString::to_string)
240          .collect::<Vec<_>>(),
241        source_map
242          .get_sources_content()
243          .iter()
244          .map(ToString::to_string)
245          .collect::<Vec<_>>(),
246        source_map
247          .get_names()
248          .iter()
249          .map(ToString::to_string)
250          .collect::<Vec<_>>(),
251      );
252      loader_context.finish_with((content.code, rspack_source_map));
253    } else {
254      loader_context.finish_with(content.code);
255    }
256
257    Ok(())
258  }
259}
260
261#[cacheable_dyn]
262#[async_trait::async_trait]
263impl Loader<RunnerContext> for LightningCssLoader {
264  fn identifier(&self) -> rspack_loader_runner::Identifier {
265    self.id
266  }
267
268  #[tracing::instrument("loader:lightningcss", skip_all, fields(
269    perfetto.track_name = "loader:lightningcss",
270    perfetto.process_name = "Loader Analysis",
271    resource =loader_context.resource(),
272  ))]
273  async fn run(&self, loader_context: &mut LoaderContext<RunnerContext>) -> Result<()> {
274    // for better diagnostic, as async_trait macro don't show beautiful error message
275    self.loader_impl(loader_context).await
276  }
277}
278
279pub fn to_static(
280  stylesheet: StyleSheet,
281  options: ParserOptions<'static, 'static>,
282) -> StyleSheet<'static, 'static> {
283  let sources = stylesheet.sources.clone();
284  let rules = stylesheet.rules.clone().into_owned();
285
286  StyleSheet::new(sources, rules, options)
287}