use std::path::PathBuf;
use super::common::{PackageError, command_exists, run_package_command};
use super::config::{NpmConfig, NpmPackageManager, PackageSpec};
pub struct NpmHandler {
config: NpmConfig,
project_dir: PathBuf,
}
impl NpmHandler {
pub fn new(config: NpmConfig, project_dir: PathBuf) -> Self {
Self {
config,
project_dir,
}
}
pub fn install(&self) -> Result<(), PackageError> {
let pm = self.detect_package_manager();
if !command_exists(pm.command()) {
return Err(PackageError::PackageManagerNotInstalled(
pm.command().to_string(),
));
}
if self.config.from_lockfile {
self.install_from_lockfile(pm)?;
} else if !self.config.packages.is_empty() {
self.install_packages(pm)?;
} else {
println!(" No npm packages configured");
}
Ok(())
}
fn detect_package_manager(&self) -> NpmPackageManager {
if let Some(pm) = self.config.package_manager {
return pm;
}
if self.project_dir.join("pnpm-lock.yaml").exists() {
NpmPackageManager::Pnpm
} else if self.project_dir.join("yarn.lock").exists() {
NpmPackageManager::Yarn
} else {
NpmPackageManager::Npm
}
}
fn install_from_lockfile(&self, pm: NpmPackageManager) -> Result<(), PackageError> {
let args: Vec<&str> = match pm {
NpmPackageManager::Npm => vec!["ci"],
NpmPackageManager::Yarn => vec!["install", "--frozen-lockfile"],
NpmPackageManager::Pnpm => vec!["install", "--frozen-lockfile"],
};
run_package_command(pm.command(), &args, &self.project_dir)
}
fn install_packages(&self, pm: NpmPackageManager) -> Result<(), PackageError> {
let packages: Vec<String> = self
.config
.packages
.iter()
.filter(|(_, spec)| !spec.is_optional())
.map(|(name, spec)| format_package_spec(name, spec))
.collect();
if packages.is_empty() {
println!(" No required npm packages to install");
return Ok(());
}
let install_cmd = match pm {
NpmPackageManager::Npm => "install",
NpmPackageManager::Yarn => "add",
NpmPackageManager::Pnpm => "add",
};
let mut args: Vec<&str> = vec![install_cmd];
args.extend(packages.iter().map(|s| s.as_str()));
run_package_command(pm.command(), &args, &self.project_dir)
}
}
fn format_package_spec(name: &str, spec: &PackageSpec) -> String {
let version = spec.version();
if version == "latest" {
name.to_string()
} else {
format!("{}@{}", name, version)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_package_spec_latest() {
let spec = PackageSpec::Version("latest".to_string());
assert_eq!(format_package_spec("typescript", &spec), "typescript");
}
#[test]
fn test_format_package_spec_version() {
let spec = PackageSpec::Version("^5.0".to_string());
assert_eq!(format_package_spec("typescript", &spec), "typescript@^5.0");
}
#[test]
fn test_detect_package_manager_default() {
let config = NpmConfig::default();
let handler = NpmHandler::new(config, PathBuf::from("/tmp/nonexistent"));
let pm = handler.detect_package_manager();
assert_eq!(pm, NpmPackageManager::Npm);
}
#[test]
fn test_detect_package_manager_explicit() {
let config = NpmConfig {
package_manager: Some(NpmPackageManager::Pnpm),
..Default::default()
};
let handler = NpmHandler::new(config, PathBuf::from("/tmp/nonexistent"));
let pm = handler.detect_package_manager();
assert_eq!(pm, NpmPackageManager::Pnpm);
}
}