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(¤t_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(¤t_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(¤t_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(¤t_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}