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}