Skip to main content

rspack_plugin_hmr/
lib.rs

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      // updated
91      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      // added
99      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(&current_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(&current_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      // In webpack, there is no need to add HotUpdateChunk to compilation.chunks,
235      // because HotUpdateChunk is no longer used after generating the manifest.
236      //
237      // However, in Rspack, we need to add HotUpdateChunk to compilation.build_chunk_graph_artifact.chunk_by_ukey
238      // because during the manifest generation, HotUpdateChunk is passed to various plugins via the ukey.
239      // The plugins then use the ukey to query compilation.build_chunk_graph_artifact.chunk_by_ukey to get the HotUpdateChunk instance.
240      // Therefore, in Rspack, after the manifest is generated, we need to manually remove the HotUpdateChunk from compilation.chunks.
241      compilation
242        .build_chunk_graph_artifact
243        .chunk_by_ukey
244        .add(hot_update_chunk);
245
246      // In webpack, compilation.chunkGraph uses a WeakMap to maintain the relationship between Chunks and Modules.
247      // This means the lifecycle of these data is tied to the Chunk, and they are garbage-collected when the Chunk is.
248      //
249      // In Rspack, we need to manually clean up the data in compilation.build_chunk_graph_artifact.chunk_graph after HotUpdateChunk is used.
250      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      // Manually clean up ChunkGraph and chunks
278      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          // Reset version to make hmr generated assets always emit
324          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  // update chunk files
347  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}