Skip to main content

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