rspack_javascript_compiler/compiler/
minify.rs

1use std::sync::Arc;
2
3use rspack_error::BatchErrors;
4use rspack_util::swc::minify_file_comments;
5use rustc_hash::FxHashMap;
6use serde::{Deserialize, Serialize};
7pub use swc_core::base::BoolOrDataConfig;
8use swc_core::{
9  atoms::Atom,
10  base::{
11    BoolOr,
12    config::{IsModule, JsMinifyCommentOption, JsMinifyFormatOptions, SourceMapsConfig},
13  },
14  common::{
15    BytePos, FileName, Mark,
16    comments::{Comments, SingleThreadedComments},
17    errors::HANDLER,
18  },
19  ecma::{
20    ast::Ident,
21    parser::{EsSyntax, Syntax},
22    transforms::base::{
23      fixer::{fixer, paren_remover},
24      helpers::{self, Helpers},
25      hygiene::hygiene,
26      resolver,
27    },
28    visit::{Visit, VisitMutWith, noop_visit_type},
29  },
30};
31pub use swc_ecma_minifier::option::{
32  MangleOptions, MinifyOptions, TopLevelOptions,
33  terser::{TerserCompressorOptions, TerserEcmaVersion},
34};
35
36use super::{
37  JavaScriptCompiler, TransformOutput,
38  stringify::{PrintOptions, SourceMapConfig},
39};
40use crate::error::with_rspack_error_handler;
41
42impl JavaScriptCompiler {
43  /// Minifies the given JavaScript source code.
44  ///
45  /// This method takes a filename, the source code to minify, minification options, and an optional function to operate on comments.
46  /// It returns a `TransformOutput` containing the minified code and an optional source map.
47  ///
48  /// # Parameters
49  ///
50  /// - `filename`: The name of the file being minified.
51  /// - `source`: The source code to minify.
52  /// - `opts`: The options for minification.
53  /// - `comments_op`: An optional function to operate on the comments in the source code.
54  ///
55  /// # Returns
56  ///
57  /// A `Result` containing a `TransformOutput` if the minification is successful, or a `BatchErrors` if an error occurs.
58  pub fn minify<S: Into<String>, F>(
59    &self,
60    filename: FileName,
61    source: S,
62    opts: JsMinifyOptions,
63    comments_op: Option<F>,
64  ) -> Result<TransformOutput, BatchErrors>
65  where
66    F: for<'a> FnOnce(&'a SingleThreadedComments),
67  {
68    self.run(|| -> Result<TransformOutput, BatchErrors> {
69      with_rspack_error_handler("Minify Error".to_string(), self.cm.clone(), |handler| {
70        let fm = self.cm.new_source_file(Arc::new(filename), source.into());
71
72        let source_map = opts
73          .source_map
74          .as_ref()
75          .map(|_| SourceMapsConfig::Bool(true))
76          .unwrap_as_option(|v| {
77            Some(match v {
78              Some(true) => SourceMapsConfig::Bool(true),
79              _ => SourceMapsConfig::Bool(false),
80            })
81          })
82          .expect("TODO:");
83
84        let mut min_opts = MinifyOptions {
85          compress: opts
86            .compress
87            .clone()
88            .unwrap_as_option(|default| match default {
89              Some(true) | None => Some(Default::default()),
90              _ => None,
91            })
92            .map(|v| v.into_config(self.cm.clone())),
93          mangle: opts
94            .mangle
95            .clone()
96            .unwrap_as_option(|default| match default {
97              Some(true) | None => Some(Default::default()),
98              _ => None,
99            }),
100          ..Default::default()
101        };
102
103        // top_level defaults to true if module is true
104
105        // https://github.com/swc-project/swc/issues/2254
106        if opts.module.unwrap_or(false) {
107          if let Some(opts) = &mut min_opts.compress
108            && opts.top_level.is_none()
109          {
110            opts.top_level = Some(TopLevelOptions { functions: true });
111          }
112
113          if let Some(opts) = &mut min_opts.mangle {
114            opts.top_level = Some(true);
115          }
116        }
117
118        let comments = SingleThreadedComments::default();
119
120        let target = opts.ecma.clone().into();
121        let program = self.parse_js(
122          fm.clone(),
123          target,
124          Syntax::Es(EsSyntax {
125            jsx: true,
126            decorators: true,
127            decorators_before_export: true,
128            import_attributes: true,
129            ..Default::default()
130          }),
131          opts
132            .module
133            .map_or_else(|| IsModule::Unknown, IsModule::Bool),
134          Some(&comments),
135        )?;
136
137        let unresolved_mark = Mark::new();
138        let top_level_mark = Mark::new();
139
140        let is_mangler_enabled = min_opts.mangle.is_some();
141
142        let program = helpers::HELPERS.set(&Helpers::new(false), || {
143          HANDLER.set(handler, || {
144            let program = program
145              .apply(&mut resolver(unresolved_mark, top_level_mark, false))
146              .apply(&mut paren_remover(Some(&comments as &dyn Comments)));
147            let mut program = swc_ecma_minifier::optimize(
148              program,
149              self.cm.clone(),
150              Some(&comments),
151              None,
152              &min_opts,
153              &swc_ecma_minifier::option::ExtraOptions {
154                unresolved_mark,
155                top_level_mark,
156                mangle_name_cache: None,
157              },
158            );
159
160            if !is_mangler_enabled {
161              program.visit_mut_with(&mut hygiene())
162            }
163            program.apply(&mut fixer(Some(&comments as &dyn Comments)))
164          })
165        });
166
167        if let Some(op) = comments_op {
168          op(&comments);
169        }
170
171        minify_file_comments(
172          &comments,
173          &opts
174            .format
175            .comments
176            .clone()
177            .into_inner()
178            .unwrap_or(BoolOr::Data(JsMinifyCommentOption::PreserveSomeComments)),
179          opts.format.preserve_annotations,
180        );
181
182        let print_options = PrintOptions {
183          source_len: fm.byte_length(),
184          source_map: self.cm.clone(),
185          target,
186          source_map_config: SourceMapConfig {
187            enable: source_map.enabled(),
188            inline_sources_content: opts.inline_sources_content,
189            emit_columns: true,
190            names: Default::default(),
191          },
192          input_source_map: None,
193          minify: opts.minify,
194          comments: Some(&comments),
195          preamble: &opts.format.preamble,
196          ascii_only: opts.format.ascii_only,
197          inline_script: opts.format.inline_script,
198        };
199
200        self.print(&program, print_options).map_err(|e| e.into())
201      })
202    })
203  }
204}
205
206#[derive(Debug, Clone, Default, Deserialize, Serialize)]
207#[serde(rename_all = "camelCase")]
208/// Represents the options for minifying JavaScript code.
209pub struct JsMinifyOptions {
210  #[serde(default = "true_as_default")]
211  /// Indicates whether to minify the code.
212  pub minify: bool,
213
214  #[serde(default)]
215  /// Configuration for compressing the code.
216  pub compress: BoolOrDataConfig<TerserCompressorOptions>,
217
218  #[serde(default)]
219  /// Configuration for mangling names in the code.
220  pub mangle: BoolOrDataConfig<MangleOptions>,
221
222  #[serde(default)]
223  /// Options for formatting the minified code.
224  pub format: JsMinifyFormatOptions,
225
226  #[serde(default)]
227  /// The ECMAScript version to target.
228  pub ecma: TerserEcmaVersion,
229
230  #[serde(default, rename = "keep_classnames")]
231  /// Indicates whether to keep class names unchanged.
232  pub keep_class_names: bool,
233
234  #[serde(default, rename = "keep_fnames")]
235  /// Indicates whether to keep function names unchanged.
236  pub keep_fn_names: bool,
237
238  #[serde(default)]
239  /// Indicates whether to wrap the code in a module.
240  pub module: Option<bool>,
241
242  #[serde(default)]
243  /// Indicates whether to support Safari 10.
244  pub safari10: bool,
245
246  #[serde(default)]
247  /// Indicates whether to scope the top level to the global object.
248  pub toplevel: bool,
249
250  #[serde(default)]
251  /// Configuration for source maps.
252  pub source_map: BoolOrDataConfig<TerserSourceMapKind>,
253
254  #[serde(default)]
255  /// The path where the minified output will be written.
256  pub output_path: Option<String>,
257
258  #[serde(default = "true_as_default")]
259  /// Indicates whether to inline the source content in the source map.
260  pub inline_sources_content: bool,
261}
262
263const fn true_as_default() -> bool {
264  true
265}
266
267#[derive(Debug, Clone, Default, Serialize, Deserialize)]
268pub struct TerserSourceMapKind {
269  pub filename: Option<String>,
270  pub url: Option<String>,
271  pub root: Option<String>,
272  pub content: Option<String>,
273}
274
275pub struct IdentCollector {
276  pub names: FxHashMap<BytePos, Atom>,
277}
278
279impl Visit for IdentCollector {
280  noop_visit_type!();
281
282  fn visit_ident(&mut self, ident: &Ident) {
283    self.names.insert(ident.span.lo, ident.sym.clone());
284  }
285}