dbg_swc/es/minifier/
reduce.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
use std::{
    env::current_exe,
    fs::{self, create_dir_all, read_to_string},
    path::{Path, PathBuf},
    process::Command,
    sync::Arc,
};

use anyhow::{Context, Result};
use clap::{ArgEnum, Args};
use rayon::prelude::*;
use sha1::{Digest, Sha1};
use swc_common::{SourceMap, GLOBALS};
use tempfile::TempDir;

use crate::{
    util::{all_js_files, parse_js, print_js, ChildGuard},
    CREDUCE_INPUT_ENV_VAR, CREDUCE_MODE_ENV_VAR,
};

/// Reduce input files to minimal reproduction cases
///
/// This command requires `creduce` and `terser` in PATH.
///
/// For `creduce`, see https://embed.cs.utah.edu/creduce/ for more information.
/// If you are using homebrew, install it with `brew install creduce`.
///
/// For `terser`, this command uses `npx terser` to invoke `terser`  for
/// comparison.
///
/// After reducing, the reduced file will be moved to `.swc-reduce` directory.
///
///
/// Note: This tool is not perfect, and it may reduce input file way too much,
/// or fail to reduce an input file.
#[derive(Debug, Args)]
pub struct ReduceCommand {
    /// The path to the input file. You can specify a directory if you want to
    /// reduce every '.js' file within a directory, in a recursive manner.
    pub path: PathBuf,

    /// In 'size' mode, this command tries to find the minimal input file where
    /// the size of the output file of swc minifier is larger than the one from
    /// terser.
    ///
    /// In 'semantics' mode, this command tries to reduce the input file to a
    /// minimal reproduction case which triggers the bug.
    #[clap(long, arg_enum)]
    pub mode: ReduceMode,

    /// If true, the input file will be removed after the reduction. This can be
    /// used for pausing and resuming the process of reducing.
    #[clap(long)]
    pub remove: bool,
}

#[derive(Debug, Clone, Copy, ArgEnum)]
pub enum ReduceMode {
    Size,
    Semantics,
}

impl ReduceCommand {
    pub fn run(self, cm: Arc<SourceMap>) -> Result<()> {
        let js_files = all_js_files(&self.path)?;

        GLOBALS.with(|globals| {
            js_files
                .into_par_iter()
                .map(|path| GLOBALS.set(globals, || self.reduce_file(cm.clone(), &path)))
                .collect::<Result<Vec<_>>>()
        })?;

        Ok(())
    }

    fn reduce_file(&self, cm: Arc<SourceMap>, src_path: &Path) -> Result<()> {
        // Strip comments to workaround a bug of creduce

        let fm = cm.load_file(src_path).context("failed to prepare file")?;
        let m = parse_js(fm)?;
        let code = print_js(cm, &m.module, false)?;

        fs::write(src_path, code.as_bytes()).context("failed to strip comments")?;

        //
        let dir = TempDir::new().context("failed to create a temp directory")?;

        let input = dir.path().join("input.js");

        fs::copy(src_path, &input).context("failed to copy")?;

        let mut c = Command::new("creduce");

        c.arg("--not-c");

        c.env(
            CREDUCE_MODE_ENV_VAR,
            match self.mode {
                ReduceMode::Size => "SIZE",
                ReduceMode::Semantics => "SEMANTICS",
            },
        );
        c.env(CREDUCE_INPUT_ENV_VAR, &input);

        let exe = current_exe()?;
        c.arg(&exe);
        c.arg(&input);
        let mut child = ChildGuard(c.spawn().context("failed to run creduce")?);
        let status = child.0.wait().context("failed to wait for creduce")?;

        if status.success() {
            move_to_data_dir(&input)?;
        }

        if let Some(1) = status.code() {
            if self.remove {
                fs::remove_file(src_path).context("failed to remove")?;
            }
        } else {
            dbg!(&status, status.code());
        }

        Ok(())
    }
}

fn move_to_data_dir(input_path: &Path) -> Result<PathBuf> {
    let src = read_to_string(input_path).context("failed to read input file")?;

    // create a Sha1 object
    let mut hasher = Sha1::new();

    // process input message
    hasher.update(src.as_bytes());

    // acquire hash digest in the form of GenericArray,
    // which in this case is equivalent to [u8; 20]
    let result = hasher.finalize();
    let hash_str = format!("{:x}", result);

    create_dir_all(format!(".swc-reduce/{}", hash_str)).context("failed to create `.data`")?;

    let to = PathBuf::from(format!(".swc-reduce/{}/input.js", hash_str));
    fs::write(&to, src.as_bytes()).context("failed to write")?;

    Ok(to)
}