turbopack_ecmascript/
minify.rs

1use std::sync::Arc;
2
3use anyhow::{Context, Result, bail};
4use bytes_str::BytesStr;
5use swc_core::{
6    atoms::atom,
7    base::try_with_handler,
8    common::{
9        BytePos, FileName, FilePathMapping, GLOBALS, LineCol, Mark, SourceMap as SwcSourceMap,
10        comments::{Comments, SingleThreadedComments},
11    },
12    ecma::{
13        self,
14        ast::{EsVersion, Program},
15        codegen::{
16            Emitter,
17            text_writer::{self, JsWriter, WriteJs},
18        },
19        minifier::option::{CompressOptions, ExtraOptions, MangleOptions, MinifyOptions},
20        parser::{Parser, StringInput, Syntax, lexer::Lexer},
21        transforms::base::{
22            fixer::paren_remover,
23            hygiene::{self, hygiene_with_config},
24        },
25    },
26};
27use tracing::{Level, instrument};
28use turbopack_core::{
29    chunk::MangleType,
30    code_builder::{Code, CodeBuilder},
31};
32
33use crate::parse::generate_js_source_map;
34
35#[instrument(level = Level::INFO, skip_all)]
36pub fn minify(code: Code, source_maps: bool, mangle: Option<MangleType>) -> Result<Code> {
37    let source_maps = source_maps
38        .then(|| code.generate_source_map_ref())
39        .transpose()?;
40
41    let source_code = BytesStr::from_utf8(code.into_source_code().into_bytes())?;
42
43    let cm = Arc::new(SwcSourceMap::new(FilePathMapping::empty()));
44    let (src, mut src_map_buf) = {
45        let fm = cm.new_source_file(FileName::Anon.into(), source_code);
46
47        // Collect all comments and pass to the minifier so that `PURE` comments are respected.
48        let comments = SingleThreadedComments::default();
49
50        let lexer = Lexer::new(
51            Syntax::default(),
52            EsVersion::latest(),
53            StringInput::from(&*fm),
54            Some(&comments),
55        );
56        let mut parser = Parser::new_from(lexer);
57
58        let program = try_with_handler(cm.clone(), Default::default(), |handler| {
59            GLOBALS.set(&Default::default(), || {
60                let program = match parser.parse_program() {
61                    Ok(program) => program,
62                    Err(err) => {
63                        err.into_diagnostic(handler).emit();
64                        bail!("failed to parse source code\n{}", fm.src)
65                    }
66                };
67                let unresolved_mark = Mark::new();
68                let top_level_mark = Mark::new();
69
70                let program = program.apply(paren_remover(Some(&comments)));
71
72                let mut program = program.apply(swc_core::ecma::transforms::base::resolver(
73                    unresolved_mark,
74                    top_level_mark,
75                    false,
76                ));
77
78                program = swc_core::ecma::minifier::optimize(
79                    program,
80                    cm.clone(),
81                    Some(&comments),
82                    None,
83                    &MinifyOptions {
84                        compress: Some(CompressOptions {
85                            // Only run 2 passes, this is a tradeoff between performance and
86                            // compression size. Default is 3 passes.
87                            passes: 2,
88                            keep_classnames: mangle.is_none(),
89                            keep_fnames: mangle.is_none(),
90                            ..Default::default()
91                        }),
92                        mangle: mangle.map(|mangle| {
93                            let reserved = vec![atom!("AbortSignal")];
94                            match mangle {
95                                MangleType::OptimalSize => MangleOptions {
96                                    reserved,
97                                    ..Default::default()
98                                },
99                                MangleType::Deterministic => MangleOptions {
100                                    reserved,
101                                    disable_char_freq: true,
102                                    ..Default::default()
103                                },
104                            }
105                        }),
106                        ..Default::default()
107                    },
108                    &ExtraOptions {
109                        top_level_mark,
110                        unresolved_mark,
111                        mangle_name_cache: None,
112                    },
113                );
114
115                if mangle.is_none() {
116                    program.mutate(hygiene_with_config(hygiene::Config {
117                        top_level_mark,
118                        ..Default::default()
119                    }));
120                }
121
122                Ok(program.apply(ecma::transforms::base::fixer::fixer(Some(
123                    &comments as &dyn Comments,
124                ))))
125            })
126        })
127        .map_err(|e| e.to_pretty_error())?;
128
129        print_program(cm.clone(), program, source_maps.is_some())?
130    };
131
132    let mut builder = CodeBuilder::new(source_maps.is_some());
133    if let Some(original_map) = source_maps.as_ref() {
134        src_map_buf.shrink_to_fit();
135        builder.push_source(
136            &src.into(),
137            Some(generate_js_source_map(
138                &*cm,
139                src_map_buf,
140                Some(original_map),
141                true,
142                // We do not inline source contents.
143                // We provide a synthesized value to `cm.new_source_file` above, so it cannot be
144                // the value user expect anyway.
145                false,
146            )?),
147        );
148    } else {
149        builder.push_source(&src.into(), None);
150    }
151    Ok(builder.build())
152}
153
154// From https://github.com/swc-project/swc/blob/11efd4e7c5e8081f8af141099d3459c3534c1e1d/crates/swc/src/lib.rs#L523-L560
155fn print_program(
156    cm: Arc<SwcSourceMap>,
157    program: Program,
158    source_maps: bool,
159) -> Result<(String, Vec<(BytePos, LineCol)>)> {
160    let mut src_map_buf = vec![];
161
162    let src = {
163        let mut buf = vec![];
164        {
165            let wr = Box::new(text_writer::omit_trailing_semi(Box::new(JsWriter::new(
166                cm.clone(),
167                "\n",
168                &mut buf,
169                source_maps.then_some(&mut src_map_buf),
170            )))) as Box<dyn WriteJs>;
171
172            let mut emitter = Emitter {
173                cfg: swc_core::ecma::codegen::Config::default().with_minify(true),
174                comments: None,
175                cm: cm.clone(),
176                wr,
177            };
178
179            emitter
180                .emit_program(&program)
181                .context("failed to emit module")?;
182        }
183        // Invalid utf8 is valid in javascript world.
184        // SAFETY: SWC generates valid utf8.
185        unsafe { String::from_utf8_unchecked(buf) }
186    };
187
188    Ok((src, src_map_buf))
189}