use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum PackageManager {
Npm,
Yarn,
Pnpm,
Bun,
Cargo,
Deno,
Uv,
Poetry,
Pipenv,
Go,
Bundler,
Composer,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum TaskRunner {
Turbo,
Nx,
Make,
Just,
GoTask,
Mise,
}
#[derive(Debug, Clone)]
pub(crate) struct Task {
pub name: String,
pub source: TaskSource,
pub description: Option<String>,
pub alias_of: Option<String>,
pub passthrough_to_turbo: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum TaskSource {
PackageJson,
Makefile,
Justfile,
Taskfile,
TurboJson,
DenoJson,
CargoAliases,
}
#[derive(Debug, Clone)]
pub(crate) struct NodeVersion {
pub expected: String,
pub source: &'static str,
}
#[derive(Debug, Clone)]
pub(crate) struct DetectionWarning {
pub source: &'static str,
pub detail: String,
}
pub(crate) struct ProjectContext {
pub root: PathBuf,
pub package_managers: Vec<PackageManager>,
pub task_runners: Vec<TaskRunner>,
pub tasks: Vec<Task>,
pub node_version: Option<NodeVersion>,
pub current_node: Option<String>,
pub is_monorepo: bool,
pub warnings: Vec<DetectionWarning>,
}
impl ProjectContext {
pub(crate) fn primary_node_pm(&self) -> Option<PackageManager> {
self.package_managers
.iter()
.copied()
.find(|pm| pm.is_node())
}
pub(crate) fn primary_pm(&self) -> Option<PackageManager> {
self.package_managers.first().copied()
}
}
impl PackageManager {
pub(crate) const fn is_node(self) -> bool {
matches!(self, Self::Npm | Self::Yarn | Self::Pnpm | Self::Bun)
}
pub(crate) const fn label(self) -> &'static str {
match self {
Self::Npm => "npm",
Self::Yarn => "yarn",
Self::Pnpm => "pnpm",
Self::Bun => "bun",
Self::Cargo => "cargo",
Self::Deno => "deno",
Self::Uv => "uv",
Self::Poetry => "poetry",
Self::Pipenv => "pipenv",
Self::Go => "go",
Self::Bundler => "bundler",
Self::Composer => "composer",
}
}
}
impl TaskRunner {
pub(crate) const fn label(self) -> &'static str {
match self {
Self::Turbo => "turbo",
Self::Nx => "nx",
Self::Make => "make",
Self::Just => "just",
Self::GoTask => "task",
Self::Mise => "mise",
}
}
}
impl TaskSource {
pub(crate) const fn label(self) -> &'static str {
match self {
Self::PackageJson => "package.json",
Self::Makefile => "Makefile",
Self::Justfile => "justfile",
Self::Taskfile => "Taskfile",
Self::TurboJson => "turbo.json",
Self::DenoJson => "deno.json",
Self::CargoAliases => "cargo",
}
}
pub(crate) fn from_label(label: &str) -> Option<Self> {
match label {
"package.json" => Some(Self::PackageJson),
"Makefile" => Some(Self::Makefile),
"justfile" => Some(Self::Justfile),
"Taskfile" => Some(Self::Taskfile),
"turbo.json" => Some(Self::TurboJson),
"deno.json" => Some(Self::DenoJson),
"cargo" => Some(Self::CargoAliases),
_ => None,
}
}
pub(crate) const fn display_order(self) -> u8 {
match self {
Self::PackageJson => 0,
Self::Makefile => 1,
Self::Justfile => 2,
Self::Taskfile => 3,
Self::TurboJson => 4,
Self::DenoJson => 5,
Self::CargoAliases => 6,
}
}
}
pub(crate) fn version_matches(expected: &str, current: &str) -> bool {
let expected = expected.trim();
let current = current.trim();
let expected_clean = expected
.trim_start_matches(">=")
.trim_start_matches("<=")
.trim_start_matches('>')
.trim_start_matches('<')
.trim_start_matches('~')
.trim_start_matches('^')
.trim_start_matches('v')
.trim();
if !expected_clean.contains('.') {
return current.starts_with(expected_clean)
&& current[expected_clean.len()..]
.chars()
.next()
.is_none_or(|c| c == '.');
}
current.starts_with(expected_clean)
&& current[expected_clean.len()..]
.chars()
.next()
.is_none_or(|c| c == '.')
}
#[cfg(test)]
mod tests {
use super::version_matches;
#[test]
fn dotted_versions_match_segment_boundaries_only() {
assert!(version_matches("20.11", "20.11.0"));
assert!(!version_matches("20.11", "20.110.0"));
}
}