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            merge_imports: self.defaults,
347            module: self.module,
348            negate_iife: self.negate_iife.unwrap_or(self.defaults),
349            passes: self.passes,
350            props: self.properties.unwrap_or(self.defaults),
351            pure_getters: match self.pure_getters {
352                TerserPureGetterOption::Bool(v) => PureGetterOption::Bool(v),
353                TerserPureGetterOption::Strict => PureGetterOption::Strict,
354                TerserPureGetterOption::Str(v) => {
355                    PureGetterOption::Str(v.split(',').map(From::from).collect())
356                }
357            },
358            reduce_fns: self.reduce_funcs.unwrap_or(self.defaults),
359            reduce_vars: self.reduce_vars.unwrap_or(self.defaults),
360            sequences: self
361                .sequences
362                .map(|v| match v {
363                    TerserSequenceOptions::Bool(v) => {
364                        if v {
365                            3
366                        } else {
367                            0
368                        }
369                    }
370                    TerserSequenceOptions::Num(v) => v,
371                })
372                .unwrap_or(if self.defaults { 3 } else { 0 }),
373            side_effects: self.side_effects.unwrap_or(self.defaults),
374            switches: self.switches.unwrap_or(self.defaults),
375            top_retain: self.top_retain.map(From::from).unwrap_or_default(),
376            top_level: self.toplevel.map(From::from),
377            typeofs: self.typeofs.unwrap_or(self.defaults),
378            unsafe_passes: self.unsafe_passes,
379            unsafe_arrows: self.unsafe_arrows,
380            unsafe_comps: self.unsafe_comps,
381            unsafe_function: self.unsafe_function,
382            unsafe_math: self.unsafe_math,
383            unsafe_symbols: self.unsafe_symbols,
384            unsafe_methods: self.unsafe_methods,
385            unsafe_proto: self.unsafe_proto,
386            unsafe_regexp: self.unsafe_regexp,
387            unsafe_undefined: self.unsafe_undefined,
388            unused: self.unused.unwrap_or(self.defaults),
389            const_to_let: self.const_to_let.unwrap_or(self.defaults),
390            pristine_globals: self.pristine_globals.unwrap_or(self.defaults),
391            pure_funcs: self
392                .pure_funcs
393                .into_iter()
394                .map(|input| {
395                    let fm = cm.new_source_file(FileName::Anon.into(), input);
396
397                    parse_file_as_expr(
398                        &fm,
399                        Default::default(),
400                        Default::default(),
401                        None,
402                        &mut Vec::new(),
403                    )
404                    .map(drop_span)
405                    .unwrap_or_else(|err| {
406                        panic!("failed to parse `pure_funcs` of minifier options: {err:?}")
407                    })
408                })
409                .collect(),
410            experimental: self
411                .experimental
412                .map(|experimental| {
413                    CompressExperimentalOptions::from_terser_with_defaults(
414                        experimental,
415                        self.defaults,
416                    )
417                })
418                .unwrap_or(CompressExperimentalOptions::from_defaults(self.defaults)),
419        }
420    }
421}
422
423impl From<TerserTopLevelOptions> for TopLevelOptions {
424    fn from(c: TerserTopLevelOptions) -> Self {
425        match c {
426            TerserTopLevelOptions::Bool(v) => TopLevelOptions { functions: v },
427            TerserTopLevelOptions::Str(..) => {
428                // TODO
429                TopLevelOptions { functions: false }
430            }
431        }
432    }
433}
434
435impl From<TerserEcmaVersion> for EsVersion {
436    fn from(v: TerserEcmaVersion) -> Self {
437        match v {
438            TerserEcmaVersion::Num(v) => match v {
439                3 => EsVersion::Es3,
440                5 => EsVersion::Es5,
441                6 | 2015 => EsVersion::Es2015,
442                2016 => EsVersion::Es2016,
443                2017 => EsVersion::Es2017,
444                2018 => EsVersion::Es2018,
445                2019 => EsVersion::Es2019,
446                2020 => EsVersion::Es2020,
447                2021 => EsVersion::Es2021,
448                2022 => EsVersion::Es2022,
449                _ => {
450                    panic!("`{v}` is not a valid ecmascript version")
451                }
452            },
453            TerserEcmaVersion::Str(v) => {
454                TerserEcmaVersion::Num(v.parse().expect("failed to parse version of ecmascript"))
455                    .into()
456            }
457        }
458    }
459}
460
461impl From<TerserTopRetainOption> for Vec<Atom> {
462    fn from(v: TerserTopRetainOption) -> Self {
463        match v {
464            TerserTopRetainOption::Str(s) => s
465                .split(',')
466                .filter(|s| s.trim() != "")
467                .map(|v| v.into())
468                .collect(),
469            TerserTopRetainOption::Seq(v) => v,
470        }
471    }
472}
473
474fn value_to_expr(v: Value) -> Box<Expr> {
475    match v {
476        Value::Null => Lit::Null(Null { span: DUMMY_SP }).into(),
477        Value::Bool(value) => Lit::Bool(Bool {
478            span: DUMMY_SP,
479            value,
480        })
481        .into(),
482        Value::Number(v) => {
483            trace_op!("Creating a numeric literal from value");
484
485            Lit::Num(Number {
486                span: DUMMY_SP,
487                value: v.as_f64().unwrap(),
488                raw: None,
489            })
490            .into()
491        }
492        Value::String(v) => {
493            let value = v.into();
494
495            Lit::Str(Str {
496                span: DUMMY_SP,
497                raw: None,
498                value,
499            })
500            .into()
501        }
502
503        Value::Array(arr) => {
504            let elems = arr
505                .into_iter()
506                .map(value_to_expr)
507                .map(|expr| Some(ExprOrSpread { spread: None, expr }))
508                .collect();
509            ArrayLit {
510                span: DUMMY_SP,
511                elems,
512            }
513            .into()
514        }
515
516        Value::Object(obj) => {
517            let props = obj
518                .into_iter()
519                .map(|(k, v)| (k, value_to_expr(v)))
520                .map(|(key, value)| KeyValueProp {
521                    key: PropName::Str(Str {
522                        span: DUMMY_SP,
523                        raw: None,
524                        value: key.into(),
525                    }),
526                    value,
527                })
528                .map(Prop::KeyValue)
529                .map(Box::new)
530                .map(PropOrSpread::Prop)
531                .collect();
532
533            ObjectLit {
534                span: DUMMY_SP,
535                props,
536            }
537            .into()
538        }
539    }
540}