pesde/src/cli/commands/add.rs
2025-01-15 21:23:40 +01:00

271 lines
7.6 KiB
Rust

use std::str::FromStr;
use anyhow::Context;
use clap::Args;
use colored::Colorize;
use semver::VersionReq;
use crate::cli::{config::read_config, AnyPackageIdentifier, VersionedPackageName};
use pesde::{
manifest::target::TargetKind,
names::PackageNames,
source::{
git::{specifier::GitDependencySpecifier, GitPackageSource},
path::{specifier::PathDependencySpecifier, PathPackageSource},
pesde::{specifier::PesdeDependencySpecifier, PesdePackageSource},
specifiers::DependencySpecifiers,
traits::{PackageSource, RefreshOptions, ResolveOptions},
workspace::{specifier::WorkspaceDependencySpecifier, WorkspacePackageSource},
PackageSources,
},
Project, RefreshedSources, DEFAULT_INDEX_NAME,
};
#[derive(Debug, Args)]
pub struct AddCommand {
/// The package name to add
#[arg(index = 1)]
name: AnyPackageIdentifier<VersionReq>,
/// The index in which to search for the package
#[arg(short, long)]
index: Option<String>,
/// The target environment of the package
#[arg(short, long)]
target: Option<TargetKind>,
/// The alias to use for the package
#[arg(short, long)]
alias: Option<String>,
/// Whether to add the package as a peer dependency
#[arg(short, long)]
peer: bool,
/// Whether to add the package as a dev dependency
#[arg(short, long, conflicts_with = "peer")]
dev: bool,
}
impl AddCommand {
pub async fn run(self, project: Project) -> anyhow::Result<()> {
let manifest = project
.deser_manifest()
.await
.context("failed to read manifest")?;
let (source, specifier) = match &self.name {
AnyPackageIdentifier::PackageName(versioned) => match &versioned {
VersionedPackageName(PackageNames::Pesde(name), version) => {
let index = manifest
.indices
.get(self.index.as_deref().unwrap_or(DEFAULT_INDEX_NAME))
.cloned();
if let Some(index) = self.index.as_ref().filter(|_| index.is_none()) {
println!("{}: index {index} not found", "error".red().bold());
return Ok(());
}
let index = match index {
Some(index) => index,
None => read_config().await?.default_index,
};
let source = PackageSources::Pesde(PesdePackageSource::new(index));
let specifier = DependencySpecifiers::Pesde(PesdeDependencySpecifier {
name: name.clone(),
version: version.clone().unwrap_or(VersionReq::STAR),
index: self.index,
target: self.target,
});
(source, specifier)
}
#[cfg(feature = "wally-compat")]
VersionedPackageName(PackageNames::Wally(name), version) => {
let index = manifest
.wally_indices
.get(self.index.as_deref().unwrap_or(DEFAULT_INDEX_NAME))
.cloned();
if let Some(index) = self.index.as_ref().filter(|_| index.is_none()) {
println!("{}: wally index {index} not found", "error".red().bold());
return Ok(());
}
let index = index.context("no wally index found")?;
let source =
PackageSources::Wally(pesde::source::wally::WallyPackageSource::new(index));
let specifier = DependencySpecifiers::Wally(
pesde::source::wally::specifier::WallyDependencySpecifier {
name: name.clone(),
version: version.clone().unwrap_or(VersionReq::STAR),
index: self.index,
},
);
(source, specifier)
}
},
AnyPackageIdentifier::Url((url, rev)) => (
PackageSources::Git(GitPackageSource::new(url.clone())),
DependencySpecifiers::Git(GitDependencySpecifier {
repo: url.clone(),
rev: rev.to_string(),
path: None,
}),
),
AnyPackageIdentifier::Workspace(VersionedPackageName(name, version)) => (
PackageSources::Workspace(WorkspacePackageSource),
DependencySpecifiers::Workspace(WorkspaceDependencySpecifier {
name: name.clone(),
version: version.clone().unwrap_or_default(),
target: self.target,
}),
),
AnyPackageIdentifier::Path(path) => (
PackageSources::Path(PathPackageSource),
DependencySpecifiers::Path(PathDependencySpecifier { path: path.clone() }),
),
};
let refreshed_sources = RefreshedSources::new();
refreshed_sources
.refresh(
&source,
&RefreshOptions {
project: project.clone(),
},
)
.await
.context("failed to refresh package source")?;
let Some(version_id) = source
.resolve(
&specifier,
&ResolveOptions {
project: project.clone(),
target: manifest.target.kind(),
refreshed_sources,
},
)
.await
.context("failed to resolve package")?
.1
.pop_last()
.map(|(v_id, _)| v_id)
else {
println!("{}: no versions found for package", "error".red().bold());
return Ok(());
};
let project_target = manifest.target.kind();
let mut manifest = toml_edit::DocumentMut::from_str(
&project
.read_manifest()
.await
.context("failed to read manifest")?,
)
.context("failed to parse manifest")?;
let dependency_key = if self.peer {
"peer_dependencies"
} else if self.dev {
"dev_dependencies"
} else {
"dependencies"
};
let alias = self.alias.unwrap_or_else(|| match &self.name {
AnyPackageIdentifier::PackageName(versioned) => versioned.0.name().to_string(),
AnyPackageIdentifier::Url((url, _)) => url
.path
.to_string()
.split('/')
.next_back()
.map(|s| s.to_string())
.unwrap_or(url.path.to_string()),
AnyPackageIdentifier::Workspace(versioned) => versioned.0.name().to_string(),
AnyPackageIdentifier::Path(path) => path
.file_name()
.map(|s| s.to_string_lossy().to_string())
.expect("path has no file name"),
});
let field = &mut manifest[dependency_key]
.or_insert(toml_edit::Item::Table(toml_edit::Table::new()))[&alias];
match specifier {
DependencySpecifiers::Pesde(spec) => {
field["name"] = toml_edit::value(spec.name.clone().to_string());
field["version"] = toml_edit::value(format!("^{}", version_id.version()));
if *version_id.target() != project_target {
field["target"] = toml_edit::value(version_id.target().to_string());
}
if let Some(index) = spec.index.filter(|i| i != DEFAULT_INDEX_NAME) {
field["index"] = toml_edit::value(index);
}
println!(
"added {}@{} {} to {dependency_key}",
spec.name,
version_id.version(),
version_id.target()
);
}
#[cfg(feature = "wally-compat")]
DependencySpecifiers::Wally(spec) => {
let name_str = spec.name.to_string();
let name_str = name_str.trim_start_matches("wally#");
field["wally"] = toml_edit::value(name_str);
field["version"] = toml_edit::value(format!("^{}", version_id.version()));
if let Some(index) = spec.index.filter(|i| i != DEFAULT_INDEX_NAME) {
field["index"] = toml_edit::value(index);
}
println!(
"added wally {name_str}@{} to {dependency_key}",
version_id.version()
);
}
DependencySpecifiers::Git(spec) => {
field["repo"] = toml_edit::value(spec.repo.to_bstring().to_string());
field["rev"] = toml_edit::value(spec.rev.clone());
println!("added git {}#{} to {dependency_key}", spec.repo, spec.rev);
}
DependencySpecifiers::Workspace(spec) => {
field["workspace"] = toml_edit::value(spec.name.clone().to_string());
if let AnyPackageIdentifier::Workspace(versioned) = self.name {
if let Some(version) = versioned.1 {
field["version"] = toml_edit::value(version.to_string());
}
}
println!(
"added workspace {}@{} to {dependency_key}",
spec.name, spec.version
);
}
DependencySpecifiers::Path(spec) => {
field["path"] = toml_edit::value(spec.path.to_string_lossy().to_string());
println!("added path {} to {dependency_key}", spec.path.display());
}
}
project
.write_manifest(manifest.to_string())
.await
.context("failed to write manifest")?;
Ok(())
}
}