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};
16use turbopack_core::{
17    chunk::{ChunkingContext, ChunkingType, ModuleChunkItemIdExt},
18    issue::IssueSource,
19    reference::ModuleReference,
20    reference_type::{CommonJsReferenceSubType, EcmaScriptModulesReferenceSubType},
21    resolve::{ModuleResolveResult, ResolveErrorMode, origin::ResolveOrigin, parse::Request},
22};
23use turbopack_resolve::ecmascript::{cjs_resolve, esm_resolve};
24
25use crate::{
26    ScopeHoistingContext,
27    ast_path_trie::{AstPathId, AstPathTrie},
28    code_gen::{CodeGen, CodeGeneration},
29    create_visitor,
30    references::{
31        esm::{EsmAssetReference, base::ReferencedAsset},
32        pattern_mapping::{PatternMapping, ResolveType},
33    },
34    runtime_functions::TURBOPACK_IMPORT,
35    utils::module_id_to_lit,
36};
37
38/// An asset reference for modules accepted via `module.hot.accept(dep, callback)` or
39/// `import.meta.hot.accept(dep, callback)`. Ensures the accepted dependency is included
40/// in the chunk graph so it can be hot-replaced at runtime.
41#[turbo_tasks::value]
42#[derive(Hash, Debug, ValueToString)]
43#[value_to_string("module.hot.accept/decline {request}")]
44pub struct ModuleHotReferenceAssetReference {
45    origin: ResolvedVc<Box<dyn ResolveOrigin>>,
46    pub request: ResolvedVc<Request>,
47    issue_source: IssueSource,
48    error_mode: ResolveErrorMode,
49    is_esm: bool,
50}
51
52#[turbo_tasks::value_impl]
53impl ModuleHotReferenceAssetReference {
54    #[turbo_tasks::function]
55    pub fn new(
56        origin: ResolvedVc<Box<dyn ResolveOrigin>>,
57        request: ResolvedVc<Request>,
58        issue_source: IssueSource,
59        error_mode: ResolveErrorMode,
60        is_esm: bool,
61    ) -> Vc<Self> {
62        Self::cell(ModuleHotReferenceAssetReference {
63            origin,
64            request,
65            issue_source,
66            error_mode,
67            is_esm,
68        })
69    }
70}
71
72#[turbo_tasks::value_impl]
73impl ModuleReference for ModuleHotReferenceAssetReference {
74    #[turbo_tasks::function]
75    async fn resolve_reference(&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    fn chunking_type(&self) -> Option<ChunkingType> {
97        Some(ChunkingType::Parallel {
98            inherit_async: false,
99            hoisted: false,
100        })
101    }
102}
103
104#[derive(PartialEq, Eq, ValueDebugFormat, NonLocalValue, Hash, Debug, Encode, Decode)]
105pub struct ModuleHotReferenceCodeGen {
106    references: Vec<ResolvedVc<ModuleHotReferenceAssetReference>>,
107    /// For ESM modules, the matching ESM import reference for each dep (if any).
108    /// This is used to generate code that re-assigns the ESM namespace variable
109    /// after an HMR update so that imported bindings reflect the updated module.
110    esm_references: Vec<Option<ResolvedVc<EsmAssetReference>>>,
111    path: AstPathId,
112}
113
114impl ModuleHotReferenceCodeGen {
115    pub fn new(
116        references: Vec<ResolvedVc<ModuleHotReferenceAssetReference>>,
117        esm_references: Vec<Option<ResolvedVc<EsmAssetReference>>>,
118        path: AstPathId,
119    ) -> Self {
120        ModuleHotReferenceCodeGen {
121            references,
122            esm_references,
123            path,
124        }
125    }
126
127    pub async fn code_generation(
128        &self,
129        trie: &AstPathTrie,
130        chunking_context: Vc<Box<dyn ChunkingContext>>,
131        scope_hoisting_context: ScopeHoistingContext<'_>,
132    ) -> Result<CodeGeneration> {
133        let resolved_ids: Vec<ReadRef<PatternMapping>> = self
134            .references
135            .iter()
136            .map(async |reference| {
137                let r = reference.await?;
138                let resolve_result = reference.resolve_reference();
139                PatternMapping::resolve_request(
140                    *r.request,
141                    *r.origin,
142                    chunking_context,
143                    resolve_result,
144                    ResolveType::ChunkItem,
145                    None,
146                )
147                .await
148            })
149            .try_join()
150            .await?;
151
152        // Resolve ESM binding re-import information for each dep.
153        // Each entry is (namespace_ident, module_id_expr) if the dep has a matching ESM import.
154        let esm_reimports: Vec<Option<(String, SyntaxContext, Expr)>> = self
155            .esm_references
156            .iter()
157            .map(async |esm_ref| {
158                let Some(esm_ref) = esm_ref else {
159                    return Ok(None);
160                };
161                let referenced_asset = esm_ref.get_referenced_asset().await?;
162                match &referenced_asset {
163                    ReferencedAsset::Some(asset) => {
164                        let ident = referenced_asset
165                            .get_ident(chunking_context, None, scope_hoisting_context)
166                            .await?;
167                        if let Some((namespace_ident, ctxt)) =
168                            ident.and_then(|i| i.into_module_namespace_ident())
169                        {
170                            let id = asset.chunk_item_id(chunking_context).await?;
171                            let module_id_expr = module_id_to_lit(&id);
172                            return Ok(Some((
173                                namespace_ident,
174                                ctxt.unwrap_or_default(),
175                                module_id_expr,
176                            )));
177                        }
178                        Ok(None)
179                    }
180                    _ => Ok(None),
181                }
182            })
183            .try_join()
184            .await?;
185
186        let is_single = self.references.len() == 1;
187
188        // Build the list of re-import assignment statements for the callback wrapper.
189        let mut reimport_stmts: Vec<Stmt> = Vec::new();
190        for (namespace_ident, ctxt, module_id_expr) in esm_reimports.iter().flatten() {
191            let name = Ident::new(namespace_ident.as_str().into(), DUMMY_SP, *ctxt);
192            let turbopack_import: Expr = TURBOPACK_IMPORT.into();
193            reimport_stmts.push(quote!(
194                "$name = $turbopack_import($id);" as Stmt,
195                name = name,
196                turbopack_import: Expr = turbopack_import,
197                id: Expr = module_id_expr.clone(),
198            ));
199        }
200        let has_reimports = !reimport_stmts.is_empty();
201
202        let mut visitors = Vec::new();
203        visitors.push(create_visitor!(
204            trie,
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(ArrowFunctionBody::FunctionBody(FunctionBody {
247                                    span: DUMMY_SP,
248                                    stmts: wrapper_stmts,
249                                })),
250                                ..Default::default()
251                            });
252                        } else {
253                            // No user callback — add one that just re-imports
254                            call_expr.args.push(ExprOrSpread {
255                                spread: None,
256                                expr: Box::new(Expr::Arrow(ArrowExpr {
257                                    span: DUMMY_SP,
258                                    params: vec![],
259                                    body: Box::new(ArrowFunctionBody::FunctionBody(FunctionBody {
260                                        span: DUMMY_SP,
261                                        stmts: wrapper_stmts,
262                                    })),
263                                    ..Default::default()
264                                })),
265                            });
266                        }
267                    }
268                }
269            }
270        ));
271
272        Ok(CodeGeneration::visitors(visitors))
273    }
274}
275
276impl From<ModuleHotReferenceCodeGen> for CodeGen {
277    fn from(val: ModuleHotReferenceCodeGen) -> Self {
278        CodeGen::ModuleHotReferenceCodeGen(val)
279    }
280}