1mod hot_module_replacement;
2
3use std::collections::hash_map;
4
5use hot_module_replacement::HotModuleReplacementRuntimeModule;
6use rspack_collections::IdentifierSet;
7use rspack_core::{
8 AssetInfo, Chunk, ChunkGraph, ChunkKind, ChunkUkey, Compilation,
9 CompilationAdditionalTreeRuntimeRequirements, CompilationAsset, CompilationParams,
10 CompilationProcessAssets, CompilationRecords, CompilerCompilation, DependencyType, LoaderContext,
11 ModuleId, ModuleIdentifier, ModuleType, NormalModuleFactoryParser, NormalModuleLoader,
12 ParserAndGenerator, ParserOptions, PathData, Plugin, RunnerContext, RuntimeGlobals,
13 RuntimeModule, RuntimeModuleExt, RuntimeSpec,
14 chunk_graph_chunk::{ChunkId, ChunkIdSet},
15 rspack_sources::{RawStringSource, SourceExt},
16};
17use rspack_error::{Diagnostic, Result};
18use rspack_hook::{plugin, plugin_hook};
19use rspack_plugin_css::parser_and_generator::CssParserAndGenerator;
20use rspack_plugin_javascript::{
21 hot_module_replacement_plugin::{
22 ImportMetaHotReplacementParserPlugin, ModuleHotReplacementParserPlugin,
23 },
24 parser_and_generator::JavaScriptParserAndGenerator,
25};
26use rustc_hash::{FxHashMap as HashMap, FxHashSet as HashSet};
27
28#[plugin]
29#[derive(Debug, Default)]
30pub struct HotModuleReplacementPlugin;
31
32#[plugin_hook(CompilerCompilation for HotModuleReplacementPlugin)]
33async fn compilation(
34 &self,
35 compilation: &mut Compilation,
36 params: &mut CompilationParams,
37) -> Result<()> {
38 compilation.set_dependency_factory(
39 DependencyType::ImportMetaHotAccept,
40 params.normal_module_factory.clone(),
41 );
42 compilation.set_dependency_factory(
43 DependencyType::ImportMetaHotDecline,
44 params.normal_module_factory.clone(),
45 );
46 compilation.set_dependency_factory(
47 DependencyType::ModuleHotAccept,
48 params.normal_module_factory.clone(),
49 );
50 compilation.set_dependency_factory(
51 DependencyType::ModuleHotDecline,
52 params.normal_module_factory.clone(),
53 );
54 Ok(())
55}
56
57#[plugin_hook(CompilationProcessAssets for HotModuleReplacementPlugin, stage = Compilation::PROCESS_ASSETS_STAGE_ADDITIONAL)]
58async fn process_assets(&self, compilation: &mut Compilation) -> Result<()> {
59 let Some(records) = compilation.records.take() else {
60 return Ok(());
61 };
62 let CompilationRecords {
63 chunks: old_chunks,
64 runtimes: all_old_runtime,
65 modules: old_all_modules,
66 runtime_modules: old_runtime_modules,
67 hash: old_hash,
68 } = records.as_ref();
69
70 if let Some(old_hash) = &old_hash
71 && let Some(hash) = &compilation.hash
72 && old_hash == hash
73 {
74 return Ok(());
75 }
76
77 let mut hot_update_main_content_by_runtime = all_old_runtime
78 .iter()
79 .map(|&runtime| (runtime, HotUpdateContent::default()))
80 .collect::<HashMap<_, HotUpdateContent>>();
81
82 if hot_update_main_content_by_runtime.is_empty() {
83 return Ok(());
84 }
85
86 let mut updated_runtime_modules: IdentifierSet = Default::default();
87 let mut updated_chunks: HashMap<ChunkUkey, HashSet<String>> = Default::default();
88 for (identifier, old_runtime_module_hash) in old_runtime_modules {
89 if let Some(new_runtime_module_hash) = compilation.runtime_modules_hash.get(identifier) {
90 if new_runtime_module_hash != old_runtime_module_hash {
92 updated_runtime_modules.insert(*identifier);
93 }
94 }
95 }
96 for identifier in compilation.runtime_modules.keys() {
97 if !old_runtime_modules.contains_key(identifier) {
98 updated_runtime_modules.insert(*identifier);
100 }
101 }
102
103 let all_module_ids: HashMap<ModuleId, ModuleIdentifier> = compilation
104 .module_ids_artifact
105 .iter()
106 .map(|(k, v)| (v.clone(), *k))
107 .collect();
108 let mut completely_removed_modules: HashSet<ModuleId> = Default::default();
109
110 for (chunk_id, (old_runtime, old_module_ids)) in old_chunks {
111 let mut remaining_modules: HashSet<ModuleId> = Default::default();
112 for old_module_id in old_module_ids {
113 if !all_module_ids.contains_key(old_module_id) {
114 completely_removed_modules.insert(old_module_id.clone());
115 } else {
116 remaining_modules.insert(old_module_id.clone());
117 }
118 }
119
120 let mut new_modules = vec![];
121 let mut new_runtime_modules = vec![];
122 let chunk_id = chunk_id.clone();
123 let new_runtime: RuntimeSpec;
124 let removed_from_runtime: RuntimeSpec;
125
126 let current_chunk = compilation
127 .build_chunk_graph_artifact
128 .chunk_by_ukey
129 .iter()
130 .find(|(_, chunk)| chunk.expect_id().eq(&chunk_id))
131 .map(|(_, chunk)| chunk);
132 let current_chunk_ukey = current_chunk.map(|c| c.ukey());
133
134 if let Some(current_chunk) = current_chunk {
135 new_runtime = current_chunk
136 .runtime()
137 .intersection(all_old_runtime)
138 .copied()
139 .collect();
140
141 if new_runtime.is_empty() {
142 continue;
143 }
144
145 new_modules = compilation
146 .build_chunk_graph_artifact
147 .chunk_graph
148 .get_chunk_modules_identifier(¤t_chunk.ukey())
149 .iter()
150 .filter_map(|&module| {
151 let module_id = ChunkGraph::get_module_id(&compilation.module_ids_artifact, module)?;
152 let Some(old_module_hashes) = old_all_modules.get(module_id) else {
153 return Some(module);
154 };
155 let old_hash = old_module_hashes.get(&chunk_id);
156 let new_hash = compilation
157 .code_generation_results
158 .get_hash(&module, Some(current_chunk.runtime()));
159 if old_hash != new_hash {
160 return Some(module);
161 }
162 None
163 })
164 .collect::<Vec<_>>();
165
166 new_runtime_modules = compilation
167 .build_chunk_graph_artifact
168 .chunk_graph
169 .get_chunk_runtime_modules_in_order(¤t_chunk.ukey(), compilation)
170 .filter(|(module, _)| updated_runtime_modules.contains(module))
171 .map(|(&module, _)| module)
172 .collect::<Vec<_>>();
173
174 removed_from_runtime = old_runtime.subtract(&new_runtime);
175 } else {
176 removed_from_runtime = old_runtime.clone();
177 new_runtime = old_runtime.clone();
178 }
179
180 for removed in removed_from_runtime.iter() {
181 if let Some(info) = hot_update_main_content_by_runtime.get_mut(removed) {
182 info.removed_chunk_ids.insert(chunk_id.clone());
183 }
184 }
185
186 for old_module_id in remaining_modules {
187 let module_identifier = all_module_ids
188 .get(&old_module_id)
189 .expect("should have module");
190 let old_hashes = old_all_modules
191 .get(&old_module_id)
192 .expect("should have module");
193 let old_hash = old_hashes.get(&chunk_id);
194 let runtimes = compilation
195 .build_chunk_graph_artifact
196 .chunk_graph
197 .get_module_runtimes(
198 *module_identifier,
199 &compilation.build_chunk_graph_artifact.chunk_by_ukey,
200 );
201 if old_runtime == &new_runtime && runtimes.contains(&new_runtime) {
202 let new_hash = compilation
203 .code_generation_results
204 .get_hash(module_identifier, Some(&new_runtime));
205 if new_hash != old_hash {
206 new_modules.push(*module_identifier);
207 }
208 } else {
209 for removed in removed_from_runtime.iter() {
210 if let Some(content) = hot_update_main_content_by_runtime.get_mut(removed) {
211 content.removed_modules.insert(old_module_id.clone());
212 }
213 }
214 }
215 }
216
217 if !new_modules.is_empty() || !new_runtime_modules.is_empty() {
218 let mut hot_update_chunk = Chunk::new(None, ChunkKind::HotUpdate);
219 hot_update_chunk.set_id(chunk_id.clone());
220 hot_update_chunk.set_runtime(if let Some(current_chunk) = current_chunk {
221 current_chunk.runtime().clone()
222 } else {
223 new_runtime.clone()
224 });
225 let ukey = hot_update_chunk.ukey();
226
227 if let Some(current_chunk) = current_chunk {
228 current_chunk
229 .groups()
230 .iter()
231 .for_each(|group| hot_update_chunk.add_group(*group))
232 }
233
234 compilation
242 .build_chunk_graph_artifact
243 .chunk_by_ukey
244 .add(hot_update_chunk);
245
246 compilation
251 .build_chunk_graph_artifact
252 .chunk_graph
253 .add_chunk(ukey);
254 for module_identifier in &new_modules {
255 compilation
256 .build_chunk_graph_artifact
257 .chunk_graph
258 .connect_chunk_and_module(ukey, *module_identifier);
259 }
260 for runtime_module in &new_runtime_modules {
261 compilation.code_generated_modules.insert(*runtime_module);
262 compilation
263 .build_chunk_graph_artifact
264 .chunk_graph
265 .connect_chunk_and_runtime_module(ukey, *runtime_module);
266 }
267
268 let mut manifest = Vec::new();
269 let mut diagnostics = Vec::new();
270 compilation
271 .plugin_driver
272 .compilation_hooks
273 .render_manifest
274 .call(compilation, &ukey, &mut manifest, &mut diagnostics)
275 .await?;
276
277 for module_identifier in new_modules {
279 compilation
280 .build_chunk_graph_artifact
281 .chunk_graph
282 .disconnect_chunk_and_module(&ukey, module_identifier);
283 }
284 for runtime_module in new_runtime_modules {
285 compilation
286 .build_chunk_graph_artifact
287 .chunk_graph
288 .disconnect_chunk_and_runtime_module(&ukey, &runtime_module);
289 }
290 compilation
291 .build_chunk_graph_artifact
292 .chunk_graph
293 .remove_chunk(&ukey);
294 #[allow(clippy::unwrap_used)]
295 let hot_update_chunk = compilation
296 .build_chunk_graph_artifact
297 .chunk_by_ukey
298 .remove(&ukey)
299 .unwrap();
300
301 compilation.extend_diagnostics(diagnostics);
302
303 for entry in manifest {
304 let filename = if entry.has_filename {
305 entry.filename.clone()
306 } else {
307 compilation
308 .get_path(
309 &compilation.options.output.hot_update_chunk_filename,
310 PathData::default()
311 .chunk_id_optional(hot_update_chunk.id().map(|id| id.as_str()))
312 .chunk_name_optional(hot_update_chunk.name_for_filename_template())
313 .hash_optional(
314 old_hash
315 .as_ref()
316 .map(|hash| hash.rendered(compilation.options.output.hash_digest_length)),
317 ),
318 )
319 .await?
320 };
321 let asset = CompilationAsset::new(
322 Some(entry.source),
323 entry
325 .info
326 .with_hot_module_replacement(Some(true))
327 .with_version(Default::default()),
328 );
329 if let Some(current_chunk_ukey) = current_chunk_ukey {
330 updated_chunks
331 .entry(current_chunk_ukey)
332 .or_default()
333 .insert(filename.clone());
334 }
335 compilation.emit_asset(filename, asset);
336 }
337
338 new_runtime.iter().for_each(|runtime| {
339 if let Some(info) = hot_update_main_content_by_runtime.get_mut(runtime) {
340 info.updated_chunk_ids.insert(chunk_id.clone());
341 }
342 });
343 }
344 }
345
346 for (chunk_ukey, files) in updated_chunks {
348 let chunk = compilation
349 .build_chunk_graph_artifact
350 .chunk_by_ukey
351 .expect_get_mut(&chunk_ukey);
352 for file in files {
353 chunk.add_file(file);
354 }
355 }
356
357 let mut hot_update_main_content_by_filename = HashMap::default();
358 for (runtime, content) in hot_update_main_content_by_runtime {
359 let filename = compilation
360 .get_path(
361 &compilation.options.output.hot_update_main_filename,
362 PathData::default().runtime(&runtime).hash_optional(
363 old_hash
364 .as_ref()
365 .map(|hash| hash.rendered(compilation.options.output.hash_digest_length)),
366 ),
367 )
368 .await?;
369 match hot_update_main_content_by_filename.entry(filename) {
370 hash_map::Entry::Occupied(mut occupied_entry) => {
371 let old_content: &mut HotUpdateContent = occupied_entry.get_mut();
372 old_content
373 .updated_chunk_ids
374 .extend(content.updated_chunk_ids);
375 old_content
376 .removed_chunk_ids
377 .extend(content.removed_chunk_ids);
378 old_content.removed_modules.extend(content.removed_modules);
379 compilation.push_diagnostic(Diagnostic::warn(
380 "HotModuleReplacementPlugin".to_string(),
381 r#"The configured output.hotUpdateMainFilename doesn't lead to unique filenames per runtime and HMR update differs between runtimes.
382This might lead to incorrect runtime behavior of the applied update.
383To fix this, make sure to include [runtime] in the output.hotUpdateMainFilename option, or use the default config."#.to_string(),
384 ));
385 }
386 hash_map::Entry::Vacant(vacant_entry) => {
387 vacant_entry.insert(content);
388 }
389 }
390 }
391 for (filename, content) in hot_update_main_content_by_filename {
392 let c: Vec<ChunkId> = content.updated_chunk_ids.into_iter().collect();
393 let r: Vec<ChunkId> = content.removed_chunk_ids.into_iter().collect();
394 let m: Vec<ModuleId> = {
395 let mut m = completely_removed_modules.clone();
396 m.extend(content.removed_modules);
397 m.into_iter().collect()
398 };
399
400 let manifest_content = serde_json::json!({
401 "c": c,
402 "r": r,
403 "m": m,
404 })
405 .to_string();
406
407 compilation.emit_asset(
408 filename,
409 CompilationAsset::new(
410 Some(
411 RawStringSource::from(if compilation.options.output.module {
412 format!("export default {manifest_content};")
413 } else {
414 manifest_content
415 })
416 .boxed(),
417 ),
418 AssetInfo::default().with_hot_module_replacement(Some(true)),
419 ),
420 );
421 }
422
423 Ok(())
424}
425
426#[plugin_hook(NormalModuleLoader for HotModuleReplacementPlugin)]
427async fn normal_module_loader(&self, context: &mut LoaderContext<RunnerContext>) -> Result<()> {
428 context.hot = true;
429 Ok(())
430}
431
432#[plugin_hook(NormalModuleFactoryParser for HotModuleReplacementPlugin)]
433async fn normal_module_factory_parser(
434 &self,
435 module_type: &ModuleType,
436 parser: &mut Box<dyn ParserAndGenerator>,
437 _parser_options: Option<&ParserOptions>,
438) -> Result<()> {
439 if let Some(parser) = parser.downcast_mut::<JavaScriptParserAndGenerator>() {
440 if module_type.is_js_auto() {
441 parser.add_parser_plugin(Box::new(ModuleHotReplacementParserPlugin::new()));
442 parser.add_parser_plugin(Box::new(ImportMetaHotReplacementParserPlugin::new()));
443 } else if module_type.is_js_dynamic() {
444 parser.add_parser_plugin(Box::new(ModuleHotReplacementParserPlugin::new()));
445 } else if module_type.is_js_esm() {
446 parser.add_parser_plugin(Box::new(ImportMetaHotReplacementParserPlugin::new()));
447 }
448 } else if matches!(
449 module_type,
450 ModuleType::Css | ModuleType::CssAuto | ModuleType::CssGlobal | ModuleType::CssModule
451 ) && let Some(parser) = parser.downcast_mut::<CssParserAndGenerator>()
452 {
453 parser.hot = true;
454 }
455
456 Ok(())
457}
458
459#[plugin_hook(CompilationAdditionalTreeRuntimeRequirements for HotModuleReplacementPlugin)]
460async fn additional_tree_runtime_requirements(
461 &self,
462 compilation: &Compilation,
463 _chunk_ukey: &ChunkUkey,
464 _runtime_requirements: &mut RuntimeGlobals,
465 runtime_modules: &mut Vec<Box<dyn RuntimeModule>>,
466) -> Result<()> {
467 runtime_modules
468 .push(HotModuleReplacementRuntimeModule::new(&compilation.runtime_template).boxed());
469
470 Ok(())
471}
472
473impl Plugin for HotModuleReplacementPlugin {
474 fn name(&self) -> &'static str {
475 "rspack.HotModuleReplacementPlugin"
476 }
477
478 fn apply(&self, ctx: &mut rspack_core::ApplyContext<'_>) -> Result<()> {
479 ctx.compiler_hooks.compilation.tap(compilation::new(self));
480 ctx
481 .compilation_hooks
482 .process_assets
483 .tap(process_assets::new(self));
484 ctx
485 .normal_module_hooks
486 .loader
487 .tap(normal_module_loader::new(self));
488 ctx
489 .normal_module_factory_hooks
490 .parser
491 .tap(normal_module_factory_parser::new(self));
492 ctx
493 .compilation_hooks
494 .additional_tree_runtime_requirements
495 .tap(additional_tree_runtime_requirements::new(self));
496 Ok(())
497 }
498}
499
500#[derive(Default)]
501struct HotUpdateContent {
502 updated_chunk_ids: ChunkIdSet,
503 removed_chunk_ids: ChunkIdSet,
504 removed_modules: HashSet<ModuleId>,
505}