rspack_loader_lightningcss/
lib.rs1use 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 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 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}