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 ident = referenced_asset
184                            .get_ident(chunking_context, None, scope_hoisting_context)
185                            .await?;
186                        if let Some((namespace_ident, ctxt)) =
187                            ident.and_then(|i| i.into_module_namespace_ident())
188                        {
189                            let id = asset.chunk_item_id(chunking_context).await?;
190                            let module_id_expr = module_id_to_lit(&id);
191                            return Ok(Some((
192                                namespace_ident,
193                                ctxt.unwrap_or_default(),
194                                module_id_expr,
195                            )));
196                        }
197                        Ok(None)
198                    }
199                    _ => Ok(None),
200                }
201            })
202            .try_join()
203            .await?;
204
205        let is_single = self.references.len() == 1;
206
207        // Build the list of re-import assignment statements for the callback wrapper.
208        let mut reimport_stmts: Vec<Stmt> = Vec::new();
209        for (namespace_ident, ctxt, module_id_expr) in esm_reimports.iter().flatten() {
210            let name = Ident::new(namespace_ident.as_str().into(), DUMMY_SP, *ctxt);
211            let turbopack_import: Expr = TURBOPACK_IMPORT.into();
212            reimport_stmts.push(quote!(
213                "$name = $turbopack_import($id);" as Stmt,
214                name = name,
215                turbopack_import: Expr = turbopack_import,
216                id: Expr = module_id_expr.clone(),
217            ));
218        }
219        let has_reimports = !reimport_stmts.is_empty();
220
221        let mut visitors = Vec::new();
222        visitors.push(create_visitor!(
223            self.path,
224            visit_mut_expr,
225            |expr: &mut Expr| {
226                if let Expr::Call(call_expr) = expr {
227                    if call_expr.args.is_empty() {
228                        return;
229                    }
230                    // Replace dep path strings with resolved module IDs
231                    if is_single {
232                        let key_expr = take(&mut *call_expr.args[0].expr);
233                        *call_expr.args[0].expr = resolved_ids[0].create_id(key_expr);
234                    } else if let Expr::Array(array_lit) = &mut *call_expr.args[0].expr {
235                        for (i, elem) in array_lit.elems.iter_mut().enumerate() {
236                            if let Some(elem) = elem
237                                && i < resolved_ids.len()
238                            {
239                                let key_expr = take(&mut *elem.expr);
240                                *elem.expr = resolved_ids[i].create_id(key_expr);
241                            }
242                        }
243                    }
244
245                    // Wrap or inject callback to re-import ESM bindings
246                    if has_reimports {
247                        let mut wrapper_stmts = reimport_stmts.clone();
248
249                        if call_expr.args.len() >= 2 {
250                            // There's a user callback — call it after re-importing
251                            let user_cb = take(&mut *call_expr.args[1].expr);
252                            wrapper_stmts.push(Stmt::Expr(ExprStmt {
253                                span: DUMMY_SP,
254                                expr: Box::new(Expr::Call(CallExpr {
255                                    span: DUMMY_SP,
256                                    callee: Callee::Expr(Box::new(user_cb)),
257                                    args: vec![],
258                                    ..Default::default()
259                                })),
260                            }));
261                            *call_expr.args[1].expr = Expr::Arrow(ArrowExpr {
262                                span: DUMMY_SP,
263                                params: vec![],
264                                body: Box::new(BlockStmtOrExpr::BlockStmt(BlockStmt {
265                                    span: DUMMY_SP,
266                                    stmts: wrapper_stmts,
267                                    ..Default::default()
268                                })),
269                                ..Default::default()
270                            });
271                        } else {
272                            // No user callback — add one that just re-imports
273                            call_expr.args.push(ExprOrSpread {
274                                spread: None,
275                                expr: Box::new(Expr::Arrow(ArrowExpr {
276                                    span: DUMMY_SP,
277                                    params: vec![],
278                                    body: Box::new(BlockStmtOrExpr::BlockStmt(BlockStmt {
279                                        span: DUMMY_SP,
280                                        stmts: wrapper_stmts,
281                                        ..Default::default()
282                                    })),
283                                    ..Default::default()
284                                })),
285                            });
286                        }
287                    }
288                }
289            }
290        ));
291
292        Ok(CodeGeneration::visitors(visitors))
293    }
294}
295
296impl From<ModuleHotReferenceCodeGen> for CodeGen {
297    fn from(val: ModuleHotReferenceCodeGen) -> Self {
298        CodeGen::ModuleHotReferenceCodeGen(val)
299    }
300}