diff --git a/.vscode/settings.json b/.vscode/settings.json index cac0e10e6..ad92582bd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { "editor.formatOnSave": true -} \ No newline at end of file +} diff --git a/crates/volta-core/fixtures/hooks/bins.json b/crates/volta-core/fixtures/hooks/bins.json index 19afc765a..81d72034c 100644 --- a/crates/volta-core/fixtures/hooks/bins.json +++ b/crates/volta-core/fixtures/hooks/bins.json @@ -10,6 +10,17 @@ "bin": "/some/bin/for/node/index" } }, + "pnpm": { + "distro": { + "bin": "/bin/to/pnpm/distro" + }, + "latest": { + "bin": "/bin/to/pnpm/latest" + }, + "index": { + "bin": "/bin/to/pnpm/index" + } + }, "yarn": { "distro": { "bin": "/bin/to/yarn/distro" diff --git a/crates/volta-core/fixtures/hooks/format_github.json b/crates/volta-core/fixtures/hooks/format_github.json index ed92eea15..07094ce91 100644 --- a/crates/volta-core/fixtures/hooks/format_github.json +++ b/crates/volta-core/fixtures/hooks/format_github.json @@ -11,6 +11,12 @@ "format": "github" } }, + "pnpm": { + "index": { + "prefix": "http://localhost/pnpm/index/", + "format": "github" + } + }, "yarn": { "index": { "prefix": "http://localhost/yarn/index/", diff --git a/crates/volta-core/fixtures/hooks/format_npm.json b/crates/volta-core/fixtures/hooks/format_npm.json index 4fcb97d34..075acac52 100644 --- a/crates/volta-core/fixtures/hooks/format_npm.json +++ b/crates/volta-core/fixtures/hooks/format_npm.json @@ -11,6 +11,12 @@ "format": "npm" } }, + "pnpm": { + "index": { + "prefix": "http://localhost/pnpm/index/", + "format": "npm" + } + }, "yarn": { "index": { "prefix": "http://localhost/yarn/index/", diff --git a/crates/volta-core/fixtures/hooks/prefixes.json b/crates/volta-core/fixtures/hooks/prefixes.json index e9c77b997..0cdbd0944 100644 --- a/crates/volta-core/fixtures/hooks/prefixes.json +++ b/crates/volta-core/fixtures/hooks/prefixes.json @@ -10,6 +10,17 @@ "prefix": "http://localhost/node/index/" } }, + "pnpm": { + "distro": { + "prefix": "http://localhost/pnpm/distro/" + }, + "latest": { + "prefix": "http://localhost/pnpm/latest/" + }, + "index": { + "prefix": "http://localhost/pnpm/index/" + } + }, "yarn": { "distro": { "prefix": "http://localhost/yarn/distro/" diff --git a/crates/volta-core/fixtures/hooks/templates.json b/crates/volta-core/fixtures/hooks/templates.json index a29828828..979b437a9 100644 --- a/crates/volta-core/fixtures/hooks/templates.json +++ b/crates/volta-core/fixtures/hooks/templates.json @@ -10,6 +10,17 @@ "template": "http://localhost/node/index/{{version}}/" } }, + "pnpm": { + "distro": { + "template": "http://localhost/pnpm/distro/{{version}}/" + }, + "latest": { + "template": "http://localhost/pnpm/latest/{{version}}/" + }, + "index": { + "template": "http://localhost/pnpm/index/{{version}}/" + } + }, "yarn": { "distro": { "template": "http://localhost/yarn/distro/{{version}}/" diff --git a/crates/volta-core/src/error/kind.rs b/crates/volta-core/src/error/kind.rs index 9043a2c16..bdbe33c9e 100644 --- a/crates/volta-core/src/error/kind.rs +++ b/crates/volta-core/src/error/kind.rs @@ -189,6 +189,9 @@ pub enum ErrorKind { command: String, }, + /// Thrown when pnpm is not set at the command-line + NoCommandLinePnpm, + /// Thrown when Yarn is not set at the command-line NoCommandLineYarn, @@ -209,7 +212,7 @@ pub enum ErrorKind { NoLocalDataDir, - /// Thrown when a user tries to pin a Yarn or npm version before pinning a Node version. + /// Thrown when a user tries to pin a npm, pnpm, or Yarn version before pinning a Node version. NoPinnedNodeVersion { tool: String, }, @@ -223,6 +226,9 @@ pub enum ErrorKind { /// Thrown when Yarn is not set in a project NoProjectYarn, + /// Thrown when pnpm is not set in a project + NoProjectPnpm, + /// Thrown when no shell profiles could be found NoShellProfile { env_profile: String, @@ -235,6 +241,9 @@ pub enum ErrorKind { /// Thrown when default Yarn is not set NoDefaultYarn, + /// Thrown when default pnpm is not set + NoDefaultPnpm, + /// Thrown when `npm link` is called with a package that isn't available NpmLinkMissingPackage { package: String, @@ -330,6 +339,11 @@ pub enum ErrorKind { tool: String, }, + /// Thrown when there is no pnpm version matching a requested semver specifier. + PnpmVersionNotFound { + matching: String, + }, + /// Thrown when executing a project-local binary fails ProjectLocalBinaryExecError { command: String, @@ -856,6 +870,12 @@ format Please ensure you have a Node version selected with `volta {} node` (see `volta help {0}` for more info).", command ), + ErrorKind::NoCommandLinePnpm => write!( + f, + "No pnpm version specified. + +Use `volta run --pnpm` to select a version (see `volta help run` for more info)." + ), ErrorKind::NoCommandLineYarn => write!( f, "No Yarn version specified. @@ -912,6 +932,12 @@ To run any Node command, first set a default version using `volta install node`" "No Node version found in this project. Use `volta pin node` to select a version (see `volta help pin` for more info)." + ), + ErrorKind::NoProjectPnpm => write!( + f, + "No pnpm version found in this project. + +Use `volta pin pnpm` to select a version (see `volta help pin` for more info)." ), ErrorKind::NoProjectYarn => write!( f, @@ -932,6 +958,12 @@ Please create one of these and try again; or you can edit your profile manually "Not in a node package. Use `volta install` to select a default version of a tool." + ), + ErrorKind::NoDefaultPnpm => write!( + f, + "pnpm is not available. + +Use `volta install pnpm` to select a default version (see `volta help install` for more info)." ), ErrorKind::NoDefaultYarn => write!( f, @@ -1096,6 +1128,13 @@ Please supply a spec in the format `[@]`.", {}", tool, PERMISSIONS_CTA ), + ErrorKind::PnpmVersionNotFound { matching } => write!( + f, + r#"Could not find pnpm version matching "{}" in the version registry. + +Please verify that the version is correct."#, + matching + ), ErrorKind::ProjectLocalBinaryExecError { command } => write!( f, "Could not execute `{}` @@ -1299,12 +1338,14 @@ Please ensure it is installed with `{} {0}`"#, package, match manager { PackageManager::Npm => "npm i -g", + PackageManager::Pnpm => "pnpm add -g", PackageManager::Yarn => "yarn global add", } ), ErrorKind::UpgradePackageWrongManager { package, manager } => { let (name, command) = match manager { PackageManager::Npm => ("npm", "npm update -g"), + PackageManager::Pnpm => ("pnpm", "pnpm update -g"), PackageManager::Yarn => ("Yarn", "yarn global upgrade"), }; write!( @@ -1455,6 +1496,7 @@ impl ErrorKind { ErrorKind::InvalidToolName { .. } => ExitCode::InvalidArguments, ErrorKind::LockAcquireError => ExitCode::FileSystemError, ErrorKind::NoBundledNpm { .. } => ExitCode::ConfigurationError, + ErrorKind::NoCommandLinePnpm => ExitCode::ConfigurationError, ErrorKind::NoCommandLineYarn => ExitCode::ConfigurationError, ErrorKind::NoDefaultNodeVersion { .. } => ExitCode::ConfigurationError, ErrorKind::NodeVersionNotFound { .. } => ExitCode::NoVersionMatch, @@ -1464,9 +1506,11 @@ impl ErrorKind { ErrorKind::NoPinnedNodeVersion { .. } => ExitCode::ConfigurationError, ErrorKind::NoPlatform => ExitCode::ConfigurationError, ErrorKind::NoProjectNodeInManifest => ExitCode::ConfigurationError, + ErrorKind::NoProjectPnpm => ExitCode::ConfigurationError, ErrorKind::NoProjectYarn => ExitCode::ConfigurationError, ErrorKind::NoShellProfile { .. } => ExitCode::EnvironmentError, ErrorKind::NotInPackage => ExitCode::ConfigurationError, + ErrorKind::NoDefaultPnpm => ExitCode::ConfigurationError, ErrorKind::NoDefaultYarn => ExitCode::ConfigurationError, ErrorKind::NpmLinkMissingPackage { .. } => ExitCode::ConfigurationError, ErrorKind::NpmLinkWrongManager { .. } => ExitCode::ConfigurationError, @@ -1490,6 +1534,7 @@ impl ErrorKind { ErrorKind::ParsePackageConfigError => ExitCode::UnknownError, ErrorKind::ParsePlatformError => ExitCode::ConfigurationError, ErrorKind::PersistInventoryError { .. } => ExitCode::FileSystemError, + ErrorKind::PnpmVersionNotFound { .. } => ExitCode::NoVersionMatch, ErrorKind::ProjectLocalBinaryExecError { .. } => ExitCode::ExecutionFailure, ErrorKind::ProjectLocalBinaryNotFound { .. } => ExitCode::FileSystemError, ErrorKind::PublishHookBothUrlAndBin => ExitCode::ConfigurationError, diff --git a/crates/volta-core/src/hook/mod.rs b/crates/volta-core/src/hook/mod.rs index d803f8f54..c148e3388 100644 --- a/crates/volta-core/src/hook/mod.rs +++ b/crates/volta-core/src/hook/mod.rs @@ -9,7 +9,7 @@ use std::path::Path; use crate::error::{Context, ErrorKind, Fallible}; use crate::layout::volta_home; use crate::project::Project; -use crate::tool::{Node, Npm, Tool}; +use crate::tool::{Node, Npm, Pnpm, Tool}; use lazycell::LazyCell; use log::debug; @@ -50,6 +50,7 @@ impl LazyHookConfig { pub struct HookConfig { node: Option>, npm: Option>, + pnpm: Option>, yarn: Option, events: Option, } @@ -118,6 +119,10 @@ impl HookConfig { self.npm.as_ref() } + pub fn pnpm(&self) -> Option<&ToolHooks> { + self.pnpm.as_ref() + } + pub fn yarn(&self) -> Option<&YarnHooks> { self.yarn.as_ref() } @@ -182,6 +187,7 @@ impl HookConfig { Self { node: None, npm: None, + pnpm: None, yarn: None, events: None, } @@ -214,6 +220,7 @@ impl HookConfig { Self { node: merge_hooks!(self, other, node), npm: merge_hooks!(self, other, npm), + pnpm: merge_hooks!(self, other, pnpm), yarn: merge_hooks!(self, other, yarn), events: merge_hooks!(self, other, events), } @@ -286,6 +293,7 @@ pub mod tests { let bin_file = fixture_dir.join("bins.json"); let hooks = HookConfig::from_file(&bin_file).unwrap().unwrap(); let node = hooks.node.unwrap(); + let pnpm = hooks.pnpm.unwrap(); let yarn = hooks.yarn.unwrap(); assert_eq!( @@ -309,6 +317,29 @@ pub mod tests { base_path: fixture_dir.clone(), }) ); + // pnpm + assert_eq!( + pnpm.distro, + Some(tool::DistroHook::Bin { + bin: "/bin/to/pnpm/distro".to_string(), + base_path: fixture_dir.clone(), + }) + ); + assert_eq!( + pnpm.latest, + Some(tool::MetadataHook::Bin { + bin: "/bin/to/pnpm/latest".to_string(), + base_path: fixture_dir.clone(), + }) + ); + assert_eq!( + pnpm.index, + Some(tool::MetadataHook::Bin { + bin: "/bin/to/pnpm/index".to_string(), + base_path: fixture_dir.clone(), + }) + ); + // Yarn assert_eq!( yarn.distro, Some(tool::DistroHook::Bin { @@ -345,6 +376,7 @@ pub mod tests { let prefix_file = fixture_dir.join("prefixes.json"); let hooks = HookConfig::from_file(&prefix_file).unwrap().unwrap(); let node = hooks.node.unwrap(); + let pnpm = hooks.pnpm.unwrap(); let yarn = hooks.yarn.unwrap(); assert_eq!( @@ -365,6 +397,26 @@ pub mod tests { "http://localhost/node/index/".to_string() )) ); + // pnpm + assert_eq!( + pnpm.distro, + Some(tool::DistroHook::Prefix( + "http://localhost/pnpm/distro/".to_string() + )) + ); + assert_eq!( + pnpm.latest, + Some(tool::MetadataHook::Prefix( + "http://localhost/pnpm/latest/".to_string() + )) + ); + assert_eq!( + pnpm.index, + Some(tool::MetadataHook::Prefix( + "http://localhost/pnpm/index/".to_string() + )) + ); + // Yarn assert_eq!( yarn.distro, Some(tool::DistroHook::Prefix( @@ -392,6 +444,7 @@ pub mod tests { let template_file = fixture_dir.join("templates.json"); let hooks = HookConfig::from_file(&template_file).unwrap().unwrap(); let node = hooks.node.unwrap(); + let pnpm = hooks.pnpm.unwrap(); let yarn = hooks.yarn.unwrap(); assert_eq!( node.distro, @@ -411,6 +464,26 @@ pub mod tests { "http://localhost/node/index/{{version}}/".to_string() )) ); + // pnpm + assert_eq!( + pnpm.distro, + Some(tool::DistroHook::Template( + "http://localhost/pnpm/distro/{{version}}/".to_string() + )) + ); + assert_eq!( + pnpm.latest, + Some(tool::MetadataHook::Template( + "http://localhost/pnpm/latest/{{version}}/".to_string() + )) + ); + assert_eq!( + pnpm.index, + Some(tool::MetadataHook::Template( + "http://localhost/pnpm/index/{{version}}/".to_string() + )) + ); + // Yarn assert_eq!( yarn.distro, Some(tool::DistroHook::Template( @@ -442,6 +515,7 @@ pub mod tests { let yarn = hooks.yarn.unwrap(); let node = hooks.node.unwrap(); let npm = hooks.npm.unwrap(); + let pnpm = hooks.pnpm.unwrap(); assert_eq!( yarn.index, Some(tool::YarnIndexHook { @@ -462,6 +536,13 @@ pub mod tests { "http://localhost/npm/index/".to_string() )) ); + // pnpm also doesn't have format + assert_eq!( + pnpm.index, + Some(tool::MetadataHook::Prefix( + "http://localhost/pnpm/index/".to_string() + )) + ); } #[test] @@ -472,6 +553,7 @@ pub mod tests { let yarn = hooks.yarn.unwrap(); let node = hooks.node.unwrap(); let npm = hooks.npm.unwrap(); + let pnpm = hooks.pnpm.unwrap(); assert_eq!( yarn.index, Some(tool::YarnIndexHook { @@ -492,6 +574,13 @@ pub mod tests { "http://localhost/npm/index/".to_string() )) ); + // pnpm also doesn't have format + assert_eq!( + pnpm.index, + Some(tool::MetadataHook::Prefix( + "http://localhost/pnpm/index/".to_string() + )) + ); } #[test] @@ -507,6 +596,7 @@ pub mod tests { .expect("Could not find project hooks.json"); let merged_hooks = project_hooks.merge(default_hooks); let node = merged_hooks.node.expect("No node config found"); + let pnpm = merged_hooks.pnpm.expect("No pnpm config found"); let yarn = merged_hooks.yarn.expect("No yarn config found"); assert_eq!( @@ -530,6 +620,26 @@ pub mod tests { base_path: project_hooks_dir, }) ); + // pnpm + assert_eq!( + pnpm.distro, + Some(tool::DistroHook::Template( + "http://localhost/pnpm/distro/{{version}}/".to_string() + )) + ); + assert_eq!( + pnpm.latest, + Some(tool::MetadataHook::Template( + "http://localhost/pnpm/latest/{{version}}/".to_string() + )) + ); + assert_eq!( + pnpm.index, + Some(tool::MetadataHook::Template( + "http://localhost/pnpm/index/{{version}}/".to_string() + )) + ); + // Yarn assert_eq!( yarn.distro, Some(tool::DistroHook::Template( @@ -566,6 +676,7 @@ pub mod tests { let merged_hooks = HookConfig::from_paths(&[project_hooks_file, default_hooks_file]).unwrap(); let node = merged_hooks.node.expect("No node config found"); + let pnpm = merged_hooks.pnpm.expect("No pnpm config found"); let yarn = merged_hooks.yarn.expect("No yarn config found"); assert_eq!( @@ -589,6 +700,26 @@ pub mod tests { base_path: project_hooks_dir, }) ); + // pnpm + assert_eq!( + pnpm.distro, + Some(tool::DistroHook::Template( + "http://localhost/pnpm/distro/{{version}}/".to_string() + )) + ); + assert_eq!( + pnpm.latest, + Some(tool::MetadataHook::Template( + "http://localhost/pnpm/latest/{{version}}/".to_string() + )) + ); + assert_eq!( + pnpm.index, + Some(tool::MetadataHook::Template( + "http://localhost/pnpm/index/{{version}}/".to_string() + )) + ); + // Yarn assert_eq!( yarn.distro, Some(tool::DistroHook::Template( diff --git a/crates/volta-core/src/hook/serial.rs b/crates/volta-core/src/hook/serial.rs index 712878bac..9a65d75a8 100644 --- a/crates/volta-core/src/hook/serial.rs +++ b/crates/volta-core/src/hook/serial.rs @@ -5,7 +5,7 @@ use std::path::Path; use super::tool; use super::RegistryFormat; use crate::error::{ErrorKind, Fallible, VoltaError}; -use crate::tool::{Node, Npm, Tool}; +use crate::tool::{Node, Npm, Pnpm, Tool}; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] @@ -129,6 +129,7 @@ impl TryFrom for super::Publish { pub struct RawHookConfig { pub node: Option>, pub npm: Option>, + pub pnpm: Option>, pub yarn: Option, pub events: Option, } @@ -172,11 +173,13 @@ impl RawHookConfig { pub fn into_hook_config(self, base_dir: &Path) -> Fallible { let node = self.node.map(|n| n.into_tool_hooks(base_dir)).transpose()?; let npm = self.npm.map(|n| n.into_tool_hooks(base_dir)).transpose()?; + let pnpm = self.pnpm.map(|p| p.into_tool_hooks(base_dir)).transpose()?; let yarn = self.yarn.map(|y| y.into_yarn_hooks(base_dir)).transpose()?; let events = self.events.map(|e| e.try_into()).transpose()?; Ok(super::HookConfig { node, npm, + pnpm, yarn, events, }) diff --git a/crates/volta-core/src/inventory.rs b/crates/volta-core/src/inventory.rs index 11cd04b31..0dd5e42ae 100644 --- a/crates/volta-core/src/inventory.rs +++ b/crates/volta-core/src/inventory.rs @@ -38,6 +38,16 @@ pub fn npm_versions() -> Fallible> { volta_home().and_then(|home| read_versions(home.npm_image_root_dir())) } +/// Checks if a given pnpm version image is available on the local machine +pub fn pnpm_available(version: &Version) -> Fallible { + volta_home().map(|home| home.pnpm_image_dir(&version.to_string()).exists()) +} + +/// Collects a set of all pnpm versions fetched on the local machine +pub fn pnpm_versions() -> Fallible> { + volta_home().and_then(|home| read_versions(home.pnpm_image_root_dir())) +} + /// Checks if a given Yarn version image is available on the local machine pub fn yarn_available(version: &Version) -> Fallible { volta_home().map(|home| home.yarn_image_dir(&version.to_string()).exists()) diff --git a/crates/volta-core/src/platform/image.rs b/crates/volta-core/src/platform/image.rs index 465cfbdc0..0d8148951 100644 --- a/crates/volta-core/src/platform/image.rs +++ b/crates/volta-core/src/platform/image.rs @@ -13,6 +13,8 @@ pub struct Image { pub node: Sourced, /// The custom version of npm, if any. `None` represents using the npm that is bundled with Node pub npm: Option>, + /// The pinned version of pnpm, if any. + pub pnpm: Option>, /// The pinned version of Yarn, if any. pub yarn: Option>, } @@ -27,6 +29,11 @@ impl Image { bins.push(home.npm_image_bin_dir(&npm_str)); } + if let Some(pnpm) = &self.pnpm { + let pnpm_str = pnpm.value.to_string(); + bins.push(home.pnpm_image_bin_dir(&pnpm_str)); + } + if let Some(yarn) = &self.yarn { let yarn_str = yarn.value.to_string(); bins.push(home.yarn_image_bin_dir(&yarn_str)); @@ -39,7 +46,7 @@ impl Image { } /// Produces a modified version of the current `PATH` environment variable that - /// will find toolchain executables (Node, Yarn) in the installation directories + /// will find toolchain executables (Node, npm, pnpm, Yarn) in the installation directories /// for the given versions instead of in the Volta shim directory. pub fn path(&self) -> Fallible { let old_path = envoy::path().unwrap_or_else(|| envoy::Var::from("")); diff --git a/crates/volta-core/src/platform/mod.rs b/crates/volta-core/src/platform/mod.rs index 0a3f34f82..649ce9caa 100644 --- a/crates/volta-core/src/platform/mod.rs +++ b/crates/volta-core/src/platform/mod.rs @@ -2,7 +2,7 @@ use std::fmt; use crate::error::{ErrorKind, Fallible}; use crate::session::Session; -use crate::tool::{Node, Npm, Yarn}; +use crate::tool::{Node, Npm, Pnpm, Yarn}; use semver::Version; mod image; @@ -161,6 +161,7 @@ impl Default for InheritOption { pub struct PlatformSpec { pub node: Version, pub npm: Option, + pub pnpm: Option, pub yarn: Option, } @@ -170,6 +171,7 @@ impl PlatformSpec { Platform { node: Sourced::with_default(self.node.clone()), npm: self.npm.clone().map(Sourced::with_default), + pnpm: self.pnpm.clone().map(Sourced::with_default), yarn: self.yarn.clone().map(Sourced::with_default), } } @@ -179,6 +181,7 @@ impl PlatformSpec { Platform { node: Sourced::with_project(self.node.clone()), npm: self.npm.clone().map(Sourced::with_project), + pnpm: self.pnpm.clone().map(Sourced::with_project), yarn: self.yarn.clone().map(Sourced::with_project), } } @@ -188,6 +191,7 @@ impl PlatformSpec { Platform { node: Sourced::with_binary(self.node.clone()), npm: self.npm.clone().map(Sourced::with_binary), + pnpm: self.pnpm.clone().map(Sourced::with_binary), yarn: self.yarn.clone().map(Sourced::with_binary), } } @@ -198,6 +202,7 @@ impl PlatformSpec { pub struct CliPlatform { pub node: Option, pub npm: InheritOption, + pub pnpm: InheritOption, pub yarn: InheritOption, } @@ -207,6 +212,7 @@ impl CliPlatform { Platform { node: self.node.map_or(base.node, Sourced::with_command_line), npm: self.npm.map(Sourced::with_command_line).inherit(base.npm), + pnpm: self.pnpm.map(Sourced::with_command_line).inherit(base.pnpm), yarn: self.yarn.map(Sourced::with_command_line).inherit(base.yarn), } } @@ -220,6 +226,7 @@ impl From for Option { Some(node) => Some(Platform { node: Sourced::with_command_line(node), npm: base.npm.map(Sourced::with_command_line).into(), + pnpm: base.pnpm.map(Sourced::with_command_line).into(), yarn: base.yarn.map(Sourced::with_command_line).into(), }), } @@ -231,6 +238,7 @@ impl From for Option { pub struct Platform { pub node: Sourced, pub npm: Option>, + pub pnpm: Option>, pub yarn: Option>, } @@ -239,12 +247,20 @@ impl Platform { /// /// Active platform is determined by first looking at the Project Platform /// - /// - If it exists and has a Yarn version, then we use the project platform - /// - If it exists but doesn't have a Yarn version, then we merge the two, - /// pulling Yarn from the user default platform, if available + /// - If there is a project platform then we use it + /// - If there is no pnpm/Yarn version in the project platform, we pull + /// pnpm/Yarn from the default platform if available, and merge the two + /// platforms into a final one /// - If there is no Project platform, then we use the user Default Platform pub fn current(session: &mut Session) -> Fallible> { if let Some(mut platform) = session.project_platform()?.map(PlatformSpec::as_project) { + if platform.pnpm.is_none() { + platform.pnpm = session + .default_platform()? + .and_then(|default_platform| default_platform.pnpm.clone()) + .map(Sourced::with_default); + } + if platform.yarn.is_none() { platform.yarn = session .default_platform()? @@ -268,6 +284,10 @@ impl Platform { Npm::new(version.clone()).ensure_fetched(session)?; } + if let Some(Sourced { value: version, .. }) = &self.pnpm { + Pnpm::new(version.clone()).ensure_fetched(session)?; + } + if let Some(Sourced { value: version, .. }) = &self.yarn { Yarn::new(version.clone()).ensure_fetched(session)?; } @@ -275,6 +295,7 @@ impl Platform { Ok(Image { node: self.node, npm: self.npm, + pnpm: self.pnpm, yarn: self.yarn, }) } diff --git a/crates/volta-core/src/platform/tests.rs b/crates/volta-core/src/platform/tests.rs index e147d9232..b4f656ce0 100644 --- a/crates/volta-core/src/platform/tests.rs +++ b/crates/volta-core/src/platform/tests.rs @@ -15,95 +15,34 @@ fn test_paths() { } #[cfg(unix)] -fn test_image_path() { - let starting_path = format!( - "/usr/bin:/blah:{}:/doesnt/matter/bin", +fn build_test_path() -> String { + format!( + "{}:/usr/bin:/bin", volta_home().unwrap().shim_dir().to_string_lossy() - ); - std::env::set_var("PATH", &starting_path); - - let node_bin = volta_home().unwrap().node_image_bin_dir("1.2.3"); - let expected_node_bin = node_bin.to_str().unwrap(); - - let npm_bin = volta_home().unwrap().npm_image_bin_dir("6.4.3"); - let expected_npm_bin = npm_bin.to_str().unwrap(); - - let yarn_bin = volta_home().unwrap().yarn_image_bin_dir("4.5.7"); - let expected_yarn_bin = yarn_bin.to_str().unwrap(); - - let v123 = Version::parse("1.2.3").unwrap(); - let v457 = Version::parse("4.5.7").unwrap(); - let v643 = Version::parse("6.4.3").unwrap(); - - let only_node = Image { - node: Sourced::with_default(v123.clone()), - npm: None, - yarn: None, - }; - - assert_eq!( - only_node.path().unwrap().into_string().unwrap(), - format!("{}:{}", expected_node_bin, starting_path) - ); - - let node_npm = Image { - node: Sourced::with_default(v123.clone()), - npm: Some(Sourced::with_default(v643.clone())), - yarn: None, - }; - - assert_eq!( - node_npm.path().unwrap().into_string().unwrap(), - format!( - "{}:{}:{}", - expected_npm_bin, expected_node_bin, starting_path - ) - ); - - let node_yarn = Image { - node: Sourced::with_default(v123.clone()), - npm: None, - yarn: Some(Sourced::with_default(v457.clone())), - }; - - assert_eq!( - node_yarn.path().unwrap().into_string().unwrap(), - format!( - "{}:{}:{}", - expected_yarn_bin, expected_node_bin, starting_path - ) - ); - - let node_npm_yarn = Image { - node: Sourced::with_default(v123), - npm: Some(Sourced::with_default(v643)), - yarn: Some(Sourced::with_default(v457)), - }; - - assert_eq!( - node_npm_yarn.path().unwrap().into_string().unwrap(), - format!( - "{}:{}:{}:{}", - expected_npm_bin, expected_yarn_bin, expected_node_bin, starting_path - ) - ); + ) } #[cfg(windows)] -fn test_image_path() { +fn build_test_path() -> String { let pathbufs = vec![ volta_home().unwrap().shim_dir().to_owned(), PathBuf::from("C:\\\\somebin"), volta_install().unwrap().root().to_owned(), PathBuf::from("D:\\\\ProbramFlies"), ]; - - let path_with_shims = std::env::join_paths(pathbufs.iter()) + std::env::join_paths(pathbufs.iter()) .unwrap() .into_string() - .expect("Could not create path containing shim dir"); + .expect("Could not create path containing shim dir") +} - std::env::set_var("PATH", &path_with_shims); +fn test_image_path() { + #[cfg(unix)] + let path_delimiter = ":"; + #[cfg(windows)] + let path_delimiter = ";"; + let path = build_test_path(); + std::env::set_var("PATH", &path); let node_bin = volta_home().unwrap().node_image_bin_dir("1.2.3"); let expected_node_bin = node_bin.to_str().unwrap(); @@ -111,101 +50,109 @@ fn test_image_path() { let npm_bin = volta_home().unwrap().npm_image_bin_dir("6.4.3"); let expected_npm_bin = npm_bin.to_str().unwrap(); + let pnpm_bin = volta_home().unwrap().pnpm_image_bin_dir("7.7.1"); + let expected_pnpm_bin = pnpm_bin.to_str().unwrap(); + let yarn_bin = volta_home().unwrap().yarn_image_bin_dir("4.5.7"); let expected_yarn_bin = yarn_bin.to_str().unwrap(); let v123 = Version::parse("1.2.3").unwrap(); let v457 = Version::parse("4.5.7").unwrap(); let v643 = Version::parse("6.4.3").unwrap(); + let v771 = Version::parse("7.7.1").unwrap(); let only_node = Image { node: Sourced::with_default(v123.clone()), npm: None, + pnpm: None, yarn: None, }; assert_eq!( only_node.path().unwrap().into_string().unwrap(), - format!("{};{}", expected_node_bin, path_with_shims), + [expected_node_bin, &path].join(path_delimiter) ); let node_npm = Image { node: Sourced::with_default(v123.clone()), npm: Some(Sourced::with_default(v643.clone())), + pnpm: None, yarn: None, }; assert_eq!( node_npm.path().unwrap().into_string().unwrap(), - format!( - "{};{};{}", - expected_npm_bin, expected_node_bin, path_with_shims - ) + [expected_npm_bin, expected_node_bin, &path].join(path_delimiter) + ); + + let node_pnpm = Image { + node: Sourced::with_default(v123.clone()), + npm: None, + pnpm: Some(Sourced::with_default(v771.clone())), + yarn: None, + }; + + assert_eq!( + node_pnpm.path().unwrap().into_string().unwrap(), + [expected_pnpm_bin, expected_node_bin, &path].join(path_delimiter) ); let node_yarn = Image { node: Sourced::with_default(v123.clone()), npm: None, + pnpm: None, yarn: Some(Sourced::with_default(v457.clone())), }; assert_eq!( node_yarn.path().unwrap().into_string().unwrap(), - format!( - "{};{};{}", - expected_yarn_bin, expected_node_bin, path_with_shims - ) + [expected_yarn_bin, expected_node_bin, &path].join(path_delimiter) + ); + + let node_npm_pnpm = Image { + node: Sourced::with_default(v123.clone()), + npm: Some(Sourced::with_default(v643.clone())), + pnpm: Some(Sourced::with_default(v771.clone())), + yarn: None, + }; + + assert_eq!( + node_npm_pnpm.path().unwrap().into_string().unwrap(), + [ + expected_npm_bin, + expected_pnpm_bin, + expected_node_bin, + &path + ] + .join(path_delimiter) ); let node_npm_yarn = Image { node: Sourced::with_default(v123), npm: Some(Sourced::with_default(v643)), + pnpm: None, yarn: Some(Sourced::with_default(v457)), }; assert_eq!( node_npm_yarn.path().unwrap().into_string().unwrap(), - format!( - "{};{};{};{}", - expected_npm_bin, expected_yarn_bin, expected_node_bin, path_with_shims - ) - ) -} - -#[cfg(unix)] -fn test_system_path() { - std::env::set_var( - "PATH", - format!( - "{}:/usr/bin:/bin", - volta_home().unwrap().shim_dir().to_string_lossy() - ), - ); - - let expected_path = String::from("/usr/bin:/bin"); - - assert_eq!( - System::path().unwrap().into_string().unwrap(), - expected_path + [ + expected_npm_bin, + expected_yarn_bin, + expected_node_bin, + &path + ] + .join(path_delimiter) ); } -#[cfg(windows)] fn test_system_path() { - let pathbufs = vec![ - volta_home().unwrap().shim_dir().to_owned(), - PathBuf::from("C:\\\\somebin"), - volta_install().unwrap().root().to_owned(), - PathBuf::from("D:\\\\ProbramFlies"), - ]; - - let path_with_shims = std::env::join_paths(pathbufs.iter()) - .unwrap() - .into_string() - .expect("Could not create path containing shim dir"); - - std::env::set_var("PATH", path_with_shims); + let path = build_test_path(); + std::env::set_var("PATH", path); + #[cfg(unix)] + let expected_path = String::from("/usr/bin:/bin"); + #[cfg(windows)] let expected_path = String::from("C:\\\\somebin;D:\\\\ProbramFlies"); assert_eq!( @@ -285,12 +232,14 @@ mod cli_platform { let test = CliPlatform { node: Some(NODE_VERSION.clone()), npm: InheritOption::default(), + pnpm: InheritOption::default(), yarn: InheritOption::default(), }; let base = Platform { node: Sourced::with_default(Version::from((10, 10, 10))), npm: None, + pnpm: None, yarn: None, }; @@ -305,12 +254,14 @@ mod cli_platform { let test = CliPlatform { node: None, npm: InheritOption::default(), + pnpm: InheritOption::default(), yarn: InheritOption::default(), }; let base = Platform { node: Sourced::with_default(NODE_VERSION.clone()), npm: None, + pnpm: None, yarn: None, }; @@ -325,12 +276,14 @@ mod cli_platform { let test = CliPlatform { node: Some(NODE_VERSION.clone()), npm: InheritOption::Some(NPM_VERSION.clone()), + pnpm: InheritOption::default(), yarn: InheritOption::default(), }; let base = Platform { node: Sourced::with_default(Version::from((10, 10, 10))), npm: Some(Sourced::with_default(Version::from((5, 6, 3)))), + pnpm: None, yarn: None, }; @@ -346,12 +299,14 @@ mod cli_platform { let test = CliPlatform { node: Some(NODE_VERSION.clone()), npm: InheritOption::Inherit, + pnpm: InheritOption::default(), yarn: InheritOption::default(), }; let base = Platform { node: Sourced::with_default(Version::from((10, 10, 10))), npm: Some(Sourced::with_default(NPM_VERSION.clone())), + pnpm: None, yarn: None, }; @@ -367,12 +322,14 @@ mod cli_platform { let test = CliPlatform { node: Some(NODE_VERSION.clone()), npm: InheritOption::None, + pnpm: InheritOption::default(), yarn: InheritOption::default(), }; let base = Platform { node: Sourced::with_default(Version::from((10, 10, 10))), npm: Some(Sourced::with_default(NPM_VERSION.clone())), + pnpm: None, yarn: None, }; @@ -386,12 +343,14 @@ mod cli_platform { let test = CliPlatform { node: Some(NODE_VERSION.clone()), npm: InheritOption::default(), + pnpm: InheritOption::default(), yarn: InheritOption::Some(YARN_VERSION.clone()), }; let base = Platform { node: Sourced::with_default(Version::from((10, 10, 10))), npm: None, + pnpm: None, yarn: Some(Sourced::with_default(Version::from((1, 10, 3)))), }; @@ -407,12 +366,14 @@ mod cli_platform { let test = CliPlatform { node: Some(NODE_VERSION.clone()), npm: InheritOption::default(), + pnpm: InheritOption::default(), yarn: InheritOption::Inherit, }; let base = Platform { node: Sourced::with_default(Version::from((10, 10, 10))), npm: None, + pnpm: None, yarn: Some(Sourced::with_default(YARN_VERSION.clone())), }; @@ -428,12 +389,14 @@ mod cli_platform { let test = CliPlatform { node: Some(NODE_VERSION.clone()), npm: InheritOption::default(), + pnpm: InheritOption::default(), yarn: InheritOption::None, }; let base = Platform { node: Sourced::with_default(Version::from((10, 10, 10))), npm: None, + pnpm: None, yarn: Some(Sourced::with_default(YARN_VERSION.clone())), }; @@ -452,6 +415,7 @@ mod cli_platform { let cli = CliPlatform { node: None, npm: InheritOption::default(), + pnpm: InheritOption::default(), yarn: InheritOption::default(), }; @@ -465,6 +429,7 @@ mod cli_platform { let cli = CliPlatform { node: Some(NODE_VERSION.clone()), npm: InheritOption::default(), + pnpm: InheritOption::default(), yarn: InheritOption::default(), }; @@ -480,6 +445,7 @@ mod cli_platform { let cli = CliPlatform { node: Some(NODE_VERSION.clone()), npm: InheritOption::Some(NPM_VERSION.clone()), + pnpm: InheritOption::default(), yarn: InheritOption::default(), }; @@ -495,6 +461,7 @@ mod cli_platform { let cli = CliPlatform { node: Some(NODE_VERSION.clone()), npm: InheritOption::None, + pnpm: InheritOption::default(), yarn: InheritOption::default(), }; @@ -508,6 +475,7 @@ mod cli_platform { let cli = CliPlatform { node: Some(NODE_VERSION.clone()), npm: InheritOption::Inherit, + pnpm: InheritOption::default(), yarn: InheritOption::default(), }; @@ -521,6 +489,7 @@ mod cli_platform { let cli = CliPlatform { node: Some(NODE_VERSION.clone()), npm: InheritOption::default(), + pnpm: InheritOption::default(), yarn: InheritOption::Some(YARN_VERSION.clone()), }; @@ -536,6 +505,7 @@ mod cli_platform { let cli = CliPlatform { node: Some(NODE_VERSION.clone()), npm: InheritOption::default(), + pnpm: InheritOption::default(), yarn: InheritOption::None, }; @@ -549,6 +519,7 @@ mod cli_platform { let cli = CliPlatform { node: Some(NODE_VERSION.clone()), npm: InheritOption::default(), + pnpm: InheritOption::default(), yarn: InheritOption::Inherit, }; diff --git a/crates/volta-core/src/project/mod.rs b/crates/volta-core/src/project/mod.rs index e4c0ac854..5d3b487d1 100644 --- a/crates/volta-core/src/project/mod.rs +++ b/crates/volta-core/src/project/mod.rs @@ -191,6 +191,7 @@ impl Project { self.platform = Some(PlatformSpec { node: version, npm: None, + pnpm: None, yarn: None, }); } @@ -211,6 +212,22 @@ impl Project { } } + /// Pins the pnpm version in this project's manifest file + pub fn pin_pnpm(&mut self, version: Option) -> Fallible<()> { + if let Some(platform) = self.platform.as_mut() { + update_manifest(&self.manifest_file, ManifestKey::Pnpm, version.as_ref())?; + + platform.pnpm = version; + + Ok(()) + } else { + Err(ErrorKind::NoPinnedNodeVersion { + tool: "pnpm".into(), + } + .into()) + } + } + /// Pins the Yarn version in this project's manifest file pub fn pin_yarn(&mut self, version: Option) -> Fallible<()> { if let Some(platform) = self.platform.as_mut() { @@ -258,6 +275,7 @@ pub(crate) fn find_closest_root(mut dir: PathBuf) -> Option { struct PartialPlatform { node: Option, npm: Option, + pnpm: Option, yarn: Option, } @@ -266,6 +284,7 @@ impl PartialPlatform { PartialPlatform { node: self.node.or(other.node), npm: self.npm.or(other.npm), + pnpm: self.pnpm.or(other.pnpm), yarn: self.yarn.or(other.yarn), } } @@ -280,6 +299,7 @@ impl TryFrom for PlatformSpec { Ok(PlatformSpec { node, npm: partial.npm, + pnpm: partial.pnpm, yarn: partial.yarn, }) } diff --git a/crates/volta-core/src/project/serial.rs b/crates/volta-core/src/project/serial.rs index c3704153a..81ed91daa 100644 --- a/crates/volta-core/src/project/serial.rs +++ b/crates/volta-core/src/project/serial.rs @@ -63,6 +63,7 @@ impl Manifest { pub(super) enum ManifestKey { Node, Npm, + Pnpm, Yarn, } @@ -71,6 +72,7 @@ impl fmt::Display for ManifestKey { f.write_str(match self { ManifestKey::Node => "node", ManifestKey::Npm => "npm", + ManifestKey::Pnpm => "pnpm", ManifestKey::Yarn => "yarn", }) } @@ -168,6 +170,8 @@ struct ToolchainSpec { #[serde(skip_serializing_if = "Option::is_none")] npm: Option, #[serde(skip_serializing_if = "Option::is_none")] + pnpm: Option, + #[serde(skip_serializing_if = "Option::is_none")] yarn: Option, #[serde(skip_serializing_if = "Option::is_none")] extends: Option, @@ -178,9 +182,15 @@ impl ToolchainSpec { fn parse_split(self) -> Fallible<(PartialPlatform, Option)> { let node = self.node.map(parse_version).transpose()?; let npm = self.npm.map(parse_version).transpose()?; + let pnpm = self.pnpm.map(parse_version).transpose()?; let yarn = self.yarn.map(parse_version).transpose()?; - let platform = PartialPlatform { node, npm, yarn }; + let platform = PartialPlatform { + node, + npm, + pnpm, + yarn, + }; Ok((platform, self.extends)) } diff --git a/crates/volta-core/src/run/binary.rs b/crates/volta-core/src/run/binary.rs index 4909677ef..e6c96e28d 100644 --- a/crates/volta-core/src/run/binary.rs +++ b/crates/volta-core/src/run/binary.rs @@ -156,6 +156,7 @@ impl DefaultBinary { let platform = Platform { node: Sourced::with_binary(bin_config.platform.node), npm: bin_config.platform.npm.map(Sourced::with_binary), + pnpm: bin_config.platform.pnpm.map(Sourced::with_binary), yarn: yarn.map(Sourced::with_binary), }; diff --git a/crates/volta-core/src/run/executor.rs b/crates/volta-core/src/run/executor.rs index 22bad773b..175223ed4 100644 --- a/crates/volta-core/src/run/executor.rs +++ b/crates/volta-core/src/run/executor.rs @@ -123,6 +123,7 @@ pub enum ToolKind { Node, Npm, Npx, + Pnpm, Yarn, ProjectLocalBinary(String), DefaultBinary(String), @@ -179,6 +180,7 @@ impl ToolCommand { ToolKind::Node => super::node::execution_context(self.platform, session)?, ToolKind::Npm => super::npm::execution_context(self.platform, session)?, ToolKind::Npx => super::npx::execution_context(self.platform, session)?, + ToolKind::Pnpm => super::pnpm::execution_context(self.platform, session)?, ToolKind::Yarn => super::yarn::execution_context(self.platform, session)?, ToolKind::DefaultBinary(bin) => { super::binary::default_execution_context(bin, self.platform, session)? @@ -226,6 +228,7 @@ impl PackageInstallCommand { let mut command = match manager { PackageManager::Npm => create_command("npm"), + PackageManager::Pnpm => create_command("pnpm"), PackageManager::Yarn => create_command("yarn"), }; command.args(args); @@ -430,6 +433,7 @@ impl PackageUpgradeCommand { let mut command = match manager { PackageManager::Npm => create_command("npm"), + PackageManager::Pnpm => create_command("pnpm"), PackageManager::Yarn => create_command("yarn"), }; command.args(args); @@ -491,7 +495,7 @@ impl From for Executor { } } -/// Executor for running an internal install (installing Node, npm, or Yarn using the `volta +/// Executor for running an internal install (installing Node, npm, pnpm or Yarn using the `volta /// install` logic) /// /// Note: This is not intended to be used for Package installs. Those should go through the diff --git a/crates/volta-core/src/run/mod.rs b/crates/volta-core/src/run/mod.rs index 5083c86e1..8b20d714d 100644 --- a/crates/volta-core/src/run/mod.rs +++ b/crates/volta-core/src/run/mod.rs @@ -16,12 +16,13 @@ mod node; mod npm; mod npx; mod parser; +mod pnpm; mod yarn; /// Environment variable set internally when a shim has been executed and the context evaluated /// /// This is set when executing a shim command. If this is already, then the built-in shims (Node, -/// npm, npx, and Yarn) will assume that the context has already been evaluated & the PATH has +/// npm, npx, pnpm and Yarn) will assume that the context has already been evaluated & the PATH has /// already been modified, so they will use the pass-through behavior. /// /// Shims should only be called recursively when the environment is misconfigured, so this will @@ -84,6 +85,7 @@ fn get_executor( Some("node") => node::command(args, session), Some("npm") => npm::command(args, session), Some("npx") => npx::command(args, session), + Some("pnpm") => pnpm::command(args, session), Some("yarn") => yarn::command(args, session), _ => binary::command(exe, args, session), } @@ -126,6 +128,7 @@ fn debug_active_image(image: &Image) { "Active Image: Node: {} npm: {} + pnpm: {} Yarn: {}", format_tool_version(&image.node), image @@ -134,6 +137,11 @@ fn debug_active_image(image: &Image) { .as_ref() .map(format_tool_version) .unwrap_or_else(|| "Bundled with Node".into()), + image + .pnpm + .as_ref() + .map(format_tool_version) + .unwrap_or_else(|| "None".into()), image .yarn .as_ref() diff --git a/crates/volta-core/src/run/parser.rs b/crates/volta-core/src/run/parser.rs index 6d1b54120..1b8ef397c 100644 --- a/crates/volta-core/src/run/parser.rs +++ b/crates/volta-core/src/run/parser.rs @@ -25,6 +25,15 @@ const NPM_UNINSTALL_ALIASES: [&str; 5] = ["un", "uninstall", "remove", "rm", "r" const NPM_LINK_ALIASES: [&str; 2] = ["link", "ln"]; /// Aliases that npm supports for the `update` command const NPM_UPDATE_ALIASES: [&str; 4] = ["update", "udpate", "upgrade", "up"]; +/// Aliases that pnpm supports for the 'remove' command, +/// see: https://pnpm.io/cli/remove +const PNPM_UNINSTALL_ALIASES: [&str; 4] = ["remove", "uninstall", "rm", "un"]; +/// Aliases that pnpm supports for the 'update' command, +/// see: https://pnpm.io/cli/update +const PNPM_UPDATE_ALIASES: [&str; 3] = ["update", "upgrade", "up"]; +/// Aliases that pnpm supports for the 'link' command +/// see: https://pnpm.io/cli/link +const PNPM_LINK_ALIASES: [&str; 2] = ["link", "ln"]; pub enum CommandArg<'a> { Global(GlobalCommand<'a>), @@ -126,6 +135,82 @@ impl<'a> CommandArg<'a> { } } + /// Parse the given set of arguments to see if they correspond to an intercepted pnpm command + #[allow(dead_code)] + pub fn for_pnpm(args: &'a [S]) -> CommandArg<'a> + where + S: AsRef, + { + // If VOLTA_UNSAFE_GLOBAL is set, then we always skip any global parsing + if env::var_os(UNSAFE_GLOBAL).is_some() { + return CommandArg::Standard; + } + + let (flags, positionals): (Vec<&OsStr>, Vec<&OsStr>) = args + .iter() + .map(AsRef::::as_ref) + .partition(|arg| is_flag(arg)); + + // The first positional argument will always be the subcommand for pnpm + match positionals.split_first() { + None => CommandArg::Standard, + Some((&subcommand, tools)) => { + let is_global = flags.iter().any(|&f| f == "--global" || f == "-g"); + // Do not intercept if a custom global dir is explicitly specified + // See: https://pnpm.io/npmrc#global-dir + let prefixed = flags.iter().any(|&f| f == "--global-dir"); + + // pnpm subcommands that support the `global` flag: + // `add`, `update`, `remove`, `link`, `list`, `outdated`, + // `why`, `env`, `root`, `bin`. + match is_global && !prefixed { + false => CommandArg::Standard, + true => match subcommand.to_str() { + // `add` + Some("add") => { + let manager = PackageManager::Pnpm; + let mut common_args = vec![subcommand]; + common_args.extend(flags); + + CommandArg::Global(GlobalCommand::Install(InstallArgs { + manager, + common_args, + tools: tools.to_vec(), + })) + } + // `update` + Some(cmd) if PNPM_UPDATE_ALIASES.iter().any(|&a| a == cmd) => { + let manager = PackageManager::Pnpm; + let mut common_args = vec![subcommand]; + common_args.extend(flags); + CommandArg::Global(GlobalCommand::Upgrade(UpgradeArgs { + manager, + common_args, + tools: tools.to_vec(), + })) + } + // `remove` + Some(cmd) if PNPM_UNINSTALL_ALIASES.iter().any(|&a| a == cmd) => { + CommandArg::Global(GlobalCommand::Uninstall(UninstallArgs { + tools: tools.to_vec(), + })) + } + // `link` + Some(cmd) if PNPM_LINK_ALIASES.iter().any(|&a| a == cmd) => { + let mut common_args = vec![subcommand]; + common_args.extend(flags); + CommandArg::Intercepted(InterceptedCommand::Link(LinkArgs { + common_args, + tools: tools.to_vec(), + })) + } + _ => CommandArg::Standard, + }, + } + } + } + } + /// Parse the given set of arguments to see if they correspond to an intercepted Yarn command pub fn for_yarn(args: &'a [S]) -> Self where diff --git a/crates/volta-core/src/run/pnpm.rs b/crates/volta-core/src/run/pnpm.rs new file mode 100644 index 000000000..77e0e8eaf --- /dev/null +++ b/crates/volta-core/src/run/pnpm.rs @@ -0,0 +1,66 @@ +use std::env; +use std::ffi::OsString; + +use super::executor::{Executor, ToolCommand, ToolKind}; +use super::{debug_active_image, debug_no_platform, RECURSION_ENV_VAR}; +use crate::error::{ErrorKind, Fallible}; +use crate::platform::{Platform, Source, System}; +use crate::session::{ActivityKind, Session}; + +pub(super) fn command(args: &[OsString], session: &mut Session) -> Fallible { + session.add_event_start(ActivityKind::Pnpm); + // Don't re-evaluate the context or global install interception if this is a recursive call + let platform = match env::var_os(RECURSION_ENV_VAR) { + Some(_) => None, + None => { + // FIXME: Figure out how to intercept pnpm global commands properly. + // This guard prevents all global commands from running, it should + // be removed when we fully implement global command interception. + let is_global = args.iter().any(|f| f == "--global" || f == "-g"); + if is_global { + return Err(ErrorKind::Unimplemented { + feature: "pnpm global commands".into(), + } + .into()); + } + + Platform::current(session)? + } + }; + + Ok(ToolCommand::new("pnpm", args, platform, ToolKind::Pnpm).into()) +} + +/// Determine the execution context (PATH and failure error message) for pnpm +pub(super) fn execution_context( + platform: Option, + session: &mut Session, +) -> Fallible<(OsString, ErrorKind)> { + match platform { + Some(plat) => { + validate_platform_pnpm(&plat)?; + + let image = plat.checkout(session)?; + let path = image.path()?; + debug_active_image(&image); + + Ok((path, ErrorKind::BinaryExecError)) + } + None => { + let path = System::path()?; + debug_no_platform(); + Ok((path, ErrorKind::NoPlatform)) + } + } +} + +fn validate_platform_pnpm(platform: &Platform) -> Fallible<()> { + match &platform.pnpm { + Some(_) => Ok(()), + None => match platform.node.source { + Source::Project => Err(ErrorKind::NoProjectPnpm.into()), + Source::Default | Source::Binary => Err(ErrorKind::NoDefaultPnpm.into()), + Source::CommandLine => Err(ErrorKind::NoCommandLinePnpm.into()), + }, + } +} diff --git a/crates/volta-core/src/session.rs b/crates/volta-core/src/session.rs index 4a9ac8937..fee84bae2 100644 --- a/crates/volta-core/src/session.rs +++ b/crates/volta-core/src/session.rs @@ -25,6 +25,7 @@ pub enum ActivityKind { Node, Npm, Npx, + Pnpm, Yarn, Volta, Tool, @@ -52,6 +53,7 @@ impl Display for ActivityKind { ActivityKind::Node => "node", ActivityKind::Npm => "npm", ActivityKind::Npx => "npx", + ActivityKind::Pnpm => "pnpm", ActivityKind::Yarn => "yarn", ActivityKind::Volta => "volta", ActivityKind::Tool => "tool", diff --git a/crates/volta-core/src/shim.rs b/crates/volta-core/src/shim.rs index 44db64df6..17eae7ec2 100644 --- a/crates/volta-core/src/shim.rs +++ b/crates/volta-core/src/shim.rs @@ -34,6 +34,7 @@ fn get_shim_list_deduped(dir: &Path) -> Fallible> { shims.insert("node".into()); shims.insert("npm".into()); shims.insert("npx".into()); + shims.insert("pnpm".into()); shims.insert("yarn".into()); Ok(shims) } diff --git a/crates/volta-core/src/tool/mod.rs b/crates/volta-core/src/tool/mod.rs index 104826275..fd27ffea3 100644 --- a/crates/volta-core/src/tool/mod.rs +++ b/crates/volta-core/src/tool/mod.rs @@ -10,6 +10,7 @@ use log::{debug, info}; pub mod node; pub mod npm; pub mod package; +pub mod pnpm; mod registry; mod serial; pub mod yarn; @@ -19,6 +20,7 @@ pub use node::{ }; pub use npm::{BundledNpm, Npm}; pub use package::{BinConfig, Package, PackageConfig, PackageManifest}; +pub use pnpm::Pnpm; pub use registry::PackageDetails; pub use yarn::Yarn; @@ -67,6 +69,7 @@ pub trait Tool: Display { pub enum Spec { Node(VersionSpec), Npm(VersionSpec), + Pnpm(VersionSpec), Yarn(VersionSpec), Package(String, VersionSpec), } @@ -83,6 +86,10 @@ impl Spec { Some(version) => Ok(Box::new(Npm::new(version))), None => Ok(Box::new(BundledNpm)), }, + Spec::Pnpm(version) => { + let version = pnpm::resolve(version, session)?; + Ok(Box::new(Pnpm::new(version))) + } Spec::Yarn(version) => { let version = yarn::resolve(version, session)?; Ok(Box::new(Yarn::new(version))) @@ -109,6 +116,10 @@ impl Spec { feature: "Uninstalling npm".into(), } .into()), + Spec::Pnpm(_) => Err(ErrorKind::Unimplemented { + feature: "Uninstalling pnpm".into(), + } + .into()), Spec::Yarn(_) => Err(ErrorKind::Unimplemented { feature: "Uninstalling yarn".into(), } @@ -122,6 +133,7 @@ impl Spec { match self { Spec::Node(_) => "Node", Spec::Npm(_) => "npm", + Spec::Pnpm(_) => "pnpm", Spec::Yarn(_) => "Yarn", Spec::Package(name, _) => name, } @@ -133,6 +145,7 @@ impl Display for Spec { let s = match self { Spec::Node(ref version) => tool_version("node", version), Spec::Npm(ref version) => tool_version("npm", version), + Spec::Pnpm(ref version) => tool_version("pnpm", version), Spec::Yarn(ref version) => tool_version("yarn", version), Spec::Package(ref name, ref version) => tool_version(name, version), }; diff --git a/crates/volta-core/src/tool/package/configure.rs b/crates/volta-core/src/tool/package/configure.rs index 4bcd003cf..175445755 100644 --- a/crates/volta-core/src/tool/package/configure.rs +++ b/crates/volta-core/src/tool/package/configure.rs @@ -31,6 +31,7 @@ pub(super) fn write_config_and_shims( let platform = PlatformSpec { node: image.node.value.clone(), npm: image.npm.clone().map(|s| s.value), + pnpm: image.pnpm.clone().map(|s| s.value), yarn: image.yarn.clone().map(|s| s.value), }; diff --git a/crates/volta-core/src/tool/package/manager.rs b/crates/volta-core/src/tool/package/manager.rs index 10d04c9bd..b40d46903 100644 --- a/crates/volta-core/src/tool/package/manager.rs +++ b/crates/volta-core/src/tool/package/manager.rs @@ -1,3 +1,4 @@ +use std::ffi::OsStr; use std::fs::File; use std::path::{Path, PathBuf}; use std::process::Command; @@ -11,6 +12,7 @@ use crate::fs::read_dir_eager; )] pub enum PackageManager { Npm, + Pnpm, Yarn, } @@ -28,9 +30,16 @@ impl PackageManager { /// contain the top-level `node-modules` #[cfg(unix)] pub fn source_root(self, package_root: PathBuf) -> PathBuf { - // On Unix, the source is always within a `lib` subdirectory, with both npm and Yarn let mut path = package_root; - path.push("lib"); + match self { + // On Unix, the source is always within a `lib` subdirectory, with both npm and Yarn + PackageManager::Npm | PackageManager::Yarn => path.push("lib"), + // pnpm puts the source node_modules directory in the global-dir + // plus a versioned subdirectory. + // FIXME: Here the subdirectory is hard-coded, I don't know if it's + // possible to retrieve it from pnpm dynamically. + PackageManager::Pnpm => path.push("5"), + } path } @@ -46,7 +55,15 @@ impl PackageManager { PackageManager::Yarn => { let mut path = package_root; path.push("lib"); - + path + } + // pnpm puts the source node_modules directory in the global-dir + // plus a versioned subdirectory. + // FIXME: Here the subdirectory is hard-coded, I don't know if it's + // possible to retrieve it from pnpm dynamically. + PackageManager::Pnpm => { + let mut path = package_root; + path.push("5"); path } } @@ -70,11 +87,11 @@ impl PackageManager { match self { // On Windows, npm leaves the binaries at the root of the `prefix` directory PackageManager::Npm => package_root, - // On Windows, Yarn still includes the `bin` subdirectory - PackageManager::Yarn => { + // On Windows, Yarn still includes the `bin` subdirectory. pnpm by + // default generates binaries into the `PNPM_HOME` path + PackageManager::Yarn | PackageManager::Pnpm => { let mut path = package_root; path.push("bin"); - path } } @@ -86,6 +103,43 @@ impl PackageManager { if let PackageManager::Yarn = self { command.env("npm_config_global_folder", self.source_root(package_root)); + } else + // FIXME: Find out if there is a perfect way to intercept pnpm global + // installs by using environment variables or whatever. + // Using `--global-dir` and `--global-bin-dir` flags here is not enough, + // because pnpm generates _absolute path_ based symlinks, and this makes + // impossible to simply move installed packages from the staging directory + // to the final `image/packages/` destination. + if let PackageManager::Pnpm = self { + // Specify the staging directory to store global package, + // see: https://pnpm.io/npmrc#global-dir + command.arg("--global-dir").arg(&package_root); + // Specify the staging directory for the bin files of globally installed packages. + // See: https://pnpm.io/npmrc#global-bin-dir (>= 6.15.0) + // and https://github.com/volta-cli/rfcs/pull/46#discussion_r933296625 + let global_bin_dir = self.binary_dir(package_root); + command.arg("--global-bin-dir").arg(&global_bin_dir); + // pnpm requires the `global-bin-dir` to be in PATH, otherwise it + // will not trigger global installs. One can also use the `PNPM_HOME` + // environment variable, which is only available in pnpm v7+, to + // pass the check. + // See: https://github.com/volta-cli/rfcs/pull/46#discussion_r861943740 + let mut new_path = global_bin_dir; + let mut command_envs = command.get_envs(); + while let Some((name, value)) = command_envs.next() { + if name == "PATH" { + if let Some(old_path) = value { + #[cfg(unix)] + let path_delimiter = OsStr::new(":"); + #[cfg(windows)] + let path_delimiter = OsStr::new(";"); + new_path = + PathBuf::from([new_path.as_os_str(), old_path].join(path_delimiter)); + break; + } + } + } + command.env("PATH", new_path); } } @@ -95,7 +149,9 @@ impl PackageManager { pub(super) fn get_installed_package(self, package_root: PathBuf) -> Option { match self { PackageManager::Npm => get_npm_package_name(self.source_dir(package_root)), - PackageManager::Yarn => get_yarn_package_name(self.source_root(package_root)), + PackageManager::Pnpm | PackageManager::Yarn => { + get_pnpm_or_yarn_package_name(self.source_root(package_root)) + } } } } @@ -141,10 +197,10 @@ fn get_single_directory_name(parent_dir: &Path) -> Option { } } -/// Determine the package name for a Yarn global install +/// Determine the package name for a pnpm or Yarn global install /// -/// Yarn creates a `package.json` file with the globally installed package as a dependency -fn get_yarn_package_name(source_root: PathBuf) -> Option { +/// pnpm/Yarn creates a `package.json` file with the globally installed package as a dependency +fn get_pnpm_or_yarn_package_name(source_root: PathBuf) -> Option { let package_file = source_root.join("package.json"); let file = File::open(package_file).ok()?; let manifest: GlobalYarnManifest = serde_json::de::from_reader(file).ok()?; diff --git a/crates/volta-core/src/tool/package/metadata.rs b/crates/volta-core/src/tool/package/metadata.rs index 9a9263b45..671b1454e 100644 --- a/crates/volta-core/src/tool/package/metadata.rs +++ b/crates/volta-core/src/tool/package/metadata.rs @@ -13,7 +13,7 @@ use semver::Version; /// Configuration information about an installed package /// -/// Will be stored in /tools/user/packages/.json +/// Will be stored in `/tools/user/packages/.json` #[derive(serde::Serialize, serde::Deserialize, PartialOrd, Ord, PartialEq, Eq)] pub struct PackageConfig { /// The package name @@ -165,6 +165,13 @@ struct RawPlatformSpec { node: Version, #[serde(with = "option_version_serde")] npm: Option, + // The magic: + // `serde(default)` to assign the pnpm field with a default value, this + // ensures a seamless migration is performed from the previous package + // platformspec which did not have a pnpm field despite the same layout.v3 + #[serde(default)] + #[serde(with = "option_version_serde")] + pnpm: Option, #[serde(with = "option_version_serde")] yarn: Option, } diff --git a/crates/volta-core/src/tool/pnpm/fetch.rs b/crates/volta-core/src/tool/pnpm/fetch.rs new file mode 100644 index 000000000..48851d892 --- /dev/null +++ b/crates/volta-core/src/tool/pnpm/fetch.rs @@ -0,0 +1,183 @@ +//! Provides fetcher for pnpm distributions + +use std::fs::{write, File}; +use std::path::Path; + +use archive::{Archive, Tarball}; +use fs_utils::ensure_containing_dir_exists; +use log::debug; +use semver::Version; + +use crate::error::{Context, ErrorKind, Fallible}; +use crate::fs::{create_staging_dir, create_staging_file, rename, set_executable}; +use crate::hook::ToolHooks; +use crate::layout::volta_home; +use crate::style::{progress_bar, tool_version}; +use crate::tool::registry::public_registry_package; +use crate::tool::{self, download_tool_error, Pnpm}; +use crate::version::VersionSpec; + +pub fn fetch(version: &Version, hooks: Option<&ToolHooks>) -> Fallible<()> { + let pnpm_dir = volta_home()?.pnpm_inventory_dir(); + let cache_file = pnpm_dir.join(Pnpm::archive_filename(&version.to_string())); + + let (archive, staging) = match load_cached_distro(&cache_file) { + Some(archive) => { + debug!( + "Loading {} from cached archive at '{}'", + tool_version("pnpm", &version), + cache_file.display(), + ); + (archive, None) + } + None => { + let staging = create_staging_file()?; + let remote_url = determine_remote_url(version, hooks)?; + let archive = fetch_remote_distro(version, &remote_url, staging.path())?; + (archive, Some(staging)) + } + }; + + unpack_archive(archive, version)?; + + if let Some(staging_file) = staging { + ensure_containing_dir_exists(&cache_file).with_context(|| { + ErrorKind::ContainingDirError { + path: cache_file.clone(), + } + })?; + staging_file + .persist(cache_file) + .with_context(|| ErrorKind::PersistInventoryError { + tool: "pnpm".into(), + })?; + } + + Ok(()) +} + +/// Unpack the pnpm archive into the image directory so that it is ready for use +fn unpack_archive(archive: Box, version: &Version) -> Fallible<()> { + let temp = create_staging_dir()?; + debug!("Unpacking pnpm into '{}'", temp.path().display()); + + let progress = progress_bar( + archive.origin(), + &tool_version("pnpm", version), + archive + .uncompressed_size() + .unwrap_or_else(|| archive.compressed_size()), + ); + let version_string = version.to_string(); + + archive + .unpack(temp.path(), &mut |_, read| { + progress.inc(read as u64); + }) + .with_context(|| ErrorKind::UnpackArchiveError { + tool: "pnpm".into(), + version: version_string.clone(), + })?; + + let bin_path = temp.path().join("package").join("bin"); + write_launcher(&bin_path, "pnpm")?; + write_launcher(&bin_path, "pnpx")?; + + #[cfg(windows)] + { + write_cmd_launcher(&bin_path, "pnpm")?; + write_cmd_launcher(&bin_path, "pnpx")?; + } + + let dest = volta_home()?.pnpm_image_dir(&version_string); + ensure_containing_dir_exists(&dest) + .with_context(|| ErrorKind::ContainingDirError { path: dest.clone() })?; + + rename(temp.path().join("package"), &dest).with_context(|| ErrorKind::SetupToolImageError { + tool: "pnpm".into(), + version: version_string.clone(), + dir: dest.clone(), + })?; + + progress.finish_and_clear(); + + // Note: We write this after the progress bar is finished to avoid display bugs with re-renders of the progress + debug!("Installing pnpm in '{}'", dest.display()); + + Ok(()) +} + +/// Return the archive if it is valid. It may have been corrupted or interrupted in the middle of +/// downloading. +// ISSUE(#134) - verify checksum +fn load_cached_distro(file: &Path) -> Option> { + if file.is_file() { + let file = File::open(file).ok()?; + Tarball::load(file).ok() + } else { + None + } +} + +/// Determine the remote URL to download from, using the hooks if avaialble +fn determine_remote_url(version: &Version, hooks: Option<&ToolHooks>) -> Fallible { + let version_str = version.to_string(); + match hooks { + Some(&ToolHooks { + distro: Some(ref hook), + .. + }) => { + debug!("Using pnpm.distro hook to determine download URL"); + let distro_file_name = Pnpm::archive_filename(&version_str); + hook.resolve(version, &distro_file_name) + } + _ => Ok(public_registry_package("pnpm", &version_str)), + } +} + +/// Fetch the distro archive from the internet +fn fetch_remote_distro( + version: &Version, + url: &str, + staging_path: &Path, +) -> Fallible> { + debug!("Downloading {} from {}", tool_version("pnpm", version), url); + Tarball::fetch(url, staging_path).with_context(download_tool_error( + tool::Spec::Pnpm(VersionSpec::Exact(version.clone())), + url, + )) +} + +/// Create executable launchers for the pnpm and pnpx binaries +fn write_launcher(base_path: &Path, tool: &str) -> Fallible<()> { + let path = base_path.join(tool); + write( + &path, + format!( + r#"#!/bin/sh +(set -o igncr) 2>/dev/null && set -o igncr; # cygwin encoding fix + +basedir=`dirname "$0"` + +case `uname` in + *CYGWIN*) basedir=`cygpath -w "$basedir"`;; +esac + +node "$basedir/{}.cjs" "$@" +"#, + tool + ), + ) + .and_then(|_| set_executable(&path)) + .with_context(|| ErrorKind::WriteLauncherError { tool: tool.into() }) +} + +/// Create CMD executable launchers for the pnpm and pnpx binaries for Windows +#[cfg(windows)] +fn write_cmd_launcher(base_path: &Path, tool: &str) -> Fallible<()> { + write( + base_path.join(format!("{}.cmd", tool)), + format!("@echo off\nnode \"%~dp0\\{}.cjs\" %*", tool), + ) + .with_context(|| ErrorKind::WriteLauncherError { tool: tool.into() }) +} diff --git a/crates/volta-core/src/tool/pnpm/mod.rs b/crates/volta-core/src/tool/pnpm/mod.rs new file mode 100644 index 000000000..bee511ce9 --- /dev/null +++ b/crates/volta-core/src/tool/pnpm/mod.rs @@ -0,0 +1,111 @@ +use semver::Version; +use std::fmt::{self, Display}; + +use crate::error::{ErrorKind, Fallible}; +use crate::inventory::pnpm_available; +use crate::session::Session; +use crate::style::tool_version; +use crate::sync::VoltaLock; + +use super::{ + check_fetched, debug_already_fetched, info_fetched, info_installed, info_pinned, + info_project_version, FetchStatus, Tool, +}; + +mod fetch; +mod resolve; + +pub use resolve::resolve; + +/// The Tool implementation for fetching and installing pnpm +pub struct Pnpm { + pub(super) version: Version, +} + +impl Pnpm { + pub fn new(version: Version) -> Self { + Pnpm { version } + } + + pub fn archive_basename(version: &str) -> String { + format!("pnpm-{}", version) + } + + pub fn archive_filename(version: &str) -> String { + format!("{}.tgz", Pnpm::archive_basename(version)) + } + + pub(crate) fn ensure_fetched(&self, session: &mut Session) -> Fallible<()> { + match check_fetched(|| pnpm_available(&self.version))? { + FetchStatus::AlreadyFetched => { + debug_already_fetched(self); + Ok(()) + } + FetchStatus::FetchNeeded(_lock) => fetch::fetch(&self.version, session.hooks()?.pnpm()), + } + } +} + +impl Tool for Pnpm { + fn fetch(self: Box, session: &mut Session) -> Fallible<()> { + self.ensure_fetched(session)?; + + info_fetched(self); + Ok(()) + } + + fn install(self: Box, session: &mut Session) -> Fallible<()> { + // Acquire a lock on the Volta directory, if possible, to prevent concurrent changes + let _lock = VoltaLock::acquire(); + self.ensure_fetched(session)?; + + session + .toolchain_mut()? + .set_active_pnpm(Some(self.version.clone()))?; + + info_installed(self); + + if let Ok(Some(project)) = session.project_platform() { + if let Some(pnpm) = &project.pnpm { + info_project_version(tool_version("pnpm", pnpm)); + } + } + Ok(()) + } + + fn pin(self: Box, session: &mut Session) -> Fallible<()> { + if session.project()?.is_some() { + self.ensure_fetched(session)?; + + // Note: We know this will succeed, since we checked above + let project = session.project_mut()?.unwrap(); + project.pin_pnpm(Some(self.version.clone()))?; + + info_pinned(self); + Ok(()) + } else { + Err(ErrorKind::NotInPackage.into()) + } + } +} + +impl Display for Pnpm { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&tool_version("pnpm", &self.version)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_pnpm_archive_basename() { + assert_eq!(Pnpm::archive_basename("1.2.3"), "pnpm-1.2.3"); + } + + #[test] + fn test_pnpm_archive_filename() { + assert_eq!(Pnpm::archive_filename("1.2.3"), "pnpm-1.2.3.tgz"); + } +} diff --git a/crates/volta-core/src/tool/pnpm/resolve.rs b/crates/volta-core/src/tool/pnpm/resolve.rs new file mode 100644 index 000000000..c32a3f1d1 --- /dev/null +++ b/crates/volta-core/src/tool/pnpm/resolve.rs @@ -0,0 +1,73 @@ +use log::debug; +use semver::{Version, VersionReq}; + +use crate::error::{ErrorKind, Fallible}; +use crate::hook::ToolHooks; +use crate::session::Session; +use crate::tool::registry::{fetch_npm_registry, public_registry_index, PackageIndex}; +use crate::tool::{PackageDetails, Pnpm}; +use crate::version::{VersionSpec, VersionTag}; + +pub fn resolve(matching: VersionSpec, session: &mut Session) -> Fallible { + let hooks = session.hooks()?.pnpm(); + match matching { + VersionSpec::Semver(requirement) => resolve_semver(requirement, hooks), + VersionSpec::Exact(version) => Ok(version), + VersionSpec::None | VersionSpec::Tag(VersionTag::Latest) => resolve_tag("latest", hooks), + VersionSpec::Tag(tag) => resolve_tag(&tag.to_string(), hooks), + } +} + +fn resolve_tag(tag: &str, hooks: Option<&ToolHooks>) -> Fallible { + let (url, mut index) = fetch_pnpm_index(hooks)?; + + match index.tags.remove(tag) { + Some(version) => { + debug!("Found pnpm@{} matching tag '{}' from {}", version, tag, url); + Ok(version) + } + None => Err(ErrorKind::PnpmVersionNotFound { + matching: tag.into(), + } + .into()), + } +} + +fn resolve_semver(matching: VersionReq, hooks: Option<&ToolHooks>) -> Fallible { + let (url, index) = fetch_pnpm_index(hooks)?; + + let details_opt = index + .entries + .into_iter() + .find(|PackageDetails { version, .. }| matching.matches(version)); + + match details_opt { + Some(details) => { + debug!( + "Found pnpm@{} matching requirement '{}' from {}", + details.version, matching, url + ); + Ok(details.version) + } + None => Err(ErrorKind::PnpmVersionNotFound { + matching: matching.to_string(), + } + .into()), + } +} + +/// Fetch the index of available pnpm versions from the npm registry +fn fetch_pnpm_index(hooks: Option<&ToolHooks>) -> Fallible<(String, PackageIndex)> { + let url = match hooks { + Some(&ToolHooks { + index: Some(ref hook), + .. + }) => { + debug!("Using pnpm.index hook to determine pnpm index URL"); + hook.resolve("pnpm")? + } + _ => public_registry_index("pnpm"), + }; + + fetch_npm_registry(url, "pnpm") +} diff --git a/crates/volta-core/src/tool/serial.rs b/crates/volta-core/src/tool/serial.rs index 3daebb096..937c8fe2e 100644 --- a/crates/volta-core/src/tool/serial.rs +++ b/crates/volta-core/src/tool/serial.rs @@ -20,6 +20,7 @@ impl Spec { match tool_name { "node" => Spec::Node(version), "npm" => Spec::Npm(version), + "pnpm" => Spec::Pnpm(version), "yarn" => Spec::Yarn(version), package => Spec::Package(package.to_string(), version), } @@ -53,6 +54,7 @@ impl Spec { Ok(match name { "node" => Spec::Node(version), "npm" => Spec::Npm(version), + "pnpm" => Spec::Pnpm(version), "yarn" => Spec::Yarn(version), package => Spec::Package(package.into(), version), }) @@ -133,7 +135,7 @@ impl Spec { /// /// We want to preserve the original order as much as possible, so we treat tools in /// the same tool category as equal. We still need to pull Node to the front of the - /// list, followed by Npm / Yarn, and then Packages last. + /// list, followed by Npm, pnpm, Yarn, and then Packages last. fn sort_comparator(left: &Spec, right: &Spec) -> Ordering { match (left, right) { (Spec::Node(_), Spec::Node(_)) => Ordering::Equal, @@ -142,6 +144,9 @@ impl Spec { (Spec::Npm(_), Spec::Npm(_)) => Ordering::Equal, (Spec::Npm(_), _) => Ordering::Less, (_, Spec::Npm(_)) => Ordering::Greater, + (Spec::Pnpm(_), Spec::Pnpm(_)) => Ordering::Equal, + (Spec::Pnpm(_), _) => Ordering::Less, + (_, Spec::Pnpm(_)) => Ordering::Greater, (Spec::Yarn(_), Spec::Yarn(_)) => Ordering::Equal, (Spec::Yarn(_), _) => Ordering::Less, (_, Spec::Yarn(_)) => Ordering::Greater, diff --git a/crates/volta-core/src/toolchain/mod.rs b/crates/volta-core/src/toolchain/mod.rs index 7637e295c..3e5ceb98a 100644 --- a/crates/volta-core/src/toolchain/mod.rs +++ b/crates/volta-core/src/toolchain/mod.rs @@ -75,6 +75,7 @@ impl Toolchain { self.platform = Some(PlatformSpec { node: node_version.clone(), npm: None, + pnpm: None, yarn: None, }); dirty = true; @@ -105,6 +106,23 @@ impl Toolchain { Ok(()) } + /// Set the active pnpm version in the default platform file. + pub fn set_active_pnpm(&mut self, pnpm: Option) -> Fallible<()> { + if let Some(platform) = self.platform.as_mut() { + if platform.pnpm != pnpm { + platform.pnpm = pnpm; + self.save()?; + } + } else if pnpm.is_some() { + return Err(ErrorKind::NoDefaultNodeVersion { + tool: "pnpm".into(), + } + .into()); + } + + Ok(()) + } + /// Set the active Npm version in the default platform file. pub fn set_active_npm(&mut self, npm: Option) -> Fallible<()> { if let Some(platform) = self.platform.as_mut() { diff --git a/crates/volta-core/src/toolchain/serial.rs b/crates/volta-core/src/toolchain/serial.rs index 92067778f..a5fc2166f 100644 --- a/crates/volta-core/src/toolchain/serial.rs +++ b/crates/volta-core/src/toolchain/serial.rs @@ -19,6 +19,9 @@ pub struct Platform { pub node: Option, #[serde(default)] #[serde(with = "option_version_serde")] + pub pnpm: Option, + #[serde(default)] + #[serde(with = "option_version_serde")] pub yarn: Option, } @@ -29,6 +32,7 @@ impl Platform { runtime: source.node.clone(), npm: source.npm.clone(), }), + pnpm: source.pnpm.clone(), yarn: source.yarn.clone(), } } @@ -55,9 +59,11 @@ impl TryFrom for Platform { impl From for Option { fn from(platform: Platform) -> Option { let yarn = platform.yarn; + let pnpm = platform.pnpm; platform.node.map(|node_version| PlatformSpec { node: node_version.runtime, npm: node_version.npm, + pnpm, yarn, }) } @@ -78,6 +84,7 @@ pub mod tests { "runtime": "4.5.6", "npm": "7.8.9" }, + "pnpm": "3.2.1", "yarn": "1.2.3" }"#; @@ -86,6 +93,7 @@ pub mod tests { let json_str = BASIC_JSON_STR.to_string(); let platform = Platform::try_from(json_str).expect("could not parse JSON string"); let expected_platform = Platform { + pnpm: Some(Version::parse("3.2.1").expect("could not parse version")), yarn: Some(Version::parse("1.2.3").expect("could not parse version")), node: Some(NodeVersion { runtime: Version::parse("4.5.6").expect("could not parse version"), @@ -101,6 +109,7 @@ pub mod tests { let platform = Platform::try_from(json_str).expect("could not parse JSON string"); let expected_platform = Platform { node: None, + pnpm: None, yarn: None, }; assert_eq!(platform, expected_platform); @@ -109,6 +118,7 @@ pub mod tests { #[test] fn test_into_json() { let platform_spec = platform::PlatformSpec { + pnpm: Some(Version::parse("3.2.1").expect("could not parse version")), yarn: Some(Version::parse("1.2.3").expect("could not parse version")), node: Version::parse("4.5.6").expect("could not parse version"), npm: Some(Version::parse("7.8.9").expect("could not parse version")), diff --git a/crates/volta-layout/src/v3.rs b/crates/volta-layout/src/v3.rs index 3762b0fb5..131494583 100644 --- a/crates/volta-layout/src/v3.rs +++ b/crates/volta-layout/src/v3.rs @@ -19,11 +19,13 @@ layout! { "inventory": inventory_dir { "node": node_inventory_dir {} "npm": npm_inventory_dir {} + "pnpm": pnpm_inventory_dir {} "yarn": yarn_inventory_dir {} } "image": image_dir { "node": node_image_root_dir {} "npm": npm_image_root_dir {} + "pnpm": pnpm_image_root_dir {} "yarn": yarn_image_root_dir {} "packages": package_image_root_dir {} } @@ -53,6 +55,14 @@ impl VoltaHome { path_buf!(self.npm_image_dir(npm), "bin") } + pub fn pnpm_image_dir(&self, version: &str) -> PathBuf { + path_buf!(self.pnpm_image_root_dir.clone(), version) + } + + pub fn pnpm_image_bin_dir(&self, version: &str) -> PathBuf { + path_buf!(self.pnpm_image_dir(version), "bin") + } + pub fn yarn_image_dir(&self, version: &str) -> PathBuf { path_buf!(self.yarn_image_root_dir.clone(), version) } diff --git a/crates/volta-migrate/src/v3/config.rs b/crates/volta-migrate/src/v3/config.rs index 216fcacb0..0d4e10c07 100644 --- a/crates/volta-migrate/src/v3/config.rs +++ b/crates/volta-migrate/src/v3/config.rs @@ -42,6 +42,8 @@ impl From for PlatformSpec { PlatformSpec { node: config_platform.node.runtime, npm: config_platform.node.npm, + // LegacyPlatform (layout.v2) doesn't have a pnpm field + pnpm: None, yarn: config_platform.yarn, } } diff --git a/src/cli.rs b/src/cli.rs index 75041254b..9f7e1d737 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -122,7 +122,7 @@ otherwise, they will be written to `stdout`. #[structopt(name = "setup", author = "", version = "")] Setup(command::Setup), - /// Run a command with custom Node, npm, and/or Yarn versions + /// Run a command with custom Node, npm, pnpm, and/or Yarn versions #[structopt(name = "run", author = "", version = "")] #[structopt(raw(setting = "structopt::clap::AppSettings::AllowLeadingHyphen"))] #[structopt(raw(setting = "structopt::clap::AppSettings::TrailingVarArg"))] diff --git a/src/command/list/human.rs b/src/command/list/human.rs index dc59e7527..d0f8786e0 100644 --- a/src/command/list/human.rs +++ b/src/command/list/human.rs @@ -306,6 +306,7 @@ fn format_package_manager(package_manager: &PackageManager) -> String { fn format_package_manager_kind(kind: PackageManagerKind) -> String { match kind { PackageManagerKind::Npm => "npm".into(), + PackageManagerKind::Pnpm => "pnpm".into(), PackageManagerKind::Yarn => "Yarn".into(), } } diff --git a/src/command/list/mod.rs b/src/command/list/mod.rs index c81d4423b..9899eaa6d 100644 --- a/src/command/list/mod.rs +++ b/src/command/list/mod.rs @@ -154,6 +154,7 @@ struct Node { #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] enum PackageManagerKind { Npm, + Pnpm, Yarn, } @@ -164,6 +165,7 @@ impl fmt::Display for PackageManagerKind { "{}", match self { PackageManagerKind::Npm => "npm", + PackageManagerKind::Pnpm => "pnpm", PackageManagerKind::Yarn => "yarn", } ) @@ -201,7 +203,8 @@ pub(crate) struct List { // `Option` with `impl FromStr for Subcommand` for `StructOpt` // because StructOpt does not currently support custom parsing for enum // variants (as detailed in commit 5f9214ae). - /// The tool to lookup - `all`, `node`, `yarn`, or the name of a package or binary. + /// The tool to lookup - `all`, `node`, `npm`, `yarn`, `pnpm`, or the name + /// of a package or binary. #[structopt(name = "tool")] subcommand: Option, @@ -233,6 +236,9 @@ enum Subcommand { /// Show locally cached npm versions. Npm, + /// Show locally cached pnpm versions. + Pnpm, + /// Show locally cached Yarn versions. Yarn, @@ -246,6 +252,7 @@ impl From<&str> for Subcommand { "all" => Subcommand::All, "node" => Subcommand::Node, "npm" => Subcommand::Npm, + "pnpm" => Subcommand::Pnpm, "yarn" => Subcommand::Yarn, s => Subcommand::PackageOrTool { name: s.into() }, } @@ -294,6 +301,7 @@ impl Command for List { Some(Subcommand::All) => Toolchain::all(project, default_platform)?, Some(Subcommand::Node) => Toolchain::node(project, default_platform, &filter)?, Some(Subcommand::Npm) => Toolchain::npm(project, default_platform, &filter)?, + Some(Subcommand::Pnpm) => Toolchain::pnpm(project, default_platform, &filter)?, Some(Subcommand::Yarn) => Toolchain::yarn(project, default_platform, &filter)?, Some(Subcommand::PackageOrTool { name }) => { Toolchain::package_or_tool(&name, project, &filter)? diff --git a/src/command/list/toolchain.rs b/src/command/list/toolchain.rs index 4f95b7af3..b15a00ec5 100644 --- a/src/command/list/toolchain.rs +++ b/src/command/list/toolchain.rs @@ -2,7 +2,9 @@ use super::{Filter, Node, Package, PackageManager, Source}; use crate::command::list::PackageManagerKind; use semver::Version; use volta_core::error::Fallible; -use volta_core::inventory::{node_versions, npm_versions, package_configs, yarn_versions}; +use volta_core::inventory::{ + node_versions, npm_versions, package_configs, pnpm_versions, yarn_versions, +}; use volta_core::platform::PlatformSpec; use volta_core::project::Project; use volta_core::tool::PackageConfig; @@ -36,6 +38,8 @@ enum Lookup { Runtime, /// Look up the npm package manager Npm, + /// Look up the pnpm package manager + Pnpm, /// Look up the Yarn package manager Yarn, } @@ -45,6 +49,7 @@ impl Lookup { move |spec| match self { Lookup::Runtime => Some(spec.node.clone()), Lookup::Npm => spec.npm.clone(), + Lookup::Pnpm => spec.pnpm.clone(), Lookup::Yarn => spec.yarn.clone(), } } @@ -133,6 +138,13 @@ impl Toolchain { version, }) .into_iter() + .chain(Lookup::Pnpm.active_tool(project, default_platform).map( + |(source, version)| PackageManager { + kind: PackageManagerKind::Pnpm, + source, + version, + }, + )) .chain(Lookup::Yarn.active_tool(project, default_platform).map( |(source, version)| PackageManager { kind: PackageManagerKind::Yarn, @@ -170,6 +182,11 @@ impl Toolchain { source: Lookup::Npm.version_source(project, default_platform, version), version: version.clone(), }) + .chain(pnpm_versions()?.iter().map(|version| PackageManager { + kind: PackageManagerKind::Pnpm, + source: Lookup::Pnpm.version_source(project, default_platform, version), + version: version.clone(), + })) .chain(yarn_versions()?.iter().map(|version| PackageManager { kind: PackageManagerKind::Yarn, source: Lookup::Yarn.version_source(project, default_platform, version), @@ -234,6 +251,33 @@ impl Toolchain { }) } + pub(super) fn pnpm( + project: Option<&Project>, + default_platform: Option<&PlatformSpec>, + filter: &Filter, + ) -> Fallible { + let managers = pnpm_versions()? + .iter() + .filter_map(|version| { + let source = Lookup::Pnpm.version_source(project, default_platform, version); + if source.allowed_with(filter) { + Some(PackageManager { + kind: PackageManagerKind::Pnpm, + source, + version: version.clone(), + }) + } else { + None + } + }) + .collect(); + + Ok(Toolchain::PackageManagers { + kind: PackageManagerKind::Pnpm, + managers, + }) + } + pub(super) fn yarn( project: Option<&Project>, default_platform: Option<&PlatformSpec>, diff --git a/src/command/run.rs b/src/command/run.rs index a385c238b..43a255ad7 100644 --- a/src/command/run.rs +++ b/src/command/run.rs @@ -9,7 +9,7 @@ use volta_core::error::{report_error, ExitCode, Fallible}; use volta_core::platform::{CliPlatform, InheritOption}; use volta_core::run::execute_tool; use volta_core::session::{ActivityKind, Session}; -use volta_core::tool::{node, npm, yarn}; +use volta_core::tool::{node, npm, pnpm, yarn}; #[derive(Debug, StructOpt)] pub(crate) struct Run { @@ -25,6 +25,14 @@ pub(crate) struct Run { #[structopt(long = "bundled-npm", conflicts_with = "npm")] bundled_npm: bool, + /// Set the custon pnpm version + #[structopt(long = "pnpm", value_name = "version", conflicts_with = "no_pnpm")] + pnpm: Option, + + /// Disables pnpm + #[structopt(long = "no-pnpm", conflicts_with = "pnpm")] + no_pnpm: bool, + /// Set the custom Yarn version #[structopt(long = "yarn", value_name = "version", conflicts_with = "no_yarn")] yarn: Option, @@ -92,6 +100,14 @@ impl Run { }, }; + let pnpm = match (self.no_pnpm, &self.pnpm) { + (true, _) => InheritOption::None, + (false, None) => InheritOption::Inherit, + (false, Some(version)) => { + InheritOption::Some(pnpm::resolve(version.parse()?, session)?) + } + }; + let yarn = match (self.no_yarn, &self.yarn) { (true, _) => InheritOption::None, (false, None) => InheritOption::Inherit, @@ -100,7 +116,12 @@ impl Run { } }; - Ok(CliPlatform { node, npm, yarn }) + Ok(CliPlatform { + node, + npm, + pnpm, + yarn, + }) } /// Convert the environment variable settings passed to the command line into a map diff --git a/tests/acceptance/corrupted_download.rs b/tests/acceptance/corrupted_download.rs index c94a00238..519cbad43 100644 --- a/tests/acceptance/corrupted_download.rs +++ b/tests/acceptance/corrupted_download.rs @@ -1,4 +1,4 @@ -use crate::support::sandbox::{sandbox, DistroMetadata, NodeFixture, Yarn1Fixture}; +use crate::support::sandbox::{sandbox, DistroMetadata, NodeFixture, PnpmFixture, Yarn1Fixture}; use hamcrest2::assert_that; use hamcrest2::prelude::*; use semver::Version; @@ -25,6 +25,30 @@ const NODE_VERSION_FIXTURES: [DistroMetadata; 2] = [ }, ]; +const PNPM_VERSION_INFO: &str = r#" +{ + "name":"pnpm", + "dist-tags": { "latest":"7.7.1" }, + "versions": { + "0.0.1": { "version":"0.0.1", "dist": { "shasum":"", "tarball":"" }}, + "7.7.1": { "version":"7.7.1", "dist": { "shasum":"", "tarball":"" }} + } +} +"#; + +const PNPM_VERSION_FIXTURES: [DistroMetadata; 2] = [ + DistroMetadata { + version: "0.0.1", + compressed_size: 10, + uncompressed_size: Some(0x0028_0000), + }, + DistroMetadata { + version: "7.7.1", + compressed_size: 518, + uncompressed_size: Some(0x0028_0000), + }, +]; + const YARN_1_VERSION_INFO: &str = r#"{ "name":"yarn", "dist-tags": { "latest": "1.2.42" }, @@ -77,6 +101,41 @@ fn install_valid_node_saves_to_inventory() { assert!(s.node_inventory_archive_exists(&Version::new(10, 99, 1040))); } +#[test] +fn install_corrupted_pnpm_leaves_inventory_unchanged() { + let s = sandbox() + .node_available_versions(NODE_VERSION_INFO) + .pnpm_available_versions(PNPM_VERSION_INFO) + .distro_mocks::(&NODE_VERSION_FIXTURES) + .distro_mocks::(&PNPM_VERSION_FIXTURES) + .build(); + + assert_that!( + s.volta("install pnpm@0.0.1"), + execs().with_status(ExitCode::UnknownError as i32) + ); + + assert!(!s.pnpm_inventory_archive_exists("0.0.1")); +} + +#[test] +fn install_valid_pnpm_saves_to_inventory() { + let s = sandbox() + .platform(r#"{ "node": { "runtime": "1.2.3", "npm": null }, "yarn": null }"#) + .node_available_versions(NODE_VERSION_INFO) + .pnpm_available_versions(PNPM_VERSION_INFO) + .distro_mocks::(&NODE_VERSION_FIXTURES) + .distro_mocks::(&PNPM_VERSION_FIXTURES) + .build(); + + assert_that!( + s.volta("install pnpm@7.7.1"), + execs().with_status(ExitCode::Success as i32) + ); + + assert!(s.pnpm_inventory_archive_exists("7.7.1")); +} + #[test] fn install_corrupted_yarn_leaves_inventory_unchanged() { let s = sandbox() diff --git a/tests/acceptance/execute_binary.rs b/tests/acceptance/execute_binary.rs index e8e171524..a5aa45fef 100644 --- a/tests/acceptance/execute_binary.rs +++ b/tests/acceptance/execute_binary.rs @@ -65,6 +65,28 @@ echo "npm args: $@" } } +fn pnpm_bin(version: &str) -> String { + cfg_if! { + if #[cfg(target_os = "windows")] { + format!( + r#"@echo off +echo pnpm version {} +echo pnpm args: %* +"#, + version + ) + } else { + format!( + r#"#!/bin/sh +echo "pnpm version {}" +echo "pnpm args: $@" +"#, + version + ) + } + } +} + fn yarn_bin(version: &str) -> String { cfg_if! { if #[cfg(target_os = "windows")] { @@ -179,6 +201,7 @@ fn default_binary_no_project() { // platform node is 11.10.1, npm is 6.7.0 // package cowsay is 1.4.0, installed with platform node // default yarn is 1.23.483 + // default pnpm is 7.7.1 // there is no local project, so it should run the default bin let s = sandbox() .platform(PLATFORM_NODE_NPM) @@ -191,6 +214,7 @@ fn default_binary_no_project() { .setup_node_binary("11.10.1", "6.7.0", &node_bin("11.10.1")) .setup_npm_binary("6.7.0", &npm_bin("6.7.0")) .setup_yarn_binary("1.23.483", &yarn_bin("1.23.483")) + .setup_pnpm_binary("7.7.1", &pnpm_bin("7.7.1")) .add_dir_to_path(PathBuf::from("/bin")) .build(); @@ -204,6 +228,7 @@ fn default_binary_no_project() { .with_stdout_does_not_contain("Node version") .with_stdout_does_not_contain("Npm version") .with_stdout_does_not_contain("Yarn version") + .with_stdout_does_not_contain("pnpm version") ); } @@ -212,6 +237,7 @@ fn default_binary_no_project_dep() { // platform node is 11.10.1, npm is 6.7.0 // package cowsay is 1.4.0, installed with platform node // default yarn is 1.23.483 + // default pnpm is 7.7.1 // local project does not have cowsay dep, so it should run the default bin let s = sandbox() .platform(PLATFORM_NODE_NPM) @@ -225,6 +251,7 @@ fn default_binary_no_project_dep() { .setup_node_binary("11.10.1", "6.7.0", &node_bin("11.10.1")) .setup_npm_binary("6.7.0", &npm_bin("6.7.0")) .setup_yarn_binary("1.23.483", &yarn_bin("1.23.483")) + .setup_pnpm_binary("7.7.1", &pnpm_bin("7.7.1")) .add_dir_to_path(PathBuf::from("/bin")) .build(); @@ -237,6 +264,7 @@ fn default_binary_no_project_dep() { .with_stdout_does_not_contain("Node version") .with_stdout_does_not_contain("Npm version") .with_stdout_does_not_contain("Yarn version") + .with_stdout_does_not_contain("pnpm version") ); } @@ -245,6 +273,7 @@ fn project_local_binary() { // platform node is 11.10.1, npm is 6.7.0 // package cowsay is 1.4.0, installed with platform node // default yarn is 1.23.483 + // default pnpm is 7.7.1 // local project has cowsay as a dep, so it should run that binary let s = sandbox() .platform(PLATFORM_NODE_NPM) @@ -259,6 +288,7 @@ fn project_local_binary() { .setup_node_binary("10.99.1040", "6.7.0", &node_bin("10.99.1040")) .setup_npm_binary("6.7.0", &npm_bin("6.7.0")) .setup_yarn_binary("1.23.483", &yarn_bin("1.23.483")) + .setup_pnpm_binary("7.7.1", &pnpm_bin("7.7.1")) .project_bins(cowsay_bin_info("1.5.0")) .add_dir_to_path(PathBuf::from("/bin")) .build(); @@ -273,6 +303,7 @@ fn project_local_binary() { .with_stdout_does_not_contain("Node version") .with_stdout_does_not_contain("Npm version") .with_stdout_does_not_contain("Yarn version") + .with_stdout_does_not_contain("pnpm version") ); } diff --git a/tests/acceptance/hooks.rs b/tests/acceptance/hooks.rs index fafbc9ab2..958ec606a 100644 --- a/tests/acceptance/hooks.rs +++ b/tests/acceptance/hooks.rs @@ -120,6 +120,23 @@ fn workspace_hooks_json() -> String { ) } +fn pnpm_hooks_json() -> String { + format!( + r#" +{{ + "pnpm": {{ + "index": {{ + "template": "{0}/pnpm/index" + }}, + "distro": {{ + "template": "{0}/pnpm/{{{{version}}}}" + }} + }} +}}"#, + mockito::server_url() + ) +} + fn yarn_hooks_json() -> String { format!( r#" @@ -320,6 +337,74 @@ fn merges_workspace_hooks() { ); } +#[test] +fn pnpm_latest_with_hook_reads_index() { + let s = sandbox() + .default_hooks(&pnpm_hooks_json()) + .env("VOLTA_LOGLEVEL", "debug") + .build(); + let _mock = mock("GET", "/pnpm/index") + .with_status(200) + .with_header("Content-Type", "application/json") + .with_body( + // Npm format for pnpm + r#"{ + "name":"pnpm", + "dist-tags": { "latest":"7.7.1" }, + "versions": { + "0.0.1": { "version":"0.0.1", "dist": { "shasum":"", "tarball":"" }}, + "6.34.0": { "version":"6.34.0", "dist": { "shasum":"", "tarball":"" }}, + "7.7.1": { "version":"7.7.1", "dist": { "shasum":"", "tarball":"" }} + } +}"#, + ) + .create(); + + assert_that!( + s.volta("install pnpm@latest"), + execs() + .with_status(ExitCode::NetworkError as i32) + .with_stderr_contains("[..]Using pnpm.index hook to determine pnpm index URL") + .with_stderr_contains("[..]Found pnpm@7.7.1 matching tag 'latest'[..]") + .with_stderr_contains("[..]Downloading pnpm@7.7.1 from[..]/pnpm/7.7.1[..]") + .with_stderr_contains("[..]Could not download pnpm@7.7.1") + ); +} + +#[test] +fn pnpm_no_version_with_hook_reads_index() { + let s = sandbox() + .default_hooks(&pnpm_hooks_json()) + .env("VOLTA_LOGLEVEL", "debug") + .build(); + let _mock = mock("GET", "/pnpm/index") + .with_status(200) + .with_header("Content-Type", "application/json") + .with_body( + // Npm format for pnpm + r#"{ + "name":"pnpm", + "dist-tags": { "latest":"7.7.1" }, + "versions": { + "0.0.1": { "version":"0.0.1", "dist": { "shasum":"", "tarball":"" }}, + "6.34.0": { "version":"6.34.0", "dist": { "shasum":"", "tarball":"" }}, + "7.7.1": { "version":"7.7.1", "dist": { "shasum":"", "tarball":"" }} + } +}"#, + ) + .create(); + + assert_that!( + s.volta("install pnpm"), + execs() + .with_status(ExitCode::NetworkError as i32) + .with_stderr_contains("[..]Using pnpm.index hook to determine pnpm index URL") + .with_stderr_contains("[..]Found pnpm@7.7.1 matching tag 'latest'[..]") + .with_stderr_contains("[..]Downloading pnpm@7.7.1 from[..]/pnpm/7.7.1[..]") + .with_stderr_contains("[..]Could not download pnpm@7.7.1") + ); +} + #[test] fn yarn_latest_with_hook_reads_latest() { let s = sandbox() diff --git a/tests/acceptance/merged_platform.rs b/tests/acceptance/merged_platform.rs index 9914477eb..286c93ea8 100644 --- a/tests/acceptance/merged_platform.rs +++ b/tests/acceptance/merged_platform.rs @@ -1,7 +1,9 @@ use std::{thread, time}; use crate::support::events_helpers::{assert_events, match_args, match_start, match_tool_end}; -use crate::support::sandbox::{sandbox, DistroMetadata, NodeFixture, NpmFixture, Yarn1Fixture}; +use crate::support::sandbox::{ + sandbox, DistroMetadata, NodeFixture, NpmFixture, PnpmFixture, Yarn1Fixture, +}; use hamcrest2::assert_that; use hamcrest2::prelude::*; use test_support::matchers::execs; @@ -23,6 +25,14 @@ const PACKAGE_JSON_WITH_NPM: &str = r#"{ } }"#; +const PACKAGE_JSON_WITH_PNPM: &str = r#"{ + "name": "with-pnpm", + "volta": { + "node": "10.99.1040", + "pnpm": "7.7.1" + } +}"#; + const PACKAGE_JSON_WITH_YARN: &str = r#"{ "name": "with-yarn", "volta": { @@ -45,6 +55,14 @@ const PLATFORM_WITH_NPM: &str = r#"{ } }"#; +const PLATFORM_WITH_PNPM: &str = r#"{ + "node":{ + "runtime":"9.27.6", + "npm":null + }, + "pnpm": "7.7.1" +}"#; + const PLATFORM_WITH_YARN: &str = r#"{ "node":{ "runtime":"9.27.6", @@ -62,6 +80,7 @@ copy %EVENTS_FILE% events.json del %EVENTS_FILE% "#; const SCRIPT_FILENAME: &str = "write-events.bat"; + const PNPM_SHIM: &str = "pnpm.exe"; const YARN_SHIM: &str = "yarn.exe"; } else if #[cfg(unix)] { // copy the tempfile (path in EVENTS_FILE env var) to events.json @@ -71,6 +90,7 @@ del %EVENTS_FILE% /bin/rm "$EVENTS_FILE" "#; const SCRIPT_FILENAME: &str = "write-events.sh"; + const PNPM_SHIM: &str = "pnpm"; const YARN_SHIM: &str = "yarn"; } else { compile_error!("Unsupported platform for tests (expected 'unix' or 'windows')."); @@ -149,6 +169,19 @@ const NPM_VERSION_FIXTURES: [DistroMetadata; 2] = [ }, ]; +const PNPM_VERSION_FIXTURES: [DistroMetadata; 2] = [ + DistroMetadata { + version: "6.34.0", + compressed_size: 500, + uncompressed_size: Some(0x0028_0000), + }, + DistroMetadata { + version: "7.7.1", + compressed_size: 518, + uncompressed_size: Some(0x0028_0000), + }, +]; + const YARN_1_VERSION_FIXTURES: [DistroMetadata; 2] = [ DistroMetadata { version: "1.12.99", @@ -338,3 +371,94 @@ fn throws_default_error_outside_project() { .with_stderr_contains("[..]Yarn is not available.") ); } + +#[test] +fn uses_project_pnpm_if_available() { + let s = sandbox() + .platform(PLATFORM_WITH_PNPM) + .package_json(PACKAGE_JSON_WITH_PNPM) + .distro_mocks::(&NODE_VERSION_FIXTURES) + .distro_mocks::(&PNPM_VERSION_FIXTURES) + .env("VOLTA_LOGLEVEL", "debug") + .env("VOLTA_WRITE_EVENTS_FILE", "true") + .default_hooks(&events_hooks_json()) + .executable_file(SCRIPT_FILENAME, EVENTS_EXECUTABLE) + .build(); + + assert_that!( + s.pnpm("--version"), + execs() + .with_status(ExitCode::Success as i32) + .with_stderr_does_not_contain("[..]pnpm is not available.") + .with_stderr_does_not_contain("[..]No pnpm version found in this project.") + .with_stderr_contains("[..]pnpm: 7.7.1 from project configuration") + ); + + thread::sleep(time::Duration::from_millis(500)); + assert_events( + &s, + vec![ + ("tool", match_start()), + ("pnpm", match_start()), + ("tool", match_tool_end(0)), + ( + "args", + match_args(format!("{} --version", PNPM_SHIM).as_str()), + ), + ], + ); +} + +#[test] +fn uses_default_pnpm_in_project_without_pnpm() { + let s = sandbox() + .platform(PLATFORM_WITH_PNPM) + .package_json(PACKAGE_JSON_NODE_ONLY) + .distro_mocks::(&NODE_VERSION_FIXTURES) + .distro_mocks::(&PNPM_VERSION_FIXTURES) + .env("VOLTA_LOGLEVEL", "debug") + .build(); + + assert_that!( + s.pnpm("--version"), + execs() + .with_status(ExitCode::Success as i32) + .with_stderr_does_not_contain("[..]pnpm is not available.") + .with_stderr_does_not_contain("[..]No pnpm version found in this project.") + .with_stderr_contains("[..]pnpm: 7.7.1 from default configuration") + ); +} + +#[test] +fn uses_default_pnpm_outside_project() { + let s = sandbox() + .platform(PLATFORM_WITH_PNPM) + .distro_mocks::(&NODE_VERSION_FIXTURES) + .distro_mocks::(&PNPM_VERSION_FIXTURES) + .env("VOLTA_LOGLEVEL", "debug") + .build(); + + assert_that!( + s.pnpm("--version"), + execs() + .with_status(ExitCode::Success as i32) + .with_stderr_does_not_contain("[..]pnpm is not available.") + .with_stderr_does_not_contain("[..]No pnpm version found in this project.") + .with_stderr_contains("[..]pnpm: 7.7.1 from default configuration") + ); +} + +#[test] +fn uses_pnpm_throws_project_error_in_project() { + let s = sandbox() + .platform(PLATFORM_NODE_ONLY) + .package_json(PACKAGE_JSON_NODE_ONLY) + .build(); + + assert_that!( + s.pnpm("--version"), + execs() + .with_status(ExitCode::ExecutionFailure as i32) + .with_stderr_contains("[..]No pnpm version found in this project.") + ); +} diff --git a/tests/acceptance/support/sandbox.rs b/tests/acceptance/support/sandbox.rs index 28d82149c..f9a77599b 100644 --- a/tests/acceptance/support/sandbox.rs +++ b/tests/acceptance/support/sandbox.rs @@ -11,7 +11,7 @@ use mockito::{self, mock, Matcher}; use semver::Version; use test_support::{self, ok_or_panic, paths, paths::PathExt, process::ProcessBuilder}; use volta_core::fs::{set_executable, symlink_file}; -use volta_core::tool::{Node, Yarn, NODE_DISTRO_ARCH, NODE_DISTRO_EXTENSION, NODE_DISTRO_OS}; +use volta_core::tool::{Node, Pnpm, Yarn, NODE_DISTRO_ARCH, NODE_DISTRO_EXTENSION, NODE_DISTRO_OS}; // version cache for node and yarn #[derive(PartialEq, Clone)] @@ -169,6 +169,10 @@ pub struct NpmFixture { pub metadata: DistroMetadata, } +pub struct PnpmFixture { + pub metadata: DistroMetadata, +} + pub struct Yarn1Fixture { pub metadata: DistroMetadata, } @@ -189,6 +193,12 @@ impl From for NpmFixture { } } +impl From for PnpmFixture { + fn from(metadata: DistroMetadata) -> Self { + Self { metadata } + } +} + impl From for Yarn1Fixture { fn from(metadata: DistroMetadata) -> Self { Self { metadata } @@ -237,6 +247,20 @@ impl DistroFixture for NpmFixture { } } +impl DistroFixture for PnpmFixture { + fn server_path(&self) -> String { + format!("/pnpm/-/pnpm-{}.tgz", self.metadata.version) + } + + fn fixture_path(&self) -> String { + format!("tests/fixtures/pnpm-{}.tgz", self.metadata.version) + } + + fn metadata(&self) -> &DistroMetadata { + &self.metadata + } +} + impl DistroFixture for Yarn1Fixture { fn server_path(&self) -> String { format!("/yarn/-/yarn-{}.tgz", self.metadata.version) @@ -288,6 +312,7 @@ impl SandboxBuilder { path_dirs: vec![volta_bin_dir()], shims: vec![ ShimBuilder::new("npm".to_string()), + ShimBuilder::new("pnpm".to_string()), ShimBuilder::new("yarn".to_string()), ], has_exec_path: false, @@ -384,6 +409,18 @@ impl SandboxBuilder { self } + /// Setup mock to return the available pnpm versions (chainable) + pub fn pnpm_available_versions(mut self, body: &str) -> Self { + let mock = mock("GET", "/pnpm") + .with_status(200) + .with_header("content-type", "application/json") + .with_body(body) + .create(); + self.root.mocks.push(mock); + + self + } + /// Setup mock to return a 404 for any GET request /// Note: Mocks are matched in reverse order, so any created _after_ this will work /// While those created before will not @@ -571,6 +608,21 @@ impl SandboxBuilder { self } + /// Write an executable pnpm binary with the input contents (chainable) + pub fn setup_pnpm_binary(mut self, version: &str, contents: &str) -> Self { + cfg_if! { + if #[cfg(target_os = "windows")] { + let pnpm_file = "pnpm.cmd"; + } else { + let pnpm_file = "pnpm"; + } + } + let pnpm_bin_file = pnpm_image_dir(version).join("bin").join(pnpm_file); + self.files + .push(FileBuilder::new(pnpm_bin_file, contents).make_executable()); + self + } + /// Write an executable yarn binary with the input contents (chainable) pub fn setup_yarn_binary(mut self, version: &str, contents: &str) -> Self { cfg_if! { @@ -622,6 +674,7 @@ impl SandboxBuilder { ok_or_panic! { fs::create_dir_all(node_cache_dir()) }; ok_or_panic! { fs::create_dir_all(node_inventory_dir()) }; ok_or_panic! { fs::create_dir_all(package_inventory_dir()) }; + ok_or_panic! { fs::create_dir_all(pnpm_inventory_dir()) }; ok_or_panic! { fs::create_dir_all(yarn_inventory_dir()) }; ok_or_panic! { fs::create_dir_all(volta_tmp_dir()) }; @@ -687,6 +740,9 @@ fn image_dir() -> PathBuf { fn node_inventory_dir() -> PathBuf { inventory_dir().join("node") } +fn pnpm_inventory_dir() -> PathBuf { + inventory_dir().join("pnpm") +} fn yarn_inventory_dir() -> PathBuf { inventory_dir().join("yarn") } @@ -729,6 +785,9 @@ fn node_image_dir(version: &str) -> PathBuf { fn npm_image_dir(version: &str) -> PathBuf { image_dir().join("npm").join(version) } +fn pnpm_image_dir(version: &str) -> PathBuf { + image_dir().join("pnpm").join(version) +} fn yarn_image_dir(version: &str) -> PathBuf { image_dir().join("yarn").join(version) } @@ -810,6 +869,14 @@ impl Sandbox { self.exec_shim("npm", cmd) } + /// Create a `ProcessBuilder` to run the volta pnpm shim. + /// Arguments can be separated by spaces. + /// Example: + /// assert_that(p.pnpm("add ember-cli"), execs()); + pub fn pnpm(&self, cmd: &str) -> ProcessBuilder { + self.exec_shim("pnpm", cmd) + } + /// Create a `ProcessBuilder` to run the volta yarn shim. /// Arguments can be separated by spaces. /// Example: @@ -849,6 +916,12 @@ impl Sandbox { .exists() } + pub fn pnpm_inventory_archive_exists(&self, version: &str) -> bool { + pnpm_inventory_dir() + .join(Pnpm::archive_filename(version)) + .exists() + } + pub fn yarn_inventory_archive_exists(&self, version: &str) -> bool { yarn_inventory_dir() .join(Yarn::archive_filename(version)) diff --git a/tests/acceptance/volta_install.rs b/tests/acceptance/volta_install.rs index b402b9cfb..d20300861 100644 --- a/tests/acceptance/volta_install.rs +++ b/tests/acceptance/volta_install.rs @@ -1,5 +1,6 @@ use crate::support::sandbox::{ - sandbox, DistroMetadata, NodeFixture, NpmFixture, Sandbox, Yarn1Fixture, YarnBerryFixture, + sandbox, DistroMetadata, NodeFixture, NpmFixture, PnpmFixture, Sandbox, Yarn1Fixture, + YarnBerryFixture, }; use hamcrest2::assert_that; use hamcrest2::prelude::*; @@ -14,6 +15,7 @@ fn platform_with_node(node: &str) -> String { "runtime": "{}", "npm": null }}, + "pnpm": null, "yarn": null }}"#, node @@ -27,6 +29,7 @@ fn platform_with_node_npm(node: &str, npm: &str) -> String { "runtime": "{}", "npm": "{}" }}, + "pnpm": null, "yarn": null }}"#, node, npm @@ -181,6 +184,36 @@ const YARN_BERRY_VERSION_FIXTURES: [DistroMetadata; 4] = [ }, ]; +const PNPM_VERSION_INFO: &str = r#" +{ + "name":"pnpm", + "dist-tags": { "latest":"7.7.1" }, + "versions": { + "0.0.1": { "version":"0.0.1", "dist": { "shasum":"", "tarball":"" }}, + "6.34.0": { "version":"6.34.0", "dist": { "shasum":"", "tarball":"" }}, + "7.7.1": { "version":"7.7.1", "dist": { "shasum":"", "tarball":"" }} + } +} +"#; + +const PNPM_VERSION_FIXTURES: [DistroMetadata; 3] = [ + DistroMetadata { + version: "0.0.1", + compressed_size: 10, + uncompressed_size: Some(0x0028_0000), + }, + DistroMetadata { + version: "6.34.0", + compressed_size: 500, + uncompressed_size: Some(0x0028_0000), + }, + DistroMetadata { + version: "7.7.1", + compressed_size: 518, + uncompressed_size: Some(0x0028_0000), + }, +]; + const NPM_VERSION_INFO: &str = r#" { "name":"npm", @@ -297,6 +330,23 @@ fn install_npm_without_node_errors() { ); } +#[test] +fn install_pnpm_without_node_errors() { + let s = sandbox() + .pnpm_available_versions(PNPM_VERSION_INFO) + .distro_mocks::(&PNPM_VERSION_FIXTURES) + .build(); + + assert_that!( + s.volta("install pnpm@7.7.1"), + execs() + .with_status(ExitCode::ConfigurationError as i32) + .with_stderr_contains( + "[..]Cannot install pnpm because the default Node version is not set." + ) + ); +} + #[test] fn install_yarn_without_node_errors() { let s = sandbox() diff --git a/tests/acceptance/volta_pin.rs b/tests/acceptance/volta_pin.rs index ad1c0b252..e460df4a0 100644 --- a/tests/acceptance/volta_pin.rs +++ b/tests/acceptance/volta_pin.rs @@ -1,5 +1,5 @@ use crate::support::sandbox::{ - sandbox, DistroMetadata, NodeFixture, NpmFixture, Yarn1Fixture, YarnBerryFixture, + sandbox, DistroMetadata, NodeFixture, NpmFixture, PnpmFixture, Yarn1Fixture, YarnBerryFixture, }; use hamcrest2::assert_that; use hamcrest2::prelude::*; @@ -47,6 +47,37 @@ fn package_json_with_pinned_node_npm(node: &str, npm: &str) -> String { ) } +fn package_json_with_pinned_node_pnpm(node_version: &str, pnpm_version: &str) -> String { + format!( + r#"{{ + "name": "test-package", + "volta": {{ + "node": "{}", + "pnpm": "{}" + }} +}}"#, + node_version, pnpm_version + ) +} + +fn package_json_with_pinned_node_npm_pnpm( + node_version: &str, + npm_version: &str, + pnpm_version: &str, +) -> String { + format!( + r#"{{ + "name": "test-package", + "volta": {{ + "node": "{}", + "npm": "{}", + "pnpm": "{}" + }} +}}"#, + node_version, npm_version, pnpm_version + ) +} + fn package_json_with_pinned_node_yarn(node_version: &str, yarn_version: &str) -> String { format!( r#"{{ @@ -229,6 +260,36 @@ const YARN_BERRY_VERSION_FIXTURES: [DistroMetadata; 4] = [ }, ]; +const PNPM_VERSION_INFO: &str = r#" +{ + "name":"pnpm", + "dist-tags": { "latest":"7.7.1" }, + "versions": { + "0.0.1": { "version":"0.0.1", "dist": { "shasum":"", "tarball":"" }}, + "6.34.0": { "version":"6.34.0", "dist": { "shasum":"", "tarball":"" }}, + "7.7.1": { "version":"7.7.1", "dist": { "shasum":"", "tarball":"" }} + } +} +"#; + +const PNPM_VERSION_FIXTURES: [DistroMetadata; 3] = [ + DistroMetadata { + version: "0.0.1", + compressed_size: 10, + uncompressed_size: Some(0x0028_0000), + }, + DistroMetadata { + version: "6.34.0", + compressed_size: 500, + uncompressed_size: Some(0x0028_0000), + }, + DistroMetadata { + version: "7.7.1", + compressed_size: 518, + uncompressed_size: Some(0x0028_0000), + }, +]; + const NPM_VERSION_FIXTURES: [DistroMetadata; 3] = [ DistroMetadata { version: "1.2.3", @@ -842,3 +903,157 @@ fn pin_node_does_not_overwrite_extends() { .read_package_json() .contains(r#""extends": "./basic.json""#)); } + +#[test] +fn pin_pnpm_no_node() { + let s = sandbox() + .package_json(BASIC_PACKAGE_JSON) + .pnpm_available_versions(PNPM_VERSION_INFO) + .distro_mocks::(&PNPM_VERSION_FIXTURES) + .build(); + + assert_that!( + s.volta("pin pnpm@7"), + execs() + .with_status(ExitCode::ConfigurationError as i32) + .with_stderr_contains( + "[..]Cannot pin pnpm because the Node version is not pinned in this project." + ) + ); + + assert_eq!(s.read_package_json(), BASIC_PACKAGE_JSON) +} + +#[test] +fn pin_pnpm() { + let s = sandbox() + .package_json(&package_json_with_pinned_node("1.2.3")) + .pnpm_available_versions(PNPM_VERSION_INFO) + .distro_mocks::(&PNPM_VERSION_FIXTURES) + .build(); + + assert_that!( + s.volta("pin pnpm@7"), + execs().with_status(ExitCode::Success as i32) + ); + + assert_eq!( + s.read_package_json(), + package_json_with_pinned_node_pnpm("1.2.3", "7.7.1"), + ) +} + +#[test] +fn pin_pnpm_reports_info() { + let s = sandbox() + .package_json(&package_json_with_pinned_node("1.2.3")) + .pnpm_available_versions(PNPM_VERSION_INFO) + .distro_mocks::(&PNPM_VERSION_FIXTURES) + .env(VOLTA_LOGLEVEL, "info") + .build(); + + assert_that!( + s.volta("pin pnpm@6"), + execs() + .with_status(ExitCode::Success as i32) + .with_stdout_contains("[..]pinned pnpm@6.34.0 in package.json") + ); +} + +#[test] +fn pin_pnpm_latest() { + let s = sandbox() + .package_json(&package_json_with_pinned_node("1.2.3")) + .pnpm_available_versions(PNPM_VERSION_INFO) + .distro_mocks::(&PNPM_VERSION_FIXTURES) + .build(); + + assert_that!( + s.volta("pin pnpm@latest"), + execs().with_status(ExitCode::Success as i32) + ); + + assert_eq!( + s.read_package_json(), + package_json_with_pinned_node_pnpm("1.2.3", "7.7.1"), + ) +} + +#[test] +fn pin_pnpm_no_version() { + let s = sandbox() + .package_json(&package_json_with_pinned_node("1.2.3")) + .pnpm_available_versions(PNPM_VERSION_INFO) + .distro_mocks::(&PNPM_VERSION_FIXTURES) + .build(); + + assert_that!( + s.volta("pin pnpm"), + execs().with_status(ExitCode::Success as i32) + ); + + assert_eq!( + s.read_package_json(), + package_json_with_pinned_node_pnpm("1.2.3", "7.7.1"), + ) +} + +#[test] +fn pin_pnpm_missing_release() { + let s = sandbox() + .package_json(&package_json_with_pinned_node("1.2.3")) + .mock_not_found() + .build(); + + assert_that!( + s.volta("pin pnpm@3.3.1"), + execs() + .with_status(ExitCode::NetworkError as i32) + .with_stderr_contains("[..]Could not download pnpm@3.3.1") + ); + + assert_eq!( + s.read_package_json(), + package_json_with_pinned_node("1.2.3"), + ) +} + +#[test] +fn pin_node_and_pnpm() { + let s = sandbox() + .package_json(BASIC_PACKAGE_JSON) + .node_available_versions(NODE_VERSION_INFO) + .distro_mocks::(&NODE_VERSION_FIXTURES) + .pnpm_available_versions(PNPM_VERSION_INFO) + .distro_mocks::(&PNPM_VERSION_FIXTURES) + .build(); + + assert_that!( + s.volta("pin node@10 pnpm@6"), + execs().with_status(ExitCode::Success as i32) + ); + + assert_eq!( + s.read_package_json(), + package_json_with_pinned_node_pnpm("10.99.1040", "6.34.0"), + ) +} + +#[test] +fn pin_pnpm_leaves_npm() { + let s = sandbox() + .package_json(&package_json_with_pinned_node_npm("1.2.3", "3.4.5")) + .pnpm_available_versions(PNPM_VERSION_INFO) + .distro_mocks::(&PNPM_VERSION_FIXTURES) + .build(); + + assert_that!( + s.volta("pin pnpm@6.34.0"), + execs().with_status(ExitCode::Success as i32) + ); + + assert_eq!( + s.read_package_json(), + package_json_with_pinned_node_npm_pnpm("1.2.3", "3.4.5", "6.34.0"), + ) +} diff --git a/tests/acceptance/volta_run.rs b/tests/acceptance/volta_run.rs index ea12f0f66..aaa600754 100644 --- a/tests/acceptance/volta_run.rs +++ b/tests/acceptance/volta_run.rs @@ -1,5 +1,5 @@ use crate::support::sandbox::{ - sandbox, DistroMetadata, NodeFixture, NpmFixture, Yarn1Fixture, YarnBerryFixture, + sandbox, DistroMetadata, NodeFixture, NpmFixture, PnpmFixture, Yarn1Fixture, YarnBerryFixture, }; use hamcrest2::assert_that; use hamcrest2::prelude::*; @@ -32,6 +32,19 @@ fn package_json_with_pinned_node_npm(node: &str, npm: &str) -> String { ) } +fn package_json_with_pinned_node_pnpm(node_version: &str, pnpm_version: &str) -> String { + format!( + r#"{{ + "name": "test-package", + "volta": {{ + "node": "{}", + "pnpm": "{}" + }} +}}"#, + node_version, pnpm_version + ) +} + fn package_json_with_pinned_node_yarn(node_version: &str, yarn_version: &str) -> String { format!( r#"{{ @@ -128,6 +141,30 @@ cfg_if::cfg_if! { } } +const PNPM_VERSION_INFO: &str = r#" +{ + "name":"pnpm", + "dist-tags": { "latest":"7.7.1" }, + "versions": { + "6.34.0": { "version":"6.34.0", "dist": { "shasum":"", "tarball":"" }}, + "7.7.1": { "version":"7.7.1", "dist": { "shasum":"", "tarball":"" }} + } +} +"#; + +const PNPM_VERSION_FIXTURES: [DistroMetadata; 2] = [ + DistroMetadata { + version: "6.34.0", + compressed_size: 500, + uncompressed_size: Some(0x0028_0000), + }, + DistroMetadata { + version: "7.7.1", + compressed_size: 518, + uncompressed_size: Some(0x0028_0000), + }, +]; + const YARN_1_VERSION_INFO: &str = r#"{ "name":"yarn", "dist-tags": { "latest":"1.12.99" }, @@ -415,3 +452,59 @@ fn force_no_yarn() { .with_stderr_contains("[..]No Yarn version found in this project.") ); } + +#[test] +fn command_line_pnpm() { + let s = sandbox() + .node_available_versions(NODE_VERSION_INFO) + .distro_mocks::(&NODE_VERSION_FIXTURES) + .pnpm_available_versions(PNPM_VERSION_INFO) + .distro_mocks::(&PNPM_VERSION_FIXTURES) + .env(VOLTA_LOGLEVEL, "debug") + .build(); + + assert_that!( + s.volta("run --node 10.99.1040 --pnpm 6.34.0 pnpm --version"), + execs() + .with_status(ExitCode::Success as i32) + .with_stderr_contains("[..]pnpm: 6.34.0 from command-line configuration") + ); +} + +#[test] +fn inherited_pnpm() { + let s = sandbox() + .node_available_versions(NODE_VERSION_INFO) + .distro_mocks::(&NODE_VERSION_FIXTURES) + .pnpm_available_versions(PNPM_VERSION_INFO) + .distro_mocks::(&PNPM_VERSION_FIXTURES) + .package_json(&package_json_with_pinned_node_pnpm("10.99.1040", "7.7.1")) + .env(VOLTA_LOGLEVEL, "debug") + .build(); + + assert_that!( + s.volta("run --node 10.99.1040 pnpm --version"), + execs() + .with_status(ExitCode::Success as i32) + .with_stderr_contains("[..]pnpm: 7.7.1 from project configuration") + ); +} + +#[test] +fn force_no_pnpm() { + let s = sandbox() + .node_available_versions(NODE_VERSION_INFO) + .distro_mocks::(&NODE_VERSION_FIXTURES) + .pnpm_available_versions(PNPM_VERSION_INFO) + .distro_mocks::(&PNPM_VERSION_FIXTURES) + .package_json(&package_json_with_pinned_node_pnpm("10.99.1040", "7.7.1")) + .env(VOLTA_LOGLEVEL, "debug") + .build(); + + assert_that!( + s.volta("run --no-pnpm pnpm --version"), + execs() + .with_status(ExitCode::ConfigurationError as i32) + .with_stderr_contains("[..]No pnpm version found in this project.") + ); +} diff --git a/tests/fixtures/pnpm-0.0.1.tgz b/tests/fixtures/pnpm-0.0.1.tgz new file mode 100644 index 000000000..429d0fab1 --- /dev/null +++ b/tests/fixtures/pnpm-0.0.1.tgz @@ -0,0 +1 @@ +CORRUPTED diff --git a/tests/fixtures/pnpm-6.34.0.tgz b/tests/fixtures/pnpm-6.34.0.tgz new file mode 100644 index 000000000..ce13cf0ba Binary files /dev/null and b/tests/fixtures/pnpm-6.34.0.tgz differ diff --git a/tests/fixtures/pnpm-7.7.1.tgz b/tests/fixtures/pnpm-7.7.1.tgz new file mode 100644 index 000000000..cd3476a46 Binary files /dev/null and b/tests/fixtures/pnpm-7.7.1.tgz differ diff --git a/wix/main.wxs b/wix/main.wxs index 544a5b426..f5711fe1d 100644 --- a/wix/main.wxs +++ b/wix/main.wxs @@ -138,6 +138,22 @@ Source='wix\shim.cmd' KeyPath='yes'/> + + + + + +