swc_ecma_minifier/option/
terser.rs

1//! Compatibility for terser config.
2
3use rustc_hash::FxHashMap;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use swc_atoms::Atom;
7use swc_common::{sync::Lrc, FileName, SourceMap, DUMMY_SP};
8use swc_ecma_ast::*;
9use swc_ecma_parser::parse_file_as_expr;
10use swc_ecma_utils::drop_span;
11
12use super::{
13    default_passes, true_by_default, CompressExperimentalOptions, CompressOptions, TopLevelOptions,
14};
15use crate::option::PureGetterOption;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18#[serde(deny_unknown_fields)]
19#[serde(untagged)]
20pub enum TerserEcmaVersion {
21    Num(usize),
22    Str(String),
23}
24
25impl Default for TerserEcmaVersion {
26    fn default() -> Self {
27        Self::Num(5)
28    }
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize, Default)]
32#[serde(deny_unknown_fields)]
33#[serde(untagged)]
34pub enum TerserPureGetterOption {
35    Bool(bool),
36    #[serde(rename = "strict")]
37    #[default]
38    Strict,
39    Str(String),
40}
41
42#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
43#[serde(deny_unknown_fields)]
44#[serde(untagged)]
45pub enum TerserInlineOption {
46    Bool(bool),
47    Num(u8),
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
51#[serde(deny_unknown_fields)]
52#[serde(untagged)]
53pub enum TerserTopLevelOptions {
54    Bool(bool),
55    Str(String),
56}
57
58#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
59#[serde(deny_unknown_fields)]
60#[serde(untagged)]
61pub enum TerserSequenceOptions {
62    Bool(bool),
63    Num(u8),
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
67#[serde(deny_unknown_fields)]
68#[serde(untagged)]
69pub enum TerserTopRetainOption {
70    Str(String),
71    Seq(Vec<Atom>),
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
75#[serde(deny_unknown_fields)]
76#[non_exhaustive]
77pub struct TerserExperimentalOptions {
78    #[serde(default)]
79    pub reduce_escaped_newline: Option<bool>,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
83#[serde(deny_unknown_fields)]
84pub struct TerserCompressorOptions {
85    #[serde(default)]
86    pub arguments: bool,
87
88    #[serde(default)]
89    pub arrows: Option<bool>,
90
91    #[serde(default)]
92    pub booleans: Option<bool>,
93
94    #[serde(default)]
95    pub booleans_as_integers: bool,
96
97    #[serde(default)]
98    pub collapse_vars: Option<bool>,
99
100    #[serde(default)]
101    pub comparisons: Option<bool>,
102
103    #[serde(default)]
104    pub computed_props: Option<bool>,
105
106    #[serde(default)]
107    pub conditionals: Option<bool>,
108
109    #[serde(default)]
110    pub dead_code: Option<bool>,
111
112    #[serde(default = "true_by_default")]
113    pub defaults: bool,
114
115    #[serde(default)]
116    pub directives: Option<bool>,
117
118    #[serde(default)]
119    pub drop_console: bool,
120
121    #[serde(default)]
122    pub drop_debugger: Option<bool>,
123
124    #[serde(default)]
125    pub ecma: TerserEcmaVersion,
126
127    #[serde(default)]
128    pub evaluate: Option<bool>,
129
130    #[serde(default)]
131    pub expression: bool,
132
133    #[serde(default)]
134    pub global_defs: FxHashMap<Atom, Value>,
135
136    #[serde(default)]
137    pub hoist_funs: bool,
138
139    #[serde(default)]
140    pub hoist_props: Option<bool>,
141
142    #[serde(default)]
143    pub hoist_vars: bool,
144
145    #[serde(default)]
146    pub ie8: bool,
147
148    #[serde(default)]
149    pub if_return: Option<bool>,
150
151    #[serde(default)]
152    pub inline: Option<TerserInlineOption>,
153
154    #[serde(default)]
155    pub join_vars: Option<bool>,
156
157    #[serde(default)]
158    pub keep_classnames: bool,
159
160    #[serde(default = "true_by_default")]
161    pub keep_fargs: bool,
162
163    #[serde(default)]
164    pub keep_fnames: bool,
165
166    #[serde(default)]
167    pub keep_infinity: bool,
168
169    #[serde(default)]
170    pub loops: Option<bool>,
171    // module        : false,
172    #[serde(default)]
173    pub negate_iife: Option<bool>,
174
175    #[serde(default = "default_passes")]
176    pub passes: usize,
177
178    #[serde(default)]
179    pub properties: Option<bool>,
180
181    #[serde(default)]
182    pub pure_getters: TerserPureGetterOption,
183
184    #[serde(default)]
185    pub pure_funcs: Vec<String>,
186
187    #[serde(default)]
188    pub reduce_funcs: Option<bool>,
189
190    #[serde(default)]
191    pub reduce_vars: Option<bool>,
192
193    #[serde(default)]
194    pub sequences: Option<TerserSequenceOptions>,
195
196    #[serde(default)]
197    pub side_effects: Option<bool>,
198
199    #[serde(default)]
200    pub switches: Option<bool>,
201
202    #[serde(default)]
203    pub top_retain: Option<TerserTopRetainOption>,
204
205    #[serde(default)]
206    pub toplevel: Option<TerserTopLevelOptions>,
207
208    #[serde(default)]
209    pub typeofs: Option<bool>,
210
211    #[serde(default)]
212    #[serde(rename = "unsafe")]
213    pub unsafe_passes: bool,
214
215    #[serde(default)]
216    pub unsafe_arrows: bool,
217
218    #[serde(default)]
219    pub unsafe_comps: bool,
220
221    #[serde(default)]
222    #[serde(rename = "unsafe_Function")]
223    pub unsafe_function: bool,
224
225    #[serde(default)]
226    pub unsafe_math: bool,
227
228    #[serde(default)]
229    pub unsafe_symbols: bool,
230
231    #[serde(default)]
232    pub unsafe_methods: bool,
233
234    #[serde(default)]
235    pub unsafe_proto: bool,
236
237    #[serde(default)]
238    pub unsafe_regexp: bool,
239
240    #[serde(default)]
241    pub unsafe_undefined: bool,
242
243    #[serde(default)]
244    pub unused: Option<bool>,
245
246    #[serde(default)]
247    pub module: bool,
248
249    #[serde(default)]
250    pub const_to_let: Option<bool>,
251
252    #[serde(default)]
253    pub pristine_globals: Option<bool>,
254
255    #[serde(default)]
256    pub experimental: Option<TerserExperimentalOptions>,
257}
258
259impl_default!(TerserCompressorOptions);
260
261impl TerserCompressorOptions {
262    pub fn into_config(self, cm: Lrc<SourceMap>) -> CompressOptions {
263        CompressOptions {
264            arguments: self.arguments,
265            arrows: self.arrows.unwrap_or(self.defaults),
266            bools: self.booleans.unwrap_or(self.defaults),
267            bools_as_ints: self.booleans_as_integers,
268            collapse_vars: self.collapse_vars.unwrap_or(self.defaults),
269            comparisons: self.comparisons.unwrap_or(self.defaults),
270            computed_props: self.computed_props.unwrap_or(self.defaults),
271            conditionals: self.conditionals.unwrap_or(self.defaults),
272            dead_code: self.dead_code.unwrap_or(self.defaults),
273            directives: self.directives.unwrap_or(self.defaults),
274            drop_console: self.drop_console,
275            drop_debugger: self.drop_debugger.unwrap_or(self.defaults),
276            ecma: self.ecma.into(),
277            evaluate: self.evaluate.unwrap_or(self.defaults),
278            expr: self.expression,
279            global_defs: self
280                .global_defs
281                .into_iter()
282                .map(|(k, v)| {
283                    let parse = |input: String| {
284                        let fm = cm.new_source_file(FileName::Anon.into(), input);
285
286                        parse_file_as_expr(
287                            &fm,
288                            Default::default(),
289                            Default::default(),
290                            None,
291                            &mut Vec::new(),
292                        )
293                        .map(drop_span)
294                        .unwrap_or_else(|err| {
295                            panic!("failed to parse `global_defs.{k}` of minifier options: {err:?}")
296                        })
297                    };
298                    let key = parse(if let Some(k) = k.strip_prefix('@') {
299                        k.to_string()
300                    } else {
301                        k.to_string()
302                    });
303
304                    (
305                        key,
306                        if k.starts_with('@') {
307                            parse(
308                                v.as_str()
309                                    .unwrap_or_else(|| {
310                                        panic!(
311                                            "Value of `global_defs.{k}` must be a string literal: "
312                                        )
313                                    })
314                                    .into(),
315                            )
316                        } else {
317                            value_to_expr(v)
318                        },
319                    )
320                })
321                .collect(),
322            hoist_fns: self.hoist_funs,
323            hoist_props: self.hoist_props.unwrap_or(self.defaults),
324            hoist_vars: self.hoist_vars,
325            ie8: self.ie8,
326            if_return: self.if_return.unwrap_or(self.defaults),
327            inline: self
328                .inline
329                .map(|v| match v {
330                    TerserInlineOption::Bool(v) => {
331                        if v {
332                            3
333                        } else {
334                            0
335                        }
336                    }
337                    TerserInlineOption::Num(n) => n,
338                })
339                .unwrap_or(if self.defaults { 3 } else { 0 }),
340            join_vars: self.join_vars.unwrap_or(self.defaults),
341            keep_classnames: self.keep_classnames,
342            keep_fargs: self.keep_fargs,
343            keep_fnames: self.keep_fnames,
344            keep_infinity: self.keep_infinity,
345            loops: self.loops.unwrap_or(self.defaults),
346            module: self.module,
347            negate_iife: self.negate_iife.unwrap_or(self.defaults),
348            passes: self.passes,
349            props: self.properties.unwrap_or(self.defaults),
350            pure_getters: match self.pure_getters {
351                TerserPureGetterOption::Bool(v) => PureGetterOption::Bool(v),
352                TerserPureGetterOption::Strict => PureGetterOption::Strict,
353                TerserPureGetterOption::Str(v) => {
354                    PureGetterOption::Str(v.split(',').map(From::from).collect())
355                }
356            },
357            reduce_fns: self.reduce_funcs.unwrap_or(self.defaults),
358            reduce_vars: self.reduce_vars.unwrap_or(self.defaults),
359            sequences: self
360                .sequences
361                .map(|v| match v {
362                    TerserSequenceOptions::Bool(v) => {
363                        if v {
364                            3
365                        } else {
366                            0
367                        }
368                    }
369                    TerserSequenceOptions::Num(v) => v,
370                })
371                .unwrap_or(if self.defaults { 3 } else { 0 }),
372            side_effects: self.side_effects.unwrap_or(self.defaults),
373            switches: self.switches.unwrap_or(self.defaults),
374            top_retain: self.top_retain.map(From::from).unwrap_or_default(),
375            top_level: self.toplevel.map(From::from),
376            typeofs: self.typeofs.unwrap_or(self.defaults),
377            unsafe_passes: self.unsafe_passes,
378            unsafe_arrows: self.unsafe_arrows,
379            unsafe_comps: self.unsafe_comps,
380            unsafe_function: self.unsafe_function,
381            unsafe_math: self.unsafe_math,
382            unsafe_symbols: self.unsafe_symbols,
383            unsafe_methods: self.unsafe_methods,
384            unsafe_proto: self.unsafe_proto,
385            unsafe_regexp: self.unsafe_regexp,
386            unsafe_undefined: self.unsafe_undefined,
387            unused: self.unused.unwrap_or(self.defaults),
388            const_to_let: self.const_to_let.unwrap_or(self.defaults),
389            pristine_globals: self.pristine_globals.unwrap_or(self.defaults),
390            pure_funcs: self
391                .pure_funcs
392                .into_iter()
393                .map(|input| {
394                    let fm = cm.new_source_file(FileName::Anon.into(), input);
395
396                    parse_file_as_expr(
397                        &fm,
398                        Default::default(),
399                        Default::default(),
400                        None,
401                        &mut Vec::new(),
402                    )
403                    .map(drop_span)
404                    .unwrap_or_else(|err| {
405                        panic!("failed to parse `pure_funcs` of minifier options: {err:?}")
406                    })
407                })
408                .collect(),
409            experimental: self
410                .experimental
411                .map(|experimental| {
412                    CompressExperimentalOptions::from_terser_with_defaults(
413                        experimental,
414                        self.defaults,
415                    )
416                })
417                .unwrap_or(CompressExperimentalOptions::from_defaults(self.defaults)),
418        }
419    }
420}
421
422impl From<TerserTopLevelOptions> for TopLevelOptions {
423    fn from(c: TerserTopLevelOptions) -> Self {
424        match c {
425            TerserTopLevelOptions::Bool(v) => TopLevelOptions { functions: v },
426            TerserTopLevelOptions::Str(..) => {
427                // TODO
428                TopLevelOptions { functions: false }
429            }
430        }
431    }
432}
433
434impl From<TerserEcmaVersion> for EsVersion {
435    fn from(v: TerserEcmaVersion) -> Self {
436        match v {
437            TerserEcmaVersion::Num(v) => match v {
438                3 => EsVersion::Es3,
439                5 => EsVersion::Es5,
440                6 | 2015 => EsVersion::Es2015,
441                2016 => EsVersion::Es2016,
442                2017 => EsVersion::Es2017,
443                2018 => EsVersion::Es2018,
444                2019 => EsVersion::Es2019,
445                2020 => EsVersion::Es2020,
446                2021 => EsVersion::Es2021,
447                2022 => EsVersion::Es2022,
448                _ => {
449                    panic!("`{v}` is not a valid ecmascript version")
450                }
451            },
452            TerserEcmaVersion::Str(v) => {
453                TerserEcmaVersion::Num(v.parse().expect("failed to parse version of ecmascript"))
454                    .into()
455            }
456        }
457    }
458}
459
460impl From<TerserTopRetainOption> for Vec<Atom> {
461    fn from(v: TerserTopRetainOption) -> Self {
462        match v {
463            TerserTopRetainOption::Str(s) => s
464                .split(',')
465                .filter(|s| s.trim() != "")
466                .map(|v| v.into())
467                .collect(),
468            TerserTopRetainOption::Seq(v) => v,
469        }
470    }
471}
472
473fn value_to_expr(v: Value) -> Box<Expr> {
474    match v {
475        Value::Null => Lit::Null(Null { span: DUMMY_SP }).into(),
476        Value::Bool(value) => Lit::Bool(Bool {
477            span: DUMMY_SP,
478            value,
479        })
480        .into(),
481        Value::Number(v) => {
482            trace_op!("Creating a numeric literal from value");
483
484            Lit::Num(Number {
485                span: DUMMY_SP,
486                value: v.as_f64().unwrap(),
487                raw: None,
488            })
489            .into()
490        }
491        Value::String(v) => {
492            let value: Atom = v.into();
493
494            Lit::Str(Str {
495                span: DUMMY_SP,
496                raw: None,
497                value,
498            })
499            .into()
500        }
501
502        Value::Array(arr) => {
503            let elems = arr
504                .into_iter()
505                .map(value_to_expr)
506                .map(|expr| Some(ExprOrSpread { spread: None, expr }))
507                .collect();
508            ArrayLit {
509                span: DUMMY_SP,
510                elems,
511            }
512            .into()
513        }
514
515        Value::Object(obj) => {
516            let props = obj
517                .into_iter()
518                .map(|(k, v)| (k, value_to_expr(v)))
519                .map(|(key, value)| KeyValueProp {
520                    key: PropName::Str(Str {
521                        span: DUMMY_SP,
522                        raw: None,
523                        value: key.into(),
524                    }),
525                    value,
526                })
527                .map(Prop::KeyValue)
528                .map(Box::new)
529                .map(PropOrSpread::Prop)
530                .collect();
531
532            ObjectLit {
533                span: DUMMY_SP,
534                props,
535            }
536            .into()
537        }
538    }
539}