1use std::{
2 path::{Path, PathBuf},
3 sync::Arc,
4};
5
6use anyhow::{Context, Result};
7use clap::Args;
8use par_iter::prelude::*;
9use swc_common::{
10 errors::{ColorConfig, Handler, HANDLER},
11 SourceFile, SourceMap, GLOBALS,
12};
13use tracing::info;
14
15use crate::util::{
16 all_js_files, gzipped_size,
17 minifier::{get_esbuild_output, get_minified, get_terser_output},
18 print_js, wrap_task,
19};
20
21#[derive(Debug, Args)]
24pub struct EnsureSize {
25 #[clap(long)]
26 pub no_terser: bool,
27
28 #[clap(long)]
29 pub no_esbuild: bool,
30
31 pub path: PathBuf,
35}
36
37impl EnsureSize {
38 pub fn run(self, cm: Arc<SourceMap>) -> Result<()> {
39 let all_files = all_js_files(&self.path)?;
40
41 info!("Using {} files", all_files.len());
42
43 let mut results = GLOBALS.with(|globals| {
44 all_files
45 .par_iter()
46 .map(|js_file| GLOBALS.set(globals, || self.check_file(cm.clone(), js_file)))
47 .filter_map(|v| v.transpose())
48 .collect::<Result<Vec<_>>>()
49 })?;
50
51 results.sort_by_key(|f| {
52 if let Some(terser) = &f.terser {
53 f.swc.mangled_size as isize - terser.mangled_size as isize
54 } else {
55 0
56 }
57 });
58
59 for f in &results {
60 if let Some(terser) = &f.terser {
61 if f.swc.mangled_size > terser.mangled_size
62 || f.swc.no_mangle_size > terser.no_mangle_size
63 || f.swc.gzipped_size > terser.gzipped_size
64 {
65 println!();
66 println!("{}", f.fm.name);
67 }
68 if f.swc.gzipped_size > terser.gzipped_size {
69 println!(" Gzipped");
70 println!(" swc: {} bytes", f.swc.gzipped_size);
71 println!(" terser: {} bytes", terser.gzipped_size);
72 }
73
74 if f.swc.mangled_size > terser.mangled_size {
75 println!(" Mangled");
76 println!(" swc: {} bytes", f.swc.mangled_size);
77 println!(" terser: {} bytes", terser.mangled_size);
78 }
79
80 if f.swc.no_mangle_size > terser.no_mangle_size {
81 println!(" No-mangle");
82 println!(" swc: {} bytes", f.swc.no_mangle_size);
83 println!(" terser: {} bytes", terser.no_mangle_size);
84 }
85 }
86 }
87 {
88 let swc_total = results.iter().map(|f| f.swc.mangled_size).sum::<usize>();
89 let terser_total = results
90 .iter()
91 .flat_map(|f| f.terser.map(|v| v.mangled_size))
92 .sum::<usize>();
93
94 println!("Total");
95 println!(" swc: {swc_total} bytes");
96 println!(" terser: {terser_total} bytes");
97 println!(" Size ratio: {}", swc_total as f64 / terser_total as f64);
98
99 let swc_smaller_file_count = results
100 .iter()
101 .filter(|f| {
102 if let Some(terser) = &f.terser {
103 f.swc.mangled_size <= terser.mangled_size
104 } else {
105 false
106 }
107 })
108 .count();
109 println!(
110 "swc produced smaller or equal output for {} files out of {} files, {:.2}%",
111 swc_smaller_file_count,
112 all_files.len(),
113 100.0 * swc_smaller_file_count as f64 / results.len() as f64
114 );
115 }
116 {
117 let swc_total = results.iter().map(|f| f.swc.gzipped_size).sum::<usize>();
118 let terser_total = results
119 .iter()
120 .flat_map(|f| f.terser.map(|v| v.gzipped_size))
121 .sum::<usize>();
122
123 println!("Total (gzipped)");
124 println!(" swc: {swc_total} bytes");
125 println!(" terser: {terser_total} bytes");
126 println!(" Size ratio: {}", swc_total as f64 / terser_total as f64);
127
128 let swc_smaller_file_count = results
129 .iter()
130 .filter(|f| {
131 if let Some(terser) = &f.terser {
132 f.swc.gzipped_size <= terser.gzipped_size
133 } else {
134 false
135 }
136 })
137 .count();
138 println!(
139 "swc produced smaller or equal output for {} files out of {} files, {:.2}%",
140 swc_smaller_file_count,
141 all_files.len(),
142 100.0 * swc_smaller_file_count as f64 / results.len() as f64
143 );
144 }
145
146 Ok(())
147 }
148
149 fn check_file(&self, cm: Arc<SourceMap>, js_file: &Path) -> Result<Option<FileSize>> {
150 wrap_task(|| {
151 info!("Checking {}", js_file.display());
152
153 let fm = cm.load_file(js_file).context("failed to load file")?;
154 let handler =
155 Handler::with_tty_emitter(ColorConfig::Never, true, false, Some(cm.clone()));
156 HANDLER.set(&handler, || {
157 let code_mangled = {
158 let minified_mangled = get_minified(cm.clone(), js_file, true, true)?;
159
160 print_js(cm.clone(), &minified_mangled.module, true)
161 .context("failed to convert ast to code")?
162 };
163
164 let swc_no_mangle = {
165 let minified_no_mangled = get_minified(cm.clone(), js_file, true, false)?;
166
167 print_js(cm, &minified_no_mangled.module, true)
168 .context("failed to convert ast to code")?
169 };
170
171 let mut file_size = FileSize {
174 fm,
175 swc: MinifierOutput {
176 mangled_size: code_mangled.len(),
177 no_mangle_size: swc_no_mangle.len(),
178 gzipped_size: gzipped_size(&code_mangled),
179 },
180 terser: Default::default(),
181 esbuild: Default::default(),
182 };
183
184 if !self.no_terser {
185 let terser_mangled = get_terser_output(js_file, true, true)?;
186 let terser_no_mangle = get_terser_output(js_file, true, false)?;
187
188 file_size.terser = Some(MinifierOutput {
189 mangled_size: terser_mangled.len(),
190 no_mangle_size: terser_no_mangle.len(),
191 gzipped_size: gzipped_size(&terser_mangled),
192 });
193 }
194
195 if !self.no_esbuild {
196 let esbuild_mangled = get_esbuild_output(js_file, true)?;
197 let esbuild_no_mangle = get_esbuild_output(js_file, false)?;
198
199 file_size.esbuild = Some(MinifierOutput {
200 mangled_size: esbuild_mangled.len(),
201 no_mangle_size: esbuild_no_mangle.len(),
202 gzipped_size: gzipped_size(&esbuild_mangled),
203 });
204 }
205
206 if file_size.terser.is_none() && file_size.esbuild.is_none() {
207 return Ok(None);
208 }
209
210 Ok(Some(file_size))
211 })
212 })
213 .with_context(|| format!("failed to check file: {}", js_file.display()))
214 }
215}
216
217#[allow(unused)]
218#[derive(Debug)]
219struct FileSize {
220 fm: Arc<SourceFile>,
221
222 swc: MinifierOutput,
223
224 terser: Option<MinifierOutput>,
225
226 esbuild: Option<MinifierOutput>,
227}
228
229#[allow(unused)]
230#[derive(Debug, Clone, Copy)]
231struct MinifierOutput {
232 mangled_size: usize,
233 no_mangle_size: usize,
234
235 gzipped_size: usize,
237}