dbg_swc/es/minifier/next/
check_size.rs1use std::{
2 cmp::Reverse,
3 env::current_dir,
4 fs::{self, create_dir_all, read_dir, remove_dir_all},
5 path::{Path, PathBuf},
6 process::{Command, Stdio},
7 sync::Arc,
8};
9
10use anyhow::{bail, Context, Result};
11use clap::Args;
12use dialoguer::{console::Term, theme::ColorfulTheme, Select};
13use rayon::{
14 prelude::{IntoParallelIterator, ParallelBridge, ParallelIterator},
15 str::ParallelString,
16};
17use serde::{de::DeserializeOwned, Deserialize};
18use swc_common::{errors::HANDLER, SourceMap, GLOBALS};
19use tracing::info;
20
21use crate::util::{
22 gzipped_size, make_pretty,
23 minifier::{get_minified, get_terser_output},
24 print_js, wrap_task,
25};
26
27#[derive(Debug, Args)]
28pub struct CheckSizeCommand {
29 #[clap(long, short = 'w', default_value = ".next/dbg-swc/minifier-check-size")]
31 workspace: PathBuf,
32
33 #[clap(long)]
35 ensure_fresh: bool,
36
37 #[clap(long)]
39 show_all: bool,
40}
41
42impl CheckSizeCommand {
43 pub fn run(self, cm: Arc<SourceMap>) -> Result<()> {
44 let app_dir = current_dir().context("failed to get current directory")?;
45
46 let files = self.store_minifier_inputs(&app_dir)?;
47
48 info!("Running minifier");
49
50 let mut files = GLOBALS.with(|globals| {
51 HANDLER.with(|handler| {
52 files
53 .into_par_iter()
54 .map(|file| {
55 GLOBALS.set(globals, || {
56 HANDLER.set(handler, || self.minify_file(cm.clone(), &file))
57 })
58 })
59 .collect::<Result<Vec<_>>>()
60 })
61 })?;
62
63 if !self.show_all {
64 info!(
65 "Skiping files which are smaller than terser output, as `--show-all` is not \
66 specified"
67 );
68
69 files.retain(|f| f.swc > f.terser);
70 }
71 files.sort_by_key(|f| Reverse(f.swc as i32 - f.terser as i32));
72
73 for file in &files {
74 println!(
75 "{}: {} bytes (swc) vs {} bytes (terser)",
76 file.path
77 .strip_prefix(self.workspace.join("inputs"))
78 .unwrap()
79 .display(),
80 file.swc,
81 file.terser
82 );
83 }
84
85 if !files.is_empty() {
86 println!("Select a file to open diff");
87 }
88
89 let items = files
90 .iter()
91 .map(|f| {
92 format!(
93 "{}: Diff: {} bytes; {} bytes (swc) vs {} bytes (terser)",
94 f.path
95 .strip_prefix(self.workspace.join("inputs"))
96 .unwrap()
97 .display(),
98 f.swc as i32 - f.terser as i32,
99 f.swc,
100 f.terser,
101 )
102 })
103 .collect::<Vec<_>>();
104
105 let selection = Select::with_theme(&ColorfulTheme::default())
106 .items(&items)
107 .default(0)
108 .interact_on_opt(&Term::stderr())?;
109
110 if let Some(selection) = selection {
111 let swc_path = self.workspace.join("swc.output.js");
112 let terser_path = self.workspace.join("terser.output.js");
113
114 let swc = get_minified(cm.clone(), &files[selection].path, true, false)?;
115
116 std::fs::write(&swc_path, print_js(cm, &swc.module, true)?.as_bytes())
117 .context("failed to write swc.output.js")?;
118
119 make_pretty(&swc_path)?;
120
121 let terser = get_terser_output(&files[selection].path, true, false)?;
122
123 std::fs::write(&terser_path, terser.as_bytes())
124 .context("failed to write terser.output.js")?;
125
126 make_pretty(&terser_path)?;
127
128 {
129 let mut c = Command::new("code");
130 c.arg("--diff");
131 c.arg(swc_path);
132 c.arg(terser_path);
133 c.output().context("failed to run vscode")?;
134 }
135 }
136
137 Ok(())
138 }
139
140 fn store_minifier_inputs(&self, app_dir: &Path) -> Result<Vec<PathBuf>> {
143 wrap_task(|| {
144 if !self.ensure_fresh
145 && self.workspace.is_dir()
146 && read_dir(self.workspace.join("inputs"))
147 .context("failed to read workspace directory")?
148 .count()
149 != 0
150 {
151 info!(
152 "Skipping `npm run build` because the cache exists and `--ensure-fresh` is \
153 not set"
154 );
155
156 return get_all_files(&self.workspace.join("inputs"))
157 .context("failed to get files from cache");
158 }
159
160 let files = self.build_app(app_dir)?;
161
162 files
163 .into_par_iter()
164 .map(|file| {
165 let file_path = self.workspace.join("inputs").join(file.name);
166 create_dir_all(file_path.parent().unwrap())
167 .context("failed to create a directory")?;
168 fs::write(&file_path, file.source).context("failed to write file")?;
169
170 Ok(file_path)
171 })
172 .collect::<Result<_>>()
173 })
174 .context("failed to extract inputs for the swc minifier")
175 }
176
177 fn build_app(&self, app_dir: &Path) -> Result<Vec<InputFile>> {
179 wrap_task(|| {
180 info!("Running `npm run build`");
181
182 let _ = remove_dir_all(app_dir.join(".next"));
184
185 let mut c = Command::new("npm");
186 c.current_dir(app_dir);
187 c.env("FORCE_COLOR", "3");
188 c.env("NEXT_DEBUG_MINIFY", "1");
189 c.arg("run").arg("build");
190
191 c.stderr(Stdio::inherit());
192
193 let output = c
194 .output()
195 .context("failed to get output of `npm run build`")?;
196
197 if !output.status.success() {
198 bail!("`npm run build` failed");
199 }
200
201 let output = String::from_utf8_lossy(&output.stdout);
202
203 output
204 .par_lines()
205 .filter(|line| line.contains("{ name:"))
206 .map(|line| {
207 parse_loose_json::<InputFile>(line).context("failed to parse input file")
208 })
209 .collect::<Result<_>>()
210 })
211 .with_context(|| format!("failed to build app in `{}`", app_dir.display()))
212 }
213
214 fn minify_file(&self, cm: Arc<SourceMap>, js_file: &Path) -> Result<CompareResult> {
215 wrap_task(|| {
216 let terser_full =
217 get_terser_output(js_file, true, true).context("failed to get terser output")?;
218
219 let swc_full = get_minified(cm.clone(), js_file, true, true)?;
220 let swc_full = print_js(cm.clone(), &swc_full.module, true)?;
221
222 Ok(CompareResult {
223 terser: gzipped_size(&terser_full),
224 swc: gzipped_size(&swc_full),
225 path: js_file.to_owned(),
226 })
227 })
228 .with_context(|| format!("failed to minify `{}`", js_file.display()))
229 }
230}
231
232struct CompareResult {
233 path: PathBuf,
234 swc: usize,
235 terser: usize,
236}
237
238#[derive(Deserialize)]
239struct InputFile {
240 name: String,
241 source: String,
242}
243
244fn parse_loose_json<T>(s: &str) -> Result<T>
245where
246 T: DeserializeOwned,
247{
248 wrap_task(|| {
249 let mut c = Command::new("node");
250
251 c.arg("-e");
252 c.arg(
253 r#"
254 function looseJsonParse(obj) {
255 return Function('"use strict";return (' + obj + ")")();
256 }
257 console.log(JSON.stringify(looseJsonParse(process.argv[1])));
258 "#,
259 );
260
261 c.arg(s);
262
263 c.stderr(Stdio::inherit());
264
265 let json_str = c
266 .output()
267 .context("failed to parse json loosely using node")?
268 .stdout;
269
270 serde_json::from_slice(&json_str).context("failed to parse json")
271 })
272 .with_context(|| format!("failed to parse loose json: {s}"))
273}
274
275fn get_all_files(path: &Path) -> Result<Vec<PathBuf>> {
276 if path.is_dir() {
277 let v = read_dir(path)
278 .with_context(|| format!("failed to read directory at `{}`", path.display()))?
279 .par_bridge()
280 .map(|entry| get_all_files(&entry?.path()).context("failed get recurse"))
281 .collect::<Result<Vec<_>>>()?;
282
283 Ok(v.into_iter().flatten().collect())
284 } else {
285 Ok(vec![path.to_path_buf()])
286 }
287}