dbg_swc/es/minifier/
reduce.rs

1use std::{
2    env::current_exe,
3    fs::{self, create_dir_all, read_to_string},
4    path::{Path, PathBuf},
5    process::Command,
6    sync::Arc,
7};
8
9use anyhow::{Context, Result};
10use clap::{ArgEnum, Args};
11use par_iter::prelude::*;
12use sha1::{Digest, Sha1};
13use swc_common::{SourceMap, GLOBALS};
14use tempfile::TempDir;
15
16use crate::{
17    util::{all_js_files, parse_js, print_js, ChildGuard},
18    CREDUCE_INPUT_ENV_VAR, CREDUCE_MODE_ENV_VAR,
19};
20
21/// Reduce input files to minimal reproduction cases
22///
23/// This command requires `creduce` and `terser` in PATH.
24///
25/// For `creduce`, see https://embed.cs.utah.edu/creduce/ for more information.
26/// If you are using homebrew, install it with `brew install creduce`.
27///
28/// For `terser`, this command uses `npx terser` to invoke `terser`  for
29/// comparison.
30///
31/// After reducing, the reduced file will be moved to `.swc-reduce` directory.
32///
33///
34/// Note: This tool is not perfect, and it may reduce input file way too much,
35/// or fail to reduce an input file.
36#[derive(Debug, Args)]
37pub struct ReduceCommand {
38    /// The path to the input file. You can specify a directory if you want to
39    /// reduce every '.js' file within a directory, in a recursive manner.
40    pub path: PathBuf,
41
42    /// In 'size' mode, this command tries to find the minimal input file where
43    /// the size of the output file of swc minifier is larger than the one from
44    /// terser.
45    ///
46    /// In 'semantics' mode, this command tries to reduce the input file to a
47    /// minimal reproduction case which triggers the bug.
48    #[clap(long, arg_enum)]
49    pub mode: ReduceMode,
50
51    /// If true, the input file will be removed after the reduction. This can be
52    /// used for pausing and resuming the process of reducing.
53    #[clap(long)]
54    pub remove: bool,
55}
56
57#[derive(Debug, Clone, Copy, ArgEnum)]
58pub enum ReduceMode {
59    Size,
60    Semantics,
61}
62
63impl ReduceCommand {
64    pub fn run(self, cm: Arc<SourceMap>) -> Result<()> {
65        let js_files = all_js_files(&self.path)?;
66
67        GLOBALS.with(|globals| {
68            js_files
69                .into_par_iter()
70                .map(|path| GLOBALS.set(globals, || self.reduce_file(cm.clone(), &path)))
71                .collect::<Result<Vec<_>>>()
72        })?;
73
74        Ok(())
75    }
76
77    fn reduce_file(&self, cm: Arc<SourceMap>, src_path: &Path) -> Result<()> {
78        // Strip comments to workaround a bug of creduce
79
80        let fm = cm.load_file(src_path).context("failed to prepare file")?;
81        let m = parse_js(fm)?;
82        let code = print_js(cm, &m.module, false)?;
83
84        fs::write(src_path, code.as_bytes()).context("failed to strip comments")?;
85
86        //
87        let dir = TempDir::new().context("failed to create a temp directory")?;
88
89        let input = dir.path().join("input.js");
90
91        fs::copy(src_path, &input).context("failed to copy")?;
92
93        let mut c = Command::new("creduce");
94
95        c.arg("--not-c");
96
97        c.env(
98            CREDUCE_MODE_ENV_VAR,
99            match self.mode {
100                ReduceMode::Size => "SIZE",
101                ReduceMode::Semantics => "SEMANTICS",
102            },
103        );
104        c.env(CREDUCE_INPUT_ENV_VAR, &input);
105
106        let exe = current_exe()?;
107        c.arg(&exe);
108        c.arg(&input);
109        let mut child = ChildGuard(c.spawn().context("failed to run creduce")?);
110        let status = child.0.wait().context("failed to wait for creduce")?;
111
112        if status.success() {
113            move_to_data_dir(&input)?;
114        }
115
116        if let Some(1) = status.code() {
117            if self.remove {
118                fs::remove_file(src_path).context("failed to remove")?;
119            }
120        } else {
121            dbg!(&status, status.code());
122        }
123
124        Ok(())
125    }
126}
127
128fn move_to_data_dir(input_path: &Path) -> Result<PathBuf> {
129    let src = read_to_string(input_path).context("failed to read input file")?;
130
131    // create a Sha1 object
132    let mut hasher = Sha1::new();
133
134    // process input message
135    hasher.update(src.as_bytes());
136
137    // acquire hash digest in the form of GenericArray,
138    // which in this case is equivalent to [u8; 20]
139    let result = hasher.finalize();
140    let hash_str = format!("{result:x}");
141
142    create_dir_all(format!(".swc-reduce/{hash_str}")).context("failed to create `.data`")?;
143
144    let to = PathBuf::from(format!(".swc-reduce/{hash_str}/input.js"));
145    fs::write(&to, src.as_bytes()).context("failed to write")?;
146
147    Ok(to)
148}