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