use std::{
ops::{Deref, DerefMut},
path::Path,
sync::{
Arc,
atomic::{AtomicU32, Ordering},
},
};
use arcstr::ArcStr;
use itertools::Itertools;
use oxc::ast::builder::AstBuilder;
use oxc_traverse::traverse_mut;
use rolldown_common::{
ClientHmrInput, ClientHmrUpdate, ExportsKind, HmrLazyChunkOutput, HmrPatch, HmrStampTable,
HmrUpdate, ImportKind, Module, ModuleIdx, ModuleTable, NormalModule, ScanMode, WatcherChangeKind,
};
use rolldown_ecmascript::{EcmaAst, EcmaCompiler, PrintCommentsOptions, PrintOptions};
use rolldown_error::BuildResult;
use rolldown_fs::FileSystem;
use rolldown_plugin::SharedPluginDriver;
use rolldown_sourcemap::{Source, SourceJoiner, SourceMap, 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,
render_ecma_module::collapse_module_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 lost_hmr_update: &'a mut bool,
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
}
}
fn is_hidden_from_hot_update_hook(id: &str) -> bool {
id.contains("?rolldown-lazy=") || id.starts_with("rolldown:") || id.starts_with("\0rolldown/")
}
impl<'a, Fs: FileSystem + Clone + 'static> HmrStage<'a, Fs> {
pub fn new(input: HmrStageInput<'a, Fs>) -> Self {
Self { input }
}
#[expect(clippy::too_many_lines)]
pub async fn compute_hmr_update_for_file_changes(
&mut self,
changed_file_paths: &FxIndexMap<String, WatcherChangeKind>,
clients: &[ClientHmrInput<'_>],
stamp_table: &mut HmrStampTable,
last_build_errored: bool,
hot_update_hook_enabled: bool,
) -> BuildResult<Vec<ClientHmrUpdate>> {
tracing::trace!(
changed_files = %changed_file_paths
.iter()
.map(|(path, kind)| format!("{path}:{kind}"))
.join(", "),
clients = %clients.iter().map(|client| client.client_id).join(", "),
"[HmrStage] starts computing HMR updates"
);
let lost_before = *self.lost_hmr_update;
let hot_update_hook_enabled = hot_update_hook_enabled
|| self.options.experimental.dev_mode.as_ref().is_some_and(|d| d.hot_update == Some(true));
let hot_update_hook_registered =
hot_update_hook_enabled && self.plugin_driver.has_hot_update_hook();
let mut changed_modules = FxIndexSet::default();
let mut hook_selected_modules = FxHashSet::default();
for (changed_file_path, event) in changed_file_paths {
let changed_path = Path::new(changed_file_path);
let changed_file_path = ArcStr::from(changed_file_path.to_slash());
let own_module_idx = self.cache.module_idx_by_abs_path.get(&changed_file_path).copied();
let mut affected_modules = FxIndexSet::default();
if let Some(module_idx) = own_module_idx {
affected_modules.insert(module_idx);
}
let mut transform_dep_modules = self
.plugin_driver
.transform_dependencies
.iter()
.filter_map(|entry| {
changed_path
.ancestors()
.any(|ancestor| entry.value().contains(ancestor))
.then_some(*entry.key())
})
.collect::<Vec<_>>();
transform_dep_modules
.sort_unstable_by_key(|module_idx| self.module_table().modules[*module_idx].stable_id());
affected_modules.extend(transform_dep_modules);
let mut hook_replaced = false;
if hot_update_hook_registered {
let default_ids = affected_modules
.iter()
.filter_map(|module_idx| match &self.module_table().modules[*module_idx] {
Module::Normal(module) if !is_hidden_from_hot_update_hook(module.id.as_arc_str()) => {
Some(ArcStr::from(module.id.as_arc_str().to_slash()))
}
_ => None,
})
.collect::<Vec<_>>();
let final_ids =
self.plugin_driver.hot_update(*event, &changed_file_path, default_ids).await?;
if let Some(final_ids) = final_ids {
hook_replaced = true;
affected_modules.clear();
for id in final_ids {
if is_hidden_from_hot_update_hook(&id) {
continue;
}
if let Some(module_idx) = self.cache.module_idx_by_abs_path.get(&*id.to_slash()) {
affected_modules.insert(*module_idx);
} else {
tracing::debug!(
"[HmrStage] dropped unknown module id returned from the hotUpdate hook: {id}"
);
}
}
}
}
for module_idx in affected_modules {
if *event == WatcherChangeKind::Delete && Some(module_idx) == own_module_idx {
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);
if hook_replaced {
hook_selected_modules.insert(module_idx);
}
}
}
}
tracing::trace!(
changed_modules = %changed_modules
.iter()
.map(|module_idx| self.module_table().modules[*module_idx].stable_id())
.join(", "),
"[HmrStage] mapped changed file paths to modules"
);
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());
}
}
if changed_modules.is_empty() {
return Ok(
clients
.iter()
.map(|client| ClientHmrUpdate {
client_id: client.client_id.to_string(),
update: HmrUpdate::Noop,
})
.collect(),
);
}
let pre_rebuild_renders = if last_build_errored {
FxHashMap::default()
} else {
let pre_rebuild_inputs = changed_modules
.iter()
.filter(|module_idx| !hook_selected_modules.contains(*module_idx))
.filter_map(|module_idx| {
self.module_table().modules[*module_idx].as_normal()?;
let ecma_ast = self.index_ecma_ast()[*module_idx].as_ref()?;
Some(ModuleRenderInput {
idx: *module_idx,
ecma_ast: ecma_ast.clone_with_another_arena(),
})
})
.collect::<Vec<_>>();
pre_rebuild_inputs
.into_par_iter()
.map(|render_input| {
let module_idx = render_input.idx;
(module_idx, self.render_module_code(render_input, 0, 0, false).0)
})
.collect::<Vec<_>>()
.into_iter()
.collect::<FxHashMap<_, _>>()
};
let modules = &self.module_table().modules;
let pre_rebuild_static_deps = changed_modules
.iter()
.filter_map(|module_idx| {
let module = modules[*module_idx].as_normal()?;
let deps = static_deps(module).map(|dep_idx| modules[dep_idx].id().clone());
Some((*module_idx, deps.collect::<FxHashSet<_>>()))
})
.collect::<FxHashMap<_, _>>();
let pre_rebuild_interop = changed_modules
.iter()
.filter_map(|module_idx| {
Some((*module_idx, importee_interop(modules[*module_idx].as_normal()?)))
})
.collect::<FxHashMap<_, _>>();
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_modules = %new_added_modules
.iter()
.map(|module_idx| module_loader_output.module_table.get(*module_idx).stable_id())
.join(", "),
"added modules"
);
*self.lost_hmr_update = true;
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 recheck_inputs = changed_modules
.iter()
.filter(|module_idx| pre_rebuild_renders.contains_key(*module_idx))
.filter_map(|module_idx| {
self.module_table().modules[*module_idx].as_normal()?;
let ecma_ast = self.index_ecma_ast()[*module_idx].as_ref()?;
Some(ModuleRenderInput { idx: *module_idx, ecma_ast: ecma_ast.clone_with_another_arena() })
})
.collect::<Vec<_>>();
let output_unchanged_modules = recheck_inputs
.into_par_iter()
.filter_map(|render_input| {
let module_idx = render_input.idx;
let (code, _) = self.render_module_code(render_input, 0, 0, false);
(pre_rebuild_renders[&module_idx] == code).then_some(module_idx)
})
.collect::<Vec<_>>()
.into_iter()
.collect::<FxHashSet<_>>();
if !output_unchanged_modules.is_empty() {
tracing::debug!(
target: "hmr",
unchanged_modules = %output_unchanged_modules
.iter()
.map(|module_idx| self.module_table().modules[*module_idx].stable_id())
.join(", "),
"skip modules whose rebuilt output is unchanged"
);
changed_modules.retain(|module_idx| !output_unchanged_modules.contains(module_idx));
}
let modules = &self.module_table().modules;
let interop_changed = changed_modules
.iter()
.copied()
.filter(|module_idx| {
let before = pre_rebuild_interop.get(module_idx);
let after = modules[*module_idx].as_normal().map(importee_interop);
before.is_some_and(|before| after.is_some_and(|after| *before != after))
})
.collect::<FxHashSet<_>>();
let rerendered_importers = if interop_changed.is_empty() {
FxIndexSet::default()
} else {
modules
.iter()
.filter_map(Module::as_normal)
.filter(|module| !changed_modules.contains(&module.idx))
.filter(|module| {
module
.import_records
.iter()
.any(|rec| rec.resolved_module.is_some_and(|dep| interop_changed.contains(&dep)))
})
.map(|module| module.idx)
.collect::<FxIndexSet<_>>()
};
let rebuild_seq = stamp_table.begin_rebuild();
for module_idx in
changed_modules.iter().chain(new_added_modules.iter()).chain(rerendered_importers.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 modules = &self.module_table().modules;
let added_static_deps = changed_modules
.iter()
.chain(new_added_modules.iter())
.filter_map(|module_idx| modules[*module_idx].as_normal())
.flat_map(|module| {
let before = pre_rebuild_static_deps.get(&module.idx);
static_deps(module)
.filter(move |dep| before.is_none_or(|before| !before.contains(modules[*dep].id())))
})
.collect::<FxIndexSet<_>>();
let mut affected = self.collect_client_update_superset(&changed_modules);
affected.extend(new_added_modules.iter().copied());
affected.extend(rerendered_importers.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);
}
}
}
}
if !added_static_deps.is_empty() {
let held = ClientHeldCopies {
shipped: client.shipped,
evaluated: client.top_level_evaluated,
stamp_table,
};
self.collect_unheld_sync_deps(
&[],
added_static_deps.iter().copied(),
None,
&held,
&mut carried,
);
}
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 });
}
*self.lost_hmr_update = lost_before;
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();
modules_to_be_updated.insert(entry_module_idx);
self.collect_unheld_sync_deps(
&[entry_module_idx],
[],
Some(entry_module_idx),
&ClientHeldCopies { shipped, evaluated, stamp_table },
&mut modules_to_be_updated,
);
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(),
Some(stamp_table),
) {
source_joiner.append_source(prelude);
}
let rendered_sources = module_render_inputs
.into_par_iter()
.enumerate()
.flat_map(|(index, render_input)| {
let affected_module_idx = render_input.idx;
let stamp = stamp_table
.render_time_stamp(self.module_table().modules[affected_module_idx].stable_id().as_str());
let (code, map) = self.render_module_code(render_input, index, stamp, true);
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 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) = map {
Box::new(SourceMapSource::new(code, map))
} else {
Box::new(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);
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 = 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,
sourcemap_filename: sourcemap_asset.as_ref().map(|asset| asset.filename.to_string()),
sourcemap: sourcemap_asset.map(|asset| asset.source.try_into_string()).transpose()?,
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());
if carried_modules.is_empty() && changed_ids.is_empty() {
return Ok(HmrUpdate::Noop);
}
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(),
Some(stamp_table),
) {
source_joiner.append_source(prelude);
}
let rendered_sources = module_render_inputs
.into_par_iter()
.enumerate()
.flat_map(|(index, render_input)| {
let affected_module_idx = render_input.idx;
let stamp = stamp_table
.render_time_stamp(self.module_table().modules[affected_module_idx].stable_id().as_str());
let (code, map) = self.render_module_code(render_input, index, stamp, true);
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 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) = map {
Box::new(SourceMapSource::new(code, map))
} else {
Box::new(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,
}))
}
fn render_module_code(
&self,
render_input: ModuleRenderInput,
unique_index: usize,
stamp: u32,
with_sourcemap: bool,
) -> (String, Option<SourceMap>) {
let ModuleRenderInput { idx: module_idx, ecma_ast: mut ast } = render_input;
let Module::Normal(module) = &self.module_table().modules[module_idx] else {
unreachable!("HMR only supports normal module");
};
let enable_sourcemap =
with_sourcemap && self.options.sourcemap.is_some() && !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_builder: AstBuilder::new(fields.allocator),
import_bindings: FxHashMap::default(),
module,
exports: oxc::allocator::Vec::new_in(&fields.allocator),
use_pife_for_module_wrappers,
dependencies: FxIndexSet::default(),
generated_load_exports_stmts: FxHashMap::default(),
generated_static_import_infos: FxHashMap::default(),
re_export_all_dependencies: FxIndexSet::default(),
generated_static_import_stmts_from_external: FxIndexMap::default(),
unique_index,
stamp,
named_exports: FxHashMap::default(),
};
traverse_mut(&mut finalizer, fields.allocator, fields.program, scoping, ());
});
let codegen = EcmaCompiler::print_with(
&ast,
PrintOptions {
sourcemap: enable_sourcemap,
filename: module.id.to_string(),
comments: PrintCommentsOptions {
legal: false, annotation: self.options.comments.annotation,
jsdoc: self.options.comments.jsdoc,
},
initial_indent: 0,
},
);
let (code, codegen_map) = match codegen.map {
Some(map) => (codegen.code, Some(map.into_owned())),
None => (codegen.code, None),
};
let map = enable_sourcemap
.then(|| {
collapse_module_sourcemap(
&module.sourcemap_chain,
codegen_map,
module.id.as_str(),
&mut Vec::new(),
)
})
.flatten();
(code, map)
}
}
struct ModuleRenderInput {
pub idx: ModuleIdx,
pub ecma_ast: EcmaAst,
}
struct ClientHeldCopies<'a> {
shipped: &'a FxHashMap<ArcStr, u32>,
evaluated: &'a FxHashMap<ArcStr, u32>,
stamp_table: &'a HmrStampTable,
}
impl ClientHeldCopies<'_> {
fn holds_current(&self, map: &FxHashMap<ArcStr, u32>, stable_id: &str) -> bool {
map.get(stable_id).is_some_and(|stamp| !self.stamp_table.is_stale(stable_id, *stamp))
}
}
fn importee_interop(module: &NormalModule) -> (ExportsKind, bool) {
(module.exports_kind, module.meta.has_lazy_export())
}
fn static_deps(module: &NormalModule) -> impl Iterator<Item = ModuleIdx> + '_ {
module
.import_records
.iter()
.filter(|rec| rec.kind.is_static())
.filter_map(|rec| rec.resolved_module)
}
impl<Fs: FileSystem + Clone + 'static> HmrStage<'_, Fs> {
fn collect_unheld_sync_deps(
&self,
walk_from: &[ModuleIdx],
targets: impl IntoIterator<Item = ModuleIdx>,
follow_dynamic_of: Option<ModuleIdx>,
held: &ClientHeldCopies<'_>,
out: &mut FxIndexSet<ModuleIdx>,
) {
let modules = &self.module_table().modules;
let mut visited = walk_from.iter().copied().collect::<FxHashSet<_>>();
let mut to_walk = walk_from.to_vec();
let mut to_sort = targets.into_iter().collect::<Vec<_>>();
loop {
while let Some(module_idx) = to_sort.pop() {
if !visited.insert(module_idx) {
continue;
}
let Module::Normal(module) = &modules[module_idx] else {
continue;
};
let stable_id = module.stable_id.as_str();
if held.holds_current(held.evaluated, stable_id) {
continue;
}
if !held.holds_current(held.shipped, stable_id) {
out.insert(module_idx);
}
to_walk.push(module_idx);
}
let Some(module_idx) = to_walk.pop() else {
break;
};
let Module::Normal(module) = &modules[module_idx] else {
continue;
};
for rec in &module.import_records {
let follow = rec.kind.is_static()
|| (Some(module_idx) == follow_dynamic_of && rec.kind == ImportKind::DynamicImport);
if follow && let Some(dep_idx) = rec.resolved_module {
to_sort.push(dep_idx);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::is_hidden_from_hot_update_hook;
#[test]
fn hidden_ids_cover_runtime_and_lazy_proxy_modules() {
assert!(is_hidden_from_hot_update_hook(rolldown_common::RUNTIME_MODULE_KEY));
assert!(is_hidden_from_hot_update_hook("rolldown:hmr"));
assert!(is_hidden_from_hot_update_hook("/app/main.js?rolldown-lazy=1"));
assert!(!is_hidden_from_hot_update_hook("/app/main.js"));
assert!(!is_hidden_from_hot_update_hook("\0virtual:team"));
}
}