1use std::collections::{HashMap, HashSet};
5use std::path::{Path, PathBuf};
6
7use crate::ModuleGraph;
8
9#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
15pub struct UpdateTarget {
16 pub boundary: PathBuf,
17 pub accepted: PathBuf,
18 pub within_circular_import: bool,
19}
20
21#[derive(Debug, PartialEq, Eq)]
22pub enum HmrDecision {
23 Update { boundaries: Vec<PathBuf> },
24 FullReload { reason: String },
25}
26
27impl ModuleGraph {
28 fn all_imports_accepted(
32 &self,
33 importer: &Path,
34 dep: &Path,
35 accepted: &HashSet<String>,
36 ) -> bool {
37 self.modules
38 .get(importer)
39 .and_then(|n| n.imported_bindings.get(dep))
40 .is_some_and(|bindings| bindings.iter().all(|b| accepted.contains(b)))
41 }
42
43 fn accepts_dep(&self, importer: &Path, dep: &Path) -> bool {
44 self.modules
45 .get(importer)
46 .is_some_and(|n| n.accepted_hmr_deps.contains(dep))
47 }
48
49 pub fn update_targets(&self, changed: &Path) -> Result<Vec<UpdateTarget>, String> {
54 if !self.modules.contains_key(changed) {
55 return Err(format!("{} is not in the module graph", changed.display()));
56 }
57 self.collect_boundaries(&[changed], &[]).map(sorted)
58 }
59
60 pub fn update_targets_from_importers(
65 &self,
66 changed: &Path,
67 ) -> Result<Vec<UpdateTarget>, String> {
68 let Some(node) = self.modules.get(changed) else {
69 return Err(format!("{} is not in the module graph", changed.display()));
70 };
71 if node.importers.is_empty() {
72 return Err(format!("{} invalidated at an entry", changed.display()));
73 }
74 let seeds: Vec<&Path> = node.importers.iter().map(PathBuf::as_path).collect();
75 self.collect_boundaries(&seeds, &[changed]).map(sorted)
76 }
77
78 pub fn propagate_update(&self, changed: &Path) -> HmrDecision {
79 self.propagate_from_seeds(&[changed])
80 }
81
82 pub fn propagate_from_seeds(&self, seeds: &[&Path]) -> HmrDecision {
83 for seed in seeds {
84 if !self.modules.contains_key(*seed) {
85 return HmrDecision::FullReload {
86 reason: format!("{} is not in the module graph", seed.display()),
87 };
88 }
89 }
90 match self.collect_boundaries(seeds, &[]) {
91 Ok(targets) => HmrDecision::Update {
92 boundaries: sorted(targets.into_iter().map(|t| t.boundary).collect()),
93 },
94 Err(reason) => HmrDecision::FullReload { reason },
95 }
96 }
97
98 pub fn dirty_closure(&self, changed: &Path) -> Vec<PathBuf> {
101 let mut dirty: Vec<PathBuf> = vec![changed.to_path_buf()];
102 let mut queue = vec![changed.to_path_buf()];
103 let mut seen: HashSet<PathBuf> = queue.iter().cloned().collect();
104 while let Some(current) = queue.pop() {
105 let Some(node) = self.modules.get(¤t) else {
106 continue;
107 };
108 if node.is_self_accepting && current != changed {
109 continue;
110 }
111 for importer in &node.importers {
112 if self.accepts_dep(importer, ¤t) {
115 continue;
116 }
117 if seen.insert(importer.clone()) {
118 dirty.push(importer.clone());
119 queue.push(importer.clone());
120 }
121 }
122 }
123 dirty.sort();
124 dirty
125 }
126
127 fn collect_boundaries<'a>(
128 &'a self,
129 seeds: &[&'a Path],
130 pre_stack: &[&'a Path],
131 ) -> Result<Vec<UpdateTarget>, String> {
132 let mut colors: HashMap<&'a Path, Color> = HashMap::new();
133 for module in pre_stack {
134 colors.insert(module, Color::Gray);
135 }
136 let mut boundaries: Vec<UpdateTarget> = Vec::new();
137 for seed in seeds {
138 self.climb(seed, &mut colors, &mut boundaries)?;
139 }
140 Ok(boundaries)
141 }
142
143 fn is_within_circular_imports(&self, boundary: &Path, colors: &HashMap<&Path, Color>) -> bool {
149 let mut stack = vec![boundary];
150 let mut seen: HashSet<&Path> = HashSet::new();
151 while let Some(current) = stack.pop() {
152 if !seen.insert(current) {
153 continue;
154 }
155 let Some(node) = self.modules.get(current) else {
156 continue;
157 };
158 for importer in &node.importers {
159 let importer = importer.as_path();
160 if importer == current || is_css_path(importer) {
161 continue;
162 }
163 if importer == boundary || colors.get(importer) == Some(&Color::Gray) {
164 return true;
165 }
166 stack.push(importer);
167 }
168 }
169 false
170 }
171
172 fn target(
173 &self,
174 boundary: &Path,
175 accepted: &Path,
176 colors: &HashMap<&Path, Color>,
177 ) -> UpdateTarget {
178 UpdateTarget {
179 boundary: boundary.to_path_buf(),
180 accepted: accepted.to_path_buf(),
181 within_circular_import: self.is_within_circular_imports(boundary, colors),
182 }
183 }
184
185 fn climb<'a>(
186 &'a self,
187 seed: &'a Path,
188 colors: &mut HashMap<&'a Path, Color>,
189 boundaries: &mut Vec<UpdateTarget>,
190 ) -> Result<(), String> {
191 enum Step<'s> {
192 Enter(&'s Path),
193 Blacken(&'s Path),
194 }
195
196 let mut stack = vec![Step::Enter(seed)];
197 while let Some(step) = stack.pop() {
198 let current = match step {
199 Step::Blacken(path) => {
200 colors.insert(path, Color::Black);
201 continue;
202 }
203 Step::Enter(path) => path,
204 };
205 match colors.get(current) {
206 Some(Color::Black) => continue,
207 Some(Color::Gray) => continue,
211 None => {}
212 }
213 let Some(node) = self.modules.get(current) else {
214 return Err(format!("{} is not in the module graph", current.display()));
215 };
216 if node.is_self_accepting {
217 boundaries.push(self.target(current, current, colors));
218 colors.insert(current, Color::Black);
219 continue;
220 }
221 let accepted_exports = node.accepted_exports.as_ref();
226 if accepted_exports.is_some() {
227 boundaries.push(self.target(current, current, colors));
228 } else if node.importers.is_empty() {
229 return Err(format!(
230 "update reached entry {} with no accepting boundary",
231 current.display()
232 ));
233 }
234 colors.insert(current, Color::Gray);
235 stack.push(Step::Blacken(current));
236 let mut importers: Vec<&Path> = node.importers.iter().map(PathBuf::as_path).collect();
237 importers.sort_unstable();
238 for importer in importers.into_iter().rev() {
239 if self.accepts_dep(importer, current) {
242 boundaries.push(self.target(importer, current, colors));
243 continue;
244 }
245 if let Some(accepted) = accepted_exports {
248 if self.all_imports_accepted(importer, current, accepted) {
249 continue;
250 }
251 }
252 stack.push(Step::Enter(importer));
253 }
254 }
255 Ok(())
256 }
257}
258
259fn sorted<T: Ord>(mut items: Vec<T>) -> Vec<T> {
261 items.sort();
262 items.dedup();
263 items
264}
265
266fn is_css_path(path: &Path) -> bool {
267 matches!(
268 path.extension().and_then(|e| e.to_str()),
269 Some("css" | "scss" | "sass" | "less" | "styl" | "stylus" | "pcss" | "postcss" | "sss")
270 )
271}
272
273#[derive(Clone, Copy, PartialEq)]
274enum Color {
275 Gray,
276 Black,
277}