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}