nestrs-cli-rs 0.1.0

Rust port of the Nest CLI for the nestrs organization.
Documentation
//! Package manager detection and install command construction.

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(" ")
}