turbopack_ecmascript/
minify.rs

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