1use crate::class_scanner::ClassScanner;
7use crate::css_generator::CssGenerator;
8use crate::error::Result;
9use std::collections::{HashMap, HashSet};
10use std::path::Path;
11
12#[derive(Debug, Clone)]
14pub struct TreeShakeConfig {
15 pub enabled: bool,
17 pub remove_unused_responsive: bool,
19 pub remove_unused_conditional: bool,
21 pub remove_unused_custom: bool,
23 pub keep_classes: HashSet<String>,
25 pub remove_classes: HashSet<String>,
27 pub analyze_dependencies: bool,
29}
30
31impl Default for TreeShakeConfig {
32 fn default() -> Self {
33 Self {
34 enabled: true,
35 remove_unused_responsive: true,
36 remove_unused_conditional: true,
37 remove_unused_custom: true,
38 keep_classes: HashSet::new(),
39 remove_classes: HashSet::new(),
40 analyze_dependencies: true,
41 }
42 }
43}
44
45#[derive(Debug, Clone)]
47pub struct TreeShakeResults {
48 pub kept_classes: HashSet<String>,
50 pub removed_classes: HashSet<String>,
52 pub original_size: usize,
54 pub optimized_size: usize,
56 pub reduction_percentage: f64,
58 pub stats: TreeShakeStats,
60}
61
62#[derive(Debug, Clone)]
64pub struct TreeShakeStats {
65 pub classes_analyzed: usize,
67 pub classes_removed: usize,
69 pub responsive_removed: usize,
71 pub conditional_removed: usize,
73 pub custom_removed: usize,
75 pub processing_time_ms: u64,
77}
78
79#[derive(Debug, Clone)]
81struct RemovalStats {
82 total_removed: usize,
83 responsive_removed: usize,
84 conditional_removed: usize,
85 custom_removed: usize,
86 removed_classes: HashSet<String>,
87}
88
89#[derive(Debug, Clone)]
91pub struct TreeShaker {
92 config: TreeShakeConfig,
93 dependency_graph: HashMap<String, HashSet<String>>,
94 reverse_dependencies: HashMap<String, HashSet<String>>,
95}
96
97impl TreeShaker {
98 pub fn new() -> Self {
100 Self {
101 config: TreeShakeConfig::default(),
102 dependency_graph: HashMap::new(),
103 reverse_dependencies: HashMap::new(),
104 }
105 }
106
107 pub fn with_config(config: TreeShakeConfig) -> Self {
109 Self {
110 config,
111 dependency_graph: HashMap::new(),
112 reverse_dependencies: HashMap::new(),
113 }
114 }
115
116 pub fn shake(&mut self, source_paths: &[&Path], css_generator: &mut CssGenerator) -> Result<TreeShakeResults> {
118 let start_time = std::time::Instant::now();
119
120 if !self.config.enabled {
121 return Ok(TreeShakeResults {
122 kept_classes: css_generator.get_rules().keys().cloned().collect(),
123 removed_classes: HashSet::new(),
124 original_size: css_generator.generate_css().len(),
125 optimized_size: css_generator.generate_css().len(),
126 reduction_percentage: 0.0,
127 stats: TreeShakeStats {
128 classes_analyzed: css_generator.rule_count(),
129 classes_removed: 0,
130 responsive_removed: 0,
131 conditional_removed: 0,
132 custom_removed: 0,
133 processing_time_ms: start_time.elapsed().as_millis() as u64,
134 },
135 });
136 }
137
138 let used_classes = self.scan_used_classes(source_paths)?;
140
141 if self.config.analyze_dependencies {
143 self.build_dependency_graph(css_generator);
144 }
145
146 let classes_to_keep = self.determine_classes_to_keep(&used_classes, css_generator);
148
149 let removal_stats = self.remove_unused_classes(css_generator, &classes_to_keep);
151
152 let original_size = css_generator.generate_css().len();
154 let optimized_size = css_generator.generate_css().len();
155 let reduction_percentage = if original_size > 0 {
156 ((original_size - optimized_size) as f64 / original_size as f64) * 100.0
157 } else {
158 0.0
159 };
160
161 let stats = TreeShakeStats {
162 classes_analyzed: css_generator.rule_count() + removal_stats.total_removed,
163 classes_removed: removal_stats.total_removed,
164 responsive_removed: removal_stats.responsive_removed,
165 conditional_removed: removal_stats.conditional_removed,
166 custom_removed: removal_stats.custom_removed,
167 processing_time_ms: start_time.elapsed().as_millis() as u64,
168 };
169
170 Ok(TreeShakeResults {
171 kept_classes: classes_to_keep,
172 removed_classes: removal_stats.removed_classes,
173 original_size,
174 optimized_size,
175 reduction_percentage,
176 stats,
177 })
178 }
179
180 fn scan_used_classes(&self, source_paths: &[&Path]) -> Result<HashSet<String>> {
182 let mut scanner = ClassScanner::new();
183 let mut all_used_classes = HashSet::new();
184
185 for path in source_paths {
186 let results = scanner.scan_directory(path)?;
187 all_used_classes.extend(results.classes);
188 }
189
190 Ok(all_used_classes)
191 }
192
193 fn build_dependency_graph(&mut self, css_generator: &CssGenerator) {
195 self.dependency_graph.clear();
196 self.reverse_dependencies.clear();
197
198 for (class_name, rule) in css_generator.get_rules() {
200 let mut dependencies = HashSet::new();
201
202 for property in &rule.properties {
204 if let Some(dep_class) = self.extract_class_dependency(&property.value) {
205 dependencies.insert(dep_class);
206 }
207 }
208
209 if !dependencies.is_empty() {
210 self.dependency_graph.insert(class_name.clone(), dependencies);
211 }
212 }
213
214 for (class, deps) in &self.dependency_graph {
216 for dep in deps {
217 self.reverse_dependencies
218 .entry(dep.clone())
219 .or_default()
220 .insert(class.clone());
221 }
222 }
223 }
224
225 fn extract_class_dependency(&self, value: &str) -> Option<String> {
227 if value.contains("var(--") {
230 if let Some(start) = value.find("var(--") {
232 if let Some(end) = value[start..].find(')') {
233 let var_name = &value[start + 6..start + end];
234 return Some(format!("--{}", var_name));
235 }
236 }
237 }
238 None
239 }
240
241 fn determine_classes_to_keep(&self, used_classes: &HashSet<String>, _css_generator: &CssGenerator) -> HashSet<String> {
243 let mut classes_to_keep = HashSet::new();
244
245 classes_to_keep.extend(used_classes.iter().cloned());
247
248 classes_to_keep.extend(self.config.keep_classes.iter().cloned());
250
251 for class in &self.config.remove_classes {
253 classes_to_keep.remove(class);
254 }
255
256 if self.config.analyze_dependencies {
258 let mut to_process: Vec<String> = classes_to_keep.iter().cloned().collect();
259 let mut processed = HashSet::new();
260
261 while let Some(class) = to_process.pop() {
262 if processed.contains(&class) {
263 continue;
264 }
265 processed.insert(class.clone());
266
267 if let Some(deps) = self.dependency_graph.get(&class) {
269 for dep in deps {
270 if !classes_to_keep.contains(dep) {
271 classes_to_keep.insert(dep.clone());
272 to_process.push(dep.clone());
273 }
274 }
275 }
276
277 if let Some(reverse_deps) = self.reverse_dependencies.get(&class) {
279 for reverse_dep in reverse_deps {
280 if !classes_to_keep.contains(reverse_dep) {
281 classes_to_keep.insert(reverse_dep.clone());
282 to_process.push(reverse_dep.clone());
283 }
284 }
285 }
286 }
287 }
288
289 classes_to_keep
290 }
291
292 fn remove_unused_classes(&self, css_generator: &mut CssGenerator, classes_to_keep: &HashSet<String>) -> RemovalStats {
294 let mut removed_classes = HashSet::new();
295 let mut responsive_removed = 0;
296 let mut conditional_removed = 0;
297 let mut custom_removed = 0;
298 let rules = css_generator.get_rules().clone();
299
300 for (class_name, _rule) in rules {
301 if !classes_to_keep.contains(&class_name) {
302 removed_classes.insert(class_name.clone());
303
304 if class_name.contains("sm:") || class_name.contains("md:") ||
306 class_name.contains("lg:") || class_name.contains("xl:") ||
307 class_name.contains("2xl:") {
308 responsive_removed += 1;
309 } else if class_name.contains("hover:") || class_name.contains("focus:") ||
310 class_name.contains("active:") || class_name.contains("disabled:") {
311 conditional_removed += 1;
312 } else if class_name.starts_with("--") || class_name.contains("var(") {
313 custom_removed += 1;
314 }
315 }
316 }
317
318 RemovalStats {
319 total_removed: removed_classes.len(),
320 responsive_removed,
321 conditional_removed,
322 custom_removed,
323 removed_classes,
324 }
325 }
326
327 pub fn get_config(&self) -> &TreeShakeConfig {
329 &self.config
330 }
331
332 pub fn set_config(&mut self, config: TreeShakeConfig) {
334 self.config = config;
335 }
336
337 pub fn keep_class(&mut self, class: String) {
339 self.config.keep_classes.insert(class);
340 }
341
342 pub fn remove_class(&mut self, class: String) {
344 self.config.remove_classes.insert(class);
345 }
346
347 pub fn clear(&mut self) {
349 self.config.keep_classes.clear();
350 self.config.remove_classes.clear();
351 self.dependency_graph.clear();
352 self.reverse_dependencies.clear();
353 }
354}
355
356impl Default for TreeShaker {
357 fn default() -> Self {
358 Self::new()
359 }
360}
361
362#[cfg(test)]
363mod tests {
364 use super::*;
365
366 #[test]
367 fn test_tree_shaker_creation() {
368 let shaker = TreeShaker::new();
369 assert!(shaker.get_config().enabled);
370 assert!(shaker.get_config().analyze_dependencies);
371 }
372
373 #[test]
374 fn test_custom_config() {
375 let config = TreeShakeConfig {
376 enabled: false,
377 remove_unused_responsive: false,
378 remove_unused_conditional: false,
379 remove_unused_custom: false,
380 keep_classes: HashSet::new(),
381 remove_classes: HashSet::new(),
382 analyze_dependencies: false,
383 };
384
385 let shaker = TreeShaker::with_config(config);
386 assert!(!shaker.get_config().enabled);
387 assert!(!shaker.get_config().analyze_dependencies);
388 }
389
390 #[test]
391 fn test_keep_and_remove_classes() {
392 let mut shaker = TreeShaker::new();
393
394 shaker.keep_class("important-class".to_string());
395 shaker.remove_class("unwanted-class".to_string());
396
397 assert!(shaker.get_config().keep_classes.contains("important-class"));
398 assert!(shaker.get_config().remove_classes.contains("unwanted-class"));
399 }
400
401 #[test]
402 fn test_clear() {
403 let mut shaker = TreeShaker::new();
404
405 shaker.keep_class("test-class".to_string());
406 shaker.remove_class("test-remove".to_string());
407
408 assert!(!shaker.get_config().keep_classes.is_empty());
409 assert!(!shaker.get_config().remove_classes.is_empty());
410
411 shaker.clear();
412
413 assert!(shaker.get_config().keep_classes.is_empty());
414 assert!(shaker.get_config().remove_classes.is_empty());
415 }
416
417 #[test]
418 fn test_dependency_extraction() {
419 let shaker = TreeShaker::new();
420
421 assert_eq!(shaker.extract_class_dependency("var(--primary-color)"), Some("--primary-color".to_string()));
423 assert_eq!(shaker.extract_class_dependency("var(--spacing-4)"), Some("--spacing-4".to_string()));
424 assert_eq!(shaker.extract_class_dependency("1rem"), None);
425 assert_eq!(shaker.extract_class_dependency("#ffffff"), None);
426 }
427
428 #[test]
429 fn test_determine_classes_to_keep() {
430 let shaker = TreeShaker::new();
431 let mut used_classes = HashSet::new();
432 used_classes.insert("p-4".to_string());
433 used_classes.insert("bg-blue-500".to_string());
434
435 let mut css_generator = CssGenerator::new();
436 css_generator.add_class("p-4").unwrap();
437 css_generator.add_class("bg-blue-500").unwrap();
438 css_generator.add_class("m-2").unwrap();
439
440 let classes_to_keep = shaker.determine_classes_to_keep(&used_classes, &css_generator);
441
442 assert!(classes_to_keep.contains("p-4"));
443 assert!(classes_to_keep.contains("bg-blue-500"));
444 assert!(!classes_to_keep.contains("m-2"));
445 }
446
447 #[test]
448 fn test_disabled_tree_shaking() {
449 let mut config = TreeShakeConfig::default();
450 config.enabled = false;
451
452 let mut shaker = TreeShaker::with_config(config);
453 let mut css_generator = CssGenerator::new();
454 css_generator.add_class("p-4").unwrap();
455
456 let temp_dir = std::env::temp_dir();
457 let results = shaker.shake(&[&temp_dir], &mut css_generator).unwrap();
458
459 assert_eq!(results.stats.classes_removed, 0);
460 assert_eq!(results.reduction_percentage, 0.0);
461 }
462}