use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use crate::ModuleGraph;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct UpdateTarget {
pub boundary: PathBuf,
pub accepted: PathBuf,
pub within_circular_import: bool,
}
#[derive(Debug, PartialEq, Eq)]
pub enum HmrDecision {
Update { boundaries: Vec<PathBuf> },
FullReload { reason: String },
}
impl ModuleGraph {
fn all_imports_accepted(
&self,
importer: &Path,
dep: &Path,
accepted: &HashSet<String>,
) -> bool {
self.modules
.get(importer)
.and_then(|n| n.imported_bindings.get(dep))
.is_some_and(|bindings| bindings.iter().all(|b| accepted.contains(b)))
}
fn accepts_dep(&self, importer: &Path, dep: &Path) -> bool {
self.modules
.get(importer)
.is_some_and(|n| n.accepted_hmr_deps.contains(dep))
}
pub fn update_targets(&self, changed: &Path) -> Result<Vec<UpdateTarget>, String> {
if !self.modules.contains_key(changed) {
return Err(format!("{} is not in the module graph", changed.display()));
}
self.collect_boundaries(&[changed], &[]).map(sorted)
}
pub fn update_targets_from_importers(
&self,
changed: &Path,
) -> Result<Vec<UpdateTarget>, String> {
let Some(node) = self.modules.get(changed) else {
return Err(format!("{} is not in the module graph", changed.display()));
};
if node.importers.is_empty() {
return Err(format!("{} invalidated at an entry", changed.display()));
}
let seeds: Vec<&Path> = node.importers.iter().map(PathBuf::as_path).collect();
self.collect_boundaries(&seeds, &[changed]).map(sorted)
}
pub fn propagate_update(&self, changed: &Path) -> HmrDecision {
self.propagate_from_seeds(&[changed])
}
pub fn propagate_from_seeds(&self, seeds: &[&Path]) -> HmrDecision {
for seed in seeds {
if !self.modules.contains_key(*seed) {
return HmrDecision::FullReload {
reason: format!("{} is not in the module graph", seed.display()),
};
}
}
match self.collect_boundaries(seeds, &[]) {
Ok(targets) => HmrDecision::Update {
boundaries: sorted(targets.into_iter().map(|t| t.boundary).collect()),
},
Err(reason) => HmrDecision::FullReload { reason },
}
}
pub fn dirty_closure(&self, changed: &Path) -> Vec<PathBuf> {
let mut dirty: Vec<PathBuf> = vec![changed.to_path_buf()];
let mut queue = vec![changed.to_path_buf()];
let mut seen: HashSet<PathBuf> = queue.iter().cloned().collect();
while let Some(current) = queue.pop() {
let Some(node) = self.modules.get(¤t) else {
continue;
};
if node.is_self_accepting && current != changed {
continue;
}
for importer in &node.importers {
if self.accepts_dep(importer, ¤t) {
continue;
}
if seen.insert(importer.clone()) {
dirty.push(importer.clone());
queue.push(importer.clone());
}
}
}
dirty.sort();
dirty
}
fn collect_boundaries<'a>(
&'a self,
seeds: &[&'a Path],
pre_stack: &[&'a Path],
) -> Result<Vec<UpdateTarget>, String> {
let mut colors: HashMap<&'a Path, Color> = HashMap::new();
for module in pre_stack {
colors.insert(module, Color::Gray);
}
let mut boundaries: Vec<UpdateTarget> = Vec::new();
for seed in seeds {
self.climb(seed, &mut colors, &mut boundaries)?;
}
Ok(boundaries)
}
fn is_within_circular_imports(&self, boundary: &Path, colors: &HashMap<&Path, Color>) -> bool {
let mut stack = vec![boundary];
let mut seen: HashSet<&Path> = HashSet::new();
while let Some(current) = stack.pop() {
if !seen.insert(current) {
continue;
}
let Some(node) = self.modules.get(current) else {
continue;
};
for importer in &node.importers {
let importer = importer.as_path();
if importer == current || is_css_path(importer) {
continue;
}
if importer == boundary || colors.get(importer) == Some(&Color::Gray) {
return true;
}
stack.push(importer);
}
}
false
}
fn target(
&self,
boundary: &Path,
accepted: &Path,
colors: &HashMap<&Path, Color>,
) -> UpdateTarget {
UpdateTarget {
boundary: boundary.to_path_buf(),
accepted: accepted.to_path_buf(),
within_circular_import: self.is_within_circular_imports(boundary, colors),
}
}
fn climb<'a>(
&'a self,
seed: &'a Path,
colors: &mut HashMap<&'a Path, Color>,
boundaries: &mut Vec<UpdateTarget>,
) -> Result<(), String> {
enum Step<'s> {
Enter(&'s Path),
Blacken(&'s Path),
}
let mut stack = vec![Step::Enter(seed)];
while let Some(step) = stack.pop() {
let current = match step {
Step::Blacken(path) => {
colors.insert(path, Color::Black);
continue;
}
Step::Enter(path) => path,
};
match colors.get(current) {
Some(Color::Black) => continue,
Some(Color::Gray) => continue,
None => {}
}
let Some(node) = self.modules.get(current) else {
return Err(format!("{} is not in the module graph", current.display()));
};
if node.is_self_accepting {
boundaries.push(self.target(current, current, colors));
colors.insert(current, Color::Black);
continue;
}
let accepted_exports = node.accepted_exports.as_ref();
if accepted_exports.is_some() {
boundaries.push(self.target(current, current, colors));
} else if node.importers.is_empty() {
return Err(format!(
"update reached entry {} with no accepting boundary",
current.display()
));
}
colors.insert(current, Color::Gray);
stack.push(Step::Blacken(current));
let mut importers: Vec<&Path> = node.importers.iter().map(PathBuf::as_path).collect();
importers.sort_unstable();
for importer in importers.into_iter().rev() {
if self.accepts_dep(importer, current) {
boundaries.push(self.target(importer, current, colors));
continue;
}
if let Some(accepted) = accepted_exports {
if self.all_imports_accepted(importer, current, accepted) {
continue;
}
}
stack.push(Step::Enter(importer));
}
}
Ok(())
}
}
fn sorted<T: Ord>(mut items: Vec<T>) -> Vec<T> {
items.sort();
items.dedup();
items
}
fn is_css_path(path: &Path) -> bool {
matches!(
path.extension().and_then(|e| e.to_str()),
Some("css" | "scss" | "sass" | "less" | "styl" | "stylus" | "pcss" | "postcss" | "sss")
)
}
#[derive(Clone, Copy, PartialEq)]
enum Color {
Gray,
Black,
}