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