next_custom_transforms/transforms/
optimize_barrel.rs

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