turbopack_ecmascript/references/esm/
binding.rs

1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use swc_core::{
4    common::Span,
5    ecma::{
6        ast::{
7            ComputedPropName, Expr, Ident, KeyValueProp, Lit, MemberExpr, MemberProp, Number, Prop,
8            PropName, SeqExpr, SimpleAssignTarget, Str,
9        },
10        visit::fields::{CalleeField, PropField},
11    },
12};
13use turbo_rcstr::RcStr;
14use turbo_tasks::{NonLocalValue, ResolvedVc, Vc, trace::TraceRawVcs};
15use turbopack_core::{chunk::ChunkingContext, module_graph::ModuleGraph};
16
17use super::EsmAssetReference;
18use crate::{
19    code_gen::{CodeGen, CodeGeneration},
20    create_visitor,
21    references::{AstPath, esm::base::ReferencedAsset},
22};
23
24#[derive(Hash, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, TraceRawVcs, NonLocalValue)]
25pub struct EsmBinding {
26    pub reference: ResolvedVc<EsmAssetReference>,
27    pub export: Option<RcStr>,
28    pub ast_path: AstPath,
29}
30
31impl EsmBinding {
32    pub fn new(
33        reference: ResolvedVc<EsmAssetReference>,
34        export: Option<RcStr>,
35        ast_path: AstPath,
36    ) -> Self {
37        EsmBinding {
38            reference,
39            export,
40            ast_path,
41        }
42    }
43
44    pub async fn code_generation(
45        &self,
46        _module_graph: Vc<ModuleGraph>,
47        chunking_context: Vc<Box<dyn ChunkingContext>>,
48    ) -> Result<CodeGeneration> {
49        let mut visitors = vec![];
50
51        let export = self.export.clone();
52        let imported_module = self.reference.get_referenced_asset();
53
54        enum ImportedIdent {
55            Module(String),
56            None,
57            Unresolvable,
58        }
59
60        let imported_ident = match &*imported_module.await? {
61            ReferencedAsset::None => ImportedIdent::None,
62            imported_module => imported_module
63                .get_ident(chunking_context)
64                .await?
65                .map_or(ImportedIdent::Unresolvable, ImportedIdent::Module),
66        };
67
68        let mut ast_path = self.ast_path.0.clone();
69        loop {
70            match ast_path.last() {
71                // Shorthand properties get special treatment because we need to rewrite them to
72                // normal key-value pairs.
73                Some(swc_core::ecma::visit::AstParentKind::Prop(PropField::Shorthand)) => {
74                    ast_path.pop();
75                    visitors.push(
76                        create_visitor!(exact ast_path, visit_mut_prop(prop: &mut Prop) {
77                            if let Prop::Shorthand(ident) = prop {
78                                // TODO: Merge with the above condition when https://rust-lang.github.io/rfcs/2497-if-let-chains.html lands.
79                                match &imported_ident {
80                                    ImportedIdent::Module(imported_ident) => {
81                                        *prop = Prop::KeyValue(KeyValueProp {
82                                            key: PropName::Ident(ident.clone().into()),
83                                            value: Box::new(make_expr(
84                                                imported_ident,
85                                                export.as_deref(),
86                                                ident.span,
87                                                false,
88                                            )),
89                                        });
90                                    }
91                                    ImportedIdent::None => {
92                                        *prop = Prop::KeyValue(KeyValueProp {
93                                            key: PropName::Ident(ident.clone().into()),
94                                            value: Expr::undefined(ident.span),
95                                        });
96                                    }
97                                    ImportedIdent::Unresolvable => {
98                                        // Do nothing, the reference will insert a throw
99                                    }
100                                }
101                            }
102                        }),
103                    );
104                    break;
105                }
106                // Any other expression can be replaced with the import accessor.
107                Some(swc_core::ecma::visit::AstParentKind::Expr(_)) => {
108                    ast_path.pop();
109                    let in_call = matches!(
110                        ast_path.last(),
111                        Some(swc_core::ecma::visit::AstParentKind::Callee(
112                            CalleeField::Expr
113                        ))
114                    );
115
116                    visitors.push(
117                        create_visitor!(exact ast_path, visit_mut_expr(expr: &mut Expr) {
118                            use swc_core::common::Spanned;
119                            match &imported_ident {
120                                ImportedIdent::Module(imported_ident) => {
121                                    *expr = make_expr(imported_ident, export.as_deref(), expr.span(), in_call);
122                                }
123                                ImportedIdent::None => {
124                                    *expr = *Expr::undefined(expr.span());
125                                }
126                                ImportedIdent::Unresolvable => {
127                                    // Do nothing, the reference will insert a throw
128                                }
129                            }
130                        }),
131                    );
132                    break;
133                }
134                Some(swc_core::ecma::visit::AstParentKind::BindingIdent(
135                    swc_core::ecma::visit::fields::BindingIdentField::Id,
136                )) => {
137                    ast_path.pop();
138
139                    // We need to handle LHS because of code like
140                    // (function (RouteKind1){})(RouteKind || RouteKind = {})
141                    if let Some(swc_core::ecma::visit::AstParentKind::SimpleAssignTarget(
142                        swc_core::ecma::visit::fields::SimpleAssignTargetField::Ident,
143                    )) = ast_path.last()
144                    {
145                        ast_path.pop();
146
147                        visitors.push(
148                        create_visitor!(exact ast_path, visit_mut_simple_assign_target(l: &mut SimpleAssignTarget) {
149                            use swc_core::common::Spanned;
150                            match &imported_ident {
151                                ImportedIdent::Module(imported_ident) => {
152                                    *l = match make_expr(imported_ident, export.as_deref(), l.span(), false) {
153                                        Expr::Ident(ident) => SimpleAssignTarget::Ident(ident.into()),
154                                        Expr::Member(member) => SimpleAssignTarget::Member(member),
155                                        _ => unreachable!(),
156                                    };
157                                }
158                                ImportedIdent::None => {
159                                    // Do nothing, cannot assign to `undefined`
160                                }
161                                ImportedIdent::Unresolvable => {
162                                    // Do nothing, the reference will insert a throw
163                                }
164                            }
165                        }));
166                        break;
167                    }
168                }
169                Some(_) => {
170                    ast_path.pop();
171                }
172                None => break,
173            }
174        }
175
176        Ok(CodeGeneration::visitors(visitors))
177    }
178}
179
180impl From<EsmBinding> for CodeGen {
181    fn from(val: EsmBinding) -> Self {
182        CodeGen::EsmBinding(val)
183    }
184}
185
186fn make_expr(imported_module: &str, export: Option<&str>, span: Span, in_call: bool) -> Expr {
187    if let Some(export) = export {
188        let mut expr = Expr::Member(MemberExpr {
189            span,
190            obj: Box::new(Expr::Ident(Ident::new(
191                imported_module.into(),
192                span,
193                Default::default(),
194            ))),
195            prop: MemberProp::Computed(ComputedPropName {
196                span,
197                expr: Box::new(Expr::Lit(Lit::Str(Str {
198                    span,
199                    value: export.into(),
200                    raw: None,
201                }))),
202            }),
203        });
204        if in_call {
205            expr = Expr::Seq(SeqExpr {
206                exprs: vec![
207                    Box::new(Expr::Lit(Lit::Num(Number {
208                        span,
209                        value: 0.0,
210                        raw: None,
211                    }))),
212                    Box::new(expr),
213                ],
214                span,
215            });
216        }
217        expr
218    } else {
219        Expr::Ident(Ident::new(imported_module.into(), span, Default::default()))
220    }
221}