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 drafts: 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.drafts.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.drafts, Some(drafts) if drafts.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.clone(),
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_or(Features::empty(), |include| Features::from_bits_truncate(*include)),
203 exclude: minimizer_options
204 .exclude
205 .as_ref()
206 .map_or(Features::empty(), |exclude| Features::from_bits_truncate(*exclude)),
207 };
208 let mut unused_symbols = HashSet::from_iter(minimizer_options.unused_symbols.clone());
209 if self.options.remove_unused_local_idents
210 && let Some(css_unused_idents) = original.info.css_unused_idents.take()
211 {
212 unused_symbols.extend(css_unused_idents.into_iter().map(String::from));
213 }
214 stylesheet
215 .minify(MinifyOptions {
216 targets,
217 unused_symbols,
218 })
219 .to_rspack_result()?;
220 stylesheet
242 .to_css(PrinterOptions {
243 minify: true,
244 source_map: source_map.as_mut(),
245 project_root: None,
246 targets,
247 analyze_dependencies: None,
248 pseudo_classes: minimizer_options.pseudo_classes
249 .as_ref()
250 .map(|pseudo_classes| lightningcss::stylesheet::PseudoClasses {
251 hover: pseudo_classes.hover.as_deref(),
252 active: pseudo_classes.active.as_deref(),
253 focus: pseudo_classes.focus.as_deref(),
254 focus_visible: pseudo_classes.focus_visible.as_deref(),
255 focus_within: pseudo_classes.focus_within.as_deref(),
256 }),
257 })
258 .to_rspack_result()?
259 };
260
261 let minimized_source = if let Some(mut source_map) = source_map {
262 SourceMapSource::new(SourceMapSourceOptions {
263 value: result.code,
264 name: filename,
265 source_map: SourceMap::from_json(
266 &source_map
267 .to_json(None)
268 .to_rspack_result()?,
269 )
270 .expect("should be able to generate source-map"),
271 original_source: Some(Arc::from(input)),
272 inner_source_map: input_source_map,
273 remove_original_source: true,
274 })
275 .boxed()
276 } else {
277 RawStringSource::from(result.code).boxed()
278 };
279
280 original.set_source(Some(minimized_source));
281 }
282 original.get_info_mut().minimized.replace(true);
283 Ok(())
284 }).map_err(MinifyError)?;
285
286 compilation.extend_diagnostics(all_warnings.into_inner().expect("should lock"));
287
288 Ok(())
289}
290
291impl Plugin for LightningCssMinimizerRspackPlugin {
292 fn name(&self) -> &'static str {
293 "rspack.LightningCssMinimizerRspackPlugin"
294 }
295
296 fn apply(&self, ctx: &mut rspack_core::ApplyContext<'_>) -> Result<()> {
297 ctx.compilation_hooks.chunk_hash.tap(chunk_hash::new(self));
298 ctx
299 .compilation_hooks
300 .process_assets
301 .tap(process_assets::new(self));
302 Ok(())
303 }
304}