use super::install;
use clap::Args;
use miette::{Context, IntoDiagnostic, miette};
use std::collections::{BTreeMap, BTreeSet, HashMap};
#[derive(Debug, Args)]
pub struct DedupeArgs {
#[arg(long)]
pub check: bool,
}
pub async fn run(args: DedupeArgs) -> miette::Result<()> {
let cwd = crate::dirs::project_root()?;
let _lock = super::take_project_lock(&cwd)?;
let manifest = super::load_manifest(&cwd.join("package.json"))?;
let existing = aube_lockfile::parse_lockfile(&cwd, &manifest).ok();
let workspace_packages = aube_workspace::find_workspace_packages(&cwd)
.into_diagnostic()
.wrap_err("failed to discover workspace packages")?;
let is_workspace = !workspace_packages.is_empty();
let mut manifests: Vec<(String, aube_manifest::PackageJson)> =
vec![(".".to_string(), manifest.clone())];
let mut ws_package_versions: HashMap<String, String> = HashMap::new();
if is_workspace {
for pkg_dir in &workspace_packages {
let pkg_manifest = aube_manifest::PackageJson::from_path(&pkg_dir.join("package.json"))
.map_err(miette::Report::new)
.wrap_err_with(|| format!("failed to read {}/package.json", pkg_dir.display()))?;
let rel_path = pkg_dir
.strip_prefix(&cwd)
.unwrap_or(pkg_dir)
.to_string_lossy()
.to_string();
if let Some(name) = &pkg_manifest.name {
let version = pkg_manifest.version.as_deref().unwrap_or("0.0.0");
ws_package_versions.insert(name.clone(), version.to_string());
}
manifests.push((rel_path, pkg_manifest));
}
}
let workspace_catalogs = super::load_workspace_catalogs(&cwd)?;
let mut resolver = super::build_resolver(&cwd, &manifest, workspace_catalogs);
let graph = resolver
.resolve_workspace(&manifests, None, &ws_package_versions)
.await
.map_err(miette::Report::new)
.wrap_err("failed to resolve dependencies")?;
let (removed, added) = diff_graphs(existing.as_ref(), &graph);
if removed.is_empty() && added.is_empty() {
eprintln!(
"Lockfile is already deduped ({} packages)",
graph.packages.len()
);
return Ok(());
}
for dep_path in &removed {
eprintln!(" - {dep_path}");
}
for dep_path in &added {
eprintln!(" + {dep_path}");
}
eprintln!(
"Dedupe: {} removed, {} added (net {} packages)",
removed.len(),
added.len(),
graph.packages.len() as i64 - existing.as_ref().map_or(0, |g| g.packages.len()) as i64,
);
if args.check {
return Err(miette!("dedupe --check: lockfile is not deduped"));
}
super::write_and_log_lockfile(&cwd, &graph, &manifest)?;
install::run(install::InstallOptions::with_mode(
super::chained_frozen_mode(install::FrozenMode::Prefer),
))
.await?;
Ok(())
}
fn diff_graphs(
existing: Option<&aube_lockfile::LockfileGraph>,
new: &aube_lockfile::LockfileGraph,
) -> (Vec<String>, Vec<String>) {
let empty: BTreeMap<String, aube_lockfile::LockedPackage> = BTreeMap::new();
let old_pkgs = existing.map(|g| &g.packages).unwrap_or(&empty);
let old_keys: BTreeSet<&String> = old_pkgs.keys().collect();
let new_keys: BTreeSet<&String> = new.packages.keys().collect();
let removed: Vec<String> = old_keys
.difference(&new_keys)
.map(|s| s.to_string())
.collect();
let added: Vec<String> = new_keys
.difference(&old_keys)
.map(|s| s.to_string())
.collect();
(removed, added)
}