next_custom_transforms/transforms/
optimize_barrel.rs

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