dbg_swc/es/minifier/next/
check_size.rs

1use 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    /// The directory store inputs to the swc minifier.
30    #[clap(long, short = 'w', default_value = ".next/dbg-swc/minifier-check-size")]
31    workspace: PathBuf,
32
33    /// Rerun `npm run build` even if `workspace` is not empty.
34    #[clap(long)]
35    ensure_fresh: bool,
36
37    /// Show every file, even if the output of swc minifier was smaller.
38    #[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    /// Invokes `npm run build` with appropriate environment variables, and
141    /// store the result in `self.workspace`.
142    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    /// Invokes `npm run build` and extacts the inputs for the swc minifier.
178    fn build_app(&self, app_dir: &Path) -> Result<Vec<InputFile>> {
179        wrap_task(|| {
180            info!("Running `npm run build`");
181
182            // Remove cache
183            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}