use std::{
ops::{Deref, DerefMut},
sync::{
Arc,
atomic::{AtomicU32, Ordering},
},
};
use arcstr::ArcStr;
use oxc_traverse::traverse_mut;
use rolldown_common::{
ClientHmrInput, ClientHmrUpdate, HmrLazyChunkOutput, HmrPatch, HmrStampTable, HmrUpdate,
ImportKind, Module, ModuleIdx, ModuleTable, ScanMode, WatcherChangeKind,
};
use rolldown_ecmascript::{EcmaAst, EcmaCompiler, PrintCommentsOptions, PrintOptions};
use rolldown_ecmascript_utils::AstFactory;
use rolldown_error::BuildResult;
use rolldown_fs::FileSystem;
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, Fs: FileSystem + Clone + 'static> {
pub options: SharedOptions,
pub fs: Fs,
pub resolver: SharedResolver<Fs>,
pub plugin_driver: SharedPluginDriver,
pub cache: &'a mut ScanStageCache,
pub next_hmr_patch_id: Arc<AtomicU32>,
}
impl<Fs: FileSystem + Clone + 'static> HmrStageInput<'_, Fs> {
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, Fs: FileSystem + Clone + 'static> {
pub(crate) input: HmrStageInput<'a, Fs>,
}
impl<'a, Fs: FileSystem + Clone + 'static> Deref for HmrStage<'a, Fs> {
type Target = HmrStageInput<'a, Fs>;
fn deref(&self) -> &Self::Target {
&self.input
}
}
impl<Fs: FileSystem + Clone + 'static> DerefMut for HmrStage<'_, Fs> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.input
}
}
impl<'a, Fs: FileSystem + Clone + 'static> HmrStage<'a, Fs> {
pub fn new(input: HmrStageInput<'a, Fs>) -> Self {
Self { input }
}
pub async fn compute_hmr_update_for_file_changes(
&mut self,
changed_file_paths: &FxIndexMap<String, WatcherChangeKind>,
clients: &[ClientHmrInput<'_>],
stamp_table: &mut HmrStampTable,
) -> BuildResult<Vec<ClientHmrUpdate>> {
tracing::trace!(
"[HmrStage] starts computing HMR updates\n - changed_file_paths: {:#?}\n - clients: {:#?}",
changed_file_paths,
clients.iter().map(|c| c.client_id).collect::<Vec<_>>(),
);
let mut changed_modules = FxIndexSet::default();
for (changed_file_path, event) in changed_file_paths {
let changed_file_path = ArcStr::from(changed_file_path.to_slash());
if let Some(module_idx) = self.cache.module_idx_by_abs_path.get(&changed_file_path) {
if *event == WatcherChangeKind::Delete {
if let Some(importers) = self.cache.importers.get(*module_idx) {
changed_modules.extend(importers.iter().map(|imp| imp.importer_idx));
}
} else {
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::trace!(
"[HmrStage] map changed file paths to module idxs\n - 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(),
);
}
for resolved_id in &self.cache.pending_rescans {
if let Some(state) = self.cache.module_id_to_idx.get(&resolved_id.id) {
changed_modules.insert(state.idx());
}
}
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<_>>(),
);
let plugin_driver = Arc::clone(&self.plugin_driver);
self.cache.merge(module_loader_output.into(), &plugin_driver)?;
let options = Arc::clone(&self.options);
self.cache.update_defer_sync_data(&options).await?;
new_added_modules
};
let rebuild_seq = stamp_table.begin_rebuild();
for module_idx in changed_modules.iter().chain(new_added_modules.iter()) {
if let Module::Normal(module) = &self.module_table().modules[*module_idx] {
stamp_table.stamp(module.stable_id.as_arc_str(), rebuild_seq);
}
}
let changed_ids = changed_modules
.iter()
.filter_map(|module_idx| {
self.module_table().modules[*module_idx]
.as_normal()
.map(|module| module.stable_id.to_string())
})
.collect::<Vec<_>>();
let mut affected = self.collect_client_update_superset(&changed_modules);
affected.extend(new_added_modules.iter().copied());
affected.retain(|idx| self.module_table().modules[*idx].is_normal());
let affected_with_stamps = affected
.iter()
.map(|module_idx| {
let stable_id = self.module_table().modules[*module_idx].stable_id().as_str();
(*module_idx, stable_id, stamp_table.render_time_stamp(stable_id))
})
.collect::<Vec<_>>();
let mut client_updates = Vec::with_capacity(clients.len());
for client in clients {
let mut carried = FxIndexSet::default();
for (module_idx, stable_id, latest_stamp) in &affected_with_stamps {
match client.shipped.get(*stable_id) {
None => {
carried.insert(*module_idx);
}
Some(stamp) => {
if *latest_stamp > *stamp {
carried.insert(*module_idx);
}
}
}
}
for (stable_id, latest_stamp) in stamp_table.iter_latest() {
if client.shipped.get(stable_id.as_str()).is_some_and(|stamp| latest_stamp > *stamp) {
if let Some(module_idx) =
self.cache.module_idx_by_stable_id.get(stable_id.as_str()).copied()
{
if self.module_table().modules[module_idx].is_normal() {
carried.insert(module_idx);
}
}
}
}
let update = self.render_hmr_patch(carried, changed_ids.clone(), stamp_table).await?;
client_updates.push(ClientHmrUpdate { client_id: client.client_id.to_string(), update });
}
Ok(client_updates)
}
fn collect_client_update_superset(
&self,
changed_modules: &FxIndexSet<ModuleIdx>,
) -> FxIndexSet<ModuleIdx> {
let mut affected = FxIndexSet::default();
let mut stack: Vec<ModuleIdx> = changed_modules.iter().copied().collect();
while let Some(module_idx) = stack.pop() {
if !affected.insert(module_idx) {
continue;
}
let Module::Normal(module) = &self.module_table().modules[module_idx] else {
continue;
};
if module.is_hmr_self_accepting_module() {
tracing::trace!(
"[HmrStage] module {} is self-accepting, stop propagation here",
module.stable_id,
);
continue;
}
for importer_idx in
module.importers_idx.iter().chain(module.dynamic_importers_idx.iter()).copied()
{
let Module::Normal(importer) = &self.module_table().modules[importer_idx] else {
continue;
};
if importer.can_accept_hmr_dependency_for(&module.id) {
continue;
}
stack.push(importer_idx);
}
}
affected.sort_unstable_by(|a, b| {
self.module_table().modules[*a].stable_id().cmp(self.module_table().modules[*b].stable_id())
});
affected
}
pub async fn compile_lazy_entry(
&mut self,
module_id: &str,
_client_id: &str,
shipped: &FxHashMap<ArcStr, u32>,
evaluated: &FxHashMap<ArcStr, u32>,
stamp_table: &HmrStampTable,
) -> BuildResult<HmrLazyChunkOutput> {
tracing::debug!(
target: "hmr",
"compile_lazy_entry: module_id: {:?}",
module_id,
);
if !self.cache.has_snapshot() {
return Err(
vec![
anyhow::anyhow!(
"Cannot compile lazy entry `{module_id}`: the last full build failed, so there is no module graph to compile against."
)
.into(),
]
.into(),
);
}
let (entry_module_idx, resolved_id) = self
.cache
.module_id_to_idx
.get(module_id)
.and_then(|state| {
let idx = state.idx();
let module = &self.module_table().modules[idx];
if let Module::Normal(module) = module {
Some((idx, module.originative_resolved_id.clone()))
} else {
None
}
})
.ok_or_else(|| {
vec![anyhow::anyhow!("Lazy entry module not found in cache. module_id={module_id}").into()]
})?;
let fetch_mode = ScanMode::Partial(vec![resolved_id]);
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 plugin_driver = Arc::clone(&self.plugin_driver);
self
.cache
.merge(module_loader_output.into(), &plugin_driver)
.map_err(|e| vec![anyhow::anyhow!(e).into()])?;
let options = Arc::clone(&self.options);
self.cache.update_defer_sync_data(&options).await?;
let mut modules_to_be_updated = FxIndexSet::default();
self.collect_sync_dependencies_for_client(
entry_module_idx,
&mut modules_to_be_updated,
shipped,
evaluated,
stamp_table,
);
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 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!("Only normal modules should be rendered");
};
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();
if let Some(prelude) = crate::hmr::module_graph_delta::render_register_graph_source(
self.module_table(),
modules_to_be_updated.iter().copied(),
) {
source_joiner.append_source(prelude);
}
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!("Only normal modules should be rendered");
};
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).into_scoping();
let mut finalizer = HmrAstFinalizer {
modules,
ast_factory: AstFactory::new(fields.allocator),
import_bindings: FxHashMap::default(),
module: affected_module,
exports: oxc::allocator::Vec::new_in(&fields.allocator),
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(),
comments: PrintCommentsOptions {
legal: false,
annotation: self.options.comments.annotation,
jsdoc: self.options.comments.jsdoc,
},
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.into_owned()))
} else {
Box::new(codegen.code)
};
[intro_comment, code_source, outro_comment]
})
.collect::<Vec<_>>();
for source in rendered_sources {
source_joiner.append_source_dyn(source);
}
let entry_stable_id = self.module_table().modules[entry_module_idx].stable_id().as_str();
source_joiner.append_source(format!(
"__rolldown_runtime__.initModule({})",
json_escape_simd::escape(entry_stable_id)
));
let (mut code, mut map) = source_joiner.join();
let lazy_patch_id = self.next_hmr_patch_id.fetch_add(1, Ordering::Relaxed);
let filename = format!("lazy_compile_{lazy_patch_id}.js");
let file_dir = self.options.cwd.as_path().join(&self.options.out_dir);
if let Some(map) = map.as_mut() {
process_code_and_sourcemap(
&self.options,
&mut code,
map,
&file_dir,
filename.as_str(),
0,
false,
None,
)
.await?;
}
let carried = modules_to_be_updated
.iter()
.map(|module_idx| {
let stable_id = self.module_table().modules[*module_idx].stable_id();
(stable_id.as_arc_str().clone(), stamp_table.render_time_stamp(stable_id.as_str()))
})
.collect();
Ok(HmrLazyChunkOutput { code, filename, carried })
}
async fn render_hmr_patch(
&self,
mut carried_modules: FxIndexSet<ModuleIdx>,
changed_ids: Vec<String>,
stamp_table: &HmrStampTable,
) -> BuildResult<HmrUpdate> {
carried_modules.retain(|idx| self.module_table().modules[*idx].is_normal());
carried_modules
.sort_by_cached_key(|module_idx| self.module_table().modules[*module_idx].id().as_str());
let index_ecma_ast = self.index_ecma_ast();
let module_render_inputs = carried_modules
.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();
if let Some(prelude) = crate::hmr::module_graph_delta::render_register_graph_source(
self.module_table(),
carried_modules.iter().copied(),
) {
source_joiner.append_source(prelude);
}
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).into_scoping();
let mut finalizer = HmrAstFinalizer {
modules,
ast_factory: AstFactory::new(fields.allocator),
import_bindings: FxHashMap::default(),
module: affected_module,
exports: oxc::allocator::Vec::new_in(&fields.allocator),
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(),
comments: PrintCommentsOptions {
legal: false, annotation: self.options.comments.annotation,
jsdoc: self.options.comments.jsdoc,
},
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.into_owned()))
} else {
Box::new(codegen.code)
};
[intro_comment, code_source, outro_comment]
})
.collect::<Vec<_>>();
for source in rendered_sources {
source_joiner.append_source_dyn(source);
}
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,
None,
)
.await?
} else {
None
};
let carried = carried_modules
.iter()
.map(|module_idx| {
let stable_id = self.module_table().modules[*module_idx].stable_id();
(stable_id.as_arc_str().clone(), stamp_table.render_time_stamp(stable_id.as_str()))
})
.collect();
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()?,
changed_ids,
seq: 0,
carried,
}))
}
}
struct ModuleRenderInput {
pub idx: ModuleIdx,
pub ecma_ast: EcmaAst,
}
impl<Fs: FileSystem + Clone + 'static> HmrStage<'_, Fs> {
fn collect_sync_dependencies_for_client(
&self,
proxy_entry_idx: ModuleIdx,
result: &mut FxIndexSet<ModuleIdx>,
shipped: &FxHashMap<ArcStr, u32>,
evaluated: &FxHashMap<ArcStr, u32>,
stamp_table: &HmrStampTable,
) {
let modules = &self.module_table().modules;
let mut stack = vec![proxy_entry_idx];
while let Some(module_idx) = stack.pop() {
if !result.insert(module_idx) {
continue;
}
let Module::Normal(module) = &modules[module_idx] else {
continue;
};
for rec in &module.import_records {
let should_follow = rec.kind.is_static()
|| (module_idx == proxy_entry_idx && rec.kind == ImportKind::DynamicImport);
if should_follow && let Some(dep_idx) = rec.resolved_module {
if result.contains(&dep_idx) {
continue;
}
if let Module::Normal(normal_dep) = &modules[dep_idx] {
let stable_id = normal_dep.stable_id.as_str();
let holds_current = |map: &FxHashMap<ArcStr, u32>| {
map.get(stable_id).is_some_and(|stamp| !stamp_table.is_stale(stable_id, *stamp))
};
if holds_current(shipped) || holds_current(evaluated) {
continue;
}
}
stack.push(dep_idx);
}
}
}
}
}