rspack_plugin_lightning_css_minimizer/
lib.rs1use std::{
2 collections::HashSet,
3 hash::Hash,
4 sync::{Arc, LazyLock, RwLock},
5};
6
7pub use lightningcss::targets::Browsers;
8use lightningcss::{
9 printer::PrinterOptions,
10 stylesheet::{MinifyOptions, ParserFlags, ParserOptions, StyleSheet},
11 targets::{Features, Targets},
12};
13use rayon::prelude::*;
14use regex::Regex;
15use rspack_core::{
16 ChunkUkey, Compilation, CompilationChunkHash, CompilationProcessAssets, Plugin,
17 diagnostics::MinifyError,
18 rspack_sources::{
19 MapOptions, ObjectPool, RawStringSource, SourceExt, SourceMap, SourceMapSource,
20 SourceMapSourceOptions,
21 },
22};
23use rspack_error::{Diagnostic, Result, ToStringResultToRspackResultExt};
24use rspack_hash::RspackHash;
25use rspack_hook::{plugin, plugin_hook};
26use rspack_util::asset_condition::{AssetConditions, AssetConditionsObject, match_object};
27use thread_local::ThreadLocal;
28
29static CSS_ASSET_REGEXP: LazyLock<Regex> =
30 LazyLock::new(|| Regex::new(r"\.css(\?.*)?$").expect("Invalid RegExp"));
31
32#[derive(Debug, Hash)]
33pub struct PluginOptions {
34 pub test: Option<AssetConditions>,
35 pub include: Option<AssetConditions>,
36 pub exclude: Option<AssetConditions>,
37 pub remove_unused_local_idents: bool,
38 pub minimizer_options: MinimizerOptions,
39}
40
41#[derive(Debug, Hash)]
42pub struct Draft {
43 pub custom_media: bool,
44}
45
46#[derive(Debug, Hash)]
47pub struct NonStandard {
48 pub deep_selector_combinator: bool,
49}
50
51#[derive(Debug, Hash)]
52pub struct PseudoClasses {
53 pub hover: Option<String>,
54 pub active: Option<String>,
55 pub focus: Option<String>,
56 pub focus_visible: Option<String>,
57 pub focus_within: Option<String>,
58}
59
60#[derive(Debug)]
61pub struct MinimizerOptions {
62 pub error_recovery: bool,
63 pub targets: Option<Browsers>,
64 pub include: Option<u32>,
65 pub exclude: Option<u32>,
66 pub draft: Option<Draft>,
67 pub non_standard: Option<NonStandard>,
68 pub pseudo_classes: Option<PseudoClasses>,
69 pub unused_symbols: Vec<String>,
70}
71
72impl Hash for MinimizerOptions {
73 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
74 self.error_recovery.hash(state);
75 self.include.hash(state);
76 self.exclude.hash(state);
77 self.draft.hash(state);
78 self.non_standard.hash(state);
79 self.unused_symbols.hash(state);
80 if let Some(pseudo_classes) = &self.pseudo_classes {
81 pseudo_classes.hover.hash(state);
82 pseudo_classes.active.hash(state);
83 pseudo_classes.focus.hash(state);
84 pseudo_classes.focus_visible.hash(state);
85 pseudo_classes.focus_within.hash(state);
86 }
87 if let Some(targets) = &self.targets {
88 targets.android.hash(state);
89 targets.chrome.hash(state);
90 targets.edge.hash(state);
91 targets.firefox.hash(state);
92 targets.ie.hash(state);
93 targets.ios_saf.hash(state);
94 targets.opera.hash(state);
95 targets.safari.hash(state);
96 targets.samsung.hash(state);
97 }
98 }
99}
100
101#[plugin]
102#[derive(Debug)]
103pub struct LightningCssMinimizerRspackPlugin {
104 options: PluginOptions,
105}
106
107impl LightningCssMinimizerRspackPlugin {
108 pub fn new(options: PluginOptions) -> Self {
109 Self::new_inner(options)
110 }
111}
112
113#[plugin_hook(CompilationChunkHash for LightningCssMinimizerRspackPlugin)]
114async fn chunk_hash(
115 &self,
116 _compilation: &Compilation,
117 _chunk_ukey: &ChunkUkey,
118 hasher: &mut RspackHash,
119) -> Result<()> {
120 self.options.hash(hasher);
121 Ok(())
122}
123
124#[plugin_hook(CompilationProcessAssets for LightningCssMinimizerRspackPlugin, stage = Compilation::PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE)]
125async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
126 let options = &self.options;
127 let minimizer_options = &self.options.minimizer_options;
128 let all_warnings: RwLock<Vec<Diagnostic>> = Default::default();
129 let condition_object = AssetConditionsObject {
130 test: options.test.as_ref(),
131 include: options.include.as_ref(),
132 exclude: options.exclude.as_ref(),
133 };
134
135 let tls: ThreadLocal<ObjectPool> = ThreadLocal::new();
136 compilation
137 .assets_mut()
138 .par_iter_mut()
139 .filter(|(filename, original)| {
140 if !CSS_ASSET_REGEXP.is_match(filename) {
141 return false;
142 }
143
144 let is_matched = match_object(&condition_object, filename);
145
146 if !is_matched || original.get_info().minimized.unwrap_or(false) {
147 return false;
148 }
149
150 true
151 })
152 .try_for_each(|(filename, original)| -> Result<()> {
153 if original.get_info().minimized.unwrap_or(false) {
154 return Ok(());
155 }
156
157 if let Some(original_source) = original.get_source() {
158 let input = original_source.source().into_string_lossy().into_owned();
159 let object_pool = tls.get_or(ObjectPool::default);
160 let input_source_map = original_source.map(object_pool, &MapOptions::default());
161
162 let mut parser_flags = ParserFlags::empty();
163 parser_flags.set(
164 ParserFlags::CUSTOM_MEDIA,
165 matches!(&minimizer_options.draft, Some(draft) if draft.custom_media),
166 );
167 parser_flags.set(
168 ParserFlags::DEEP_SELECTOR_COMBINATOR,
169 matches!(&minimizer_options.non_standard, Some(non_standard) if non_standard.deep_selector_combinator),
170 );
171
172 let mut source_map = input_source_map
173 .as_ref()
174 .map(|input_source_map| -> Result<_> {
175 let mut sm =
176 parcel_sourcemap::SourceMap::new(input_source_map.source_root().unwrap_or("/"));
177 sm.add_source(filename);
178 sm.set_source_content(0, &input).to_rspack_result()?;
179 Ok(sm)
180 })
181 .transpose()?;
182 let result = {
183 let warnings: Arc<RwLock<Vec<_>>> = Default::default();
184 let mut stylesheet = StyleSheet::parse(
185 &input,
186 ParserOptions {
187 filename: filename.to_string(),
188 css_modules: None,
189 source_index: 0,
190 error_recovery: minimizer_options.error_recovery,
191 warnings: Some(warnings.clone()),
192 flags: parser_flags,
193 },
194 )
195 .to_rspack_result()?;
196
197 let targets = Targets {
198 browsers: minimizer_options.targets,
199 include: minimizer_options
200 .include
201 .as_ref()
202 .map(|include| Features::from_bits_truncate(*include))
203 .unwrap_or(Features::empty()),
204 exclude: minimizer_options
205 .exclude
206 .as_ref()
207 .map(|exclude| Features::from_bits_truncate(*exclude))
208 .unwrap_or(Features::empty()),
209 };
210 let mut unused_symbols = HashSet::from_iter(minimizer_options.unused_symbols.clone());
211 if self.options.remove_unused_local_idents
212 && let Some(css_unused_idents) = original.info.css_unused_idents.take()
213 {
214 unused_symbols.extend(css_unused_idents);
215 }
216 stylesheet
217 .minify(MinifyOptions {
218 targets,
219 unused_symbols,
220 })
221 .to_rspack_result()?;
222 stylesheet
244 .to_css(PrinterOptions {
245 minify: true,
246 source_map: source_map.as_mut(),
247 project_root: None,
248 targets,
249 analyze_dependencies: None,
250 pseudo_classes: minimizer_options.pseudo_classes
251 .as_ref()
252 .map(|pseudo_classes| lightningcss::stylesheet::PseudoClasses {
253 hover: pseudo_classes.hover.as_deref(),
254 active: pseudo_classes.active.as_deref(),
255 focus: pseudo_classes.focus.as_deref(),
256 focus_visible: pseudo_classes.focus_visible.as_deref(),
257 focus_within: pseudo_classes.focus_within.as_deref(),
258 }),
259 })
260 .to_rspack_result()?
261 };
262
263 let minimized_source = if let Some(mut source_map) = source_map {
264 SourceMapSource::new(SourceMapSourceOptions {
265 value: result.code,
266 name: filename,
267 source_map: SourceMap::from_json(
268 &source_map
269 .to_json(None)
270 .to_rspack_result()?,
271 )
272 .expect("should be able to generate source-map"),
273 original_source: Some(Arc::from(input)),
274 inner_source_map: input_source_map,
275 remove_original_source: true,
276 })
277 .boxed()
278 } else {
279 RawStringSource::from(result.code).boxed()
280 };
281
282 original.set_source(Some(minimized_source));
283 }
284 original.get_info_mut().minimized.replace(true);
285 Ok(())
286 }).map_err(MinifyError)?;
287
288 compilation.extend_diagnostics(all_warnings.into_inner().expect("should lock"));
289
290 Ok(())
291}
292
293impl Plugin for LightningCssMinimizerRspackPlugin {
294 fn name(&self) -> &'static str {
295 "rspack.LightningCssMinimizerRspackPlugin"
296 }
297
298 fn apply(&self, ctx: &mut rspack_core::ApplyContext<'_>) -> Result<()> {
299 ctx.compilation_hooks.chunk_hash.tap(chunk_hash::new(self));
300 ctx
301 .compilation_hooks
302 .process_assets
303 .tap(process_assets::new(self));
304 Ok(())
305 }
306}