rspack_javascript_compiler/compiler/
minify.rs1use 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 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 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")]
208pub struct JsMinifyOptions {
210 #[serde(default = "true_as_default")]
211 pub minify: bool,
213
214 #[serde(default)]
215 pub compress: BoolOrDataConfig<TerserCompressorOptions>,
217
218 #[serde(default)]
219 pub mangle: BoolOrDataConfig<MangleOptions>,
221
222 #[serde(default)]
223 pub format: JsMinifyFormatOptions,
225
226 #[serde(default)]
227 pub ecma: TerserEcmaVersion,
229
230 #[serde(default, rename = "keep_classnames")]
231 pub keep_class_names: bool,
233
234 #[serde(default, rename = "keep_fnames")]
235 pub keep_fn_names: bool,
237
238 #[serde(default)]
239 pub module: Option<bool>,
241
242 #[serde(default)]
243 pub safari10: bool,
245
246 #[serde(default)]
247 pub toplevel: bool,
249
250 #[serde(default)]
251 pub source_map: BoolOrDataConfig<TerserSourceMapKind>,
253
254 #[serde(default)]
255 pub output_path: Option<String>,
257
258 #[serde(default = "true_as_default")]
259 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}