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}