Skip to main content

cargo_mate/tools/
wasm_optimize.rs

1use super::{Tool, Result, ToolError, common_options, parse_output_format, OutputFormat};
2use clap::{Arg, ArgMatches, Command};
3use colored::*;
4use std::path::Path;
5use std::process::Command as ProcessCommand;
6use std::fs;
7use regex::Regex;
8#[derive(Debug, Clone)]
9pub struct WasmOptimizeTool;
10#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
11struct OptimizationReport {
12    original_size: u64,
13    optimized_size: u64,
14    reduction_percentage: f64,
15    steps_completed: Vec<String>,
16    build_time: f64,
17    tools_used: Vec<String>,
18    recommendations: Vec<String>,
19    timestamp: String,
20}
21#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
22struct WasmAnalysis {
23    file_size: u64,
24    function_count: usize,
25    export_count: usize,
26    import_count: usize,
27    has_debug_info: bool,
28    has_names_section: bool,
29    optimization_level: String,
30}
31impl WasmOptimizeTool {
32    pub fn new() -> Self {
33        Self
34    }
35    fn check_wasm_tools(&self) -> Result<Vec<String>> {
36        let tools = vec![
37            ("wasm-pack", "WebAssembly build tool"), ("wasm-opt", "Binaryen optimizer"),
38            ("wasm-strip", "Debug info stripper"), ("twiggy", "WASM size analyzer"),
39            ("cargo-wasm", "Cargo WASM builder"),
40        ];
41        let mut available = Vec::new();
42        for (tool, description) in tools {
43            if self.is_tool_available(tool) {
44                available.push(tool.to_string());
45            } else {
46                println!(
47                    "āš ļø  {} ({}) not found - some optimizations may be skipped", tool
48                    .yellow(), description
49                );
50            }
51        }
52        Ok(available)
53    }
54    fn is_tool_available(&self, tool: &str) -> bool {
55        ProcessCommand::new(tool)
56            .arg("--version")
57            .output()
58            .map(|output| output.status.success())
59            .unwrap_or(false)
60    }
61    fn configure_library(&self, lib_type: &str, verbose: bool) -> Result<()> {
62        let cargo_toml_path = "Cargo.toml";
63        if !Path::new(cargo_toml_path).exists() {
64            return Err(
65                ToolError::InvalidArguments(
66                    "Cargo.toml not found in current directory".to_string(),
67                ),
68            );
69        }
70        let content = fs::read_to_string(cargo_toml_path)?;
71        let mut lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
72        let mut in_lib_section = false;
73        let mut lib_section_start = None;
74        let mut lib_section_end = None;
75        for (i, line) in lines.iter().enumerate() {
76            if line.trim() == "[lib]" {
77                in_lib_section = true;
78                lib_section_start = Some(i);
79            } else if in_lib_section && line.trim().starts_with('[')
80                && line.trim() != "[lib]"
81            {
82                lib_section_end = Some(i);
83                break;
84            } else if in_lib_section && i == lines.len() - 1 {
85                lib_section_end = Some(i + 1);
86            }
87        }
88        let new_lib_section = match lib_type {
89            "cdylib" => {
90                vec![
91                    "[lib]".to_string(), "name = \"cargo_mate\"".to_string(),
92                    "crate-type = [\"cdylib\"]".to_string()
93                ]
94            }
95            "rlib" => {
96                vec![
97                    "[lib]".to_string(), "name = \"cargo_mate\"".to_string(),
98                    "crate-type = [\"rlib\"]".to_string()
99                ]
100            }
101            "both" => {
102                vec![
103                    "[lib]".to_string(), "name = \"cargo_mate\"".to_string(),
104                    "crate-type = [\"cdylib\", \"rlib\"]".to_string()
105                ]
106            }
107            "minimal" => {
108                vec![
109                    "[lib]".to_string(), "name = \"cargo_mate\"".to_string(),
110                    "crate-type = [\"cdylib\"]".to_string()
111                ]
112            }
113            _ => {
114                return Err(
115                    ToolError::InvalidArguments(
116                        format!("Unknown lib-type: {}", lib_type),
117                    ),
118                );
119            }
120        };
121        if let Some(start) = lib_section_start {
122            let end = lib_section_end.unwrap_or(start + 1);
123            lines.splice(start..end, new_lib_section);
124        } else {
125            let mut insert_pos = 0;
126            for (i, line) in lines.iter().enumerate() {
127                if line.trim().starts_with('[') && !line.trim().starts_with("[[") {
128                    insert_pos = i;
129                    break;
130                }
131            }
132            lines
133                .splice(
134                    insert_pos..insert_pos,
135                    [new_lib_section, vec!["".to_string()]].concat(),
136                );
137        }
138        let new_content = lines.join("\n");
139        fs::write(cargo_toml_path, new_content)?;
140        if verbose {
141            println!("āœ… Configured library type: {}", lib_type.green());
142        }
143        Ok(())
144    }
145    fn find_wasm_files(&self, directory: &str) -> Result<Vec<String>> {
146        let mut wasm_files = Vec::new();
147        self.find_wasm_files_recursive(directory, &mut wasm_files)?;
148        Ok(wasm_files)
149    }
150    fn find_wasm_files_recursive(
151        &self,
152        dir: &str,
153        files: &mut Vec<String>,
154    ) -> Result<()> {
155        let path = Path::new(dir);
156        if !path.exists() {
157            return Ok(());
158        }
159        for entry in fs::read_dir(path)? {
160            let entry = entry?;
161            let path = entry.path();
162            if path.is_dir() {
163                let dir_name = path.file_name().unwrap_or_default().to_string_lossy();
164                if !matches!(dir_name.as_ref(), "target" | ".git" | "node_modules") {
165                    self.find_wasm_files_recursive(&path.to_string_lossy(), files)?;
166                }
167            } else if let Some(ext) = path.extension() {
168                if ext == "wasm" {
169                    files.push(path.to_string_lossy().to_string());
170                }
171            }
172        }
173        Ok(())
174    }
175    fn analyze_wasm_file(&self, file_path: &str) -> Result<WasmAnalysis> {
176        let metadata = fs::metadata(file_path)?;
177        let file_size = metadata.len();
178        let output = ProcessCommand::new("wasm-objdump")
179            .args(&["-x", file_path])
180            .output();
181        let (
182            function_count,
183            export_count,
184            import_count,
185            has_debug_info,
186            has_names_section,
187        ) = match output {
188            Ok(output) if output.status.success() => {
189                let stdout = String::from_utf8_lossy(&output.stdout);
190                let function_count = stdout
191                    .lines()
192                    .filter(|l| l.contains("func["))
193                    .count();
194                let export_count = stdout
195                    .lines()
196                    .filter(|l| l.contains("export"))
197                    .count();
198                let import_count = stdout
199                    .lines()
200                    .filter(|l| l.contains("import"))
201                    .count();
202                let has_debug_info = stdout.contains("debug") || stdout.contains("name");
203                let has_names_section = stdout.contains("name section");
204                (
205                    function_count,
206                    export_count,
207                    import_count,
208                    has_debug_info,
209                    has_names_section,
210                )
211            }
212            _ => (0, 0, 0, false, false),
213        };
214        Ok(WasmAnalysis {
215            file_size,
216            function_count,
217            export_count,
218            import_count,
219            has_debug_info,
220            has_names_section,
221            optimization_level: "unknown".to_string(),
222        })
223    }
224    fn run_wasm_pack_build(&self, release: bool, target: &str) -> Result<String> {
225        let mut args = vec!["build"];
226        if release {
227            args.push("--release");
228        }
229        args.extend(&["--target", target]);
230        let output = ProcessCommand::new("wasm-pack")
231            .args(&args)
232            .output()
233            .map_err(|e| ToolError::ExecutionFailed(
234                format!("Failed to run wasm-pack: {}", e),
235            ))?;
236        if !output.status.success() {
237            let stderr = String::from_utf8_lossy(&output.stderr);
238            return Err(
239                ToolError::ExecutionFailed(format!("wasm-pack build failed: {}", stderr)),
240            );
241        }
242        let wasm_file = "pkg/package_bg.wasm";
243        if Path::new(wasm_file).exists() {
244            Ok(wasm_file.to_string())
245        } else {
246            Ok("target/wasm32-unknown-emscripten/release/*.wasm".to_string())
247        }
248    }
249    fn optimize_with_wasm_opt(
250        &self,
251        input_file: &str,
252        output_file: &str,
253        level: &str,
254    ) -> Result<()> {
255        let optimization_level = match level {
256            "basic" => "-O",
257            "aggressive" => "-O3",
258            "size" => "-Os",
259            "maximum" => "-O4",
260            _ => "-O2",
261        };
262        let output = ProcessCommand::new("wasm-opt")
263            .args(&[optimization_level, input_file, "-o", output_file])
264            .output()
265            .map_err(|e| ToolError::ExecutionFailed(
266                format!("Failed to run wasm-opt: {}", e),
267            ))?;
268        if !output.status.success() {
269            let stderr = String::from_utf8_lossy(&output.stderr);
270            return Err(
271                ToolError::ExecutionFailed(format!("wasm-opt failed: {}", stderr)),
272            );
273        }
274        Ok(())
275    }
276    fn strip_debug_info(&self, input_file: &str, output_file: &str) -> Result<()> {
277        let output = ProcessCommand::new("wasm-strip")
278            .args(&[input_file, "-o", output_file])
279            .output()
280            .map_err(|e| ToolError::ExecutionFailed(
281                format!("Failed to run wasm-strip: {}", e),
282            ))?;
283        if !output.status.success() {
284            let stderr = String::from_utf8_lossy(&output.stderr);
285            return Err(
286                ToolError::ExecutionFailed(format!("wasm-strip failed: {}", stderr)),
287            );
288        }
289        Ok(())
290    }
291    fn analyze_with_twiggy(&self, file_path: &str) -> Result<String> {
292        let output = ProcessCommand::new("twiggy")
293            .args(&["top", "-n", "20", file_path])
294            .output()
295            .map_err(|e| ToolError::ExecutionFailed(
296                format!("Failed to run twiggy: {}", e),
297            ))?;
298        if !output.status.success() {
299            let stderr = String::from_utf8_lossy(&output.stderr);
300            return Err(
301                ToolError::ExecutionFailed(format!("twiggy analysis failed: {}", stderr)),
302            );
303        }
304        Ok(String::from_utf8_lossy(&output.stdout).to_string())
305    }
306    fn display_report(
307        &self,
308        report: &OptimizationReport,
309        output_format: OutputFormat,
310        verbose: bool,
311    ) {
312        match output_format {
313            OutputFormat::Human => {
314                println!("\n{}", "šŸš€ WASM Optimization Report".bold().blue());
315                println!("{}", "═".repeat(50).blue());
316                println!("\nšŸ“Š Size Optimization:");
317                println!(
318                    "  • Original: {:.2} KB", report.original_size as f64 / 1024.0
319                );
320                println!(
321                    "  • Optimized: {:.2} KB", report.optimized_size as f64 / 1024.0
322                );
323                println!("  • Reduction: {:.1}%", report.reduction_percentage);
324                if report.reduction_percentage > 0.0 {
325                    println!(
326                        "  • {}", format!("āœ… Saved {:.2} KB", (report.original_size -
327                        report.optimized_size) as f64 / 1024.0) .green()
328                    );
329                }
330                println!("\nšŸ”§ Tools Used:");
331                for tool in &report.tools_used {
332                    println!("  • {}", tool.green());
333                }
334                println!("\nāš™ļø  Optimization Steps:");
335                for step in &report.steps_completed {
336                    println!("  • {}", step.cyan());
337                }
338                if verbose {
339                    println!("\nšŸ’” Recommendations:");
340                    for rec in &report.recommendations {
341                        println!("  • {}", rec.yellow());
342                    }
343                }
344                if report.build_time > 0.0 {
345                    println!("\nā±ļø  Build Time: {:.2}s", report.build_time);
346                }
347            }
348            OutputFormat::Json => {
349                let json = serde_json::to_string_pretty(report)
350                    .unwrap_or_else(|_| "{}".to_string());
351                println!("{}", json);
352            }
353            OutputFormat::Table => {
354                println!(
355                    "{:<20} {:<15} {:<15} {:<12}", "Metric", "Original", "Optimized",
356                    "Reduction"
357                );
358                println!("{}", "─".repeat(70));
359                println!(
360                    "{:<20} {:<15} {:<15} {:.1}%", "File Size (KB)", format!("{:.2}",
361                    report.original_size as f64 / 1024.0), format!("{:.2}", report
362                    .optimized_size as f64 / 1024.0), report.reduction_percentage
363                );
364                println!(
365                    "{:<20} {:<15} {:<15} {:.1}%", "Build Time (s)", "N/A",
366                    format!("{:.2}", report.build_time), 0.0
367                );
368            }
369        }
370    }
371}
372impl Tool for WasmOptimizeTool {
373    fn name(&self) -> &'static str {
374        "wasm-optimize"
375    }
376    fn description(&self) -> &'static str {
377        "One-command WASM optimization pipeline"
378    }
379    fn command(&self) -> Command {
380        Command::new(self.name())
381            .about(self.description())
382            .long_about(
383                "Complete WebAssembly optimization pipeline with multiple tools and strategies.
384
385EXAMPLES:
386    cm tool wasm-optimize --release --aggressive
387    cm tool wasm-optimize --target web --size-optimized --lib-type cdylib
388    cm tool wasm-optimize --analyze-only --verbose --lib-type minimal
389    cm tool wasm-optimize --lib-type both --target nodejs",
390            )
391            .args(
392                &[
393                    Arg::new("release")
394                        .long("release")
395                        .help("Build in release mode")
396                        .action(clap::ArgAction::SetTrue),
397                    Arg::new("target")
398                        .long("target")
399                        .short('t')
400                        .help("WASM target")
401                        .default_value("web")
402                        .value_parser(["web", "nodejs", "bundler", "no-modules"]),
403                    Arg::new("optimization")
404                        .long("optimization")
405                        .short('O')
406                        .help("Optimization level")
407                        .default_value("balanced")
408                        .value_parser([
409                            "none",
410                            "basic",
411                            "balanced",
412                            "aggressive",
413                            "size",
414                            "maximum",
415                        ]),
416                    Arg::new("strip-debug")
417                        .long("strip-debug")
418                        .help("Strip debug information")
419                        .action(clap::ArgAction::SetTrue),
420                    Arg::new("analyze-only")
421                        .long("analyze-only")
422                        .help("Only analyze without optimization")
423                        .action(clap::ArgAction::SetTrue),
424                    Arg::new("input")
425                        .long("input")
426                        .short('i')
427                        .help("Input WASM file (auto-detect if not specified)"),
428                    Arg::new("wasm-output")
429                        .long("wasm-output")
430                        .help("Output file for optimized WASM")
431                        .default_value("optimized.wasm"),
432                    Arg::new("analyze-size")
433                        .long("analyze-size")
434                        .help("Analyze size with twiggy")
435                        .action(clap::ArgAction::SetTrue),
436                    Arg::new("lib-type")
437                        .long("lib-type")
438                        .short('l')
439                        .help("Library type to use for WASM compilation")
440                        .default_value("cdylib")
441                        .value_parser(["cdylib", "rlib", "both", "minimal"]),
442                ],
443            )
444            .args(&common_options())
445    }
446    fn execute(&self, matches: &ArgMatches) -> Result<()> {
447        let release = matches.get_flag("release");
448        let target = matches.get_one::<String>("target").unwrap();
449        let optimization = matches.get_one::<String>("optimization").unwrap();
450        let strip_debug = matches.get_flag("strip-debug");
451        let analyze_only = matches.get_flag("analyze-only");
452        let input_file = matches.get_one::<String>("input");
453        let output_file = matches.get_one::<String>("wasm-output").unwrap();
454        let analyze_size = matches.get_flag("analyze-size");
455        let lib_type = matches.get_one::<String>("lib-type").unwrap();
456        let output_format = parse_output_format(matches);
457        let verbose = matches.get_flag("verbose");
458        println!(
459            "šŸš€ {} - Optimizing WebAssembly", "CargoMate WASM Optimize".bold().blue()
460        );
461        if verbose {
462            println!("šŸ“š Using library type: {}", lib_type.cyan());
463        }
464        self.configure_library(lib_type, verbose)?;
465        let available_tools = self.check_wasm_tools()?;
466        if available_tools.is_empty() {
467            return Err(
468                ToolError::ExecutionFailed(
469                    "No WASM tools found. Install wasm-pack, wasm-opt, or similar tools"
470                        .to_string(),
471                ),
472            );
473        }
474        let wasm_file = if let Some(input) = input_file {
475            if !Path::new(input).exists() {
476                return Err(
477                    ToolError::InvalidArguments(
478                        format!("Input file {} not found", input),
479                    ),
480                );
481            }
482            input.clone()
483        } else {
484            let wasm_files = self.find_wasm_files(".")?;
485            if wasm_files.is_empty() {
486                if available_tools.contains(&"wasm-pack".to_string()) {
487                    println!("šŸ“¦ No WASM file found, building with wasm-pack...");
488                    self.run_wasm_pack_build(release, target)?
489                } else {
490                    return Err(
491                        ToolError::InvalidArguments(
492                            "No WASM files found and wasm-pack not available".to_string(),
493                        ),
494                    );
495                }
496            } else if wasm_files.len() == 1 {
497                wasm_files[0].clone()
498            } else {
499                println!("šŸ“ Multiple WASM files found:");
500                for (i, file) in wasm_files.iter().enumerate() {
501                    println!("  {}. {}", i + 1, file);
502                }
503                return Err(
504                    ToolError::InvalidArguments(
505                        "Multiple WASM files found, specify --input".to_string(),
506                    ),
507                );
508            }
509        };
510        let original_analysis = self.analyze_wasm_file(&wasm_file)?;
511        let original_size = original_analysis.file_size;
512        if verbose {
513            println!("\nšŸ“Š Original WASM Analysis:");
514            println!("  • File size: {:.2} KB", original_size as f64 / 1024.0);
515            println!("  • Functions: {}", original_analysis.function_count);
516            println!("  • Exports: {}", original_analysis.export_count);
517            println!("  • Imports: {}", original_analysis.import_count);
518            println!(
519                "  • Has debug info: {}", if original_analysis.has_debug_info { "Yes" }
520                else { "No" }
521            );
522        }
523        if analyze_only {
524            self.display_report(
525                &OptimizationReport {
526                    original_size,
527                    optimized_size: original_size,
528                    reduction_percentage: 0.0,
529                    steps_completed: vec!["Analysis only".to_string()],
530                    build_time: 0.0,
531                    tools_used: available_tools,
532                    recommendations: vec![
533                        "Use optimization flags to reduce size".to_string()
534                    ],
535                    timestamp: chrono::Utc::now().to_rfc3339(),
536                },
537                output_format,
538                verbose,
539            );
540            return Ok(());
541        }
542        let mut steps_completed = Vec::new();
543        let mut current_file = wasm_file.clone();
544        let mut optimized_size = original_size;
545        if strip_debug && available_tools.contains(&"wasm-strip".to_string()) {
546            let stripped_file = format!("{}.stripped", current_file);
547            match self.strip_debug_info(&current_file, &stripped_file) {
548                Ok(_) => {
549                    let stripped_size = fs::metadata(&stripped_file)?.len();
550                    optimized_size = stripped_size;
551                    current_file = stripped_file;
552                    steps_completed
553                        .push(
554                            format!(
555                                "Stripped debug info: {:.2} KB → {:.2} KB", original_size
556                                as f64 / 1024.0, optimized_size as f64 / 1024.0
557                            ),
558                        );
559                }
560                Err(e) => {
561                    println!("āš ļø  Debug stripping failed: {}", e);
562                }
563            }
564        }
565        if available_tools.contains(&"wasm-opt".to_string()) && optimization != "none" {
566            let temp_file = format!("{}.optimized", current_file);
567            match self.optimize_with_wasm_opt(&current_file, &temp_file, optimization) {
568                Ok(_) => {
569                    let new_size = fs::metadata(&temp_file)?.len();
570                    let reduction = ((optimized_size as f64 - new_size as f64)
571                        / optimized_size as f64) * 100.0;
572                    optimized_size = new_size;
573                    current_file = temp_file;
574                    steps_completed
575                        .push(
576                            format!(
577                                "Optimized with {}: {:.1}% reduction", optimization,
578                                reduction
579                            ),
580                        );
581                }
582                Err(e) => {
583                    println!("āš ļø  Optimization failed: {}", e);
584                }
585            }
586        }
587        let mut size_analysis = String::new();
588        if analyze_size && available_tools.contains(&"twiggy".to_string()) {
589            match self.analyze_with_twiggy(&current_file) {
590                Ok(analysis) => {
591                    size_analysis = analysis;
592                    steps_completed.push("Size analysis completed".to_string());
593                }
594                Err(e) => {
595                    println!("āš ļø  Size analysis failed: {}", e);
596                }
597            }
598        }
599        if current_file != output_file.to_string() {
600            fs::copy(&current_file, output_file)?;
601        }
602        let mut recommendations = Vec::new();
603        let reduction_percentage = if original_size > 0 {
604            ((original_size as f64 - optimized_size as f64) / original_size as f64)
605                * 100.0
606        } else {
607            0.0
608        };
609        if reduction_percentage < 10.0 {
610            recommendations
611                .push("Consider using more aggressive optimization levels".to_string());
612        }
613        if original_analysis.has_debug_info {
614            recommendations
615                .push(
616                    "Debug information is present - use --strip-debug for production"
617                        .to_string(),
618                );
619        }
620        if original_analysis.function_count > 1000 {
621            recommendations
622                .push(
623                    "Large number of functions detected - consider code splitting"
624                        .to_string(),
625                );
626        }
627        let report = OptimizationReport {
628            original_size,
629            optimized_size,
630            reduction_percentage,
631            steps_completed,
632            build_time: 0.0,
633            tools_used: available_tools,
634            recommendations,
635            timestamp: chrono::Utc::now().to_rfc3339(),
636        };
637        self.display_report(&report, output_format, verbose);
638        if verbose && !size_analysis.is_empty() {
639            println!("\nšŸ“ˆ Size Analysis (Top Contributors):");
640            for line in size_analysis.lines().take(10) {
641                println!("  {}", line);
642            }
643        }
644        if reduction_percentage > 0.0 {
645            println!(
646                "\nāœ… {} optimized and saved to {}", "WASM file".green(), output_file
647            );
648        }
649        Ok(())
650    }
651}
652impl Default for WasmOptimizeTool {
653    fn default() -> Self {
654        Self::new()
655    }
656}