next_custom_transforms/transforms/
optimize_barrel.rs

1use std::collections::HashMap;
2
3use serde::Deserialize;
4use swc_core::{
5    atoms::{atom, Atom},
6    common::DUMMY_SP,
7    ecma::{
8        ast::*,
9        utils::private_ident,
10        visit::{fold_pass, Fold},
11    },
12};
13
14#[derive(Clone, Debug, Deserialize)]
15pub struct Config {
16    pub wildcard: bool,
17}
18
19pub fn optimize_barrel(config: Config) -> impl Pass {
20    fold_pass(OptimizeBarrel {
21        wildcard: config.wildcard,
22    })
23}
24
25#[derive(Debug, Default)]
26struct OptimizeBarrel {
27    wildcard: bool,
28}
29
30impl Fold for OptimizeBarrel {
31    fn fold_module_items(&mut self, items: Vec<ModuleItem>) -> Vec<ModuleItem> {
32        // One pre-pass to find all the local idents that we are referencing, so we can
33        // handle the case of `import foo from 'a'; export { foo };` correctly.
34
35        // Map of "local ident" -> ("source module", "orig ident")
36        let mut local_idents = HashMap::new();
37        for item in &items {
38            if let ModuleItem::ModuleDecl(ModuleDecl::Import(import_decl)) = item {
39                for spec in &import_decl.specifiers {
40                    let src = import_decl.src.value.clone();
41                    match spec {
42                        ImportSpecifier::Named(s) => {
43                            local_idents.insert(
44                                s.local.sym.clone(),
45                                (
46                                    src.clone(),
47                                    match &s.imported {
48                                        Some(n) => match &n {
49                                            ModuleExportName::Ident(n) => n.sym.clone(),
50                                            ModuleExportName::Str(n) => n.value.clone(),
51                                        },
52                                        None => s.local.sym.clone(),
53                                    },
54                                ),
55                            );
56                        }
57                        ImportSpecifier::Namespace(s) => {
58                            local_idents.insert(s.local.sym.clone(), (src.clone(), atom!("*")));
59                        }
60                        ImportSpecifier::Default(s) => {
61                            local_idents
62                                .insert(s.local.sym.clone(), (src.clone(), atom!("default")));
63                        }
64                    }
65                }
66            }
67        }
68
69        // The second pass to rebuild the module items.
70        let mut new_items = vec![];
71
72        // Exported meta information.
73        let mut export_map = vec![];
74        let mut export_wildcards = vec![];
75
76        // We only apply this optimization to barrel files. Here we consider
77        // a barrel file to be a file that only exports from other modules.
78
79        // Besides that, lit expressions are allowed as well ("use client", etc.).
80        let mut allowed_directives = true;
81        let mut directives = vec![];
82
83        let mut is_barrel = true;
84        for item in &items {
85            match item {
86                ModuleItem::ModuleDecl(decl) => {
87                    allowed_directives = false;
88                    match decl {
89                        ModuleDecl::Import(_) => {}
90                        // export { foo } from './foo';
91                        ModuleDecl::ExportNamed(export_named) => {
92                            for spec in &export_named.specifiers {
93                                match spec {
94                                    ExportSpecifier::Namespace(s) => {
95                                        let name_str = match &s.name {
96                                            ModuleExportName::Ident(n) => n.sym.clone(),
97                                            ModuleExportName::Str(n) => n.value.clone(),
98                                        };
99                                        if let Some(src) = &export_named.src {
100                                            export_map.push((
101                                                name_str.clone(),
102                                                src.value.clone(),
103                                                atom!("*"),
104                                            ));
105                                        } else if self.wildcard {
106                                            export_map.push((
107                                                name_str.clone(),
108                                                Atom::default(),
109                                                atom!("*"),
110                                            ));
111                                        } else {
112                                            is_barrel = false;
113                                            break;
114                                        }
115                                    }
116                                    ExportSpecifier::Named(s) => {
117                                        let orig_str = match &s.orig {
118                                            ModuleExportName::Ident(n) => n.sym.clone(),
119                                            ModuleExportName::Str(n) => n.value.clone(),
120                                        };
121                                        let name_str = match &s.exported {
122                                            Some(n) => match &n {
123                                                ModuleExportName::Ident(n) => n.sym.clone(),
124                                                ModuleExportName::Str(n) => n.value.clone(),
125                                            },
126                                            None => orig_str.clone(),
127                                        };
128
129                                        if let Some(src) = &export_named.src {
130                                            export_map.push((
131                                                name_str.clone(),
132                                                src.value.clone(),
133                                                orig_str.clone(),
134                                            ));
135                                        } else if let Some((src, orig)) =
136                                            local_idents.get(&orig_str)
137                                        {
138                                            export_map.push((
139                                                name_str.clone(),
140                                                src.clone(),
141                                                orig.clone(),
142                                            ));
143                                        } else if self.wildcard {
144                                            export_map.push((
145                                                name_str.clone(),
146                                                Atom::default(),
147                                                orig_str.clone(),
148                                            ));
149                                        } else {
150                                            is_barrel = false;
151                                            break;
152                                        }
153                                    }
154                                    _ => {
155                                        if !self.wildcard {
156                                            is_barrel = false;
157                                            break;
158                                        }
159                                    }
160                                }
161                            }
162                        }
163                        ModuleDecl::ExportAll(export_all) => {
164                            export_wildcards.push(export_all.src.value.to_string());
165                        }
166                        ModuleDecl::ExportDecl(export_decl) => {
167                            // Export declarations are not allowed in barrel files.
168                            if !self.wildcard {
169                                is_barrel = false;
170                                break;
171                            }
172
173                            match &export_decl.decl {
174                                Decl::Class(class) => {
175                                    export_map.push((
176                                        class.ident.sym.clone(),
177                                        Atom::default(),
178                                        Atom::default(),
179                                    ));
180                                }
181                                Decl::Fn(func) => {
182                                    export_map.push((
183                                        func.ident.sym.clone(),
184                                        Atom::default(),
185                                        Atom::default(),
186                                    ));
187                                }
188                                Decl::Var(var) => {
189                                    let ids = collect_idents_in_var_decls(&var.decls);
190                                    for id in ids {
191                                        export_map.push((id, Atom::default(), Atom::default()));
192                                    }
193                                }
194                                _ => {}
195                            }
196                        }
197                        _ => {
198                            if !self.wildcard {
199                                // Other expressions are not allowed in barrel files.
200                                is_barrel = false;
201                                break;
202                            }
203                        }
204                    }
205                }
206                ModuleItem::Stmt(stmt) => match stmt {
207                    Stmt::Expr(expr) => match &*expr.expr {
208                        Expr::Lit(l) => {
209                            if let Lit::Str(s) = l {
210                                if allowed_directives && s.value.starts_with("use ") {
211                                    directives.push(s.value.to_string());
212                                }
213                            } else {
214                                allowed_directives = false;
215                            }
216                        }
217                        _ => {
218                            allowed_directives = false;
219                            if !self.wildcard {
220                                is_barrel = false;
221                                break;
222                            }
223                        }
224                    },
225                    _ => {
226                        allowed_directives = false;
227                        if !self.wildcard {
228                            is_barrel = false;
229                            break;
230                        }
231                    }
232                },
233            }
234        }
235
236        // If the file is not a barrel file, we export nothing.
237        if !is_barrel {
238            new_items = vec![];
239        } else {
240            // Otherwise we export the meta information.
241            new_items.push(ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
242                span: DUMMY_SP,
243                decl: Decl::Var(Box::new(VarDecl {
244                    span: DUMMY_SP,
245                    kind: VarDeclKind::Const,
246                    decls: vec![VarDeclarator {
247                        span: DUMMY_SP,
248                        name: Pat::Ident(BindingIdent {
249                            id: private_ident!("__next_private_export_map__"),
250                            type_ann: None,
251                        }),
252                        init: Some(Box::new(Expr::Lit(Lit::Str(Str {
253                            span: DUMMY_SP,
254                            value: serde_json::to_string(&export_map).unwrap().into(),
255                            raw: None,
256                        })))),
257                        definite: false,
258                    }],
259                    ..Default::default()
260                })),
261            })));
262
263            // Push "export *" statements for each wildcard export.
264            for src in export_wildcards {
265                new_items.push(ModuleItem::ModuleDecl(ModuleDecl::ExportAll(ExportAll {
266                    span: DUMMY_SP,
267                    src: Box::new(Str {
268                        span: DUMMY_SP,
269                        value: format!("__barrel_optimize__?names=__PLACEHOLDER__!=!{src}").into(),
270                        raw: None,
271                    }),
272                    with: None,
273                    type_only: false,
274                })));
275            }
276
277            // Push directives.
278            if !directives.is_empty() {
279                new_items.push(ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
280                    span: DUMMY_SP,
281                    decl: Decl::Var(Box::new(VarDecl {
282                        span: DUMMY_SP,
283                        kind: VarDeclKind::Const,
284                        decls: vec![VarDeclarator {
285                            span: DUMMY_SP,
286                            name: Pat::Ident(BindingIdent {
287                                id: private_ident!("__next_private_directive_list__"),
288                                type_ann: None,
289                            }),
290                            init: Some(Box::new(Expr::Lit(Lit::Str(Str {
291                                span: DUMMY_SP,
292                                value: serde_json::to_string(&directives).unwrap().into(),
293                                raw: None,
294                            })))),
295                            definite: false,
296                        }],
297                        ..Default::default()
298                    })),
299                })));
300            }
301        }
302
303        new_items
304    }
305}
306
307fn collect_idents_in_array_pat(elems: &[Option<Pat>]) -> Vec<Atom> {
308    let mut ids = Vec::new();
309
310    for elem in elems.iter().flatten() {
311        match elem {
312            Pat::Ident(ident) => {
313                ids.push(ident.sym.clone());
314            }
315            Pat::Array(array) => {
316                ids.extend(collect_idents_in_array_pat(&array.elems));
317            }
318            Pat::Object(object) => {
319                ids.extend(collect_idents_in_object_pat(&object.props));
320            }
321            Pat::Rest(rest) => {
322                if let Pat::Ident(ident) = &*rest.arg {
323                    ids.push(ident.sym.clone());
324                }
325            }
326            _ => {}
327        }
328    }
329
330    ids
331}
332
333fn collect_idents_in_object_pat(props: &[ObjectPatProp]) -> Vec<Atom> {
334    let mut ids = Vec::new();
335
336    for prop in props {
337        match prop {
338            ObjectPatProp::KeyValue(KeyValuePatProp { key, value }) => {
339                if let PropName::Ident(ident) = key {
340                    ids.push(ident.sym.clone());
341                }
342
343                match &**value {
344                    Pat::Ident(ident) => {
345                        ids.push(ident.sym.clone());
346                    }
347                    Pat::Array(array) => {
348                        ids.extend(collect_idents_in_array_pat(&array.elems));
349                    }
350                    Pat::Object(object) => {
351                        ids.extend(collect_idents_in_object_pat(&object.props));
352                    }
353                    _ => {}
354                }
355            }
356            ObjectPatProp::Assign(AssignPatProp { key, .. }) => {
357                ids.push(key.sym.clone());
358            }
359            ObjectPatProp::Rest(RestPat { arg, .. }) => {
360                if let Pat::Ident(ident) = &**arg {
361                    ids.push(ident.sym.clone());
362                }
363            }
364        }
365    }
366
367    ids
368}
369
370fn collect_idents_in_var_decls(decls: &[VarDeclarator]) -> Vec<Atom> {
371    let mut ids = Vec::new();
372
373    for decl in decls {
374        match &decl.name {
375            Pat::Ident(ident) => {
376                ids.push(ident.sym.clone());
377            }
378            Pat::Array(array) => {
379                ids.extend(collect_idents_in_array_pat(&array.elems));
380            }
381            Pat::Object(object) => {
382                ids.extend(collect_idents_in_object_pat(&object.props));
383            }
384            _ => {}
385        }
386    }
387
388    ids
389}