diff --git a/Cargo.toml b/Cargo.toml index 8b5bec6..974445b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ path = "src/main.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +async-trait = "0.1.62" clap = { version = "4.1.1", features = ["derive"] } crossterm = "0.25.0" dialoguer = "0.10.3" diff --git a/src/main.rs b/src/main.rs index 313ab1d..d65bb74 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,7 +32,7 @@ async fn main() -> Result<()> { args::Command::Version => { print_version(); Ok(()) - }, + } args::Command::Install(v) => install_version(v.version).await, args::Command::Default(v) => set_default_version(v.version).await, args::Command::Exec(args) => { @@ -51,7 +51,8 @@ fn print_version() { println!("{} v{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); } -pub async fn install_version(version: NodeVersion) -> Result<()> { +/// Installs a given node version +async fn install_version(version: NodeVersion) -> Result<()> { if VERSION_FILE_PATH.exists() { fs::remove_file(&*VERSION_FILE_PATH) .await @@ -59,14 +60,16 @@ pub async fn install_version(version: NodeVersion) -> Result<()> { } let repo = get_repository().await?; - if repo.is_installed(&version)? && !Confirm::new() + if repo.is_installed(&version)? + && !Confirm::new() .with_prompt(format!( "The version {} is already installed. Reinstall?", version.to_string().bold() )) .default(false) .interact() - .unwrap() { + .unwrap() + { return Ok(()); } repo.install_version(&version).await?; @@ -75,7 +78,8 @@ pub async fn install_version(version: NodeVersion) -> Result<()> { Ok(()) } -pub async fn set_default_version(version: NodeVersion) -> Result<()> { +/// Sets a default system wide node version +async fn set_default_version(version: NodeVersion) -> Result<()> { let mut mapper = get_mapper().await?; if !mapper.repository().is_installed(&version)? @@ -96,8 +100,9 @@ pub async fn set_default_version(version: NodeVersion) -> Result<()> { Ok(()) } +/// Exectues a given command #[inline] -pub async fn exec(command: String, args: Vec) -> Result { +async fn exec(command: String, args: Vec) -> Result { let mapper = get_mapper().await?; let active_version = mapper.active_version(); @@ -109,7 +114,8 @@ pub async fn exec(command: String, args: Vec) -> Result { Ok(exit_status.code().unwrap_or(0)) } -pub async fn refresh() -> Result<()> { +/// Refreshes the version cache and mapped binaries +async fn refresh() -> Result<()> { get_mapper().await?.remap().await?; fs::remove_file(&*VERSION_FILE_PATH) .await @@ -119,7 +125,8 @@ pub async fn refresh() -> Result<()> { Ok(()) } -pub async fn list_versions() -> Result<()> { +/// Lists all available node versions +async fn list_versions() -> Result<()> { let mapper = get_mapper().await?; let versions = mapper.repository().installed_versions().await?; let active_version = mapper diff --git a/src/mapper/mod.rs b/src/mapper/mod.rs index 89040a3..e30487d 100644 --- a/src/mapper/mod.rs +++ b/src/mapper/mod.rs @@ -1,4 +1,4 @@ -use std::{env, ffi::OsString, process::ExitStatus, str::FromStr}; +use std::{ffi::OsString, process::ExitStatus}; use tokio::fs; @@ -8,12 +8,16 @@ use crate::{ repository::{NodeVersion, Repository}, }; -use self::{mapped_command::MappedCommand, mapped_dir::map_node_bin, package_info::PackageInfo}; +use self::{ + mapped_command::MappedCommand, + mapped_dir::map_node_bin, + version_detection::{ParallelDetector, VersionDetector}, +}; use miette::{IntoDiagnostic, Result}; mod mapped_command; mod mapped_dir; -mod package_info; +mod version_detection; /// Responsible for mapping to node executables /// and managing node versions @@ -76,19 +80,10 @@ impl Mapper { } async fn get_version() -> Option { - if let Some(version) = PackageInfo::find() + ParallelDetector::detect_version() .await .ok() - .and_then(|i| i) - .and_then(|i| i.engines) - .and_then(|e| e.node) - { - Some(NodeVersion::Req(version)) - } else { - env::var("NODE_VERSION") - .ok() - .and_then(|v| NodeVersion::from_str(&v).ok()) - } + .and_then(|v| v) } /// creates wrapper scripts for the current version diff --git a/src/mapper/version_detection/env_detector.rs b/src/mapper/version_detection/env_detector.rs new file mode 100644 index 0000000..cc91803 --- /dev/null +++ b/src/mapper/version_detection/env_detector.rs @@ -0,0 +1,19 @@ +use std::str::FromStr; + +use miette::{Context, IntoDiagnostic}; + +use crate::repository::NodeVersion; + +use super::VersionDetector; + +pub struct EnvDetector; + +#[async_trait::async_trait] +impl VersionDetector for EnvDetector { + async fn detect_version() -> miette::Result> { + std::env::var("NODE_VERSION") + .into_diagnostic() + .context("Reading version from environment") + .map(|v| NodeVersion::from_str(&v).ok()) + } +} diff --git a/src/mapper/version_detection/mod.rs b/src/mapper/version_detection/mod.rs new file mode 100644 index 0000000..094df9d --- /dev/null +++ b/src/mapper/version_detection/mod.rs @@ -0,0 +1,38 @@ +use async_trait::async_trait; + +use futures::future; +use miette::Result; +mod env_detector; +mod package_json_detector; +mod version_file_detector; + +use crate::repository::NodeVersion; + +use self::{ + env_detector::EnvDetector, package_json_detector::PackageJsonDetector, + version_file_detector::VersionFileDetector, +}; + +#[async_trait] +pub trait VersionDetector { + async fn detect_version() -> Result>; +} + +pub struct ParallelDetector; + +#[async_trait] +impl VersionDetector for ParallelDetector { + async fn detect_version() -> Result> { + let version = future::join_all(vec![ + VersionFileDetector::detect_version(), + PackageJsonDetector::detect_version(), + EnvDetector::detect_version(), + ]) + .await + .into_iter() + .filter_map(Result::ok) + .find_map(|v| v); + + Ok(version) + } +} diff --git a/src/mapper/package_info.rs b/src/mapper/version_detection/package_json_detector.rs similarity index 64% rename from src/mapper/package_info.rs rename to src/mapper/version_detection/package_json_detector.rs index a07492f..751255d 100644 --- a/src/mapper/package_info.rs +++ b/src/mapper/version_detection/package_json_detector.rs @@ -6,7 +6,11 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use tokio::fs; -use crate::error::ParseJsonError; +use crate::{error::ParseJsonError, repository::NodeVersion, utils::find_in_parents}; + +use super::VersionDetector; + +pub struct PackageJsonDetector; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct PackageInfo { @@ -24,25 +28,25 @@ pub struct EngineInfo { other: HashMap, } +#[async_trait::async_trait] +impl VersionDetector for PackageJsonDetector { + async fn detect_version() -> Result> { + Ok(PackageInfo::find() + .await? + .and_then(|p| p.engines) + .and_then(|e| e.node) + .map(NodeVersion::Req)) + } +} + impl PackageInfo { pub async fn find() -> Result> { - let mut dir = std::env::current_dir().into_diagnostic()?; - let file_path = dir.join("package.json"); - - if file_path.exists() { - let info = Self::load(&file_path).await?; + let dir = std::env::current_dir().into_diagnostic()?; + if let Some(path) = find_in_parents(dir, "package.json") { + let info = Self::load(&path).await?; Ok(Some(info)) } else { - while let Some(parent) = dir.parent() { - dir = parent.to_owned(); - let file_path = dir.join("package.json"); - - if file_path.exists() { - let info = Self::load(&file_path).await?; - return Ok(Some(info)); - } - } Ok(None) } } diff --git a/src/mapper/version_detection/version_file_detector.rs b/src/mapper/version_detection/version_file_detector.rs new file mode 100644 index 0000000..725d1ff --- /dev/null +++ b/src/mapper/version_detection/version_file_detector.rs @@ -0,0 +1,27 @@ +use std::str::FromStr; + +use miette::{Context, IntoDiagnostic}; +use tokio::fs; + +use crate::{repository::NodeVersion, utils::find_in_parents}; + +use super::VersionDetector; + +pub struct VersionFileDetector; + +#[async_trait::async_trait] +impl VersionDetector for VersionFileDetector { + async fn detect_version() -> miette::Result> { + let dir = std::env::current_dir().into_diagnostic()?; + + if let Some(path) = find_in_parents(dir, ".node-version") { + let version_string = fs::read_to_string(path) + .await + .into_diagnostic() + .context("Reading version file.")?; + Ok(NodeVersion::from_str(&version_string).ok()) + } else { + Ok(None) + } + } +} diff --git a/src/repository/mod.rs b/src/repository/mod.rs index e20e356..99e0f07 100644 --- a/src/repository/mod.rs +++ b/src/repository/mod.rs @@ -46,10 +46,12 @@ impl FromStr for NodeVersion { "latest" => Self::Latest, "lts" => Self::LatestLts, _ => { - if let Ok(req) = VersionReq::parse(s) { + let version_string = s.trim().trim_start_matches('v'); + + if let Ok(req) = VersionReq::parse(version_string) { Self::Req(req) } else { - Self::Lts(s.to_lowercase()) + Self::Lts(version_string.to_lowercase()) } } }; diff --git a/src/utils.rs b/src/utils.rs index d99c4d4..3a37480 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,7 @@ -use std::time::Duration; +use std::{ + path::{Path, PathBuf}, + time::Duration, +}; use indicatif::{ProgressBar, ProgressStyle}; @@ -26,3 +29,27 @@ pub fn progress_spinner() -> ProgressBar { pb.enable_steady_tick(Duration::from_millis(50)); pb } + +pub fn find_in_parents>(origin: PathBuf, name: P) -> Option { + for part in dir_parts(origin) { + let file = part.join(&name); + if file.exists() { + return Some(file); + } + } + + None +} + +/// Returns a list of paths for the current dir up to the very top +pub fn dir_parts(path: PathBuf) -> Vec { + let mut current: &Path = &path; + let mut parts = vec![path.to_owned()]; + + while let Some(parent) = current.parent() { + current = parent; + parts.push(parent.to_owned()) + } + + parts +}