swc/
builder.rs

1use std::sync::Arc;
2
3use compat::es2015::regenerator;
4use either::Either;
5use rustc_hash::FxHashMap;
6use swc_atoms::Atom;
7use swc_common::{
8    comments::Comments, errors::Handler, sync::Lrc, util::take::Take, FileName, Mark, SourceMap,
9};
10use swc_ecma_ast::{EsVersion, Module, Pass, Script};
11use swc_ecma_minifier::option::{terser::TerserTopLevelOptions, MinifyOptions};
12use swc_ecma_parser::Syntax;
13use swc_ecma_transforms::{
14    compat,
15    feature::{enable_available_feature_from_es_version, FeatureFlag},
16    fixer::{fixer, paren_remover},
17    helpers,
18    hygiene::{self, hygiene_with_config},
19    modules::{self, path::ImportResolver},
20    optimization::const_modules,
21    resolver, Assumptions,
22};
23use swc_ecma_visit::{noop_visit_mut_type, visit_mut_pass, VisitMut, VisitMutWith};
24use swc_visit::Optional;
25
26use crate::config::{GlobalPassOption, JsMinifyOptions, ModuleConfig};
27
28/// Builder is used to create a high performance `Compiler`.
29pub struct PassBuilder<'a, 'b, P: Pass> {
30    cm: &'a Arc<SourceMap>,
31    handler: &'b Handler,
32    env: Option<swc_ecma_preset_env::Config>,
33    pass: P,
34    /// [Mark] for top level bindings .
35    top_level_mark: Mark,
36
37    /// [Mark] for unresolved refernces.
38    unresolved_mark: Mark,
39
40    target: EsVersion,
41    loose: bool,
42    assumptions: Assumptions,
43    hygiene: Option<hygiene::Config>,
44    fixer: bool,
45    inject_helpers: bool,
46    minify: Option<JsMinifyOptions>,
47    regenerator: regenerator::Config,
48}
49
50impl<'a, 'b, P: Pass> PassBuilder<'a, 'b, P> {
51    pub fn new(
52        cm: &'a Arc<SourceMap>,
53        handler: &'b Handler,
54        loose: bool,
55        assumptions: Assumptions,
56        top_level_mark: Mark,
57        unresolved_mark: Mark,
58        pass: P,
59    ) -> Self {
60        PassBuilder {
61            cm,
62            handler,
63            env: None,
64            pass,
65            top_level_mark,
66            unresolved_mark,
67            target: EsVersion::Es5,
68            loose,
69            assumptions,
70            hygiene: Some(Default::default()),
71            fixer: true,
72            inject_helpers: true,
73            minify: None,
74            regenerator: Default::default(),
75        }
76    }
77
78    pub fn then<N>(self, next: N) -> PassBuilder<'a, 'b, (P, N)>
79    where
80        N: Pass,
81    {
82        let pass = (self.pass, next);
83        PassBuilder {
84            cm: self.cm,
85            handler: self.handler,
86            env: self.env,
87            pass,
88            top_level_mark: self.top_level_mark,
89            unresolved_mark: self.unresolved_mark,
90            target: self.target,
91            loose: self.loose,
92            assumptions: self.assumptions,
93            hygiene: self.hygiene,
94            fixer: self.fixer,
95            inject_helpers: self.inject_helpers,
96            minify: self.minify,
97            regenerator: self.regenerator,
98        }
99    }
100
101    pub fn skip_helper_injection(mut self, skip: bool) -> Self {
102        self.inject_helpers = !skip;
103        self
104    }
105
106    pub fn minify(mut self, options: Option<JsMinifyOptions>) -> Self {
107        self.minify = options;
108        self
109    }
110
111    /// Note: fixer is enabled by default.
112    pub fn fixer(mut self, enable: bool) -> Self {
113        self.fixer = enable;
114        self
115    }
116
117    /// Note: hygiene is enabled by default.
118    ///
119    /// If you pass [None] to this method, the `hygiene` pass will be disabled.
120    pub fn hygiene(mut self, config: Option<hygiene::Config>) -> Self {
121        self.hygiene = config;
122        self
123    }
124
125    pub fn const_modules(
126        self,
127        globals: FxHashMap<Atom, FxHashMap<Atom, String>>,
128    ) -> PassBuilder<'a, 'b, (P, impl Pass)> {
129        let cm = self.cm.clone();
130        self.then(const_modules(cm, globals))
131    }
132
133    pub fn inline_globals(self, c: GlobalPassOption) -> PassBuilder<'a, 'b, (P, impl Pass)> {
134        let pass = c.build(self.cm, self.handler);
135        self.then(pass)
136    }
137
138    pub fn target(mut self, target: EsVersion) -> Self {
139        self.target = target;
140        self
141    }
142
143    pub fn preset_env(mut self, env: Option<swc_ecma_preset_env::Config>) -> Self {
144        self.env = env;
145        self
146    }
147
148    pub fn regenerator(mut self, config: regenerator::Config) -> Self {
149        self.regenerator = config;
150        self
151    }
152
153    /// # Arguments
154    /// ## module
155    ///  - Use `None` if you want swc to emit import statements.
156    ///
157    ///
158    /// Returned pass includes
159    ///
160    ///  - compatibility helper
161    ///  - module handler
162    ///  - helper injector
163    ///  - identifier hygiene handler if enabled
164    ///  - fixer if enabled
165    pub fn finalize<'cmt>(
166        self,
167        syntax: Syntax,
168        module: Option<ModuleConfig>,
169        comments: Option<&'cmt dyn Comments>,
170        resolver: Option<(FileName, Arc<dyn ImportResolver>)>,
171    ) -> impl 'cmt + Pass
172    where
173        P: 'cmt,
174    {
175        let (need_analyzer, import_interop, ignore_dynamic) = match module {
176            Some(ModuleConfig::CommonJs(ref c)) => (true, c.import_interop(), c.ignore_dynamic),
177            Some(ModuleConfig::Amd(ref c)) => {
178                (true, c.config.import_interop(), c.config.ignore_dynamic)
179            }
180            Some(ModuleConfig::Umd(ref c)) => {
181                (true, c.config.import_interop(), c.config.ignore_dynamic)
182            }
183            Some(ModuleConfig::SystemJs(_))
184            | Some(ModuleConfig::Es6(..))
185            | Some(ModuleConfig::NodeNext(..))
186            | None => (false, true.into(), true),
187        };
188
189        let mut feature_flag = FeatureFlag::empty();
190
191        // compat
192        let compat_pass = if let Some(env) = self.env {
193            Either::Left(swc_ecma_preset_env::preset_env(
194                self.unresolved_mark,
195                comments,
196                env,
197                self.assumptions,
198                &mut feature_flag,
199            ))
200        } else {
201            let assumptions = self.assumptions;
202
203            feature_flag = enable_available_feature_from_es_version(self.target);
204
205            Either::Right((
206                Optional::new(
207                    compat::class_fields_use_set::class_fields_use_set(assumptions.pure_getters),
208                    assumptions.set_public_class_fields,
209                ),
210                Optional::new(
211                    compat::es2022::es2022(
212                        compat::es2022::Config {
213                            class_properties: compat::es2022::class_properties::Config {
214                                private_as_properties: assumptions.private_fields_as_properties,
215                                constant_super: assumptions.constant_super,
216                                set_public_fields: assumptions.set_public_class_fields,
217                                no_document_all: assumptions.no_document_all,
218                                pure_getter: assumptions.pure_getters,
219                            },
220                        },
221                        self.unresolved_mark,
222                    ),
223                    should_enable(self.target, EsVersion::Es2022),
224                ),
225                Optional::new(
226                    compat::es2021::es2021(),
227                    should_enable(self.target, EsVersion::Es2021),
228                ),
229                Optional::new(
230                    compat::es2020::es2020(
231                        compat::es2020::Config {
232                            nullish_coalescing: compat::es2020::nullish_coalescing::Config {
233                                no_document_all: assumptions.no_document_all,
234                            },
235                            optional_chaining: compat::es2020::optional_chaining::Config {
236                                no_document_all: assumptions.no_document_all,
237                                pure_getter: assumptions.pure_getters,
238                            },
239                        },
240                        self.unresolved_mark,
241                    ),
242                    should_enable(self.target, EsVersion::Es2020),
243                ),
244                Optional::new(
245                    compat::es2019::es2019(),
246                    should_enable(self.target, EsVersion::Es2019),
247                ),
248                Optional::new(
249                    compat::es2018(compat::es2018::Config {
250                        object_rest_spread: compat::es2018::object_rest_spread::Config {
251                            no_symbol: assumptions.object_rest_no_symbols,
252                            set_property: assumptions.set_spread_properties,
253                            pure_getters: assumptions.pure_getters,
254                        },
255                    }),
256                    should_enable(self.target, EsVersion::Es2018),
257                ),
258                Optional::new(
259                    compat::es2017(
260                        compat::es2017::Config {
261                            async_to_generator: compat::es2017::async_to_generator::Config {
262                                ignore_function_name: assumptions.ignore_function_name,
263                                ignore_function_length: assumptions.ignore_function_length,
264                            },
265                        },
266                        self.unresolved_mark,
267                    ),
268                    should_enable(self.target, EsVersion::Es2017),
269                ),
270                Optional::new(
271                    compat::es2016(),
272                    should_enable(self.target, EsVersion::Es2016),
273                ),
274                Optional::new(
275                    compat::es2015(
276                        self.unresolved_mark,
277                        comments,
278                        compat::es2015::Config {
279                            classes: compat::es2015::classes::Config {
280                                constant_super: assumptions.constant_super,
281                                no_class_calls: assumptions.no_class_calls,
282                                set_class_methods: assumptions.set_class_methods,
283                                super_is_callable_constructor: assumptions
284                                    .super_is_callable_constructor,
285                            },
286                            computed_props: compat::es2015::computed_props::Config {
287                                loose: self.loose,
288                            },
289                            for_of: compat::es2015::for_of::Config {
290                                assume_array: false,
291                                loose: self.loose,
292                            },
293                            spread: compat::es2015::spread::Config { loose: self.loose },
294                            destructuring: compat::es2015::destructuring::Config {
295                                loose: self.loose,
296                            },
297                            regenerator: self.regenerator,
298                            template_literal: compat::es2015::template_literal::Config {
299                                ignore_to_primitive: assumptions.ignore_to_primitive_hint,
300                                mutable_template: assumptions.mutable_template_object,
301                            },
302                            parameters: compat::es2015::parameters::Config {
303                                ignore_function_length: assumptions.ignore_function_length,
304                            },
305                            typescript: syntax.typescript(),
306                        },
307                    ),
308                    should_enable(self.target, EsVersion::Es2015),
309                ),
310                Optional::new(
311                    compat::es3(true),
312                    cfg!(feature = "es3") && self.target == EsVersion::Es3,
313                ),
314            ))
315        };
316
317        let is_mangler_enabled = self
318            .minify
319            .as_ref()
320            .map(|v| v.mangle.is_obj() || v.mangle.is_true())
321            .unwrap_or(false);
322
323        (
324            self.pass,
325            Optional::new(
326                paren_remover(comments.map(|v| v as &dyn Comments)),
327                self.fixer,
328            ),
329            compat_pass,
330            // module / helper
331            Optional::new(
332                modules::import_analysis::import_analyzer(import_interop, ignore_dynamic),
333                need_analyzer,
334            ),
335            Optional::new(
336                helpers::inject_helpers(self.unresolved_mark),
337                self.inject_helpers,
338            ),
339            ModuleConfig::build(
340                self.cm.clone(),
341                comments,
342                module,
343                self.unresolved_mark,
344                feature_flag,
345                resolver,
346            ),
347            visit_mut_pass(MinifierPass {
348                options: self.minify,
349                cm: self.cm.clone(),
350                comments,
351                top_level_mark: self.top_level_mark,
352            }),
353            Optional::new(
354                hygiene_with_config(swc_ecma_transforms_base::hygiene::Config {
355                    top_level_mark: self.top_level_mark,
356                    ..self.hygiene.clone().unwrap_or_default()
357                }),
358                self.hygiene.is_some() && !is_mangler_enabled,
359            ),
360            Optional::new(fixer(comments.map(|v| v as &dyn Comments)), self.fixer),
361        )
362    }
363}
364
365struct MinifierPass<'a> {
366    options: Option<JsMinifyOptions>,
367    cm: Lrc<SourceMap>,
368    comments: Option<&'a dyn Comments>,
369    top_level_mark: Mark,
370}
371
372impl VisitMut for MinifierPass<'_> {
373    noop_visit_mut_type!(fail);
374
375    fn visit_mut_module(&mut self, m: &mut Module) {
376        if let Some(options) = &self.options {
377            let opts = MinifyOptions {
378                compress: options
379                    .compress
380                    .clone()
381                    .unwrap_as_option(|default| match default {
382                        Some(true) => Some(Default::default()),
383                        _ => None,
384                    })
385                    .map(|mut v| {
386                        if v.const_to_let.is_none() {
387                            v.const_to_let = Some(true);
388                        }
389                        if v.toplevel.is_none() {
390                            v.toplevel = Some(TerserTopLevelOptions::Bool(true));
391                        }
392
393                        v.into_config(self.cm.clone())
394                    }),
395                mangle: options
396                    .mangle
397                    .clone()
398                    .unwrap_as_option(|default| match default {
399                        Some(true) => Some(Default::default()),
400                        _ => None,
401                    }),
402                ..Default::default()
403            };
404
405            if opts.compress.is_none() && opts.mangle.is_none() {
406                return;
407            }
408
409            m.visit_mut_with(&mut hygiene_with_config(
410                swc_ecma_transforms_base::hygiene::Config {
411                    top_level_mark: self.top_level_mark,
412                    ..Default::default()
413                },
414            ));
415
416            let unresolved_mark = Mark::new();
417            let top_level_mark = Mark::new();
418
419            m.visit_mut_with(&mut resolver(unresolved_mark, top_level_mark, false));
420
421            m.map_with_mut(|m| {
422                swc_ecma_minifier::optimize(
423                    m.into(),
424                    self.cm.clone(),
425                    self.comments.as_ref().map(|v| v as &dyn Comments),
426                    None,
427                    &opts,
428                    &swc_ecma_minifier::option::ExtraOptions {
429                        unresolved_mark,
430                        top_level_mark,
431                        mangle_name_cache: None,
432                    },
433                )
434                .expect_module()
435            })
436        }
437    }
438
439    fn visit_mut_script(&mut self, m: &mut Script) {
440        if let Some(options) = &self.options {
441            let opts = MinifyOptions {
442                compress: options
443                    .compress
444                    .clone()
445                    .unwrap_as_option(|default| match default {
446                        Some(true) => Some(Default::default()),
447                        _ => None,
448                    })
449                    .map(|mut v| {
450                        if v.const_to_let.is_none() {
451                            v.const_to_let = Some(true);
452                        }
453
454                        v.module = false;
455
456                        v.into_config(self.cm.clone())
457                    }),
458                mangle: options
459                    .mangle
460                    .clone()
461                    .unwrap_as_option(|default| match default {
462                        Some(true) => Some(Default::default()),
463                        _ => None,
464                    }),
465                ..Default::default()
466            };
467
468            if opts.compress.is_none() && opts.mangle.is_none() {
469                return;
470            }
471
472            m.visit_mut_with(&mut hygiene_with_config(
473                swc_ecma_transforms_base::hygiene::Config {
474                    top_level_mark: self.top_level_mark,
475                    ..Default::default()
476                },
477            ));
478
479            let unresolved_mark = Mark::new();
480            let top_level_mark = Mark::new();
481
482            m.visit_mut_with(&mut resolver(unresolved_mark, top_level_mark, false));
483
484            m.map_with_mut(|m| {
485                swc_ecma_minifier::optimize(
486                    m.into(),
487                    self.cm.clone(),
488                    self.comments.as_ref().map(|v| v as &dyn Comments),
489                    None,
490                    &opts,
491                    &swc_ecma_minifier::option::ExtraOptions {
492                        unresolved_mark,
493                        top_level_mark,
494                        mangle_name_cache: None,
495                    },
496                )
497                .expect_script()
498            })
499        }
500    }
501}
502
503pub(crate) fn should_enable(target: EsVersion, feature: EsVersion) -> bool {
504    target < feature
505}