diff --git a/Cargo.lock b/Cargo.lock index e1402e365..2a602b450 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1657,7 +1657,7 @@ dependencies = [ "volta-core", "volta-migrate", "which", - "winreg", + "winreg 0.53.0", ] [[package]] @@ -1701,7 +1701,7 @@ dependencies = [ "volta-layout", "walkdir", "which", - "winreg", + "winreg 0.55.0", ] [[package]] @@ -2015,6 +2015,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + [[package]] name = "winsafe" version = "0.0.19" diff --git a/crates/volta-core/src/tool/node/fetch.rs b/crates/volta-core/src/tool/node/fetch.rs index c969eb4db..ad317d671 100644 --- a/crates/volta-core/src/tool/node/fetch.rs +++ b/crates/volta-core/src/tool/node/fetch.rs @@ -15,7 +15,7 @@ use archive::{self, Archive}; use cfg_if::cfg_if; use fs_utils::ensure_containing_dir_exists; use log::debug; -use node_semver::Version; +use node_semver::{Identifier, Version}; use serde::Deserialize; cfg_if! { @@ -23,13 +23,23 @@ cfg_if! { // TODO: We need to reconsider our mocking strategy in light of mockito deprecating the // SERVER_URL constant: Since our acceptance tests run the binary in a separate process, // we can't use `mockito::server_url()`, which relies on shared memory. - fn public_node_server_root() -> String { + fn public_node_server_root(version: &Version) -> String { #[allow(deprecated)] - mockito::SERVER_URL.to_string() + let base = mockito::SERVER_URL.to_string(); + + if is_nightly_version(version) { + format!("{}/download/nightly", base) + } else { + base + } } } else { - fn public_node_server_root() -> String { - "https://nodejs.org/dist".to_string() + fn public_node_server_root(version: &Version) -> String { + if is_nightly_version(version) { + "https://nodejs.org/download/nightly".to_string() + } else { + "https://nodejs.org/dist".to_string() + } } } } @@ -47,6 +57,13 @@ fn npm_manifest_path(version: &Version) -> PathBuf { manifest } +fn is_nightly_version(version: &Version) -> bool { + matches!( + version.pre_release.first(), + Some(Identifier::AlphaNumeric(pre)) if pre.starts_with("nightly") + ) +} + pub fn fetch(version: &Version, hooks: Option<&ToolHooks>) -> Fallible { let home = volta_home()?; let node_dir = home.node_inventory_dir(); @@ -162,7 +179,7 @@ fn determine_remote_url(version: &Version, hooks: Option<&ToolHooks>) -> F } _ => Ok(format!( "{}/v{}/{}", - public_node_server_root(), + public_node_server_root(version), version, distro_file_name )), @@ -217,3 +234,32 @@ fn save_default_npm_version(node: &Version, npm: &Version) -> Fallible<()> { } }) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn nightly_versions_use_nightly_root() { + let version = Version::parse("22.0.0-nightly20240101abcd").unwrap(); + + let url = determine_remote_url(&version, None).expect("should build nightly URL"); + + assert!( + url.contains("/download/nightly/v22.0.0-nightly20240101abcd/"), + "expected nightly download URL, got {url}" + ); + } + + #[test] + fn stable_versions_use_release_root() { + let version = Version::parse("22.0.0").unwrap(); + + let url = determine_remote_url(&version, None).expect("should build release URL"); + + assert!( + url.contains("/dist/v22.0.0/") || url.contains("/v22.0.0/"), + "expected release download URL, got {url}" + ); + } +} diff --git a/crates/volta-core/src/tool/node/resolve.rs b/crates/volta-core/src/tool/node/resolve.rs index 1666f5e02..92d6e6ed1 100644 --- a/crates/volta-core/src/tool/node/resolve.rs +++ b/crates/volta-core/src/tool/node/resolve.rs @@ -33,11 +33,18 @@ cfg_if! { fn public_node_version_index() -> String { format!("{}/node-dist/index.json", SERVER_URL) } + fn public_node_nightly_index() -> String { + format!("{}/node-nightly/index.json", SERVER_URL) + } } else { /// Returns the URL of the index of available Node versions on the public Node server. fn public_node_version_index() -> String { "https://nodejs.org/dist/index.json".to_string() } + /// Returns the URL of the index of available nightly Node versions on the public Node server. + fn public_node_nightly_index() -> String { + "https://nodejs.org/download/nightly/index.json".to_string() + } } } @@ -48,7 +55,8 @@ pub fn resolve(matching: VersionSpec, session: &mut Session) -> Fallible Ok(version), VersionSpec::None | VersionSpec::Tag(VersionTag::Lts) => resolve_lts(hooks), VersionSpec::Tag(VersionTag::Latest) => resolve_latest(hooks), - // Node doesn't have "tagged" versions (apart from 'latest' and 'lts'), so custom tags will always be an error + // Node doesn't have "tagged" versions (apart from 'latest', 'lts', and 'nightly'), so other custom tags are an error + VersionSpec::Tag(VersionTag::Custom(tag)) if tag == "nightly" => resolve_nightly(hooks), VersionSpec::Tag(VersionTag::Custom(tag)) => { Err(ErrorKind::NodeVersionNotFound { matching: tag }.into()) } @@ -83,6 +91,42 @@ fn resolve_latest(hooks: Option<&ToolHooks>) -> Fallible { } } +fn resolve_nightly(hooks: Option<&ToolHooks>) -> Fallible { + let url = match hooks { + Some(&ToolHooks { + latest: Some(ref hook), + .. + }) => { + debug!("Using node.latest hook to determine node nightly index URL"); + hook.resolve("nightly/index.json")? + } + Some(&ToolHooks { + index: Some(ref hook), + .. + }) => { + debug!("Using node.index hook to determine node nightly index URL"); + hook.resolve("nightly/index.json")? + } + _ => public_node_nightly_index(), + }; + + let version_opt = match_node_version(&url, |_| true)?; + + match version_opt { + Some(version) => { + debug!( + "Found latest nightly node version ({}) from {}", + version, url + ); + Ok(version) + } + None => Err(ErrorKind::NodeVersionNotFound { + matching: "nightly".into(), + } + .into()), + } +} + fn resolve_lts(hooks: Option<&ToolHooks>) -> Fallible { let url = match hooks { Some(&ToolHooks { diff --git a/crates/volta-core/src/tool/serial.rs b/crates/volta-core/src/tool/serial.rs index 37463e17f..b1d8e2d78 100644 --- a/crates/volta-core/src/tool/serial.rs +++ b/crates/volta-core/src/tool/serial.rs @@ -155,15 +155,16 @@ impl Spec { /// Determine if a given string is "version-like". /// -/// This means it is either 'latest', 'lts', a Version, or a Version Range. +/// This means it is either 'latest', 'lts', 'nightly', a Version, or a Version Range. fn is_version_like(value: &str) -> bool { - matches!( - value.parse(), + match value.parse::() { Ok(VersionSpec::Exact(_)) - | Ok(VersionSpec::Semver(_)) - | Ok(VersionSpec::Tag(VersionTag::Latest)) - | Ok(VersionSpec::Tag(VersionTag::Lts)) - ) + | Ok(VersionSpec::Semver(_)) + | Ok(VersionSpec::Tag(VersionTag::Latest)) + | Ok(VersionSpec::Tag(VersionTag::Lts)) => true, + Ok(VersionSpec::Tag(VersionTag::Custom(tag))) if tag == "nightly" => true, + _ => false, + } } #[cfg(test)] @@ -180,6 +181,7 @@ mod tests { const MINOR: &str = "3.0"; const PATCH: &str = "3.0.0"; const BETA: &str = "beta"; + const NIGHTLY: &str = "nightly"; /// Convenience macro for generating the @ string. macro_rules! versioned_tool { @@ -224,6 +226,11 @@ mod tests { Spec::try_from_str(&versioned_tool!(tool, LTS)).expect("succeeds"), Spec::Node(VersionSpec::Tag(VersionTag::Lts)) ); + + assert_eq!( + Spec::try_from_str(&versioned_tool!(tool, NIGHTLY)).expect("succeeds"), + Spec::Node(VersionSpec::Tag(VersionTag::Custom(NIGHTLY.into()))) + ); } #[test] @@ -414,6 +421,25 @@ mod tests { ); } + #[test] + fn special_cases_tool_space_nightly() { + let name = "node"; + let version = "nightly"; + let args: Vec = vec![name.into(), version.into()]; + + let err = Spec::from_strings(&args, PIN).unwrap_err(); + + assert_eq!( + err.kind(), + &ErrorKind::InvalidInvocation { + action: PIN.into(), + name: name.into(), + version: version.into() + }, + "`volta node nightly` results in the correct error" + ); + } + #[test] fn leaves_other_scenarios_alone() { let empty: Vec<&str> = Vec::new(); diff --git a/src/command/fetch.rs b/src/command/fetch.rs index e2d3b7355..bf7ec716e 100644 --- a/src/command/fetch.rs +++ b/src/command/fetch.rs @@ -6,7 +6,7 @@ use crate::command::Command; #[derive(clap::Args)] pub(crate) struct Fetch { - /// Tools to fetch, like `node`, `yarn@latest` or `your-package@^14.4.3`. + /// Tools to fetch, like `node@nightly`, `yarn@latest` or `your-package@^14.4.3`. #[arg(value_name = "tool[@version]", required = true)] tools: Vec, } diff --git a/src/command/install.rs b/src/command/install.rs index 1450c0127..9a3d5d116 100644 --- a/src/command/install.rs +++ b/src/command/install.rs @@ -6,7 +6,7 @@ use crate::command::Command; #[derive(clap::Args)] pub(crate) struct Install { - /// Tools to install, like `node`, `yarn@latest` or `your-package@^14.4.3`. + /// Tools to install, like `node@nightly`, `yarn@latest` or `your-package@^14.4.3`. #[arg(value_name = "tool[@version]", required = true)] tools: Vec, } diff --git a/src/command/pin.rs b/src/command/pin.rs index 03bb21c5a..b380d5eb9 100644 --- a/src/command/pin.rs +++ b/src/command/pin.rs @@ -6,7 +6,7 @@ use crate::command::Command; #[derive(clap::Args)] pub(crate) struct Pin { - /// Tools to pin, like `node@lts` or `yarn@^1.14`. + /// Tools to pin, like `node@lts`, `node@nightly`, or `yarn@^1.14`. #[arg(value_name = "tool[@version]", required = true)] tools: Vec, }