use std::collections::BTreeSet;
use std::path::Path;
use anyhow::Result;
use crate::git::{ChangeKind, ChangedFile, file_at};
use crate::graph::{DependencyGraph, Node, normalize_path};
use crate::parser::{MODULE_INIT, ParsedModule, is_source_file, parse_module};
use crate::workspace::{Package, package_for_path};
#[derive(Clone, Debug, Default)]
pub struct ChangeSeeds {
pub nodes: BTreeSet<Node>,
pub direct_packages: BTreeSet<String>,
}
pub fn find_change_seeds(
root: &Path,
base: &str,
changes: &[ChangedFile],
packages: &[Package],
graph: &DependencyGraph,
) -> Result<ChangeSeeds> {
let mut result = ChangeSeeds::default();
for change in changes {
if !is_source_file(&change.path) {
if let Some(package) = package_for_path(packages, &change.path) {
let relative = change
.path
.strip_prefix(&package.dir)
.unwrap_or(&change.path);
let file_name = change
.path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or_default();
let is_rust_source = change
.path
.extension()
.is_some_and(|extension| extension == "rs")
&& relative
.components()
.any(|component| component.as_os_str() == "src");
let is_build_input = matches!(
file_name,
"package.json"
| "wrangler.json"
| "wrangler.jsonc"
| "tsconfig.json"
| "Cargo.toml"
| "build.rs"
) || file_name.starts_with("vite.config.");
if is_rust_source || is_build_input {
result.direct_packages.insert(package.name.clone());
}
}
continue;
}
if let Some(package) = package_for_path(packages, &change.path) {
let target_node = DependencyGraph::target_node(&package.name);
let relative = change
.path
.strip_prefix(&package.dir)
.unwrap_or(&change.path);
let is_runtime_source = relative
.components()
.any(|component| component.as_os_str() == "src")
|| package.entrypoint.as_ref() == Some(&change.path);
let is_unlinked_target = graph
.targets
.iter()
.any(|target| target.package == package.name)
&& !graph.edges.contains_key(&target_node);
if is_runtime_source && is_unlinked_target {
result.direct_packages.insert(package.name.clone());
}
}
let current_path = normalize_path(&change.path);
let current = graph.modules.get(¤t_path);
let old_relative = match &change.kind {
ChangeKind::Renamed { old_path } => old_path.as_path(),
_ => change.path.strip_prefix(root).unwrap_or(&change.path),
};
let old_path = root.join(old_relative);
let old_source = match change.kind {
ChangeKind::Added => None,
_ => file_at(root, base, &old_path)?,
};
let old = old_source
.as_deref()
.map(|source| parse_module(&old_path, source))
.transpose()?;
match (old.as_ref(), current) {
(None, Some(current)) => {
add_all_runtime_symbols(&mut result.nodes, ¤t_path, current)
}
(Some(old), Some(current)) => {
add_changed_symbols(&mut result.nodes, ¤t_path, old, current);
}
(Some(_), None) => {
if let Some(package) = package_for_path(packages, &change.path) {
result.direct_packages.insert(package.name.clone());
}
}
(None, None) => {}
}
}
Ok(result)
}
fn add_changed_symbols(
seeds: &mut BTreeSet<Node>,
path: &Path,
old: &ParsedModule,
current: &ParsedModule,
) {
let removed_runtime_symbol = old.symbols.iter().any(|(name, old_symbol)| {
name.as_str() != MODULE_INIT && old_symbol.runtime && !current.symbols.contains_key(name)
});
for (name, current_symbol) in ¤t.symbols {
if !current_symbol.runtime {
continue;
}
let changed = old
.symbols
.get(name)
.is_none_or(|old_symbol| old_symbol.fingerprint != current_symbol.fingerprint);
if changed || (removed_runtime_symbol && name.as_str() != MODULE_INIT) {
seeds.insert(Node::new(path, name));
}
}
}
fn add_all_runtime_symbols(seeds: &mut BTreeSet<Node>, path: &Path, module: &ParsedModule) {
for (name, symbol) in &module.symbols {
if symbol.runtime {
seeds.insert(Node::new(path, name));
}
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::*;
#[test]
fn seeds_only_changed_declaration() {
let old = parse_module(
Path::new("shared.ts"),
"export const used = 1; export const untouched = 2;",
)
.unwrap();
let current = parse_module(
Path::new("shared.ts"),
"export const used = 3; export const untouched = 2;",
)
.unwrap();
let mut seeds = BTreeSet::new();
add_changed_symbols(&mut seeds, Path::new("shared.ts"), &old, ¤t);
assert!(seeds.contains(&Node::new("shared.ts", "used")));
assert!(!seeds.contains(&Node::new("shared.ts", "untouched")));
}
}