rspack_loader_lightningcss/
lib.rs1use std::{
2 borrow::Cow,
3 sync::{Arc, RwLock},
4};
5
6use config::Config;
7use cow_utils::CowUtils;
8use derive_more::Debug;
9pub use lightningcss;
10use lightningcss::{
11 printer::{PrinterOptions, PseudoClasses},
12 stylesheet::{MinifyOptions, ParserFlags, ParserOptions, StyleSheet},
13 targets::{Features, Targets},
14 traits::IntoOwned,
15};
16use rspack_cacheable::{cacheable, cacheable_dyn, with::Skip};
17use rspack_core::{
18 Loader, LoaderContext, RunnerContext,
19 rspack_sources::{
20 MapOptions, Mapping, ObjectPool, OriginalLocation, Source, SourceMap, SourceMapSource,
21 SourceMapSourceOptions, encode_mappings,
22 },
23};
24use rspack_error::{Result, ToStringResultToRspackResultExt};
25use rspack_loader_runner::Identifier;
26use tokio::sync::Mutex;
27
28pub mod config;
29mod plugin;
30
31pub use plugin::LightningcssLoaderPlugin;
32
33pub const LIGHTNINGCSS_LOADER_IDENTIFIER: &str = "builtin:lightningcss-loader";
34
35pub type LightningcssLoaderVisitor = Box<dyn Send + Fn(&mut StyleSheet<'static, 'static>)>;
36
37#[cacheable]
38#[derive(Debug)]
39pub struct LightningCssLoader {
40 id: Identifier,
41 #[debug(skip)]
42 #[cacheable(with=Skip)]
43 visitors: Option<Mutex<Vec<LightningcssLoaderVisitor>>>,
44 config: Config,
45}
46
47impl LightningCssLoader {
48 pub fn new(
49 visitors: Option<Vec<LightningcssLoaderVisitor>>,
50 config: Config,
51 ident: &str,
52 ) -> Self {
53 Self {
54 id: ident.into(),
55 visitors: visitors.map(|v| Mutex::new(v)),
56 config,
57 }
58 }
59
60 async fn loader_impl(&self, loader_context: &mut LoaderContext<RunnerContext>) -> Result<()> {
61 let Some(resource_path) = loader_context.resource_path() else {
62 return Ok(());
63 };
64
65 let filename = resource_path.as_str().to_string();
66
67 let Some(content) = loader_context.take_content() else {
68 return Ok(());
69 };
70
71 let content_str = match &content {
72 rspack_core::Content::String(s) => Cow::Borrowed(s.as_str()),
73 rspack_core::Content::Buffer(buf) => String::from_utf8_lossy(buf),
74 };
75
76 let mut parser_flags = ParserFlags::empty();
77 parser_flags.set(
78 ParserFlags::CUSTOM_MEDIA,
79 matches!(&self.config.drafts, Some(drafts) if drafts.custom_media),
80 );
81 parser_flags.set(
82 ParserFlags::DEEP_SELECTOR_COMBINATOR,
83 matches!(&self.config.non_standard, Some(non_standard) if non_standard.deep_selector_combinator),
84 );
85
86 let error_recovery = self.config.error_recovery.unwrap_or(true);
87 let warnings = if error_recovery {
88 Some(Arc::new(RwLock::new(Vec::new())))
89 } else {
90 None
91 };
92
93 let option = ParserOptions {
94 filename: filename.clone(),
95 css_modules: None,
96 source_index: 0,
97 error_recovery,
98 warnings: warnings.clone(),
99 flags: parser_flags,
100 };
101 let stylesheet = StyleSheet::parse(&content_str, option.clone()).to_rspack_result()?;
102 let mut stylesheet = to_static(
129 stylesheet,
130 ParserOptions {
131 filename: filename.clone(),
132 css_modules: None,
133 source_index: 0,
134 error_recovery: true,
135 warnings: None,
136 flags: ParserFlags::empty(),
137 },
138 );
139
140 if let Some(visitors) = &self.visitors {
141 let visitors = visitors.lock().await;
142 for v in visitors.iter() {
143 v(&mut stylesheet);
144 }
145 }
146
147 let targets = Targets {
148 browsers: self.config.targets,
149 include: self
150 .config
151 .include
152 .as_ref()
153 .map_or(Features::empty(), |include| {
154 Features::from_bits_truncate(*include)
155 }),
156 exclude: self
157 .config
158 .exclude
159 .as_ref()
160 .map_or(Features::empty(), |exclude| {
161 Features::from_bits_truncate(*exclude)
162 }),
163 };
164
165 let unused_symbols = self
166 .config
167 .unused_symbols
168 .clone()
169 .map(|unused_symbols| unused_symbols.into_iter().collect())
170 .unwrap_or_default();
171
172 stylesheet
173 .minify(MinifyOptions {
174 targets,
175 unused_symbols,
176 })
177 .to_rspack_result()?;
178
179 let mut parcel_source_map = if loader_context.context.source_map_kind.enabled() {
180 let mut sm = parcel_sourcemap::SourceMap::new(&loader_context.context.options.context);
181 sm.add_source(&filename);
182 sm.set_source_content(0, &content_str).to_rspack_result()?;
183 Some(sm)
184 } else {
185 None
186 };
187
188 let content = stylesheet
189 .to_css(PrinterOptions {
190 minify: self.config.minify.unwrap_or(false),
191 source_map: parcel_source_map.as_mut(),
192 project_root: None,
193 targets,
194 analyze_dependencies: None,
195 pseudo_classes: self
196 .config
197 .pseudo_classes
198 .as_ref()
199 .map(|pseudo_classes| PseudoClasses {
200 hover: pseudo_classes.hover.as_deref(),
201 active: pseudo_classes.active.as_deref(),
202 focus: pseudo_classes.focus.as_deref(),
203 focus_visible: pseudo_classes.focus_visible.as_deref(),
204 focus_within: pseudo_classes.focus_within.as_deref(),
205 }),
206 })
207 .to_rspack_result_with_message(|e| format!("failed to generate css: {e}"))?;
208
209 if let Some(parcel_source_map) = parcel_source_map {
210 let mappings = encode_mappings(parcel_source_map.get_mappings().iter().map(|mapping| {
211 Mapping {
213 generated_line: mapping.generated_line + 1,
214 generated_column: mapping.generated_column,
215 original: mapping.original.map(|original| OriginalLocation {
216 source_index: original.source,
217 original_line: original.original_line + 1,
218 original_column: original.original_column,
219 name_index: original.name,
220 }),
221 }
222 }));
223
224 let mut posix_context = loader_context
225 .context
226 .options
227 .context
228 .cow_replace("\\", "/");
229 if !posix_context.ends_with('/') {
230 posix_context.to_mut().push('/');
231 }
232 let posix_context = posix_context.into_owned();
233
234 let rspack_source_map = SourceMap::new(
235 mappings,
236 parcel_source_map
239 .get_sources()
240 .iter()
241 .map(|source| {
242 if source.starts_with('/') || source.contains(':') {
243 source.clone()
244 } else {
245 let mut absolute_source = String::with_capacity(posix_context.len() + source.len());
246 absolute_source.push_str(&posix_context);
247 absolute_source.push_str(source);
248 absolute_source
249 }
250 })
251 .collect::<Vec<_>>(),
252 parcel_source_map
253 .get_sources_content()
254 .iter()
255 .map(|source_content| Arc::from(source_content.clone()))
256 .collect::<Vec<_>>(),
257 parcel_source_map
258 .get_names()
259 .iter()
260 .map(ToString::to_string)
261 .collect::<Vec<_>>(),
262 );
263
264 let posix_name = filename.cow_replace("\\", "/");
265 let source_map_source = SourceMapSource::new(SourceMapSourceOptions {
266 value: content.code.clone(),
267 name: posix_name,
268 source_map: rspack_source_map,
269 original_source: None,
270 inner_source_map: loader_context.take_source_map(),
271 remove_original_source: false,
272 });
273 let source_map = source_map_source.map(&ObjectPool::default(), &MapOptions::default());
274 loader_context.finish_with((content.code, source_map));
275 } else {
276 loader_context.finish_with(content.code);
277 }
278
279 Ok(())
280 }
281}
282
283#[cacheable_dyn]
284#[async_trait::async_trait]
285impl Loader<RunnerContext> for LightningCssLoader {
286 fn identifier(&self) -> rspack_loader_runner::Identifier {
287 self.id
288 }
289
290 #[tracing::instrument("loader:lightningcss", skip_all, fields(
291 perfetto.track_name = "loader:lightningcss",
292 perfetto.process_name = "Loader Analysis",
293 resource =loader_context.resource(),
294 ))]
295 async fn run(&self, loader_context: &mut LoaderContext<RunnerContext>) -> Result<()> {
296 self.loader_impl(loader_context).await
298 }
299}
300
301pub fn to_static(
302 stylesheet: StyleSheet,
303 options: ParserOptions<'static, 'static>,
304) -> StyleSheet<'static, 'static> {
305 let sources = stylesheet.sources.clone();
306 let rules = stylesheet.rules.clone().into_owned();
307
308 StyleSheet::new(sources, rules, options)
309}