use crate::error::{Error, MonorepoError, Result};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
pub enum PackageManagerKind {
Npm,
Yarn,
Pnpm,
Bun,
Jsr,
}
impl PackageManagerKind {
#[must_use]
pub fn command(&self) -> &'static str {
match self {
Self::Npm => "npm",
Self::Yarn => "yarn",
Self::Pnpm => "pnpm",
Self::Bun => "bun",
Self::Jsr => "jsr",
}
}
#[must_use]
pub fn lock_file(&self) -> &'static str {
match self {
Self::Npm => "package-lock.json",
Self::Yarn => "yarn.lock",
Self::Pnpm => "pnpm-lock.yaml",
Self::Bun => "bun.lockb",
Self::Jsr => "jsr.json",
}
}
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::Npm => "npm",
Self::Yarn => "yarn",
Self::Pnpm => "pnpm",
Self::Bun => "bun",
Self::Jsr => "jsr",
}
}
#[must_use]
pub fn supports_workspaces(&self) -> bool {
match self {
Self::Npm | Self::Yarn | Self::Pnpm | Self::Bun => true,
Self::Jsr => false,
}
}
#[must_use]
pub fn workspace_config_file(&self) -> Option<&'static str> {
match self {
Self::Npm | Self::Yarn | Self::Bun | Self::Jsr => None, Self::Pnpm => Some("pnpm-workspace.yaml"),
}
}
}
impl<'de> serde::Deserialize<'de> for PackageManagerKind {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.to_lowercase().as_str() {
"npm" => Ok(PackageManagerKind::Npm),
"yarn" => Ok(PackageManagerKind::Yarn),
"pnpm" => Ok(PackageManagerKind::Pnpm),
"bun" => Ok(PackageManagerKind::Bun),
"jsr" => Ok(PackageManagerKind::Jsr),
_ => Err(serde::de::Error::unknown_variant(&s, &["npm", "yarn", "pnpm", "bun", "jsr"])),
}
}
}
#[derive(Debug, Clone)]
pub struct PackageManager {
pub(crate) kind: PackageManagerKind,
pub(crate) root: PathBuf,
}
impl PackageManager {
#[must_use]
pub fn new(kind: PackageManagerKind, root: impl Into<PathBuf>) -> Self {
Self { kind, root: root.into() }
}
pub fn detect(path: impl AsRef<Path>) -> Result<Self> {
let default_config = crate::config::PackageManagerConfig::default();
Self::detect_with_config(path, &default_config)
}
pub fn detect_with_config(
path: impl AsRef<Path>,
config: &crate::config::PackageManagerConfig,
) -> Result<Self> {
let path = path.as_ref();
if config.detect_from_env
&& let Ok(env_manager) = std::env::var(&config.env_var_name)
{
let kind = match env_manager.to_lowercase().as_str() {
"npm" => Some(PackageManagerKind::Npm),
"yarn" => Some(PackageManagerKind::Yarn),
"pnpm" => Some(PackageManagerKind::Pnpm),
"bun" => Some(PackageManagerKind::Bun),
"jsr" => Some(PackageManagerKind::Jsr),
_ => None,
};
if let Some(kind) = kind {
let lock_file = if let Some(custom_lock) = config.custom_lock_files.get(&kind) {
custom_lock.as_str()
} else {
kind.lock_file()
};
if path.join(lock_file).exists() {
return Ok(Self::new(kind, path));
}
}
}
for &kind in &config.detection_order {
let lock_file = if let Some(custom_lock) = config.custom_lock_files.get(&kind) {
custom_lock.as_str()
} else {
kind.lock_file()
};
if kind == PackageManagerKind::Npm {
if path.join(lock_file).exists() || path.join("npm-shrinkwrap.json").exists() {
return Ok(Self::new(kind, path));
}
} else if path.join(lock_file).exists() {
return Ok(Self::new(kind, path));
}
}
if let Some(fallback_kind) = config.fallback {
let fallback_lock =
if let Some(custom_lock) = config.custom_lock_files.get(&fallback_kind) {
custom_lock.as_str()
} else {
fallback_kind.lock_file()
};
if path.join(fallback_lock).exists() {
return Ok(Self::new(fallback_kind, path));
}
}
Err(Error::Monorepo(MonorepoError::ManagerNotFound))
}
#[must_use]
pub fn kind(&self) -> PackageManagerKind {
self.kind
}
#[must_use]
pub fn root(&self) -> &Path {
&self.root
}
#[must_use]
pub fn command(&self) -> &'static str {
self.kind.command()
}
#[must_use]
pub fn lock_file(&self) -> &'static str {
self.kind.lock_file()
}
#[must_use]
pub fn lock_file_path(&self) -> PathBuf {
let lock_file = if self.kind == PackageManagerKind::Npm
&& !self.root.join(PackageManagerKind::Npm.lock_file()).exists()
&& self.root.join("npm-shrinkwrap.json").exists()
{
"npm-shrinkwrap.json"
} else {
self.kind.lock_file()
};
self.root.join(lock_file)
}
#[must_use]
pub fn supports_workspaces(&self) -> bool {
self.kind.supports_workspaces()
}
#[must_use]
pub fn workspace_config_path(&self) -> Option<PathBuf> {
self.kind.workspace_config_file().map(|file| self.root.join(file))
}
}