diff --git a/Cargo.lock b/Cargo.lock index 44aa745..356cc85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,7 +85,7 @@ dependencies = [ "progress-read", "tar", "tee", - "thiserror 2.0.19", + "thiserror", "verbatim", "zip", ] @@ -167,12 +167,6 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" -[[package]] -name = "bytecount" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" - [[package]] name = "bytes" version = "1.12.1" @@ -284,7 +278,7 @@ checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" dependencies = [ "encode_unicode", "libc", - "unicode-width 0.2.2", + "unicode-width", "windows-sys 0.61.2", ] @@ -745,7 +739,7 @@ checksum = "9433806cd6b4ec1aba79c021c7e4c58fb4c3b9977c085062e611ac929998fb0c" dependencies = [ "console", "portable-atomic", - "unicode-width 0.2.2", + "unicode-width", "unit-prefix", "web-time", ] @@ -828,40 +822,12 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" -[[package]] -name = "miette" -version = "7.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" -dependencies = [ - "cfg-if", - "miette-derive", - "unicode-width 0.1.14", -] - -[[package]] -name = "miette-derive" -version = "7.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -901,29 +867,6 @@ dependencies = [ "libc", ] -[[package]] -name = "node-semver" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b1a233ea5dc37d2cfba31cfc87a5a56cc2a9c04e3672c15d179ca118dae40a7" -dependencies = [ - "bytecount", - "miette", - "nom", - "serde", - "thiserror 1.0.69", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "num-conv" version = "0.2.2" @@ -956,11 +899,11 @@ dependencies = [ "fs-utils", "fs_extra", "indicatif", - "node-semver", "once_cell", "openssl", "regex", "retry", + "semver", "serde", "serde_json", "tempfile", @@ -1174,7 +1117,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.19", + "thiserror", ] [[package]] @@ -1328,6 +1271,12 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.229" @@ -1497,33 +1446,13 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ - "thiserror-impl 2.0.19", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", + "thiserror-impl", ] [[package]] @@ -1586,12 +1515,6 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - [[package]] name = "unicode-width" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index 443a392..6370d6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,6 @@ envoy = "0.1.3" fs_extra = "1.3" fs-utils = { path = "crates/fs-utils" } indicatif = "0.18.6" -node-semver = "2.2.0" once_cell = "1.21.4" regex = "1.13.1" retry = "2.2.0" @@ -31,6 +30,7 @@ tempfile = "3.27.0" terminal_size = "0.4.4" time = { version = "0.3.54", features = ["local-offset"] } version-compare = "0.2.1" +semver = "1.0.28" [target.'cfg(target_os = "linux")'.dependencies] openssl = { version = "0.10.81", features = ["vendored"] } diff --git a/src/cli/install.rs b/src/cli/install.rs index 191b1d4..742075c 100644 --- a/src/cli/install.rs +++ b/src/cli/install.rs @@ -1,7 +1,4 @@ -use crate::{ - node::Node, - utils::{help::node_version_parse, notice::Notice}, -}; +use crate::{module::NodeVersionResolver, node::Node, utils::notice::Notice}; use anyhow::Result; #[derive(clap::Args)] @@ -12,7 +9,7 @@ pub struct Install { impl super::Command for Install { fn run(self) -> Result<()> { - let version = node_version_parse(&self.version)?; + let version = NodeVersionResolver::parse(&self.version)?; Node::new(version).ensure_fetched()?; let _ = Notice::from_version().send(); diff --git a/src/cli/list.rs b/src/cli/list.rs index a8955c8..07e2e03 100644 --- a/src/cli/list.rs +++ b/src/cli/list.rs @@ -1,11 +1,8 @@ -use crate::{ - module::{Context, Groups, Setting}, - utils::help::node_version_parse, -}; +use crate::module::{Context, Groups, NodeVersionResolver, Setting}; use anyhow::Result; -use fs_extra::dir::{ls, DirEntryAttr, DirEntryValue}; +use fs_extra::dir::{DirEntryAttr, DirEntryValue, ls}; use std::{cmp::Ordering, collections::HashSet}; -use version_compare::{compare, Cmp}; +use version_compare::{Cmp, compare}; #[derive(clap::Args)] pub struct List { @@ -38,7 +35,7 @@ impl List { DirEntryValue::String(s) => s, _ => return None, }; - node_version_parse(version_str) + NodeVersionResolver::parse(version_str) .ok() .map(|_| version_str.to_string()) }) diff --git a/src/cli/uninstall.rs b/src/cli/uninstall.rs index d43dfdb..64e3539 100644 --- a/src/cli/uninstall.rs +++ b/src/cli/uninstall.rs @@ -1,8 +1,8 @@ use crate::{ - module::Setting, - utils::{help::node_version_parse, notice::Notice}, + module::{NodeVersionResolver, Setting}, + utils::notice::Notice, }; -use anyhow::{bail, Result}; +use anyhow::{Result, bail}; use console::style; use fs_extra::dir; @@ -14,7 +14,7 @@ pub struct Uninstall { impl super::Command for Uninstall { fn run(self) -> Result<()> { - let version = node_version_parse(&self.version)?; + let version = NodeVersionResolver::parse(&self.version)?; let path = Setting::global()? .get_directory()? .join(&version.to_string()); diff --git a/src/cli/use.rs b/src/cli/use.rs index 9a646cc..99e3ae1 100644 --- a/src/cli/use.rs +++ b/src/cli/use.rs @@ -1,11 +1,8 @@ use crate::{ - module::{nvmd_home, Groups, Projects, Setting}, - utils::{ - help::{node_strict_available, node_version_parse}, - notice::Notice, - }, + module::{Groups, NodeVersionResolver, Projects, Setting, nvmd_home}, + utils::{help::node_strict_available, notice::Notice}, }; -use anyhow::{anyhow, bail, Result}; +use anyhow::{Result, anyhow, bail}; use fs_extra::file::write_all; #[derive(clap::Args)] @@ -34,16 +31,16 @@ impl Use { bail!("Group@{} can only be used for projects", &self.version) } - let version = node_version_parse(&self.version)?; - if !node_strict_available(&version.to_string())? { + let version = NodeVersionResolver::resolve(&self.version)?; + if !node_strict_available(&version)? { bail!("Node@v{} has not been installed", &version); } let default_path = nvmd_home()?.default_path(); - write_all(default_path, &version.to_string())?; + write_all(default_path, &version)?; eprintln!("Now using node v{}", &version); - let _ = Notice::from_current(version.to_string()).send(); + let _ = Notice::from_current(version.clone()).send(); Ok(()) } @@ -59,7 +56,7 @@ impl Use { &self.version ) })?, - None => node_version_parse(&self.version)?.to_string(), + None => NodeVersionResolver::resolve(&self.version)?, }; if !node_strict_available(&version)? { diff --git a/src/cli/which.rs b/src/cli/which.rs index 40adeee..40fee71 100644 --- a/src/cli/which.rs +++ b/src/cli/which.rs @@ -1,5 +1,5 @@ -use crate::{module::Setting, utils::help::node_version_parse}; -use anyhow::{bail, Result}; +use crate::module::{NodeVersionResolver, Setting}; +use anyhow::{Result, bail}; #[derive(clap::Args)] pub struct Which { @@ -9,10 +9,8 @@ pub struct Which { impl super::Command for Which { fn run(self) -> Result<()> { - let version = node_version_parse(&self.version)?; - let mut path = Setting::global()? - .get_directory()? - .join(&version.to_string()); + let version = NodeVersionResolver::resolve(&self.version)?; + let mut path = Setting::global()?.get_directory()?.join(&version); if cfg!(unix) { path.push("bin"); } diff --git a/src/module/context.rs b/src/module/context.rs index 97c7f7f..77ca6a2 100644 --- a/src/module/context.rs +++ b/src/module/context.rs @@ -1,6 +1,6 @@ use super::nvmd_home; -use crate::module::Setting; -use anyhow::{anyhow, bail, Context as AnyhowContext, Result}; +use crate::module::{NodeVersionResolver, Setting}; +use anyhow::{Context as AnyhowContext, Result, anyhow, bail}; use fs_extra::file::read_to_string; use once_cell::sync::OnceCell; use std::{ @@ -82,7 +82,7 @@ fn get_version() -> Result> { if let Ok(env_version) = std::env::var("NVMD_NODE_VERSION") { let v = env_version.trim(); if !v.is_empty() { - return Ok(Some(v.to_string())); + return Ok(Some(NodeVersionResolver::resolve(v)?)); } } @@ -92,7 +92,7 @@ fn get_version() -> Result> { let content = read_to_string(&path)?; let t = content.trim(); if !t.is_empty() { - return Ok(Some(t.to_string())); + return Ok(Some(NodeVersionResolver::resolve(t)?)); } } @@ -103,7 +103,7 @@ fn get_version() -> Result> { let content = read_to_string(&default_path)?; let t = content.trim(); if !t.is_empty() { - return Ok(Some(t.to_string())); + return Ok(Some(NodeVersionResolver::resolve(t)?)); } } diff --git a/src/module/mod.rs b/src/module/mod.rs index da0e0bb..c61fd6a 100644 --- a/src/module/mod.rs +++ b/src/module/mod.rs @@ -4,6 +4,7 @@ mod home; mod package; mod project; mod setting; +mod version; pub use context::Context; pub use group::Groups; @@ -11,3 +12,4 @@ pub use home::nvmd_home; pub use package::{PackageJson, Packages}; pub use project::Projects; pub use setting::Setting; +pub use version::*; diff --git a/src/module/version.rs b/src/module/version.rs new file mode 100644 index 0000000..6ee0d3f --- /dev/null +++ b/src/module/version.rs @@ -0,0 +1,164 @@ +use anyhow::{Context as _, Result, anyhow}; +use std::fs; + +use crate::module::Setting; + +#[derive(Debug, PartialEq, Eq)] +enum NodeVersionRequest { + Exact(semver::Version), + Major(u64), + MajorMinor(u64, u64), +} + +pub struct NodeVersionResolver; + +impl NodeVersionResolver { + /// Parse exact node version. + /// + /// Examples: + /// v14.21.3 -> 14.21.3 + /// 14.21.3 -> 14.21.3 + pub fn parse(input: &str) -> Result { + let version = Self::normalize(input); + semver::Version::parse(version).with_context(|| { + anyhow!( + "Failed to parse Node version {} \nPlease ensure the correct version is specified.", + input + ) + }) + } + + /// Resolve user input to latest installed node version. + /// + /// Examples: + /// 14 -> 14.21.3 + /// 14.18 -> 14.18.3 + /// 14.18.3 -> 14.18.3 + pub fn resolve(input: &str) -> Result { + let request = Self::parse_request(input)?; + let versions_dir = Setting::global()?.get_directory()?; + let versions = fs::read_dir(&versions_dir) + .with_context(|| { + format!( + "failed to read the directory \"{}\"", + versions_dir.display() + ) + })? + .filter_map(|entry| { + let entry = entry.ok()?; + let file_type = entry.file_type().ok()?; + if !file_type.is_dir() { + return None; + } + + let version = entry.file_name().into_string().ok()?; + Self::parse(&version).ok() + }) + .collect::>(); + + Self::latest_matching(&request, versions) + .map(|version| version.to_string()) + .ok_or_else(|| anyhow!("Node@v{} has not been installed", input)) + } + + fn parse_request(input: &str) -> Result { + let version = Self::normalize(input); + let parts = version.split('.').collect::>(); + + match parts.as_slice() { + [major] if !major.is_empty() => major + .parse::() + .map(NodeVersionRequest::Major) + .with_context(|| { + anyhow!( + "Failed to parse Node version {} \nPlease ensure the correct version is specified.", + input + ) + }), + [major, minor] if !major.is_empty() && !minor.is_empty() => { + let major = major.parse::().with_context(|| { + anyhow!( + "Failed to parse Node version {} \nPlease ensure the correct version is specified.", + input + ) + })?; + let minor = minor.parse::().with_context(|| { + anyhow!( + "Failed to parse Node version {} \nPlease ensure the correct version is specified.", + input + ) + })?; + Ok(NodeVersionRequest::MajorMinor(major, minor)) + } + _ => Self::parse(version).map(NodeVersionRequest::Exact), + } + } + + fn latest_matching( + request: &NodeVersionRequest, + mut versions: Vec, + ) -> Option { + versions.retain(|version| match request { + NodeVersionRequest::Major(major) => version.major == *major, + NodeVersionRequest::MajorMinor(major, minor) => { + version.major == *major && version.minor == *minor + } + NodeVersionRequest::Exact(exact) => version == exact, + }); + versions.sort(); + versions.pop() + } + + fn normalize(input: &str) -> &str { + let input = input.trim(); + input.strip_prefix('v').unwrap_or(input) + } +} + +#[cfg(test)] +mod tests { + use super::{NodeVersionRequest, NodeVersionResolver}; + + #[test] + fn parse_partial_and_exact_version_requests() { + assert_eq!( + NodeVersionResolver::parse_request("14").unwrap(), + NodeVersionRequest::Major(14) + ); + assert_eq!( + NodeVersionResolver::parse_request("14.21").unwrap(), + NodeVersionRequest::MajorMinor(14, 21) + ); + assert_eq!( + NodeVersionResolver::parse_request("v14.21.3").unwrap(), + NodeVersionRequest::Exact(semver::Version::parse("14.21.3").unwrap()) + ); + } + + #[test] + fn choose_latest_installed_matching_semver_version_request() { + let versions = ["14.18.3", "14.21.3", "16.17.0", "18.19.1"] + .into_iter() + .map(|version| semver::Version::parse(version).unwrap()) + .collect::>(); + + assert_eq!( + super::NodeVersionResolver::latest_matching( + &NodeVersionRequest::Major(14), + versions.clone() + ) + .unwrap() + .to_string(), + "14.21.3" + ); + assert_eq!( + super::NodeVersionResolver::latest_matching( + &NodeVersionRequest::MajorMinor(14, 18), + versions + ) + .unwrap() + .to_string(), + "14.18.3" + ); + } +} diff --git a/src/node/fetch.rs b/src/node/fetch.rs index 9abf077..ce8c5f1 100644 --- a/src/node/fetch.rs +++ b/src/node/fetch.rs @@ -2,17 +2,17 @@ /// Copyright (c) 2017, LinkedIn Corporation. /// https://github.com/volta-cli/volta /// -use super::{tool_version, Node}; +use super::{Node, tool_version}; use crate::module::Setting; use crate::utils::progress::progress_bar; -use anyhow::{anyhow, Context, Result}; +use anyhow::{Context, Result, anyhow}; use archive::Archive; use fs_utils::ensure_containing_dir_exists; -use node_semver::Version; use retry::delay::Fibonacci; -use retry::{retry, OperationResult}; +use retry::{OperationResult, retry}; +use semver::Version; use std::{fs::File, path::Path}; -use tempfile::{tempdir_in, NamedTempFile, TempDir}; +use tempfile::{NamedTempFile, TempDir, tempdir_in}; pub fn fetch(version: &Version) -> Result<()> { let install_dir = Setting::global()?.get_directory()?; diff --git a/src/node/mod.rs b/src/node/mod.rs index 830eded..3271c32 100644 --- a/src/node/mod.rs +++ b/src/node/mod.rs @@ -1,8 +1,8 @@ use std::fmt::{self, Display}; -use anyhow::{anyhow, Context, Result}; +use anyhow::{Context, Result, anyhow}; use cfg_if::cfg_if; -use node_semver::Version; +use semver::Version; use crate::utils::help::node_available; diff --git a/src/utils/help.rs b/src/utils/help.rs index 5e64f0e..ab4dbac 100644 --- a/src/utils/help.rs +++ b/src/utils/help.rs @@ -1,6 +1,6 @@ -use crate::module::nvmd_home; use crate::module::Setting; -use anyhow::{anyhow, bail, Context, Result}; +use crate::module::nvmd_home; +use anyhow::{Context, Result, anyhow, bail}; use fs_extra::file::{remove, write_all}; use serde::de::DeserializeOwned; use std::{fs, path::PathBuf}; @@ -27,15 +27,6 @@ pub fn write_json(path: &PathBuf, data: &T) -> Result<()> { Ok(()) } -pub fn node_version_parse(input: &str) -> Result { - node_semver::Version::parse(input).with_context(|| { - anyhow!( - "Failed to parse Node version {} \nPlease ensure the correct version is specified.", - input - ) - }) -} - pub fn node_strict_available(version: &str) -> Result { let mut path = Setting::global()?.get_directory()?.join(version); @@ -55,10 +46,6 @@ pub fn node_available(version: &str) -> Result { .map(|dir| dir.join(version).exists()) } -pub fn sanitize_version(version: &str) -> String { - version.strip_prefix('v').unwrap_or(version).to_string() -} - #[cfg(unix)] pub fn link_package(name: &str) -> Result<()> { use std::os::unix::fs; @@ -76,7 +63,7 @@ pub fn link_package(name: &str) -> Result<()> { #[cfg(windows)] pub fn link_package(name: &str) -> Result<()> { - use fs_extra::file::{copy, CopyOptions}; + use fs_extra::file::{CopyOptions, copy}; let home = nvmd_home()?; let exe_source = home.bin_dir().join("nvmd.exe");