use std::fmt;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use crate::runners::{ProcessRunner, Runner, RunnerCommand, RunnerFactory, RunnerKind};
pub mod abstract_package_manager;
pub mod npm_package_manager;
pub mod package_manager;
pub mod package_manager_commands;
pub mod package_manager_factory;
pub mod pnpm_package_manager;
pub mod project_dependency;
pub mod yarn_package_manager;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub enum PackageManager {
Npm,
Yarn,
Pnpm,
}
impl PackageManager {
pub const fn as_str(self) -> &'static str {
match self {
Self::Npm => "npm",
Self::Yarn => "yarn",
Self::Pnpm => "pnpm",
}
}
pub const fn display_name(self) -> &'static str {
match self {
Self::Npm => "NPM",
Self::Yarn => "YARN",
Self::Pnpm => "PNPM",
}
}
pub const fn runner_kind(self) -> RunnerKind {
match self {
Self::Npm => RunnerKind::Npm,
Self::Yarn => RunnerKind::Yarn,
Self::Pnpm => RunnerKind::Pnpm,
}
}
pub const fn commands(self) -> PackageManagerCommands {
match self {
Self::Npm => PackageManagerCommands {
install: "install",
add: "install",
update: "update",
remove: "uninstall",
save_flag: "--save",
save_dev_flag: "--save-dev",
silent_flag: "--silent",
},
Self::Yarn => PackageManagerCommands {
install: "install",
add: "add",
update: "upgrade",
remove: "remove",
save_flag: "",
save_dev_flag: "-D",
silent_flag: "--silent",
},
Self::Pnpm => PackageManagerCommands {
install: "install --strict-peer-dependencies=false",
add: "install --strict-peer-dependencies=false",
update: "update",
remove: "uninstall",
save_flag: "--save",
save_dev_flag: "--save-dev",
silent_flag: "--reporter=silent",
},
}
}
}
impl fmt::Display for PackageManager {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for PackageManager {
type Err = PackageManagerError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"npm" => Ok(Self::Npm),
"yarn" => Ok(Self::Yarn),
"pnpm" => Ok(Self::Pnpm),
_ => Err(PackageManagerError::Unsupported(value.to_owned())),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PackageManagerCommands {
pub install: &'static str,
pub add: &'static str,
pub update: &'static str,
pub remove: &'static str,
pub save_flag: &'static str,
pub save_dev_flag: &'static str,
pub silent_flag: &'static str,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ProjectDependency {
pub name: String,
pub version: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PackageManagerClient {
manager: PackageManager,
runner: ProcessRunner,
}
impl PackageManagerClient {
pub fn new(manager: PackageManager) -> Self {
Self {
manager,
runner: RunnerFactory::create(manager.runner_kind()),
}
}
pub const fn manager(&self) -> PackageManager {
self.manager
}
pub const fn name(&self) -> &'static str {
self.manager.display_name()
}
pub const fn commands(&self) -> PackageManagerCommands {
self.manager.commands()
}
pub fn install_command(&self, project_directory: impl Into<PathBuf>) -> RunnerCommand {
let cli = self.commands();
let command = join_non_empty([cli.install, cli.silent_flag]);
self.runner
.describe(command, true, Some(project_directory.into()))
}
pub fn version_command(&self) -> RunnerCommand {
self.runner.describe("--version", true, None)
}
pub fn add_production_command(&self, dependencies: &[&str], tag: &str) -> RunnerCommand {
let cli = self.commands();
let command = join_non_empty([cli.add, cli.save_flag]);
let args = dependencies_with_tag(dependencies, tag);
self.runner
.describe(format!("{command} {args}"), true, None)
}
pub fn add_development_command(&self, dependencies: &[&str], tag: &str) -> RunnerCommand {
let cli = self.commands();
let args = dependencies_with_tag(dependencies, tag);
self.runner.describe(
format!("{} {} {}", cli.add, cli.save_dev_flag, args),
true,
None,
)
}
pub fn update_production_command(&self, dependencies: &[&str]) -> RunnerCommand {
self.update_command(dependencies)
}
pub fn update_development_command(&self, dependencies: &[&str]) -> RunnerCommand {
self.update_command(dependencies)
}
pub fn delete_production_command(&self, dependencies: &[&str]) -> RunnerCommand {
let cli = self.commands();
let command = join_non_empty([cli.remove, cli.save_flag]);
self.runner
.describe(format!("{command} {}", dependencies.join(" ")), true, None)
}
pub fn delete_development_command(&self, dependencies: &[&str]) -> RunnerCommand {
let cli = self.commands();
self.runner.describe(
format!(
"{} {} {}",
cli.remove,
cli.save_dev_flag,
dependencies.join(" ")
),
true,
None,
)
}
pub fn raw_full_command(&self, command: impl AsRef<str>) -> String {
self.runner.raw_full_command(command)
}
fn update_command(&self, dependencies: &[&str]) -> RunnerCommand {
self.runner.describe(
format!("{} {}", self.commands().update, dependencies.join(" ")),
true,
None,
)
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct PackageManagerFactory;
impl PackageManagerFactory {
pub fn create(name: impl AsRef<str>) -> Result<PackageManagerClient, PackageManagerError> {
Ok(PackageManagerClient::new(name.as_ref().parse()?))
}
pub fn create_manager(manager: PackageManager) -> PackageManagerClient {
PackageManagerClient::new(manager)
}
pub fn find_in_dir(directory: impl AsRef<Path>) -> PackageManagerClient {
let manager = detect_package_manager(directory).unwrap_or(PackageManager::Npm);
Self::create_manager(manager)
}
}
pub fn detect_package_manager(directory: impl AsRef<Path>) -> io::Result<PackageManager> {
let entries = fs::read_dir(directory)?;
let mut has_yarn_lock_file = false;
let mut has_pnpm_lock_file = false;
for entry in entries {
let file_name = entry?.file_name();
if file_name == "yarn.lock" {
has_yarn_lock_file = true;
} else if file_name == "pnpm-lock.yaml" {
has_pnpm_lock_file = true;
}
}
if has_yarn_lock_file {
Ok(PackageManager::Yarn)
} else if has_pnpm_lock_file {
Ok(PackageManager::Pnpm)
} else {
Ok(PackageManager::Npm)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PackageManagerError {
Unsupported(String),
}
impl fmt::Display for PackageManagerError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unsupported(name) => write!(formatter, "Package manager {name} is not managed."),
}
}
}
impl std::error::Error for PackageManagerError {}
fn dependencies_with_tag(dependencies: &[&str], tag: &str) -> String {
dependencies
.iter()
.map(|dependency| format!("{dependency}@{tag}"))
.collect::<Vec<_>>()
.join(" ")
}
fn join_non_empty<const N: usize>(parts: [&str; N]) -> String {
parts
.into_iter()
.filter(|part| !part.is_empty())
.collect::<Vec<_>>()
.join(" ")
}