Skip to main content

turbopack_ecmascript/references/
hot_module.rs

1use std::mem::take;
2
3use anyhow::Result;
4use bincode::{Decode, Encode};
5use swc_core::{
6    common::{DUMMY_SP, SyntaxContext},
7    ecma::ast::{
8        ArrowExpr, ArrowFunctionBody, CallExpr, Callee, Expr, ExprOrSpread, ExprStmt, FunctionBody,
9        Ident, Stmt,
10    },
11    quote,
12};
13use turbo_tasks::{
14    NonLocalValue, ReadRef, ResolvedVc, TryJoinIterExt, ValueToString, Vc, debug::ValueDebugFormat,
15    trace::TraceRawVcs,
16};
17use turbopack_core::{
18    chunk::{ChunkingContext, ChunkingType, ModuleChunkItemIdExt},
19    issue::IssueSource,
20    reference::ModuleReference,
21    reference_type::{CommonJsReferenceSubType, EcmaScriptModulesReferenceSubType},
22    resolve::{ModuleResolveResult, ResolveErrorMode, origin::ResolveOrigin, parse::Request},
23};
24use turbopack_resolve::ecmascript::{cjs_resolve, esm_resolve};
25
26use crate::{
27    ScopeHoistingContext,
28    code_gen::{CodeGen, CodeGeneration},
29    create_visitor,
30    references::{
31        AstPath,
32        esm::{EsmAssetReference, base::ReferencedAsset},
33        pattern_mapping::{PatternMapping, ResolveType},
34    },
35    runtime_functions::TURBOPACK_IMPORT,
36    utils::module_id_to_lit,
37};
38
39/// An asset reference for modules accepted via `module.hot.accept(dep, callback)` or
40/// `import.meta.hot.accept(dep, callback)`. Ensures the accepted dependency is included
41/// in the chunk graph so it can be hot-replaced at runtime.
42#[turbo_tasks::value]
43#[derive(Hash, Debug, ValueToString)]
44#[value_to_string("module.hot.accept/decline {request}")]
45pub struct ModuleHotReferenceAssetReference {
46    origin: ResolvedVc<Box<dyn ResolveOrigin>>,
47    pub request: ResolvedVc<Request>,
48    issue_source: IssueSource,
49    error_mode: ResolveErrorMode,
50    is_esm: bool,
51}
52
53#[turbo_tasks::value_impl]
54impl ModuleHotReferenceAssetReference {
55    #[turbo_tasks::function]
56    pub fn new(
57        origin: ResolvedVc<Box<dyn ResolveOrigin>>,
58        request: ResolvedVc<Request>,
59        issue_source: IssueSource,
60        error_mode: ResolveErrorMode,
61        is_esm: bool,
62    ) -> Vc<Self> {
63        Self::cell(ModuleHotReferenceAssetReference {
64            origin,
65            request,
66            issue_source,
67            error_mode,
68            is_esm,
69        })
70    }
71}
72
73#[turbo_tasks::value_impl]
74impl ModuleReference for ModuleHotReferenceAssetReference {
75    #[turbo_tasks::function]
76    async fn resolve_reference(&self) -> Result<Vc<ModuleResolveResult>> {
77        if self.is_esm {
78            esm_resolve(
79                *self.origin,
80                *self.request,
81                EcmaScriptModulesReferenceSubType::Undefined,
82                self.error_mode,
83                Some(self.issue_source),
84            )
85            .await
86        } else {
87            Ok(cjs_resolve(
88                *self.origin,
89                *self.request,
90                CommonJsReferenceSubType::Undefined,
91                Some(self.issue_source),
92                self.error_mode,
93            ))
94        }
95    }
96
97    fn chunking_type(&self) -> Option<ChunkingType> {
98        Some(ChunkingType::Parallel {
99            inherit_async: false,
100            hoisted: false,
101        })
102    }
103}
104
105#[derive(
106    PartialEq, Eq, TraceRawVcs, ValueDebugFormat, NonLocalValue, Hash, Debug, Encode, Decode,
107)]
108pub struct ModuleHotReferenceCodeGen {
109    references: Vec<ResolvedVc<ModuleHotReferenceAssetReference>>,
110    /// For ESM modules, the matching ESM import reference for each dep (if any).
111    /// This is used to generate code that re-assigns the ESM namespace variable
112    /// after an HMR update so that imported bindings reflect the updated module.
113    esm_references: Vec<Option<ResolvedVc<EsmAssetReference>>>,
114    path: AstPath,
115}
116
117impl ModuleHotReferenceCodeGen {
118    pub fn new(
119        references: Vec<ResolvedVc<ModuleHotReferenceAssetReference>>,
120        esm_references: Vec<Option<ResolvedVc<EsmAssetReference>>>,
121        path: AstPath,
122    ) -> Self {
123        ModuleHotReferenceCodeGen {
124            references,
125            esm_references,
126            path,
127        }
128    }
129
130    pub async fn code_generation(
131        &self,
132        chunking_context: Vc<Box<dyn ChunkingContext>>,
133        scope_hoisting_context: ScopeHoistingContext<'_>,
134    ) -> Result<CodeGeneration> {
135        let resolved_ids: Vec<ReadRef<PatternMapping>> = self
136            .references
137            .iter()
138            .map(async |reference| {
139                let r = reference.await?;
140                let resolve_result = reference.resolve_reference();
141                PatternMapping::resolve_request(
142                    *r.request,
143                    *r.origin,
144                    chunking_context,
145                    resolve_result,
146                    ResolveType::ChunkItem,
147                    None,
148                )
149                .await
150            })
151            .try_join()
152            .await?;
153
154        // Resolve ESM binding re-import information for each dep.
155        // Each entry is (namespace_ident, module_id_expr) if the dep has a matching ESM import.
156        let esm_reimports: Vec<Option<(String, SyntaxContext, Expr)>> = self
157            .esm_references
158            .iter()
159            .map(async |esm_ref| {
160                let Some(esm_ref) = esm_ref else {
161                    return Ok(None);
162                };
163                let referenced_asset = esm_ref.get_referenced_asset().await?;
164                match &referenced_asset {
165                    ReferencedAsset::Some(asset) => {
166                        let ident = referenced_asset
167                            .get_ident(chunking_context, None, scope_hoisting_context)
168                            .await?;
169                        if let Some((namespace_ident, ctxt)) =
170                            ident.and_then(|i| i.into_module_namespace_ident())
171                        {
172                            let id = asset.chunk_item_id(chunking_context).await?;
173                            let module_id_expr = module_id_to_lit(&id);
174                            return Ok(Some((
175                                namespace_ident,
176                                ctxt.unwrap_or_default(),
177                                module_id_expr,
178                            )));
179                        }
180                        Ok(None)
181                    }
182                    _ => Ok(None),
183                }
184            })
185            .try_join()
186            .await?;
187
188        let is_single = self.references.len() == 1;
189
190        // Build the list of re-import assignment statements for the callback wrapper.
191        let mut reimport_stmts: Vec<Stmt> = Vec::new();
192        for (namespace_ident, ctxt, module_id_expr) in esm_reimports.iter().flatten() {
193            let name = Ident::new(namespace_ident.as_str().into(), DUMMY_SP, *ctxt);
194            let turbopack_import: Expr = TURBOPACK_IMPORT.into();
195            reimport_stmts.push(quote!(
196                "$name = $turbopack_import($id);" as Stmt,
197                name = name,
198                turbopack_import: Expr = turbopack_import,
199                id: Expr = module_id_expr.clone(),
200            ));
201        }
202        let has_reimports = !reimport_stmts.is_empty();
203
204        let mut visitors = Vec::new();
205        visitors.push(create_visitor!(
206            self.path,
207            visit_mut_expr,
208            |expr: &mut Expr| {
209                if let Expr::Call(call_expr) = expr {
210                    if call_expr.args.is_empty() {
211                        return;
212                    }
213                    // Replace dep path strings with resolved module IDs
214                    if is_single {
215                        let key_expr = take(&mut *call_expr.args[0].expr);
216                        *call_expr.args[0].expr = resolved_ids[0].create_id(key_expr);
217                    } else if let Expr::Array(array_lit) = &mut *call_expr.args[0].expr {
218                        for (i, elem) in array_lit.elems.iter_mut().enumerate() {
219                            if let Some(elem) = elem
220                                && i < resolved_ids.len()
221                            {
222                                let key_expr = take(&mut *elem.expr);
223                                *elem.expr = resolved_ids[i].create_id(key_expr);
224                            }
225                        }
226                    }
227
228                    // Wrap or inject callback to re-import ESM bindings
229                    if has_reimports {
230                        let mut wrapper_stmts = reimport_stmts.clone();
231
232                        if call_expr.args.len() >= 2 {
233                            // There's a user callback — call it after re-importing
234                            let user_cb = take(&mut *call_expr.args[1].expr);
235                            wrapper_stmts.push(Stmt::Expr(ExprStmt {
236                                span: DUMMY_SP,
237                                expr: Box::new(Expr::Call(CallExpr {
238                                    span: DUMMY_SP,
239                                    callee: Callee::Expr(Box::new(user_cb)),
240                                    args: vec![],
241                                    ..Default::default()
242                                })),
243                            }));
244                            *call_expr.args[1].expr = Expr::Arrow(ArrowExpr {
245                                span: DUMMY_SP,
246                                params: vec![],
247                                body: Box::new(ArrowFunctionBody::FunctionBody(FunctionBody {
248                                    span: DUMMY_SP,
249                                    stmts: wrapper_stmts,
250                                })),
251                                ..Default::default()
252                            });
253                        } else {
254                            // No user callback — add one that just re-imports
255                            call_expr.args.push(ExprOrSpread {
256                                spread: None,
257                                expr: Box::new(Expr::Arrow(ArrowExpr {
258                                    span: DUMMY_SP,
259                                    params: vec![],
260                                    body: Box::new(ArrowFunctionBody::FunctionBody(FunctionBody {
261                                        span: DUMMY_SP,
262                                        stmts: wrapper_stmts,
263                                    })),
264                                    ..Default::default()
265                                })),
266                            });
267                        }
268                    }
269                }
270            }
271        ));
272
273        Ok(CodeGeneration::visitors(visitors))
274    }
275}
276
277impl From<ModuleHotReferenceCodeGen> for CodeGen {
278    fn from(val: ModuleHotReferenceCodeGen) -> Self {
279        CodeGen::ModuleHotReferenceCodeGen(val)
280    }
281}