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_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(|reference| async move {
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                )
148                .await
149            })
150            .try_join()
151            .await?;
152
153        // Resolve ESM binding re-import information for each dep.
154        // Each entry is (namespace_ident, module_id_expr) if the dep has a matching ESM import.
155        let esm_reimports: Vec<Option<(String, SyntaxContext, Expr)>> = self
156            .esm_references
157            .iter()
158            .map(|esm_ref| async move {
159                let Some(esm_ref) = esm_ref else {
160                    return Ok(None);
161                };
162                let referenced_asset = esm_ref.get_referenced_asset().await?;
163                match &referenced_asset {
164                    ReferencedAsset::Some(asset) => {
165                        let ident = referenced_asset
166                            .get_ident(chunking_context, None, scope_hoisting_context)
167                            .await?;
168                        if let Some((namespace_ident, ctxt)) =
169                            ident.and_then(|i| i.into_module_namespace_ident())
170                        {
171                            let id = asset.chunk_item_id(chunking_context).await?;
172                            let module_id_expr = module_id_to_lit(&id);
173                            return Ok(Some((
174                                namespace_ident,
175                                ctxt.unwrap_or_default(),
176                                module_id_expr,
177                            )));
178                        }
179                        Ok(None)
180                    }
181                    _ => Ok(None),
182                }
183            })
184            .try_join()
185            .await?;
186
187        let is_single = self.references.len() == 1;
188
189        // Build the list of re-import assignment statements for the callback wrapper.
190        let mut reimport_stmts: Vec<Stmt> = Vec::new();
191        for (namespace_ident, ctxt, module_id_expr) in esm_reimports.iter().flatten() {
192            let name = Ident::new(namespace_ident.as_str().into(), DUMMY_SP, *ctxt);
193            let turbopack_import: Expr = TURBOPACK_IMPORT.into();
194            reimport_stmts.push(quote!(
195                "$name = $turbopack_import($id);" as Stmt,
196                name = name,
197                turbopack_import: Expr = turbopack_import,
198                id: Expr = module_id_expr.clone(),
199            ));
200        }
201        let has_reimports = !reimport_stmts.is_empty();
202
203        let mut visitors = Vec::new();
204        visitors.push(create_visitor!(
205            self.path,
206            visit_mut_expr,
207            |expr: &mut Expr| {
208                if let Expr::Call(call_expr) = expr {
209                    if call_expr.args.is_empty() {
210                        return;
211                    }
212                    // Replace dep path strings with resolved module IDs
213                    if is_single {
214                        let key_expr = take(&mut *call_expr.args[0].expr);
215                        *call_expr.args[0].expr = resolved_ids[0].create_id(key_expr);
216                    } else if let Expr::Array(array_lit) = &mut *call_expr.args[0].expr {
217                        for (i, elem) in array_lit.elems.iter_mut().enumerate() {
218                            if let Some(elem) = elem
219                                && i < resolved_ids.len()
220                            {
221                                let key_expr = take(&mut *elem.expr);
222                                *elem.expr = resolved_ids[i].create_id(key_expr);
223                            }
224                        }
225                    }
226
227                    // Wrap or inject callback to re-import ESM bindings
228                    if has_reimports {
229                        let mut wrapper_stmts = reimport_stmts.clone();
230
231                        if call_expr.args.len() >= 2 {
232                            // There's a user callback — call it after re-importing
233                            let user_cb = take(&mut *call_expr.args[1].expr);
234                            wrapper_stmts.push(Stmt::Expr(ExprStmt {
235                                span: DUMMY_SP,
236                                expr: Box::new(Expr::Call(CallExpr {
237                                    span: DUMMY_SP,
238                                    callee: Callee::Expr(Box::new(user_cb)),
239                                    args: vec![],
240                                    ..Default::default()
241                                })),
242                            }));
243                            *call_expr.args[1].expr = Expr::Arrow(ArrowExpr {
244                                span: DUMMY_SP,
245                                params: vec![],
246                                body: Box::new(BlockStmtOrExpr::BlockStmt(BlockStmt {
247                                    span: DUMMY_SP,
248                                    stmts: wrapper_stmts,
249                                    ..Default::default()
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(BlockStmtOrExpr::BlockStmt(BlockStmt {
261                                        span: DUMMY_SP,
262                                        stmts: wrapper_stmts,
263                                        ..Default::default()
264                                    })),
265                                    ..Default::default()
266                                })),
267                            });
268                        }
269                    }
270                }
271            }
272        ));
273
274        Ok(CodeGeneration::visitors(visitors))
275    }
276}
277
278impl From<ModuleHotReferenceCodeGen> for CodeGen {
279    fn from(val: ModuleHotReferenceCodeGen) -> Self {
280        CodeGen::ModuleHotReferenceCodeGen(val)
281    }
282}