next_custom_transforms/transforms/
track_dynamic_imports.rs1use rustc_hash::FxHashMap;
2use swc_core::{
3 common::{
4 BytePos, DUMMY_SP, Mark, Span, Spanned, SyntaxContext,
5 comments::{Comment, CommentKind, Comments},
6 source_map::PURE_SP,
7 util::take::Take,
8 },
9 ecma::{
10 ast::*,
11 utils::{prepend_stmt, private_ident, quote_ident, quote_str},
12 visit::{VisitMut, VisitMutWith, noop_visit_mut_type, visit_mut_pass},
13 },
14 quote,
15};
16
17pub fn track_dynamic_imports<C: Comments>(
18 unresolved_mark: Mark,
19 comments: C,
20) -> impl VisitMut + Pass {
21 visit_mut_pass(ImportReplacer::new(unresolved_mark, comments))
22}
23
24struct ImportReplacer<C: Comments> {
25 comments: C,
26 unresolved_ctxt: SyntaxContext,
27 has_dynamic_import: bool,
28 wrapper_function_local_ident: Ident,
29 import_export_names: FxHashMap<BytePos, Vec<String>>,
31}
32
33impl<C: Comments> ImportReplacer<C> {
34 pub fn new(unresolved_mark: Mark, comments: C) -> Self {
35 ImportReplacer {
36 comments,
37 unresolved_ctxt: SyntaxContext::empty().apply_mark(unresolved_mark),
38 has_dynamic_import: false,
39 wrapper_function_local_ident: private_ident!("$$trackDynamicImport__"),
40 import_export_names: Default::default(),
41 }
42 }
43}
44
45fn find_awaited_import_call_span(expr: &Expr) -> Option<Span> {
50 let mut current: &Expr = expr;
51 let mut seen_await = false;
52 loop {
53 match current {
54 Expr::Call(CallExpr {
55 callee: Callee::Import(_),
56 span,
57 ..
58 }) if seen_await => {
59 break Some(*span);
60 }
61 Expr::Await(AwaitExpr { arg, .. }) => {
62 seen_await = true;
63 current = arg;
64 }
65 Expr::Paren(ParenExpr { expr, .. }) => {
66 current = expr;
67 }
68 _ => break None,
69 }
70 }
71}
72
73fn extract_export_names_from_pat(pat: &ObjectPat) -> Option<Vec<String>> {
77 let mut names = Vec::new();
78 for prop in &pat.props {
79 match prop {
80 ObjectPatProp::KeyValue(KeyValuePatProp { key, .. }) => match key {
81 PropName::Ident(ident) => names.push(ident.sym.to_string()),
82 PropName::Str(s) => names.push(s.value.to_string_lossy().into_owned()),
83 _ => return None,
85 },
86 ObjectPatProp::Assign(AssignPatProp { key, .. }) => {
87 names.push(key.sym.to_string());
88 }
89 ObjectPatProp::Rest(_) => return None,
91 }
92 }
93 Some(names)
94}
95
96impl<C: Comments> VisitMut for ImportReplacer<C> {
97 noop_visit_mut_type!();
98
99 fn visit_mut_program(&mut self, program: &mut Program) {
100 program.visit_mut_children_with(self);
101 if self.has_dynamic_import {
104 let import_args = MakeNamedImportArgs {
105 original_ident: quote_ident!("trackDynamicImport").into(),
106 local_ident: self.wrapper_function_local_ident.clone(),
107 source: "private-next-rsc-track-dynamic-import",
108 unresolved_ctxt: self.unresolved_ctxt,
109 };
110 match program {
111 Program::Module(module) => {
112 prepend_stmt(&mut module.body, make_named_import_esm(import_args));
113 }
114 Program::Script(script) => {
115 prepend_stmt(&mut script.body, make_named_import_cjs(import_args));
119 }
120 }
121 }
122 }
123
124 fn visit_mut_var_declarator(&mut self, decl: &mut VarDeclarator) {
125 if let Some(init) = &decl.init
133 && let Some(import_span) = find_awaited_import_call_span(init)
134 && let Pat::Object(obj_pat) = &decl.name
135 && let Some(names) = extract_export_names_from_pat(obj_pat)
136 {
137 self.import_export_names.insert(import_span.lo, names);
138 }
139
140 decl.visit_mut_children_with(self);
141 }
142
143 fn visit_mut_expr(&mut self, expr: &mut Expr) {
144 expr.visit_mut_children_with(self);
145
146 if let Expr::Call(
150 call_expr @ CallExpr {
151 callee: Callee::Import(_),
152 ..
153 },
154 ) = expr
155 {
156 self.has_dynamic_import = true;
157
158 if let Some(names) = self.import_export_names.remove(&call_expr.span.lo)
160 && let Some(first_arg) = call_expr.args.first()
161 {
162 let comment_text = if names.is_empty() {
163 " webpackExports: [] ".to_string()
164 } else {
165 let names_json: Vec<String> =
166 names.iter().map(|n| format!("\"{}\"", n)).collect();
167 format!(" webpackExports: [{}] ", names_json.join(", "))
168 };
169 self.comments.add_leading(
170 first_arg.span_lo(),
171 Comment {
172 span: DUMMY_SP,
173 kind: CommentKind::Block,
174 text: comment_text.into(),
175 },
176 );
177 }
178
179 let replacement_expr = quote!(
180 "$wrapper_fn($expr)" as Expr,
181 wrapper_fn = self.wrapper_function_local_ident.clone(),
182 expr: Expr = expr.take()
183 )
184 .with_span(PURE_SP);
185 *expr = replacement_expr
186 }
187 }
188}
189
190struct MakeNamedImportArgs<'a> {
191 original_ident: Ident,
192 local_ident: Ident,
193 source: &'a str,
194 unresolved_ctxt: SyntaxContext,
195}
196
197fn make_named_import_esm(args: MakeNamedImportArgs) -> ModuleItem {
198 let MakeNamedImportArgs {
199 original_ident,
200 local_ident,
201 source,
202 ..
203 } = args;
204 let mut item = quote!(
205 "import { $original_ident as $local_ident } from 'dummy'" as ModuleItem,
206 original_ident = original_ident,
207 local_ident = local_ident,
208 );
209 let decl = item.as_mut_module_decl().unwrap().as_mut_import().unwrap();
211 *decl.src = source.into();
212 item
213}
214
215fn make_named_import_cjs(args: MakeNamedImportArgs) -> Stmt {
216 let MakeNamedImportArgs {
217 original_ident,
218 local_ident,
219 source,
220 unresolved_ctxt,
221 } = args;
222 quote!(
223 "const { [$original_name]: $local_ident } = $require($source)" as Stmt,
224 original_name: Expr = quote_str!(original_ident.sym).into(),
225 local_ident = local_ident,
226 source: Expr = quote_str!(source).into(),
227 require = quote_ident!(unresolved_ctxt, "require")
231 )
232}