use std::{
ops::{Deref, DerefMut},
sync::{
Arc,
atomic::{AtomicU32, Ordering},
},
};
use arcstr::ArcStr;
use oxc_traverse::traverse_mut;
use rolldown_common::{
ClientHmrInput, ClientHmrUpdate, HmrBoundary, HmrBoundaryOutput, HmrPatch, HmrUpdate, Module,
ModuleIdx, ModuleTable, ScanMode,
};
use rolldown_ecmascript::{EcmaAst, EcmaCompiler, PrintOptions};
use rolldown_ecmascript_utils::AstSnippet;
use rolldown_error::BuildResult;
use rolldown_fs::OsFileSystem;
use rolldown_plugin::SharedPluginDriver;
use rolldown_sourcemap::{Source, SourceJoiner, SourceMapSource};
#[cfg(not(target_family = "wasm"))]
use rolldown_utils::rayon::IndexedParallelIterator;
use rolldown_utils::{
concat_string,
indexmap::{FxIndexMap, FxIndexSet},
rayon::{IntoParallelIterator, ParallelIterator},
};
use rustc_hash::{FxHashMap, FxHashSet};
use sugar_path::SugarPath;
use crate::{
SharedOptions, SharedResolver, hmr::hmr_ast_finalizer::HmrAstFinalizer,
module_loader::ModuleLoader, type_alias::IndexEcmaAst, types::scan_stage_cache::ScanStageCache,
utils::process_code_and_sourcemap::process_code_and_sourcemap,
};
pub struct HmrStageInput<'a> {
pub options: SharedOptions,
pub fs: OsFileSystem,
pub resolver: SharedResolver,
pub plugin_driver: SharedPluginDriver,
pub cache: &'a mut ScanStageCache,
pub next_hmr_patch_id: Arc<AtomicU32>,
}
impl HmrStageInput<'_> {
pub fn module_table(&self) -> &ModuleTable {
&self.cache.get_snapshot().module_table
}
pub fn index_ecma_ast(&self) -> &IndexEcmaAst {
&self.cache.get_snapshot().index_ecma_ast
}
}
pub struct HmrStage<'a> {
pub(crate) input: HmrStageInput<'a>,
}
impl<'a> Deref for HmrStage<'a> {
type Target = HmrStageInput<'a>;
fn deref(&self) -> &Self::Target {
&self.input
}
}
impl DerefMut for HmrStage<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.input
}
}
impl<'a> HmrStage<'a> {
pub fn new(input: HmrStageInput<'a>) -> Self {
Self { input }
}
pub async fn compute_update_for_calling_invalidate(
&mut self,
invalidate_caller: String,
first_invalidated_by: Option<String>,
client_id: &str,
executed_modules: &FxHashSet<String>,
) -> BuildResult<HmrUpdate> {
tracing::debug!(
target: "hmr",
"compute_update_for_calling_invalidate: caller: {:?}, first_invalidated_by: {:?}",
invalidate_caller,
first_invalidated_by,
);
let module_idx = self
.cache
.module_idx_by_stable_id
.get(&invalidate_caller)
.copied()
.unwrap_or_else(|| panic!("Not found modules for file: {invalidate_caller}"));
let caller = self.module_table().modules[module_idx].as_normal().unwrap();
let temp_client = ClientHmrInput { client_id, executed_modules };
if !temp_client.is_module_executed(&caller.stable_id) {
return Ok(HmrUpdate::Noop);
}
if !caller.is_hmr_self_accepting_module() {
return Ok(HmrUpdate::FullReload {
reason: "not self accepting for this invalidation".to_string(),
});
}
if caller.importers_idx.is_empty() {
return Ok(HmrUpdate::FullReload {
reason: format!(
"There are no importers to handle `import.meta.hot.invalidate()` called by `{}`",
caller.stable_id
),
});
}
let mut stale_modules = caller.importers_idx.clone();
stale_modules.swap_remove(&caller.idx);
let temp_client = ClientHmrInput { client_id, executed_modules };
let mut results = self
.compute_hmr_update(
&stale_modules,
&FxIndexSet::default(),
first_invalidated_by,
&[temp_client],
)
.await?;
Ok(results.pop().unwrap().update)
}
pub async fn compute_hmr_update_for_file_changes(
&mut self,
changed_file_paths: &[String],
clients: &[ClientHmrInput<'_>],
) -> BuildResult<Vec<ClientHmrUpdate>> {
tracing::debug!(target: "hmr", "compute_hmr_update_for_file_changes: {:?}", changed_file_paths);
let mut changed_modules = FxIndexSet::default();
for changed_file_path in changed_file_paths {
let changed_file_path = ArcStr::from(changed_file_path.to_slash().unwrap());
if let Some(module_idx) = self.cache.module_idx_by_abs_path.get(&changed_file_path) {
changed_modules.insert(*module_idx);
}
for entry in self.plugin_driver.transform_dependencies.iter() {
let module_idx = *entry.key();
let deps = entry.value();
if deps.contains(&changed_file_path) {
changed_modules.insert(module_idx);
}
}
}
tracing::debug!(
target: "hmr",
"initial changed modules {:?}",
changed_modules.iter()
.map(|module_idx| self.module_table().modules[*module_idx].stable_id())
.collect::<Vec<_>>(),
);
if changed_modules.is_empty() {
return Ok(
clients
.iter()
.map(|client| ClientHmrUpdate {
client_id: client.client_id.to_string(),
update: HmrUpdate::Noop,
})
.collect(),
);
}
self.compute_hmr_update(&changed_modules, &changed_modules, None, clients).await
}
async fn compute_hmr_update(
&mut self,
stale_modules: &FxIndexSet<ModuleIdx>,
changed_modules: &FxIndexSet<ModuleIdx>,
first_invalidated_by: Option<String>,
clients: &[ClientHmrInput<'_>],
) -> BuildResult<Vec<ClientHmrUpdate>> {
let mut clients_prerequisites = Vec::with_capacity(clients.len());
for client in clients {
let prerequisites =
self.compute_out_hmr_prerequisites(stale_modules, first_invalidated_by.as_deref(), client);
tracing::debug!(
target: "hmr",
"computed prerequisites for client {}: boundaries {:?}, require_full_reload: {}",
client.client_id,
prerequisites.boundaries.iter()
.map(|boundary| self.module_table().modules[boundary.boundary].stable_id())
.collect::<Vec<_>>(),
prerequisites.require_full_reload,
);
clients_prerequisites.push((client.client_id.to_string(), prerequisites));
}
let new_added_modules = if changed_modules.is_empty() {
FxIndexSet::default()
} else {
let modules_to_be_refetched = changed_modules
.iter()
.filter_map(|module_idx| {
let module = &self.module_table().modules[*module_idx];
if let Module::Normal(module) = module {
Some(module.originative_resolved_id.clone())
} else {
None
}
})
.collect::<Vec<_>>();
let fetch_mode = ScanMode::Partial(modules_to_be_refetched);
let mut module_loader = ModuleLoader::new(
self.fs.clone(),
Arc::clone(&self.options),
Arc::clone(&self.resolver),
Arc::clone(&self.plugin_driver),
self.cache,
fetch_mode.is_full(),
None,
)?;
let module_loader_output = module_loader.fetch_modules(fetch_mode).await?;
drop(module_loader);
let new_added_modules = module_loader_output.new_added_modules_from_partial_scan.clone();
tracing::debug!(
target: "hmr",
"New added modules: {:?}",
new_added_modules
.iter()
.map(|module_idx| module_loader_output.module_table.get(*module_idx).stable_id())
.collect::<Vec<_>>(),
);
self.cache.merge(module_loader_output.into()).map_err(|e| vec![anyhow::anyhow!(e).into()])?;
let options = Arc::clone(&self.options);
let resolver = Arc::clone(&self.resolver);
self.cache.update_defer_sync_data(&options, &resolver).await?;
new_added_modules
};
let mut client_updates = Vec::with_capacity(clients.len());
for (client_id, prerequisites) in clients_prerequisites {
let update = if prerequisites.require_full_reload {
HmrUpdate::FullReload {
reason: prerequisites.full_reload_reason.unwrap_or_else(|| "Unknown reason".to_string()),
}
} else {
self.render_hmr_patch_from_prerequisites(prerequisites, &new_added_modules).await?
};
client_updates.push(ClientHmrUpdate { client_id, update });
}
Ok(client_updates)
}
#[expect(dead_code, clippy::too_many_lines)]
async fn compute_hmr_update_single(
&mut self,
stale_modules: &FxIndexSet<ModuleIdx>,
changed_modules: &FxIndexSet<ModuleIdx>,
first_invalidated_by: Option<String>,
client: &ClientHmrInput<'_>,
) -> BuildResult<HmrUpdate> {
let hmr_prerequisites =
self.compute_out_hmr_prerequisites(stale_modules, first_invalidated_by.as_deref(), client);
tracing::debug!(
target: "hmr",
"computed out `hmr_boundaries` {:?}",
hmr_prerequisites.boundaries.iter()
.map(|boundary| self.module_table().modules[boundary.boundary].stable_id())
.collect::<Vec<_>>(),
);
tracing::debug!(
target: "hmr",
"computed out `stale_modules` {:?}",
hmr_prerequisites.modules_to_be_updated.iter()
.map(|module_idx| self.module_table().modules[*module_idx].stable_id())
.collect::<Vec<_>>(),
);
let mut modules_to_be_updated = hmr_prerequisites.modules_to_be_updated;
if !changed_modules.is_empty() {
let modules_to_be_refetched = changed_modules
.iter()
.filter_map(|module_idx| {
let module = &self.module_table().modules[*module_idx];
if let Module::Normal(module) = module {
Some(module.originative_resolved_id.clone())
} else {
None
}
})
.collect::<Vec<_>>();
let fetch_mode = ScanMode::Partial(modules_to_be_refetched);
let mut module_loader = ModuleLoader::new(
self.fs.clone(),
Arc::clone(&self.options),
Arc::clone(&self.resolver),
Arc::clone(&self.plugin_driver),
self.cache,
fetch_mode.is_full(),
None,
)?;
let module_loader_output = module_loader.fetch_modules(fetch_mode).await?;
drop(module_loader);
tracing::debug!(
target: "hmr",
"New added modules` {:?}",
module_loader_output
.new_added_modules_from_partial_scan
.iter()
.map(|module_idx| module_loader_output.module_table.get(*module_idx).stable_id())
.collect::<Vec<_>>(),
);
modules_to_be_updated
.extend(module_loader_output.new_added_modules_from_partial_scan.clone());
self.cache.merge(module_loader_output.into()).map_err(|e| vec![anyhow::anyhow!(e).into()])?;
let options = Arc::clone(&self.options);
let resolver = Arc::clone(&self.resolver);
self.cache.update_defer_sync_data(&options, &resolver).await?;
modules_to_be_updated.retain(|idx| self.module_table().modules[*idx].is_normal());
}
if hmr_prerequisites.require_full_reload {
return Ok(HmrUpdate::FullReload {
reason: hmr_prerequisites
.full_reload_reason
.unwrap_or_else(|| "Unknown reason".to_string()),
});
}
modules_to_be_updated
.sort_by_cached_key(|module_idx| self.module_table().modules[*module_idx].id());
let module_idx_to_init_fn_name = modules_to_be_updated
.iter()
.enumerate()
.map(|(index, module_idx)| {
let Module::Normal(module) = &self.module_table().modules[*module_idx] else {
unreachable!(
"External modules should be removed before. But got {:?}",
self.module_table().modules[*module_idx].id()
);
};
let prefix = if module.exports_kind.is_commonjs() { "require" } else { "init" };
(*module_idx, format!("{}_{}_{}", prefix, module.repr_name, index))
})
.collect::<FxHashMap<_, _>>();
let index_ecma_ast = self.index_ecma_ast();
let module_render_inputs = modules_to_be_updated
.iter()
.copied()
.map(|affected_module_idx| {
let affected_module = &self.module_table().modules[affected_module_idx];
let Module::Normal(affected_module) = affected_module else {
unreachable!("HMR only supports normal module");
};
debug_assert_eq!(affected_module_idx, affected_module.idx);
let ecma_ast =
index_ecma_ast[affected_module_idx].as_ref().expect("Normal module should have an AST");
ModuleRenderInput {
idx: affected_module.idx,
ecma_ast: ecma_ast.clone_with_another_arena(),
}
})
.collect::<Vec<_>>();
let mut source_joiner = SourceJoiner::default();
let rendered_sources = module_render_inputs
.into_par_iter()
.enumerate()
.flat_map(|(index, render_input)| {
let ModuleRenderInput { idx: affected_module_idx, ecma_ast: mut ast } = render_input;
let affected_module = &self.module_table().modules[affected_module_idx];
let Module::Normal(affected_module) = affected_module else {
unreachable!("HMR only supports normal module");
};
let enable_sourcemap = self.options.sourcemap.is_some() && !affected_module.is_virtual();
let use_pife_for_module_wrappers =
self.options.optimization.is_pife_for_module_wrappers_enabled();
let modules = &self.module_table().modules;
ast.program.with_mut(|fields| {
let scoping = EcmaAst::make_semantic(fields.program, false).into_scoping();
let mut finalizer = HmrAstFinalizer {
modules,
alloc: fields.allocator,
snippet: AstSnippet::new(fields.allocator),
builder: &oxc::ast::AstBuilder::new(fields.allocator),
import_bindings: FxHashMap::default(),
module: affected_module,
exports: oxc::allocator::Vec::new_in(fields.allocator),
affected_module_idx_to_init_fn_name: &module_idx_to_init_fn_name,
use_pife_for_module_wrappers,
dependencies: FxIndexSet::default(),
imports: FxHashSet::default(),
generated_static_import_infos: FxHashMap::default(),
re_export_all_dependencies: FxIndexSet::default(),
generated_static_import_stmts_from_external: FxIndexMap::default(),
unique_index: index,
named_exports: FxHashMap::default(),
};
traverse_mut(&mut finalizer, fields.allocator, fields.program, scoping, ());
});
let codegen = EcmaCompiler::print_with(
&ast,
PrintOptions {
sourcemap: enable_sourcemap,
filename: affected_module.id.to_string(),
print_legal_comments: false, initial_indent: 0,
},
);
let intro_comment: Box<dyn Source + Send> =
Box::new(concat_string!("//#region ", affected_module.debug_id));
let outro_comment: Box<dyn Source + Send> = Box::new(concat_string!("//#endregion"));
let code_source: Box<dyn Source + Send> = if let Some(map) = codegen.map {
Box::new(SourceMapSource::new(codegen.code, map))
} else {
Box::new(codegen.code)
};
[intro_comment, code_source, outro_comment]
})
.collect::<Vec<_>>();
for source in rendered_sources {
source_joiner.append_source_dyn(source);
}
hmr_prerequisites.boundaries.iter().for_each(|boundary| {
let init_fn_name = &module_idx_to_init_fn_name[&boundary.accepted_via];
source_joiner.append_source(format!("{init_fn_name}()"));
});
source_joiner.append_source(format!(
"__rolldown_runtime__.applyUpdates([{}]);",
hmr_prerequisites
.boundaries
.iter()
.map(|boundary| {
let boundary_mod = &self.module_table().modules[boundary.boundary];
let accepted_via = &self.module_table().modules[boundary.accepted_via];
format!("['{}', '{}']", boundary_mod.stable_id(), accepted_via.stable_id())
})
.collect::<Vec<_>>()
.join(",")
));
let (mut code, mut map) = source_joiner.join();
let hmr_patch_id = self.next_hmr_patch_id.fetch_add(1, Ordering::Relaxed);
let filename = format!("hmr_patch_{hmr_patch_id}.js");
let file_dir = self.options.cwd.as_path().join(&self.options.out_dir);
let sourcemap_asset = if let Some(map) = map.as_mut() {
process_code_and_sourcemap(
&self.options,
&mut code,
map,
&file_dir,
filename.as_str(),
0,
false,
)
.await?
} else {
None
};
Ok(HmrUpdate::Patch(HmrPatch {
code,
filename,
sourcemap_filename: sourcemap_asset.as_ref().map(|asset| asset.filename.to_string()),
sourcemap: sourcemap_asset.map(|asset| asset.source.try_into_string()).transpose()?,
hmr_boundaries: hmr_prerequisites
.boundaries
.into_iter()
.map(|boundary| HmrBoundaryOutput {
boundary: self.module_table().modules[boundary.boundary].stable_id().into(),
accepted_via: self.module_table().modules[boundary.accepted_via].stable_id().into(),
})
.collect(),
}))
}
async fn render_hmr_patch_from_prerequisites(
&self,
hmr_prerequisites: HmrPrerequisites,
new_added_modules: &FxIndexSet<ModuleIdx>,
) -> BuildResult<HmrUpdate> {
let mut modules_to_be_updated = hmr_prerequisites.modules_to_be_updated;
modules_to_be_updated.extend(new_added_modules.iter().copied());
modules_to_be_updated.retain(|idx| self.module_table().modules[*idx].is_normal());
modules_to_be_updated
.sort_by_cached_key(|module_idx| self.module_table().modules[*module_idx].id());
let module_idx_to_init_fn_name = modules_to_be_updated
.iter()
.enumerate()
.map(|(index, module_idx)| {
let Module::Normal(module) = &self.module_table().modules[*module_idx] else {
unreachable!(
"External modules should be removed before. But got {:?}",
self.module_table().modules[*module_idx].id()
);
};
let prefix = if module.exports_kind.is_commonjs() { "require" } else { "init" };
(*module_idx, format!("{}_{}_{}", prefix, module.repr_name, index))
})
.collect::<FxHashMap<_, _>>();
let index_ecma_ast = self.index_ecma_ast();
let module_render_inputs = modules_to_be_updated
.iter()
.copied()
.map(|affected_module_idx| {
let affected_module = &self.module_table().modules[affected_module_idx];
let Module::Normal(affected_module) = affected_module else {
unreachable!("HMR only supports normal module");
};
debug_assert_eq!(affected_module_idx, affected_module.idx);
let ecma_ast =
index_ecma_ast[affected_module_idx].as_ref().expect("Normal module should have an AST");
ModuleRenderInput {
idx: affected_module.idx,
ecma_ast: ecma_ast.clone_with_another_arena(),
}
})
.collect::<Vec<_>>();
let mut source_joiner = SourceJoiner::default();
let rendered_sources = module_render_inputs
.into_par_iter()
.enumerate()
.flat_map(|(index, render_input)| {
let ModuleRenderInput { idx: affected_module_idx, ecma_ast: mut ast } = render_input;
let affected_module = &self.module_table().modules[affected_module_idx];
let Module::Normal(affected_module) = affected_module else {
unreachable!("HMR only supports normal module");
};
let enable_sourcemap = self.options.sourcemap.is_some() && !affected_module.is_virtual();
let use_pife_for_module_wrappers =
self.options.optimization.is_pife_for_module_wrappers_enabled();
let modules = &self.module_table().modules;
ast.program.with_mut(|fields| {
let scoping = EcmaAst::make_semantic(fields.program, false).into_scoping();
let mut finalizer = HmrAstFinalizer {
modules,
alloc: fields.allocator,
snippet: AstSnippet::new(fields.allocator),
builder: &oxc::ast::AstBuilder::new(fields.allocator),
import_bindings: FxHashMap::default(),
module: affected_module,
exports: oxc::allocator::Vec::new_in(fields.allocator),
affected_module_idx_to_init_fn_name: &module_idx_to_init_fn_name,
use_pife_for_module_wrappers,
dependencies: FxIndexSet::default(),
imports: FxHashSet::default(),
generated_static_import_infos: FxHashMap::default(),
re_export_all_dependencies: FxIndexSet::default(),
generated_static_import_stmts_from_external: FxIndexMap::default(),
unique_index: index,
named_exports: FxHashMap::default(),
};
traverse_mut(&mut finalizer, fields.allocator, fields.program, scoping, ());
});
let codegen = EcmaCompiler::print_with(
&ast,
PrintOptions {
sourcemap: enable_sourcemap,
filename: affected_module.id.to_string(),
print_legal_comments: false, initial_indent: 0,
},
);
let intro_comment: Box<dyn Source + Send> =
Box::new(concat_string!("//#region ", affected_module.debug_id));
let outro_comment: Box<dyn Source + Send> = Box::new(concat_string!("//#endregion"));
let code_source: Box<dyn Source + Send> = if let Some(map) = codegen.map {
Box::new(SourceMapSource::new(codegen.code, map))
} else {
Box::new(codegen.code)
};
[intro_comment, code_source, outro_comment]
})
.collect::<Vec<_>>();
for source in rendered_sources {
source_joiner.append_source_dyn(source);
}
hmr_prerequisites.boundaries.iter().for_each(|boundary| {
let init_fn_name = &module_idx_to_init_fn_name[&boundary.accepted_via];
source_joiner.append_source(format!("{init_fn_name}()"));
});
source_joiner.append_source(format!(
"__rolldown_runtime__.applyUpdates([{}]);",
hmr_prerequisites
.boundaries
.iter()
.map(|boundary| {
let boundary_mod = &self.module_table().modules[boundary.boundary];
let accepted_via = &self.module_table().modules[boundary.accepted_via];
format!("['{}', '{}']", boundary_mod.stable_id(), accepted_via.stable_id())
})
.collect::<Vec<_>>()
.join(",")
));
let (mut code, mut map) = source_joiner.join();
let hmr_patch_id = self.next_hmr_patch_id.fetch_add(1, Ordering::Relaxed);
let filename = format!("hmr_patch_{hmr_patch_id}.js");
let file_dir = self.options.cwd.as_path().join(&self.options.out_dir);
let sourcemap_asset = if let Some(map) = map.as_mut() {
process_code_and_sourcemap(
&self.options,
&mut code,
map,
&file_dir,
filename.as_str(),
0,
false,
)
.await?
} else {
None
};
Ok(HmrUpdate::Patch(HmrPatch {
code,
filename,
sourcemap_filename: sourcemap_asset.as_ref().map(|asset| asset.filename.to_string()),
sourcemap: sourcemap_asset.map(|asset| asset.source.try_into_string()).transpose()?,
hmr_boundaries: hmr_prerequisites
.boundaries
.into_iter()
.map(|boundary| HmrBoundaryOutput {
boundary: self.module_table().modules[boundary.boundary].stable_id().into(),
accepted_via: self.module_table().modules[boundary.accepted_via].stable_id().into(),
})
.collect(),
}))
}
fn propagate_update(
&self,
module_idx: ModuleIdx,
hmr_boundaries: &mut FxIndexSet<HmrBoundary>,
propagate_stack: &mut Vec<ModuleIdx>,
modules_to_be_updated: &mut FxIndexSet<ModuleIdx>,
client: &ClientHmrInput,
) -> PropagateUpdateStatus {
modules_to_be_updated.insert(module_idx);
let Module::Normal(module) = &self.module_table().modules[module_idx] else {
return PropagateUpdateStatus::ReachHmrBoundary;
};
if let Some(circular_start_index) = propagate_stack
.iter()
.enumerate()
.find_map(|(index, each_module_idx)| (module_idx == *each_module_idx).then_some(index))
{
let cycle_chain = propagate_stack[circular_start_index..]
.iter()
.copied()
.chain(std::iter::once(module_idx))
.rev()
.collect::<Vec<_>>();
return PropagateUpdateStatus::Circular(cycle_chain);
}
if module.is_hmr_self_accepting_module() {
hmr_boundaries.insert(HmrBoundary { boundary: module_idx, accepted_via: module_idx });
return PropagateUpdateStatus::ReachHmrBoundary;
} else if module.importers_idx.is_empty() {
return PropagateUpdateStatus::NoBoundary(module_idx);
}
let mut importers_idx = module.importers_idx.iter().copied().collect::<Vec<_>>();
importers_idx
.sort_by_key(|importer_idx| self.module_table().modules[*importer_idx].stable_id());
for importer_idx in importers_idx {
let Module::Normal(importer) = &self.module_table().modules[importer_idx] else {
continue;
};
if !client.is_module_executed(&importer.stable_id) {
continue;
}
if importer.can_accept_hmr_dependency_for(&module.id) {
modules_to_be_updated.insert(module_idx);
hmr_boundaries.insert(HmrBoundary { boundary: importer_idx, accepted_via: module_idx });
continue;
}
propagate_stack.push(module_idx);
let status = self.propagate_update(
importer_idx,
hmr_boundaries,
propagate_stack,
modules_to_be_updated,
client,
);
propagate_stack.pop();
if !status.is_reach_hmr_boundary() {
return status;
}
}
PropagateUpdateStatus::ReachHmrBoundary
}
fn compute_out_hmr_prerequisites(
&self,
stale_modules: &FxIndexSet<ModuleIdx>,
first_invalidated_by: Option<&str>,
client: &ClientHmrInput,
) -> HmrPrerequisites {
let mut hmr_boundaries = FxIndexSet::default();
let mut require_full_reload = false;
let mut full_reload_reason = None;
let mut modules_to_be_updated = FxIndexSet::default();
for stale_module in stale_modules.iter().copied() {
if require_full_reload {
break;
}
let mut boundaries = FxIndexSet::default();
if !client.is_module_executed(self.module_table().modules[stale_module].stable_id()) {
continue;
}
let propagate_update_status = self.propagate_update(
stale_module,
&mut boundaries,
&mut vec![],
&mut modules_to_be_updated,
client,
);
match propagate_update_status {
PropagateUpdateStatus::Circular(cycle_chain) => {
require_full_reload = true;
full_reload_reason = Some(format!(
"circular import chain: {}",
cycle_chain
.iter()
.map(|module_idx| self.module_table().modules[*module_idx].stable_id())
.collect::<Vec<_>>()
.join(" -> ")
));
break;
}
PropagateUpdateStatus::NoBoundary(idx) => {
require_full_reload = true;
let module = &self.module_table().modules[idx];
full_reload_reason =
Some(format!("no hmr boundary found for module `{}`", module.stable_id()));
break;
}
PropagateUpdateStatus::ReachHmrBoundary => {}
}
if let Some(first_invalidated_by) = first_invalidated_by.as_ref() {
if boundaries.iter().any(|boundary| {
self.module_table().modules[boundary.accepted_via].stable_id() == *first_invalidated_by
}) {
require_full_reload = true;
continue;
}
}
hmr_boundaries.extend(boundaries);
}
HmrPrerequisites {
boundaries: hmr_boundaries,
modules_to_be_updated,
require_full_reload,
full_reload_reason,
}
}
}
struct HmrPrerequisites {
boundaries: FxIndexSet<HmrBoundary>,
modules_to_be_updated: FxIndexSet<ModuleIdx>,
require_full_reload: bool,
full_reload_reason: Option<String>,
}
enum PropagateUpdateStatus {
Circular(Vec<ModuleIdx>), ReachHmrBoundary,
NoBoundary(ModuleIdx),
}
impl PropagateUpdateStatus {
pub fn is_reach_hmr_boundary(&self) -> bool {
matches!(self, Self::ReachHmrBoundary)
}
}
struct ModuleRenderInput {
pub idx: ModuleIdx,
pub ecma_ast: EcmaAst,
}