use json_escape_simd::escape;
use rolldown_common::{
HmrStampTable, ImportKind, ImportRecordIdx, ImportRecordMeta, Module, ModuleIdx, ModuleTable,
NormalModule, RUNTIME_MODULE_KEY, Specifier,
};
use rustc_hash::{FxHashMap, FxHashSet};
const ANY_BINDING: &str = "*";
pub fn static_import_bindings(module: &NormalModule) -> FxHashMap<ImportRecordIdx, Vec<&str>> {
let mut names_by_record: FxHashMap<ImportRecordIdx, Vec<&str>> = FxHashMap::default();
for (record_idx, record) in module.import_records.iter_enumerated() {
if !record.kind.is_static() {
continue;
}
let names = names_by_record.entry(record_idx).or_default();
if record.kind != ImportKind::Import || record.meta.contains(ImportRecordMeta::IsExportStar) {
names.push(ANY_BINDING);
}
}
for named_import in module.named_imports.values() {
let Some(names) = names_by_record.get_mut(&named_import.record_idx) else { continue };
let name = match &named_import.imported {
Specifier::Star => ANY_BINDING,
Specifier::Literal(name) => name.as_str(),
};
if !names.contains(&name) {
names.push(name);
}
}
names_by_record
}
fn edge_bindings<'a>(
module_table: &ModuleTable,
module: &'a NormalModule,
out_edges: &[usize],
id_to_index: &FxHashMap<ModuleIdx, usize>,
) -> Vec<Option<Vec<&'a str>>> {
let names_by_record = static_import_bindings(module);
let mut out_bindings: Vec<Option<Vec<&str>>> = vec![None; out_edges.len()];
for (record_idx, record) in module.import_records.iter_enumerated() {
if !record.kind.is_static() {
continue;
}
let Some(target_idx) = record.resolved_module else { continue };
let Module::Normal(target) = &module_table.modules[target_idx] else { continue };
if !target.is_hmr_partially_accepting_module() {
continue;
}
let Some(target_pos) = id_to_index.get(&target_idx) else { continue };
let Some(edge_pos) = out_edges.iter().position(|pos| pos == target_pos) else { continue };
let names = out_bindings[edge_pos].get_or_insert_with(Vec::new);
for name in names_by_record.get(&record_idx).into_iter().flatten() {
if !names.contains(name) {
names.push(name);
}
}
}
out_bindings
}
pub fn render_register_graph_source(
module_table: &ModuleTable,
carried_modules: impl IntoIterator<Item = ModuleIdx>,
stamp_table: Option<&HmrStampTable>,
) -> Option<String> {
let mut ids: Vec<ModuleIdx> = Vec::new();
let mut id_to_index: FxHashMap<ModuleIdx, usize> = FxHashMap::default();
for idx in carried_modules {
let Module::Normal(module) = &module_table.modules[idx] else { continue };
if module.id.as_str() == RUNTIME_MODULE_KEY {
continue;
}
id_to_index.entry(idx).or_insert_with(|| {
ids.push(idx);
ids.len() - 1
});
}
let local_count = ids.len();
if local_count == 0 {
return None;
}
let mut edges: Vec<Vec<usize>> = Vec::with_capacity(local_count);
let mut bindings: Vec<(usize, Vec<Option<Vec<&str>>>)> = Vec::new();
let mut dynamic_edges: Vec<(usize, Vec<usize>)> = Vec::new();
let mut seen_static = FxHashSet::default();
let mut seen_dynamic = FxHashSet::default();
for i in 0..local_count {
let Module::Normal(module) = &module_table.modules[ids[i]] else {
unreachable!("carried rows are filtered to normal modules above");
};
seen_static.clear();
seen_dynamic.clear();
let mut needs_bindings = false;
let mut out_edges = Vec::new();
let mut dyn_out_edges = Vec::new();
for record in &module.import_records {
if !record.kind.is_static() && !record.kind.is_dynamic() {
continue;
}
let Some(target_idx) = record.resolved_module else { continue };
let Module::Normal(target) = &module_table.modules[target_idx] else { continue };
if target.id.as_str() == RUNTIME_MODULE_KEY {
continue;
}
let target_pos = *id_to_index.entry(target_idx).or_insert_with(|| {
ids.push(target_idx);
ids.len() - 1
});
if record.kind.is_static() {
if seen_static.insert(target_pos) {
out_edges.push(target_pos);
}
needs_bindings |= target.is_hmr_partially_accepting_module();
} else if seen_dynamic.insert(target_pos) {
dyn_out_edges.push(target_pos);
}
}
if needs_bindings {
bindings.push((i, edge_bindings(module_table, module, &out_edges, &id_to_index)));
}
edges.push(out_edges);
if !dyn_out_edges.is_empty() {
dynamic_edges.push((i, dyn_out_edges));
}
}
let mut source = String::with_capacity(ids.len() * 32);
source.push_str("__rolldown_runtime__.registerGraph({ids:[");
for (i, idx) in ids.iter().enumerate() {
if i > 0 {
source.push(',');
}
source.push_str(&escape(module_table.modules[*idx].stable_id().as_str()));
}
source.push_str("],localCount:");
source.push_str(itoa::Buffer::new().format(local_count));
source.push_str(",edges:[");
for (i, out_edges) in edges.iter().enumerate() {
if i > 0 {
source.push(',');
}
source.push('[');
for (j, target_pos) in out_edges.iter().enumerate() {
if j > 0 {
source.push(',');
}
source.push_str(itoa::Buffer::new().format(*target_pos));
}
source.push(']');
}
source.push(']');
if !bindings.is_empty() {
source.push_str(",bindings:{");
for (n, (row, out_bindings)) in bindings.iter().enumerate() {
if n > 0 {
source.push(',');
}
source.push_str(itoa::Buffer::new().format(*row));
source.push_str(":[");
let len = out_bindings.iter().rposition(Option::is_some).map_or(0, |pos| pos + 1);
for (j, names) in out_bindings[..len].iter().enumerate() {
if j > 0 {
source.push(',');
}
match names {
None => source.push_str("null"),
Some(names) => {
source.push('[');
for (k, name) in names.iter().enumerate() {
if k > 0 {
source.push(',');
}
source.push_str(&escape(name));
}
source.push(']');
}
}
}
source.push(']');
}
source.push('}');
}
if !dynamic_edges.is_empty() {
source.push_str(",dynamicEdges:{");
for (n, (row, out_edges)) in dynamic_edges.iter().enumerate() {
if n > 0 {
source.push(',');
}
source.push_str(itoa::Buffer::new().format(*row));
source.push_str(":[");
for (j, target_pos) in out_edges.iter().enumerate() {
if j > 0 {
source.push(',');
}
source.push_str(itoa::Buffer::new().format(*target_pos));
}
source.push(']');
}
source.push('}');
}
if let Some(stamp_table) = stamp_table {
source.push_str(",stamps:{");
let mut first = true;
for (row, idx) in ids[..local_count].iter().enumerate() {
let stamp = stamp_table.render_time_stamp(module_table.modules[*idx].stable_id().as_str());
if stamp == 0 {
continue;
}
if !first {
source.push(',');
}
first = false;
source.push_str(itoa::Buffer::new().format(row));
source.push(':');
source.push_str(itoa::Buffer::new().format(stamp));
}
source.push('}');
}
source.push_str("});");
tracing::debug!(
target: "hmr",
"registerGraph manifest: {} bytes, {} carried modules, {} interned ids",
source.len(),
local_count,
ids.len(),
);
Some(source)
}