Skip to main content

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}