swc_bundler/bundler/
finalize.rs

1use std::path::{Path, PathBuf};
2
3use anyhow::Error;
4use relative_path::RelativePath;
5use rustc_hash::FxHashMap;
6use swc_atoms::atom;
7use swc_common::{util::move_map::MoveMap, FileName, Mark, DUMMY_SP};
8use swc_ecma_ast::*;
9use swc_ecma_transforms_base::{
10    fixer::fixer,
11    helpers::{inject_helpers, Helpers, HELPERS},
12    hygiene::hygiene,
13};
14use swc_ecma_utils::{contains_top_level_await, find_pat_ids, private_ident, ExprFactory};
15use swc_ecma_visit::{noop_fold_type, Fold, FoldWith, VisitMutWith};
16
17use crate::{hash::calc_hash, Bundle, BundleKind, Bundler, Load, ModuleType, Resolve};
18
19impl<L, R> Bundler<'_, L, R>
20where
21    L: Load,
22    R: Resolve,
23{
24    /// This method do
25    ///
26    /// - inject helpers
27    /// - rename chunks
28    /// - invoke fixer
29    pub(super) fn finalize(
30        &self,
31        bundles: Vec<Bundle>,
32        unresolved_mark: Mark,
33    ) -> Result<Vec<Bundle>, Error> {
34        self.run(|| {
35            let mut new = Vec::with_capacity(bundles.len());
36            let mut renamed = FxHashMap::default();
37
38            for mut bundle in bundles {
39                bundle.module = self.optimize(bundle.module);
40
41                if !self.config.disable_hygiene {
42                    bundle.module.visit_mut_with(&mut hygiene());
43                }
44
45                bundle.module = self.may_wrap_with_iife(bundle.module);
46
47                if !self.config.disable_fixer {
48                    bundle.module.visit_mut_with(&mut fixer(None));
49                }
50
51                {
52                    // Inject swc helpers
53                    let swc_helpers = *self
54                        .scope
55                        .get_module(bundle.id)
56                        .expect("module should exist at this point")
57                        .swc_helpers
58                        .lock();
59
60                    HELPERS.set(&Helpers::from_data(swc_helpers), || {
61                        bundle
62                            .module
63                            .visit_mut_with(&mut inject_helpers(unresolved_mark));
64                    });
65                }
66
67                match bundle.kind {
68                    BundleKind::Named { .. } => {
69                        // Inject helpers
70                        let helpers = self
71                            .scope
72                            .get_module(bundle.id)
73                            .expect("module should exist at this point")
74                            .helpers;
75
76                        helpers.add_to(&mut bundle.module.body);
77
78                        new.push(bundle);
79                    }
80                    BundleKind::Lib { name } => {
81                        let hash = calc_hash(self.cm.clone(), &bundle.module)?;
82                        let mut new_name = PathBuf::from(name);
83                        let key = new_name.clone();
84                        let file_name = new_name
85                            .file_name()
86                            .map(|path| -> PathBuf {
87                                let path = Path::new(path);
88                                let ext = path.extension();
89                                if let Some(ext) = ext {
90                                    return format!(
91                                        "{}-{}.{}",
92                                        path.file_stem().unwrap().to_string_lossy(),
93                                        hash,
94                                        ext.to_string_lossy()
95                                    )
96                                    .into();
97                                }
98                                format!("{}-{}", path.file_stem().unwrap().to_string_lossy(), hash,)
99                                    .into()
100                            })
101                            .expect("javascript file should have name");
102                        new_name.pop();
103                        new_name = new_name.join(file_name.clone());
104
105                        renamed.insert(key, new_name.to_string_lossy().to_string());
106
107                        new.push(Bundle {
108                            kind: BundleKind::Named {
109                                name: file_name.display().to_string(),
110                            },
111                            ..bundle
112                        })
113                    }
114                    _ => new.push(bundle),
115                }
116            }
117
118            if new.len() == 1 {
119                return Ok(new);
120            }
121
122            new = new.move_map(|bundle| {
123                let path = match &*self.scope.get_module(bundle.id).unwrap().fm.name {
124                    FileName::Real(ref v) => v.clone(),
125                    _ => {
126                        tracing::error!("Cannot rename: not a real file");
127                        return bundle;
128                    }
129                };
130
131                let module = {
132                    // Change imports
133                    let mut v = Renamer {
134                        resolver: &self.resolver,
135                        base: &path,
136                        renamed: &renamed,
137                    };
138                    bundle.module.fold_with(&mut v)
139                };
140
141                Bundle { module, ..bundle }
142            });
143
144            Ok(new)
145        })
146    }
147
148    fn may_wrap_with_iife(&self, module: Module) -> Module {
149        if self.config.module != ModuleType::Iife {
150            return module;
151        }
152
153        let is_async = contains_top_level_await(&module);
154
155        // Properties of returned object
156        let mut props = Vec::new();
157
158        let mut body = BlockStmt {
159            span: module.span,
160            stmts: module
161                .body
162                .into_iter()
163                .filter_map(|item| {
164                    let decl = match item {
165                        ModuleItem::ModuleDecl(v) => v,
166                        ModuleItem::Stmt(stmt) => return Some(stmt),
167                        #[cfg(swc_ast_unknown)]
168                        _ => panic!("unable to access unknown nodes"),
169                    };
170
171                    match decl {
172                        ModuleDecl::ExportNamed(NamedExport { src: Some(..), .. })
173                        | ModuleDecl::TsImportEquals(_)
174                        | ModuleDecl::TsExportAssignment(_)
175                        | ModuleDecl::TsNamespaceExport(_)
176                        | ModuleDecl::Import(_) => None,
177
178                        ModuleDecl::ExportDecl(export) => {
179                            match &export.decl {
180                                Decl::Class(ClassDecl { ident, .. })
181                                | Decl::Fn(FnDecl { ident, .. }) => {
182                                    props.push(PropOrSpread::Prop(Box::new(Prop::Shorthand(
183                                        ident.clone(),
184                                    ))));
185                                }
186                                Decl::Var(decl) => {
187                                    let ids: Vec<Ident> = find_pat_ids(decl);
188                                    props.extend(
189                                        ids.into_iter()
190                                            .map(Prop::Shorthand)
191                                            .map(Box::new)
192                                            .map(PropOrSpread::Prop),
193                                    );
194                                }
195                                _ => unreachable!(),
196                            }
197
198                            Some(export.decl.into())
199                        }
200
201                        ModuleDecl::ExportNamed(NamedExport {
202                            specifiers,
203                            src: None,
204                            ..
205                        }) => {
206                            for s in specifiers {
207                                match s {
208                                    ExportSpecifier::Namespace(..) => {
209                                        // unreachable
210                                    }
211                                    ExportSpecifier::Default(s) => {
212                                        props.push(PropOrSpread::Prop(Box::new(Prop::KeyValue(
213                                            KeyValueProp {
214                                                key: PropName::Ident(IdentName::new(
215                                                    atom!("default"),
216                                                    DUMMY_SP,
217                                                )),
218                                                value: s.exported.into(),
219                                            },
220                                        ))));
221                                    }
222                                    ExportSpecifier::Named(s) => match s.exported {
223                                        Some(ModuleExportName::Ident(exported)) => {
224                                            let orig = match s.orig {
225                                                ModuleExportName::Ident(ident) => ident,
226                                                ModuleExportName::Str(..) => unimplemented!(
227                                                    "module string names unimplemented"
228                                                ),
229                                                #[cfg(swc_ast_unknown)]
230                                                _ => panic!("unable to access unknown nodes"),
231                                            };
232                                            props.push(PropOrSpread::Prop(Box::new(
233                                                Prop::KeyValue(KeyValueProp {
234                                                    key: PropName::Ident(exported.into()),
235                                                    value: orig.into(),
236                                                }),
237                                            )));
238                                        }
239                                        Some(ModuleExportName::Str(..)) => {
240                                            unimplemented!("module string names unimplemented")
241                                        }
242                                        #[cfg(swc_ast_unknown)]
243                                        Some(_) => panic!("unable to access unknown nodes"),
244                                        None => {
245                                            let orig = match s.orig {
246                                                ModuleExportName::Ident(ident) => ident,
247                                                ModuleExportName::Str(..) => unimplemented!(
248                                                    "module string names unimplemented"
249                                                ),
250                                                #[cfg(swc_ast_unknown)]
251                                                _ => panic!("unable to access unknown nodes"),
252                                            };
253                                            props.push(PropOrSpread::Prop(Box::new(
254                                                Prop::Shorthand(orig),
255                                            )));
256                                        }
257                                    },
258                                    #[cfg(swc_ast_unknown)]
259                                    _ => panic!("unable to access unknown nodes"),
260                                }
261                            }
262
263                            None
264                        }
265
266                        ModuleDecl::ExportDefaultDecl(export) => match export.decl {
267                            DefaultDecl::Class(expr) => {
268                                let ident = expr.ident;
269                                let ident =
270                                    ident.unwrap_or_else(|| private_ident!("_default_decl"));
271
272                                props.push(PropOrSpread::Prop(Box::new(Prop::KeyValue(
273                                    KeyValueProp {
274                                        key: PropName::Ident(IdentName::new(
275                                            atom!("default"),
276                                            export.span,
277                                        )),
278                                        value: ident.clone().into(),
279                                    },
280                                ))));
281
282                                Some(
283                                    ClassDecl {
284                                        ident,
285                                        class: expr.class,
286                                        declare: false,
287                                    }
288                                    .into(),
289                                )
290                            }
291                            DefaultDecl::Fn(expr) => {
292                                let ident = expr.ident;
293                                let ident =
294                                    ident.unwrap_or_else(|| private_ident!("_default_decl"));
295
296                                props.push(PropOrSpread::Prop(Box::new(Prop::KeyValue(
297                                    KeyValueProp {
298                                        key: PropName::Ident(IdentName::new(
299                                            atom!("default"),
300                                            export.span,
301                                        )),
302                                        value: ident.clone().into(),
303                                    },
304                                ))));
305
306                                Some(
307                                    FnDecl {
308                                        ident,
309                                        function: expr.function,
310                                        declare: false,
311                                    }
312                                    .into(),
313                                )
314                            }
315                            DefaultDecl::TsInterfaceDecl(_) => None,
316                            #[cfg(swc_ast_unknown)]
317                            _ => panic!("unable to access unknown nodes"),
318                        },
319                        ModuleDecl::ExportDefaultExpr(export) => {
320                            let default_var = private_ident!("default");
321                            props.push(PropOrSpread::Prop(Box::new(Prop::Shorthand(
322                                default_var.clone(),
323                            ))));
324                            let var = VarDeclarator {
325                                span: DUMMY_SP,
326                                name: default_var.into(),
327                                init: Some(export.expr),
328                                definite: false,
329                            };
330                            Some(
331                                VarDecl {
332                                    span: DUMMY_SP,
333                                    kind: VarDeclKind::Const,
334                                    declare: false,
335                                    decls: vec![var],
336                                    ..Default::default()
337                                }
338                                .into(),
339                            )
340                        }
341
342                        ModuleDecl::ExportAll(_) => None,
343
344                        #[cfg(swc_ast_unknown)]
345                        _ => panic!("unable to access unknown nodes"),
346                    }
347                })
348                .collect(),
349            ..Default::default()
350        };
351        body.stmts.push(
352            ReturnStmt {
353                span: DUMMY_SP,
354                arg: Some(
355                    ObjectLit {
356                        span: DUMMY_SP,
357                        props,
358                    }
359                    .into(),
360                ),
361            }
362            .into(),
363        );
364
365        let f = Function {
366            is_generator: false,
367            is_async,
368            span: DUMMY_SP,
369            body: Some(body),
370            ..Default::default()
371        };
372
373        let invoked_fn_expr = FnExpr {
374            ident: None,
375            function: Box::new(f),
376        };
377
378        let iife = CallExpr {
379            span: DUMMY_SP,
380            callee: invoked_fn_expr.as_callee(),
381            args: Default::default(),
382            ..Default::default()
383        }
384        .into();
385
386        Module {
387            span: DUMMY_SP,
388            shebang: None,
389            body: vec![ModuleItem::Stmt(Stmt::Expr(ExprStmt {
390                span: DUMMY_SP,
391                expr: iife,
392            }))],
393        }
394    }
395}
396
397/// Import renamer. This pass changes import path.
398struct Renamer<'a, R>
399where
400    R: Resolve,
401{
402    resolver: R,
403    base: &'a PathBuf,
404    renamed: &'a FxHashMap<PathBuf, String>,
405}
406
407impl<R> Fold for Renamer<'_, R>
408where
409    R: Resolve,
410{
411    noop_fold_type!();
412
413    fn fold_import_decl(&mut self, import: ImportDecl) -> ImportDecl {
414        let resolved = match self.resolver.resolve(
415            &FileName::Real(self.base.clone()),
416            &import.src.value.to_string_lossy(),
417        ) {
418            Ok(v) => match v.filename {
419                FileName::Real(v) => v,
420                _ => panic!("rename_bundles called with non-path module"),
421            },
422            Err(_) => return import,
423        };
424
425        if let Some(v) = self.renamed.get(&resolved) {
426            // We use parent because RelativePath uses ../common-[hash].js
427            // if we use `entry-a.js` as a base.
428            //
429            // entry-a.js
430            // common.js
431            let base = self
432                .base
433                .parent()
434                .unwrap_or(self.base)
435                .as_os_str()
436                .to_string_lossy();
437            let base = RelativePath::new(&*base);
438            let v = base.relative(v);
439            let value = v.as_str();
440            return ImportDecl {
441                src: Box::new(Str {
442                    value: if value.starts_with('.') {
443                        value.into()
444                    } else {
445                        format!("./{value}").into()
446                    },
447                    ..*import.src
448                }),
449                ..import
450            };
451        }
452
453        import
454    }
455}