turbopack_ecmascript/references/esm/binding.rs
1use anyhow::Result;
2use bincode::{Decode, Encode};
3use rustc_hash::FxHashMap;
4use swc_core::ecma::{
5 ast::{Expr, KeyValueProp, Prop, PropName, SimpleAssignTarget},
6 visit::fields::{CalleeField, PropField},
7};
8use turbo_rcstr::RcStr;
9use turbo_tasks::{FxIndexMap, NonLocalValue, ResolvedVc, Vc};
10use turbopack_core::chunk::ChunkingContext;
11
12use crate::{
13 ScopeHoistingContext,
14 ast_path_trie::{AstPathId, AstPathTrie},
15 code_gen::CodeGeneration,
16 create_visitor,
17 references::esm::{
18 EsmAssetReference,
19 base::{ReferencedAsset, ReferencedAssetIdent},
20 },
21};
22
23#[derive(Clone, Debug, PartialEq, Eq, NonLocalValue, Encode, Decode, Hash)]
24struct EsmBinding {
25 export: Option<RcStr>,
26 ast_path: AstPathId,
27 keep_this: bool,
28}
29
30#[derive(Default, Clone, Debug)]
31pub struct EsmBindingsBuilder {
32 bindings: FxIndexMap<ResolvedVc<EsmAssetReference>, Vec<EsmBinding>>,
33}
34
35#[derive(Default, Clone, Debug, PartialEq, Eq, NonLocalValue, Encode, Decode, Hash)]
36pub struct EsmBindings {
37 #[allow(clippy::type_complexity)]
38 bindings: Box<[(ResolvedVc<EsmAssetReference>, Box<[EsmBinding]>)]>,
39}
40
41impl EsmBindingsBuilder {
42 pub fn add(
43 &mut self,
44 reference: ResolvedVc<EsmAssetReference>,
45 export: Option<RcStr>,
46 ast_path: AstPathId,
47 ) {
48 self.bindings
49 .entry(reference)
50 .or_default()
51 .push(EsmBinding {
52 export,
53 ast_path,
54 keep_this: false,
55 });
56 }
57
58 /// Where possible, bind the namespace to `this` when the named import is called.
59 pub fn add_keep_this(
60 &mut self,
61 reference: ResolvedVc<EsmAssetReference>,
62 export: Option<RcStr>,
63 ast_path: AstPathId,
64 ) {
65 self.bindings
66 .entry(reference)
67 .or_default()
68 .push(EsmBinding {
69 export,
70 ast_path,
71 keep_this: true,
72 });
73 }
74
75 pub fn build(self) -> Option<EsmBindings> {
76 if self.bindings.is_empty() {
77 return None;
78 }
79
80 Some(EsmBindings {
81 bindings: self
82 .bindings
83 .into_iter()
84 .map(|(k, v)| (k, v.into_boxed_slice()))
85 .collect(),
86 })
87 }
88}
89
90impl EsmBindings {
91 pub async fn code_generation(
92 &self,
93 trie: &AstPathTrie,
94 chunking_context: Vc<Box<dyn ChunkingContext>>,
95 scope_hoisting_context: ScopeHoistingContext<'_>,
96 ) -> Result<CodeGeneration> {
97 let mut visitors = vec![];
98
99 let unused_references = chunking_context.unused_references();
100
101 for (reference, bindings) in &self.bindings {
102 if unused_references
103 .contains_key(&ResolvedVc::upcast(*reference))
104 .await?
105 {
106 continue;
107 }
108 let imported_module = reference.get_referenced_asset().await?;
109
110 let mut exports_cache: FxHashMap<Option<RcStr>, _> = FxHashMap::default();
111 #[derive(Clone)]
112 enum ImportedIdent {
113 Module(ReferencedAssetIdent),
114 None,
115 /// Empty module (alias set to `false`): namespace imports resolve to `{}`.
116 Empty,
117 Unresolvable,
118 }
119 for EsmBinding {
120 export,
121 ast_path,
122 keep_this,
123 } in bindings
124 {
125 let imported_ident = match &imported_module {
126 ReferencedAsset::None => ImportedIdent::None,
127 ReferencedAsset::Empty => {
128 if export.is_some() {
129 // Named/default binding from an empty module → `undefined`
130 ImportedIdent::None
131 } else {
132 // Namespace import from an empty module → `{}`
133 ImportedIdent::Empty
134 }
135 }
136 imported_module => match exports_cache.entry(export.clone()) {
137 std::collections::hash_map::Entry::Vacant(entry) => {
138 let ident: ImportedIdent = imported_module
139 .get_ident(chunking_context, export.clone(), scope_hoisting_context)
140 .await?
141 .map_or(ImportedIdent::Unresolvable, ImportedIdent::Module);
142 entry.insert(ident.clone());
143 ident
144 }
145 std::collections::hash_map::Entry::Occupied(entry) => entry.get().clone(),
146 },
147 };
148
149 // Walk up from the binding towards the root, stopping at the innermost node kind we
150 // know how to rewrite.
151 let mut ast_path = *ast_path;
152 loop {
153 match trie.get(ast_path) {
154 // Shorthand properties get special treatment because we need to rewrite
155 // them to normal key-value pairs.
156 Some(swc_core::ecma::visit::AstParentKind::Prop(PropField::Shorthand)) => {
157 ast_path = trie.parent_or_root(ast_path);
158 visitors.push(create_visitor!(
159 exact,
160 trie,
161 ast_path,
162 visit_mut_prop,
163 |prop: &mut Prop| {
164 if let Prop::Shorthand(ident) = prop {
165 match &imported_ident {
166 ImportedIdent::Module(imported_ident) => {
167 *prop = Prop::KeyValue(KeyValueProp {
168 key: PropName::Ident(ident.clone().into()),
169 value: Box::new(
170 imported_ident.as_expr(ident.span, false),
171 ),
172 });
173 }
174 ImportedIdent::None => {
175 *prop = Prop::KeyValue(KeyValueProp {
176 key: PropName::Ident(ident.clone().into()),
177 value: Expr::undefined(ident.span),
178 });
179 }
180 ImportedIdent::Empty => {
181 use swc_core::quote;
182 *prop = Prop::KeyValue(KeyValueProp {
183 key: PropName::Ident(ident.clone().into()),
184 value: Box::new(quote!("{}" as Expr)),
185 });
186 }
187 ImportedIdent::Unresolvable => {
188 // Do nothing, the reference will insert a throw
189 }
190 }
191 }
192 }
193 ));
194 break;
195 }
196 // Any other expression can be replaced with the import accessor.
197 Some(swc_core::ecma::visit::AstParentKind::Expr(_)) => {
198 ast_path = trie.parent_or_root(ast_path);
199 let in_call = !keep_this
200 && matches!(
201 trie.get(ast_path),
202 Some(swc_core::ecma::visit::AstParentKind::Callee(
203 CalleeField::Expr
204 ))
205 );
206
207 visitors.push(create_visitor!(
208 exact,
209 trie,
210 ast_path,
211 visit_mut_expr,
212 |expr: &mut Expr| {
213 use swc_core::common::Spanned;
214 match &imported_ident {
215 ImportedIdent::Module(imported_ident) => {
216 *expr = imported_ident.as_expr(expr.span(), in_call);
217 }
218 ImportedIdent::None => {
219 *expr = *Expr::undefined(expr.span());
220 }
221 ImportedIdent::Empty => {
222 use swc_core::quote;
223 *expr = quote!("{}" as Expr);
224 }
225 ImportedIdent::Unresolvable => {
226 // Do nothing, the reference will insert a throw
227 }
228 }
229 }
230 ));
231 break;
232 }
233 // We need to handle LHS because of code like
234 // (function (RouteKind1){})(RouteKind || RouteKind = {})
235 Some(swc_core::ecma::visit::AstParentKind::SimpleAssignTarget(_)) => {
236 ast_path = trie.parent_or_root(ast_path);
237
238 visitors.push(create_visitor!(
239 exact,
240 trie,
241 ast_path,
242 visit_mut_simple_assign_target,
243 |l: &mut SimpleAssignTarget| {
244 use swc_core::common::Spanned;
245 match &imported_ident {
246 ImportedIdent::Module(imported_ident) => {
247 *l = imported_ident
248 .as_expr_individual(l.span())
249 .map_either(
250 |i| SimpleAssignTarget::Ident(i.into()),
251 SimpleAssignTarget::Member,
252 )
253 .into_inner();
254 }
255 ImportedIdent::None | ImportedIdent::Empty => {
256 // Do nothing, cannot assign to `undefined` or `{}`
257 }
258 ImportedIdent::Unresolvable => {
259 // Do nothing, the reference will insert a throw
260 }
261 }
262 }
263 ));
264 break;
265 }
266 Some(_) => {
267 ast_path = trie.parent_or_root(ast_path);
268 }
269 None => break,
270 }
271 }
272 }
273 }
274
275 Ok(CodeGeneration::visitors(visitors))
276 }
277}