use crate::config::DependencyConfig;
use crate::error::{VersionError, VersionResult};
use crate::types::dependency::{is_local_protocol, is_workspace_protocol};
use crate::types::{
DependencyType, DependencyUpdate, PackageInfo, UpdateReason, Version, VersionBump,
};
use crate::version::DependencyGraph;
use crate::version::resolution::{PackageUpdate, VersionResolution};
use std::collections::{HashMap, HashSet};
#[derive(Debug)]
pub struct DependencyPropagator<'a> {
graph: &'a DependencyGraph,
packages: &'a HashMap<String, PackageInfo>,
config: &'a DependencyConfig,
}
impl<'a> DependencyPropagator<'a> {
#[must_use]
pub fn new(
graph: &'a DependencyGraph,
packages: &'a HashMap<String, PackageInfo>,
config: &'a DependencyConfig,
) -> Self {
Self { graph, packages, config }
}
pub fn propagate(&self, resolution: &mut VersionResolution) -> VersionResult<()> {
let mut updated_packages: HashMap<String, Version> = HashMap::new();
for update in &resolution.updates {
updated_packages.insert(update.name.clone(), update.next_version.clone());
}
let mut processed: HashSet<String> = HashSet::new();
for update in &resolution.updates {
processed.insert(update.name.clone());
}
let mut current_depth = 0;
let mut current_level: Vec<String> =
resolution.updates.iter().map(|u| u.name.clone()).collect();
while !current_level.is_empty() && current_depth < self.config.max_depth {
let mut next_level: Vec<String> = Vec::new();
for package_name in ¤t_level {
let dependents = self.graph.dependents(package_name);
for dependent_name in dependents {
if processed.contains(&dependent_name) {
continue;
}
let dependent_pkg = self.packages.get(&dependent_name).ok_or_else(|| {
VersionError::PackageNotFound {
name: dependent_name.clone(),
workspace_root: std::path::PathBuf::new(),
}
})?;
if !self.should_propagate(dependent_pkg, package_name) {
continue;
}
let current_version = dependent_pkg.version();
let propagation_bump = self.parse_propagation_bump()?;
let next_version = current_version.bump(propagation_bump)?;
let update = PackageUpdate::new(
dependent_name.clone(),
dependent_pkg.path().to_path_buf(),
current_version,
next_version.clone(),
UpdateReason::DependencyPropagation {
triggered_by: package_name.clone(),
depth: current_depth + 1,
},
);
updated_packages.insert(dependent_name.clone(), next_version);
processed.insert(dependent_name.clone());
next_level.push(dependent_name);
resolution.add_update(update);
}
}
current_level = next_level;
current_depth += 1;
}
self.update_dependency_specs(resolution, &updated_packages)?;
Ok(())
}
fn should_propagate(&self, dependent_pkg: &PackageInfo, dependency_name: &str) -> bool {
let all_deps = dependent_pkg.all_dependencies();
for (dep_name, version_spec, dep_type) in all_deps {
if dep_name != dependency_name {
continue;
}
let type_enabled = match dep_type {
DependencyType::Regular => self.config.propagate_dependencies,
DependencyType::Dev => self.config.propagate_dev_dependencies,
DependencyType::Peer => self.config.propagate_peer_dependencies,
DependencyType::Optional => false, };
if !type_enabled {
return false;
}
if self.should_skip_version_spec(&version_spec) {
return false;
}
return true;
}
false
}
fn should_skip_version_spec(&self, version_spec: &str) -> bool {
if self.config.skip_workspace_protocol && is_workspace_protocol(version_spec) {
return true;
}
if is_local_protocol(version_spec) {
if self.config.skip_file_protocol && version_spec.starts_with("file:") {
return true;
}
if self.config.skip_link_protocol && version_spec.starts_with("link:") {
return true;
}
if self.config.skip_portal_protocol && version_spec.starts_with("portal:") {
return true;
}
}
false
}
fn parse_propagation_bump(&self) -> VersionResult<VersionBump> {
match self.config.propagation_bump.as_str() {
"major" => Ok(VersionBump::Major),
"minor" => Ok(VersionBump::Minor),
"patch" => Ok(VersionBump::Patch),
"none" => Ok(VersionBump::None),
_ => Err(VersionError::InvalidBumpType {
bump_type: self.config.propagation_bump.clone(),
}),
}
}
fn update_dependency_specs(
&self,
resolution: &mut VersionResolution,
updated_packages: &HashMap<String, Version>,
) -> VersionResult<()> {
let update_count = resolution.updates.len();
for i in 0..update_count {
let update_name = resolution.updates[i].name.clone();
let pkg =
self.packages.get(&update_name).ok_or_else(|| VersionError::PackageNotFound {
name: update_name.clone(),
workspace_root: std::path::PathBuf::new(),
})?;
let all_deps = pkg.all_dependencies();
let mut dep_updates: Vec<DependencyUpdate> = Vec::new();
for (dep_name, old_spec, dep_type) in all_deps {
if let Some(new_version) = updated_packages.get(&dep_name) {
if self.should_skip_version_spec(&old_spec) {
continue;
}
let new_spec = self.calculate_new_version_spec(&old_spec, new_version);
if new_spec != old_spec {
dep_updates
.push(DependencyUpdate::new(dep_name, dep_type, old_spec, new_spec));
}
}
}
for dep_update in dep_updates {
resolution.updates[i].add_dependency_update(dep_update);
}
}
Ok(())
}
fn calculate_new_version_spec(&self, old_spec: &str, new_version: &Version) -> String {
let trimmed = old_spec.trim();
if trimmed.starts_with('^') {
format!("^{}", new_version)
} else if trimmed.starts_with('~') {
format!("~{}", new_version)
} else if trimmed.starts_with(">=") {
format!(">={}", new_version)
} else if trimmed.starts_with('>') {
format!(">{}", new_version)
} else if trimmed.starts_with("<=") {
format!("<={}", new_version)
} else if trimmed.starts_with('<') {
format!("<{}", new_version)
} else if trimmed.starts_with('=') {
format!("={}", new_version)
} else {
new_version.to_string()
}
}
}