use crate::error::UpgradeError;
use crate::types::DependencyType;
use crate::upgrade::UpgradeSelection;
use crate::upgrade::detection::{DependencyUpgrade, PackageUpgrades};
use crate::upgrade::registry::UpgradeType;
use chrono::Utc;
use package_json::PackageJson;
use std::collections::HashSet;
use std::path::PathBuf;
use sublime_standard_tools::filesystem::AsyncFileSystem;
use super::result::{AppliedUpgrade, ApplySummary, UpgradeResult};
use crate::error::UpgradeResult as ErrorResult;
pub async fn apply_upgrades<F: AsyncFileSystem>(
available_upgrades: Vec<PackageUpgrades>,
selection: UpgradeSelection,
dry_run: bool,
fs: &F,
) -> ErrorResult<UpgradeResult> {
let filtered = filter_upgrades(available_upgrades, &selection);
if filtered.is_empty() {
let summary = ApplySummary::new();
return Ok(if dry_run {
UpgradeResult::dry_run(vec![], summary)
} else {
UpgradeResult::applied(vec![], vec![], None, None, summary)
});
}
let mut applied_upgrades = Vec::new();
let mut modified_files = Vec::new();
let mut packages_modified = HashSet::new();
for package_upgrades in filtered {
let package_path = package_upgrades.package_path.clone();
match apply_package_upgrades(package_upgrades, dry_run, fs).await {
Ok(package_result) => {
if !package_result.applied.is_empty() {
packages_modified.insert(package_result.package_path.clone());
applied_upgrades.extend(package_result.applied);
if !dry_run {
modified_files.push(package_result.package_path);
}
}
}
Err(e) => {
eprintln!("Failed to apply upgrades to {}: {}", package_path.display(), e);
}
}
}
let summary = build_summary(&applied_upgrades, packages_modified.len());
Ok(if dry_run {
UpgradeResult::dry_run(applied_upgrades, summary)
} else {
UpgradeResult::applied(applied_upgrades, modified_files, None, None, summary)
})
}
fn filter_upgrades(
available: Vec<PackageUpgrades>,
selection: &UpgradeSelection,
) -> Vec<PackageUpgrades> {
available
.into_iter()
.filter_map(|package| {
if !selection.matches_package(&package.package_name) {
return None;
}
let filtered_upgrades: Vec<DependencyUpgrade> = package
.upgrades
.into_iter()
.filter(|upgrade| {
if !selection.matches_type(upgrade.upgrade_type) {
return false;
}
if !selection.matches_dependency(&upgrade.name) {
return false;
}
true
})
.collect();
if filtered_upgrades.is_empty() {
None
} else {
Some(PackageUpgrades {
package_name: package.package_name,
package_path: package.package_path,
current_version: package.current_version,
upgrades: filtered_upgrades,
})
}
})
.collect()
}
struct PackageApplyResult {
package_path: PathBuf,
applied: Vec<AppliedUpgrade>,
}
async fn apply_package_upgrades<F: AsyncFileSystem>(
package: PackageUpgrades,
dry_run: bool,
fs: &F,
) -> ErrorResult<PackageApplyResult> {
let package_json_path = package.package_path.join("package.json");
let content = fs.read_file_string(package_json_path.as_path()).await.map_err(|e| {
UpgradeError::FileSystemError { path: package_json_path.clone(), reason: e.to_string() }
})?;
let mut pkg_json: PackageJson = serde_json::from_str(&content).map_err(|e| {
UpgradeError::PackageJsonError { path: package_json_path.clone(), reason: e.to_string() }
})?;
let mut applied = Vec::new();
for upgrade in package.upgrades {
if apply_single_upgrade(&mut pkg_json, &upgrade) {
applied.push(AppliedUpgrade {
package_path: package.package_path.clone(),
dependency_name: upgrade.name,
dependency_type: upgrade.dependency_type,
old_version: upgrade.current_version,
new_version: upgrade.latest_version,
upgrade_type: upgrade.upgrade_type,
});
}
}
if !dry_run && !applied.is_empty() {
let updated_content = serialize_package_json(&pkg_json, &content)?;
fs.write_file(package_json_path.as_path(), updated_content.as_bytes()).await.map_err(
|e| UpgradeError::ApplyFailed {
path: package_json_path.clone(),
reason: e.to_string(),
},
)?;
}
Ok(PackageApplyResult { package_path: package.package_path, applied })
}
fn apply_single_upgrade(pkg_json: &mut PackageJson, upgrade: &DependencyUpgrade) -> bool {
let deps = match upgrade.dependency_type {
DependencyType::Regular => &mut pkg_json.dependencies,
DependencyType::Dev => &mut pkg_json.dev_dependencies,
DependencyType::Peer => &mut pkg_json.peer_dependencies,
DependencyType::Optional => &mut pkg_json.optional_dependencies,
};
if let Some(deps_map) = deps
&& let Some(version) = deps_map.get_mut(&upgrade.name)
{
let new_spec = preserve_version_prefix(&upgrade.current_version, &upgrade.latest_version);
*version = new_spec;
return true;
}
false
}
pub fn preserve_version_prefix(old_version: &str, new_version: &str) -> String {
let old_trimmed = old_version.trim();
if old_trimmed.starts_with('^') {
format!("^{}", new_version)
} else if old_trimmed.starts_with('~') {
format!("~{}", new_version)
} else if old_trimmed.starts_with('=') {
format!("={}", new_version)
} else if old_trimmed.starts_with(">=") {
format!(">={}", new_version)
} else if old_trimmed.starts_with('>') {
format!(">{}", new_version)
} else {
new_version.to_string()
}
}
fn serialize_package_json(pkg_json: &PackageJson, original_content: &str) -> ErrorResult<String> {
let indent = detect_indentation(original_content);
let mut serialized = if indent.contains('\t') {
serde_json::to_string_pretty(pkg_json)
.map_err(|e| UpgradeError::PackageJsonError {
path: PathBuf::from(""),
reason: e.to_string(),
})?
.replace(" ", "\t")
} else {
let space_count = indent.len();
let pretty = serde_json::to_string_pretty(pkg_json).map_err(|e| {
UpgradeError::PackageJsonError { path: PathBuf::from(""), reason: e.to_string() }
})?;
if space_count == 2 {
pretty
} else {
let target_indent = " ".repeat(space_count);
pretty
.lines()
.map(|line| {
let leading_spaces = line.len() - line.trim_start().len();
if leading_spaces > 0 {
let indent_level = leading_spaces / 2;
format!("{}{}", target_indent.repeat(indent_level), line.trim_start())
} else {
line.to_string()
}
})
.collect::<Vec<_>>()
.join("\n")
}
};
if original_content.ends_with('\n') && !serialized.ends_with('\n') {
serialized.push('\n');
}
Ok(serialized)
}
pub(crate) fn detect_indentation(content: &str) -> String {
for line in content.lines() {
let trimmed = line.trim_start();
if !trimmed.is_empty() && line.len() > trimmed.len() {
let indent = &line[0..line.len() - trimmed.len()];
if indent.contains('\t') {
return "\t".to_string();
} else if !indent.is_empty() {
let space_count = indent.len();
return " ".repeat(space_count);
}
}
}
" ".to_string()
}
fn build_summary(applied: &[AppliedUpgrade], packages_modified: usize) -> ApplySummary {
let mut major_count = 0;
let mut minor_count = 0;
let mut patch_count = 0;
for upgrade in applied {
match upgrade.upgrade_type {
UpgradeType::Major => major_count += 1,
UpgradeType::Minor => minor_count += 1,
UpgradeType::Patch => patch_count += 1,
}
}
ApplySummary {
packages_modified,
dependencies_upgraded: applied.len(),
direct_updates: applied.len(), propagated_updates: 0, dependency_updates: applied.len(),
major_upgrades: major_count,
minor_upgrades: minor_count,
patch_upgrades: patch_count,
applied_at: Utc::now(),
}
}