From e3b682fd48d0baa219b00e69fbf1b8e83e63ca8f Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 31 Oct 2025 10:58:44 +0800 Subject: [PATCH] feat(cli): add package manager utility commands --- crates/vite_install/src/commands/cache.rs | 261 +++ crates/vite_install/src/commands/config.rs | 380 ++++ crates/vite_install/src/commands/list.rs | 698 +++++++ crates/vite_install/src/commands/mod.rs | 8 + crates/vite_install/src/commands/owner.rs | 151 ++ crates/vite_install/src/commands/pack.rs | 551 ++++++ crates/vite_install/src/commands/prune.rs | 198 ++ crates/vite_install/src/commands/publish.rs | 404 ++++ crates/vite_install/src/commands/view.rs | 140 ++ packages/cli/binding/src/cli.rs | 323 +++ packages/cli/binding/src/commands/mod.rs | 1 + packages/cli/binding/src/commands/pm.rs | 225 +++ .../exit-non-zero-on-cmd-not-exists/snap.txt | 2 +- .../snap-tests/cli-helper-message/snap.txt | 20 + .../snap-tests/cli-helper-message/steps.json | 3 +- .../command-cache-npm10/package.json | 5 + .../snap-tests/command-cache-npm10/snap.txt | 5 + .../snap-tests/command-cache-npm10/steps.json | 10 + .../command-cache-pnpm10/package.json | 5 + .../snap-tests/command-cache-pnpm10/snap.txt | 14 + .../command-cache-pnpm10/steps.json | 10 + .../command-cache-yarn4/package.json | 5 + .../snap-tests/command-cache-yarn4/snap.txt | 5 + .../snap-tests/command-cache-yarn4/steps.json | 10 + .../snap-tests/command-config-npm10/.npmrc | 2 + .../command-config-npm10/package.json | 5 + .../snap-tests/command-config-npm10/snap.txt | 6 + .../command-config-npm10/steps.json | 10 + .../command-config-pnpm10/package.json | 5 + .../snap-tests/command-config-pnpm10/snap.txt | 21 + .../command-config-pnpm10/steps.json | 12 + .../command-config-yarn1/package.json | 5 + .../snap-tests/command-config-yarn1/snap.txt | 18 + .../command-config-yarn1/steps.json | 10 + .../command-config-yarn4/package.json | 5 + .../snap-tests/command-config-yarn4/snap.txt | 14 + .../command-config-yarn4/steps.json | 10 + .../package.json | 8 + .../packages/app/package.json | 10 + .../packages/utils/package.json | 8 + .../snap.txt | 169 ++ .../steps.json | 13 + .../command-list-npm10/package.json | 14 + .../snap-tests/command-list-npm10/snap.txt | 261 +++ .../snap-tests/command-list-npm10/steps.json | 19 + .../package.json | 5 + .../packages/app/package.json | 10 + .../packages/utils/package.json | 8 + .../pnpm-workspace.yaml | 2 + .../snap.txt | 139 ++ .../steps.json | 18 + .../command-list-pnpm10/package.json | 14 + .../snap-tests/command-list-pnpm10/snap.txt | 195 ++ .../snap-tests/command-list-pnpm10/steps.json | 24 + .../command-list-yarn1/package.json | 11 + .../snap-tests/command-list-yarn1/snap.txt | 121 ++ .../snap-tests/command-list-yarn1/steps.json | 21 + .../command-list-yarn4/package.json | 11 + .../snap-tests/command-list-yarn4/snap.txt | 2 + .../snap-tests/command-list-yarn4/steps.json | 8 + .../command-owner-npm10/package.json | 5 + .../snap-tests/command-owner-npm10/snap.txt | 2 + .../snap-tests/command-owner-npm10/steps.json | 8 + .../command-owner-pnpm10/package.json | 5 + .../snap-tests/command-owner-pnpm10/snap.txt | 16 + .../command-owner-pnpm10/steps.json | 9 + .../command-owner-yarn1/package.json | 5 + .../snap-tests/command-owner-yarn1/snap.txt | 2 + .../snap-tests/command-owner-yarn1/steps.json | 8 + .../command-owner-yarn4/package.json | 5 + .../snap-tests/command-owner-yarn4/snap.txt | 2 + .../snap-tests/command-owner-yarn4/steps.json | 8 + .../package.json | 8 + .../packages/app/package.json | 4 + .../packages/utils/package.json | 5 + .../snap.txt | 217 ++ .../steps.json | 13 + .../command-pack-npm10/package.json | 11 + .../snap-tests/command-pack-npm10/snap.txt | 123 ++ .../snap-tests/command-pack-npm10/steps.json | 11 + .../package.json | 5 + .../packages/app/package.json | 4 + .../packages/utils/package.json | 5 + .../pnpm-workspace.yaml | 2 + .../snap.txt | 194 ++ .../steps.json | 16 + .../command-pack-pnpm10/package.json | 11 + .../snap-tests/command-pack-pnpm10/snap.txt | 114 ++ .../snap-tests/command-pack-pnpm10/steps.json | 15 + .../package.json | 8 + .../packages/app/package.json | 4 + .../packages/utils/package.json | 5 + .../snap.txt | 66 + .../steps.json | 15 + .../command-pack-yarn4/package.json | 11 + .../snap-tests/command-pack-yarn4/snap.txt | 32 + .../snap-tests/command-pack-yarn4/steps.json | 12 + .../command-prune-npm10/package.json | 14 + .../snap-tests/command-prune-npm10/snap.txt | 37 + .../snap-tests/command-prune-npm10/steps.json | 14 + .../command-prune-pnpm10/package.json | 14 + .../snap-tests/command-prune-pnpm10/snap.txt | 139 ++ .../command-prune-pnpm10/steps.json | 14 + .../command-prune-yarn4/package.json | 14 + .../snap-tests/command-prune-yarn4/snap.txt | 15 + .../snap-tests/command-prune-yarn4/steps.json | 9 + .../command-publish-npm10/package.json | 5 + .../snap-tests/command-publish-npm10/snap.txt | 2 + .../command-publish-npm10/steps.json | 8 + .../command-publish-pnpm10/package.json | 5 + .../command-publish-pnpm10/snap.txt | 25 + .../command-publish-pnpm10/steps.json | 9 + .../command-publish-yarn1/package.json | 5 + .../snap-tests/command-publish-yarn1/snap.txt | 2 + .../command-publish-yarn1/steps.json | 8 + .../command-publish-yarn4/package.json | 5 + .../snap-tests/command-publish-yarn4/snap.txt | 2 + .../command-publish-yarn4/steps.json | 8 + .../command-view-npm10/package.json | 5 + .../snap-tests/command-view-npm10/snap.txt | 8 + .../snap-tests/command-view-npm10/steps.json | 10 + .../command-view-pnpm10/package.json | 5 + .../snap-tests/command-view-pnpm10/snap.txt | 80 + .../snap-tests/command-view-pnpm10/steps.json | 16 + .../command-view-yarn1/package.json | 5 + .../snap-tests/command-view-yarn1/snap.txt | 20 + .../snap-tests/command-view-yarn1/steps.json | 9 + .../command-view-yarn4/package.json | 5 + .../snap-tests/command-view-yarn4/snap.txt | 20 + .../snap-tests/command-view-yarn4/steps.json | 9 + .../__snapshots__/utils.spec.ts.snap | 12 + packages/tools/src/__tests__/utils.spec.ts | 18 + packages/tools/src/utils.ts | 18 +- rfcs/pm-command-group.md | 1738 +++++++++++++++++ 134 files changed, 7962 insertions(+), 4 deletions(-) create mode 100644 crates/vite_install/src/commands/cache.rs create mode 100644 crates/vite_install/src/commands/config.rs create mode 100644 crates/vite_install/src/commands/list.rs create mode 100644 crates/vite_install/src/commands/owner.rs create mode 100644 crates/vite_install/src/commands/pack.rs create mode 100644 crates/vite_install/src/commands/prune.rs create mode 100644 crates/vite_install/src/commands/publish.rs create mode 100644 crates/vite_install/src/commands/view.rs create mode 100644 packages/cli/binding/src/commands/pm.rs create mode 100644 packages/global/snap-tests/command-cache-npm10/package.json create mode 100644 packages/global/snap-tests/command-cache-npm10/snap.txt create mode 100644 packages/global/snap-tests/command-cache-npm10/steps.json create mode 100644 packages/global/snap-tests/command-cache-pnpm10/package.json create mode 100644 packages/global/snap-tests/command-cache-pnpm10/snap.txt create mode 100644 packages/global/snap-tests/command-cache-pnpm10/steps.json create mode 100644 packages/global/snap-tests/command-cache-yarn4/package.json create mode 100644 packages/global/snap-tests/command-cache-yarn4/snap.txt create mode 100644 packages/global/snap-tests/command-cache-yarn4/steps.json create mode 100644 packages/global/snap-tests/command-config-npm10/.npmrc create mode 100644 packages/global/snap-tests/command-config-npm10/package.json create mode 100644 packages/global/snap-tests/command-config-npm10/snap.txt create mode 100644 packages/global/snap-tests/command-config-npm10/steps.json create mode 100644 packages/global/snap-tests/command-config-pnpm10/package.json create mode 100644 packages/global/snap-tests/command-config-pnpm10/snap.txt create mode 100644 packages/global/snap-tests/command-config-pnpm10/steps.json create mode 100644 packages/global/snap-tests/command-config-yarn1/package.json create mode 100644 packages/global/snap-tests/command-config-yarn1/snap.txt create mode 100644 packages/global/snap-tests/command-config-yarn1/steps.json create mode 100644 packages/global/snap-tests/command-config-yarn4/package.json create mode 100644 packages/global/snap-tests/command-config-yarn4/snap.txt create mode 100644 packages/global/snap-tests/command-config-yarn4/steps.json create mode 100644 packages/global/snap-tests/command-list-npm10-with-workspace/package.json create mode 100644 packages/global/snap-tests/command-list-npm10-with-workspace/packages/app/package.json create mode 100644 packages/global/snap-tests/command-list-npm10-with-workspace/packages/utils/package.json create mode 100644 packages/global/snap-tests/command-list-npm10-with-workspace/snap.txt create mode 100644 packages/global/snap-tests/command-list-npm10-with-workspace/steps.json create mode 100644 packages/global/snap-tests/command-list-npm10/package.json create mode 100644 packages/global/snap-tests/command-list-npm10/snap.txt create mode 100644 packages/global/snap-tests/command-list-npm10/steps.json create mode 100644 packages/global/snap-tests/command-list-pnpm10-with-workspace/package.json create mode 100644 packages/global/snap-tests/command-list-pnpm10-with-workspace/packages/app/package.json create mode 100644 packages/global/snap-tests/command-list-pnpm10-with-workspace/packages/utils/package.json create mode 100644 packages/global/snap-tests/command-list-pnpm10-with-workspace/pnpm-workspace.yaml create mode 100644 packages/global/snap-tests/command-list-pnpm10-with-workspace/snap.txt create mode 100644 packages/global/snap-tests/command-list-pnpm10-with-workspace/steps.json create mode 100644 packages/global/snap-tests/command-list-pnpm10/package.json create mode 100644 packages/global/snap-tests/command-list-pnpm10/snap.txt create mode 100644 packages/global/snap-tests/command-list-pnpm10/steps.json create mode 100644 packages/global/snap-tests/command-list-yarn1/package.json create mode 100644 packages/global/snap-tests/command-list-yarn1/snap.txt create mode 100644 packages/global/snap-tests/command-list-yarn1/steps.json create mode 100644 packages/global/snap-tests/command-list-yarn4/package.json create mode 100644 packages/global/snap-tests/command-list-yarn4/snap.txt create mode 100644 packages/global/snap-tests/command-list-yarn4/steps.json create mode 100644 packages/global/snap-tests/command-owner-npm10/package.json create mode 100644 packages/global/snap-tests/command-owner-npm10/snap.txt create mode 100644 packages/global/snap-tests/command-owner-npm10/steps.json create mode 100644 packages/global/snap-tests/command-owner-pnpm10/package.json create mode 100644 packages/global/snap-tests/command-owner-pnpm10/snap.txt create mode 100644 packages/global/snap-tests/command-owner-pnpm10/steps.json create mode 100644 packages/global/snap-tests/command-owner-yarn1/package.json create mode 100644 packages/global/snap-tests/command-owner-yarn1/snap.txt create mode 100644 packages/global/snap-tests/command-owner-yarn1/steps.json create mode 100644 packages/global/snap-tests/command-owner-yarn4/package.json create mode 100644 packages/global/snap-tests/command-owner-yarn4/snap.txt create mode 100644 packages/global/snap-tests/command-owner-yarn4/steps.json create mode 100644 packages/global/snap-tests/command-pack-npm10-with-workspace/package.json create mode 100644 packages/global/snap-tests/command-pack-npm10-with-workspace/packages/app/package.json create mode 100644 packages/global/snap-tests/command-pack-npm10-with-workspace/packages/utils/package.json create mode 100644 packages/global/snap-tests/command-pack-npm10-with-workspace/snap.txt create mode 100644 packages/global/snap-tests/command-pack-npm10-with-workspace/steps.json create mode 100644 packages/global/snap-tests/command-pack-npm10/package.json create mode 100644 packages/global/snap-tests/command-pack-npm10/snap.txt create mode 100644 packages/global/snap-tests/command-pack-npm10/steps.json create mode 100644 packages/global/snap-tests/command-pack-pnpm10-with-workspace/package.json create mode 100644 packages/global/snap-tests/command-pack-pnpm10-with-workspace/packages/app/package.json create mode 100644 packages/global/snap-tests/command-pack-pnpm10-with-workspace/packages/utils/package.json create mode 100644 packages/global/snap-tests/command-pack-pnpm10-with-workspace/pnpm-workspace.yaml create mode 100644 packages/global/snap-tests/command-pack-pnpm10-with-workspace/snap.txt create mode 100644 packages/global/snap-tests/command-pack-pnpm10-with-workspace/steps.json create mode 100644 packages/global/snap-tests/command-pack-pnpm10/package.json create mode 100644 packages/global/snap-tests/command-pack-pnpm10/snap.txt create mode 100644 packages/global/snap-tests/command-pack-pnpm10/steps.json create mode 100644 packages/global/snap-tests/command-pack-yarn4-with-workspace/package.json create mode 100644 packages/global/snap-tests/command-pack-yarn4-with-workspace/packages/app/package.json create mode 100644 packages/global/snap-tests/command-pack-yarn4-with-workspace/packages/utils/package.json create mode 100644 packages/global/snap-tests/command-pack-yarn4-with-workspace/snap.txt create mode 100644 packages/global/snap-tests/command-pack-yarn4-with-workspace/steps.json create mode 100644 packages/global/snap-tests/command-pack-yarn4/package.json create mode 100644 packages/global/snap-tests/command-pack-yarn4/snap.txt create mode 100644 packages/global/snap-tests/command-pack-yarn4/steps.json create mode 100644 packages/global/snap-tests/command-prune-npm10/package.json create mode 100644 packages/global/snap-tests/command-prune-npm10/snap.txt create mode 100644 packages/global/snap-tests/command-prune-npm10/steps.json create mode 100644 packages/global/snap-tests/command-prune-pnpm10/package.json create mode 100644 packages/global/snap-tests/command-prune-pnpm10/snap.txt create mode 100644 packages/global/snap-tests/command-prune-pnpm10/steps.json create mode 100644 packages/global/snap-tests/command-prune-yarn4/package.json create mode 100644 packages/global/snap-tests/command-prune-yarn4/snap.txt create mode 100644 packages/global/snap-tests/command-prune-yarn4/steps.json create mode 100644 packages/global/snap-tests/command-publish-npm10/package.json create mode 100644 packages/global/snap-tests/command-publish-npm10/snap.txt create mode 100644 packages/global/snap-tests/command-publish-npm10/steps.json create mode 100644 packages/global/snap-tests/command-publish-pnpm10/package.json create mode 100644 packages/global/snap-tests/command-publish-pnpm10/snap.txt create mode 100644 packages/global/snap-tests/command-publish-pnpm10/steps.json create mode 100644 packages/global/snap-tests/command-publish-yarn1/package.json create mode 100644 packages/global/snap-tests/command-publish-yarn1/snap.txt create mode 100644 packages/global/snap-tests/command-publish-yarn1/steps.json create mode 100644 packages/global/snap-tests/command-publish-yarn4/package.json create mode 100644 packages/global/snap-tests/command-publish-yarn4/snap.txt create mode 100644 packages/global/snap-tests/command-publish-yarn4/steps.json create mode 100644 packages/global/snap-tests/command-view-npm10/package.json create mode 100644 packages/global/snap-tests/command-view-npm10/snap.txt create mode 100644 packages/global/snap-tests/command-view-npm10/steps.json create mode 100644 packages/global/snap-tests/command-view-pnpm10/package.json create mode 100644 packages/global/snap-tests/command-view-pnpm10/snap.txt create mode 100644 packages/global/snap-tests/command-view-pnpm10/steps.json create mode 100644 packages/global/snap-tests/command-view-yarn1/package.json create mode 100644 packages/global/snap-tests/command-view-yarn1/snap.txt create mode 100644 packages/global/snap-tests/command-view-yarn1/steps.json create mode 100644 packages/global/snap-tests/command-view-yarn4/package.json create mode 100644 packages/global/snap-tests/command-view-yarn4/snap.txt create mode 100644 packages/global/snap-tests/command-view-yarn4/steps.json create mode 100644 rfcs/pm-command-group.md diff --git a/crates/vite_install/src/commands/cache.rs b/crates/vite_install/src/commands/cache.rs new file mode 100644 index 0000000000..05f9a9f973 --- /dev/null +++ b/crates/vite_install/src/commands/cache.rs @@ -0,0 +1,261 @@ +use std::{collections::HashMap, process::ExitStatus}; + +use vite_command::run_command; +use vite_error::Error; +use vite_path::AbsolutePath; + +use crate::package_manager::{ + PackageManager, PackageManagerType, ResolveCommandResult, format_path_env, +}; + +/// Options for the cache command. +#[derive(Debug)] +pub struct CacheCommandOptions<'a> { + pub subcommand: &'a str, + pub pass_through_args: Option<&'a [String]>, +} + +impl PackageManager { + /// Run the cache command with the package manager. + /// Returns ExitStatus with success (0) if the command is not supported. + #[must_use] + pub async fn run_cache_command( + &self, + options: &CacheCommandOptions<'_>, + cwd: impl AsRef, + ) -> Result { + let Some(resolve_command) = self.resolve_cache_command(options) else { + // Command not supported, return success + return Ok(ExitStatus::default()); + }; + run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) + .await + } + + /// Resolve the cache command. + /// Returns None if the command is not supported by the package manager. + #[must_use] + pub fn resolve_cache_command( + &self, + options: &CacheCommandOptions, + ) -> Option { + let bin_name: String; + let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); + let mut args: Vec = Vec::new(); + + match self.client { + PackageManagerType::Pnpm => { + bin_name = "pnpm".into(); + + match options.subcommand { + "dir" | "path" => { + args.push("store".into()); + args.push("path".into()); + } + "clean" => { + args.push("store".into()); + args.push("prune".into()); + } + _ => { + println!( + "Warning: pnpm cache subcommand '{}' not supported", + options.subcommand + ); + return None; + } + } + } + PackageManagerType::Npm => { + bin_name = "npm".into(); + + match options.subcommand { + "dir" | "path" => { + // npm uses 'config get cache' to get cache directory + args.push("config".into()); + args.push("get".into()); + args.push("cache".into()); + } + "clean" => { + args.push("cache".into()); + args.push("clean".into()); + } + _ => { + println!( + "Warning: npm cache subcommand '{}' not supported", + options.subcommand + ); + return None; + } + } + } + PackageManagerType::Yarn => { + bin_name = "yarn".into(); + let is_yarn1 = self.version.starts_with("1."); + + match options.subcommand { + "dir" | "path" => { + if is_yarn1 { + args.push("cache".into()); + args.push("dir".into()); + } else { + args.push("config".into()); + args.push("get".into()); + args.push("cacheFolder".into()); + } + } + "clean" => { + args.push("cache".into()); + args.push("clean".into()); + } + _ => { + println!( + "Warning: yarn cache subcommand '{}' not supported", + options.subcommand + ); + return None; + } + } + } + } + + // Add pass-through args + if let Some(pass_through_args) = options.pass_through_args { + args.extend_from_slice(pass_through_args); + } + + Some(ResolveCommandResult { bin_path: bin_name, args, envs }) + } +} + +#[cfg(test)] +mod tests { + use tempfile::{TempDir, tempdir}; + use vite_path::AbsolutePathBuf; + use vite_str::Str; + + use super::*; + + fn create_temp_dir() -> TempDir { + tempdir().expect("Failed to create temp directory") + } + + fn create_mock_package_manager(pm_type: PackageManagerType, version: &str) -> PackageManager { + let temp_dir = create_temp_dir(); + let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + let install_dir = temp_dir_path.join("install"); + + PackageManager { + client: pm_type, + package_name: pm_type.to_string().into(), + version: Str::from(version), + hash: None, + bin_name: pm_type.to_string().into(), + workspace_root: temp_dir_path.clone(), + install_dir, + } + } + + #[test] + fn test_pnpm_cache_dir() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = pm.resolve_cache_command(&CacheCommandOptions { + subcommand: "dir", + pass_through_args: None, + }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["store", "path"]); + } + + #[test] + fn test_npm_cache_dir() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = pm.resolve_cache_command(&CacheCommandOptions { + subcommand: "dir", + pass_through_args: None, + }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["config", "get", "cache"]); + } + + #[test] + fn test_yarn1_cache_dir() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.0"); + let result = pm.resolve_cache_command(&CacheCommandOptions { + subcommand: "dir", + pass_through_args: None, + }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["cache", "dir"]); + } + + #[test] + fn test_yarn2_cache_dir() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); + let result = pm.resolve_cache_command(&CacheCommandOptions { + subcommand: "dir", + pass_through_args: None, + }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["config", "get", "cacheFolder"]); + } + + #[test] + fn test_pnpm_cache_clean() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = pm.resolve_cache_command(&CacheCommandOptions { + subcommand: "clean", + pass_through_args: None, + }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["store", "prune"]); + } + + #[test] + fn test_npm_cache_clean() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = pm.resolve_cache_command(&CacheCommandOptions { + subcommand: "clean", + pass_through_args: None, + }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["cache", "clean"]); + } + + #[test] + fn test_yarn1_cache_clean() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.0"); + let result = pm.resolve_cache_command(&CacheCommandOptions { + subcommand: "clean", + pass_through_args: None, + }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["cache", "clean"]); + } + + #[test] + fn test_yarn2_cache_clean() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); + let result = pm.resolve_cache_command(&CacheCommandOptions { + subcommand: "clean", + pass_through_args: None, + }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["cache", "clean"]); + } +} diff --git a/crates/vite_install/src/commands/config.rs b/crates/vite_install/src/commands/config.rs new file mode 100644 index 0000000000..cf2dd630da --- /dev/null +++ b/crates/vite_install/src/commands/config.rs @@ -0,0 +1,380 @@ +use std::{collections::HashMap, process::ExitStatus}; + +use vite_command::run_command; +use vite_error::Error; +use vite_path::AbsolutePath; + +use crate::package_manager::{ + PackageManager, PackageManagerType, ResolveCommandResult, format_path_env, +}; + +/// Options for the config command. +#[derive(Debug)] +pub struct ConfigCommandOptions<'a> { + pub subcommand: &'a str, + pub key: Option<&'a str>, + pub value: Option<&'a str>, + pub json: bool, + pub location: Option<&'a str>, + pub pass_through_args: Option<&'a [String]>, +} + +impl PackageManager { + /// Run the config command with the package manager. + #[must_use] + pub async fn run_config_command( + &self, + options: &ConfigCommandOptions<'_>, + cwd: impl AsRef, + ) -> Result { + let resolve_command = self.resolve_config_command(options); + run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) + .await + } + + /// Resolve the config command. + #[must_use] + pub fn resolve_config_command(&self, options: &ConfigCommandOptions) -> ResolveCommandResult { + let bin_name: String = self.client.to_string(); + let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); + let mut args: Vec = Vec::new(); + + match self.client { + PackageManagerType::Pnpm => { + args.push("config".into()); + args.push(options.subcommand.to_string()); + + if let Some(key) = options.key { + args.push(key.to_string()); + } + + if let Some(value) = options.value { + args.push(value.to_string()); + } + + if options.json { + args.push("--json".into()); + } + + if let Some(location) = options.location { + args.push("--location".into()); + args.push(location.to_string()); + } + } + PackageManagerType::Npm => { + args.push("config".into()); + args.push(options.subcommand.to_string()); + + if let Some(key) = options.key { + args.push(key.to_string()); + } + + if let Some(value) = options.value { + args.push(value.to_string()); + } + + if options.json { + args.push("--json".into()); + } + + if let Some(location) = options.location { + args.push("--location".into()); + args.push(location.to_string()); + } + } + PackageManagerType::Yarn => { + args.push("config".into()); + + let is_yarn1 = self.version.starts_with("1."); + + // yarn@2+ uses 'unset' instead of 'delete', and no subcommand for 'list' + if options.subcommand == "delete" && !is_yarn1 { + args.push("unset".into()); + } else if options.subcommand == "list" && !is_yarn1 { + // yarn@2+: 'yarn config' with no subcommand lists all + // Don't add 'list' + } else { + args.push(options.subcommand.to_string()); + } + + if let Some(key) = options.key { + args.push(key.to_string()); + } + + if let Some(value) = options.value { + args.push(value.to_string()); + } + + if options.json { + args.push("--json".into()); + } + + // Handle --location parameter + if let Some(location) = options.location { + if !is_yarn1 { + // yarn@2+: map 'global' to --home + if location == "global" { + args.push("--home".into()); + } + } else { + // yarn@1: use --global for global location + if location == "global" { + args.push("--global".into()); + } else { + println!("Warning: yarn@1 does not support --location, ignoring flag"); + } + } + } + } + } + + // Add pass-through args + if let Some(pass_through_args) = options.pass_through_args { + args.extend_from_slice(pass_through_args); + } + + ResolveCommandResult { bin_path: bin_name, args, envs } + } +} + +#[cfg(test)] +mod tests { + use tempfile::{TempDir, tempdir}; + use vite_path::AbsolutePathBuf; + use vite_str::Str; + + use super::*; + + fn create_temp_dir() -> TempDir { + tempdir().expect("Failed to create temp directory") + } + + fn create_mock_package_manager(pm_type: PackageManagerType, version: &str) -> PackageManager { + let temp_dir = create_temp_dir(); + let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + let install_dir = temp_dir_path.join("install"); + + PackageManager { + client: pm_type, + package_name: pm_type.to_string().into(), + version: Str::from(version), + hash: None, + bin_name: pm_type.to_string().into(), + workspace_root: temp_dir_path.clone(), + install_dir, + } + } + + #[test] + fn test_pnpm_config_set() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = pm.resolve_config_command(&ConfigCommandOptions { + subcommand: "set", + key: Some("registry"), + value: Some("https://registry.npmjs.org"), + json: false, + location: None, + pass_through_args: None, + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["config", "set", "registry", "https://registry.npmjs.org"]); + } + + #[test] + fn test_npm_config_set() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = pm.resolve_config_command(&ConfigCommandOptions { + subcommand: "set", + key: Some("registry"), + value: Some("https://registry.npmjs.org"), + json: false, + location: None, + pass_through_args: None, + }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["config", "set", "registry", "https://registry.npmjs.org"]); + } + + #[test] + fn test_config_set_with_json() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = pm.resolve_config_command(&ConfigCommandOptions { + subcommand: "set", + key: Some("registry"), + value: Some("https://registry.npmjs.org"), + json: true, + location: None, + pass_through_args: None, + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!( + result.args, + vec!["config", "set", "registry", "https://registry.npmjs.org", "--json"] + ); + } + + #[test] + fn test_config_set_with_location_global() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = pm.resolve_config_command(&ConfigCommandOptions { + subcommand: "set", + key: Some("registry"), + value: Some("https://registry.npmjs.org"), + json: false, + location: Some("global"), + pass_through_args: None, + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!( + result.args, + vec!["config", "set", "registry", "https://registry.npmjs.org", "--location", "global"] + ); + } + + #[test] + fn test_yarn2_config_set_location_global() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); + let result = pm.resolve_config_command(&ConfigCommandOptions { + subcommand: "set", + key: Some("registry"), + value: Some("https://registry.npmjs.org"), + json: false, + location: Some("global"), + pass_through_args: None, + }); + assert_eq!(result.bin_path, "yarn"); + assert_eq!( + result.args, + vec!["config", "set", "registry", "https://registry.npmjs.org", "--home"] + ); + } + + #[test] + fn test_yarn1_config_set() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.0"); + let result = pm.resolve_config_command(&ConfigCommandOptions { + subcommand: "set", + key: Some("registry"), + value: Some("https://registry.npmjs.org"), + json: false, + location: None, + pass_through_args: None, + }); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["config", "set", "registry", "https://registry.npmjs.org"]); + } + + #[test] + fn test_pnpm_config_set_global() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = pm.resolve_config_command(&ConfigCommandOptions { + subcommand: "set", + key: Some("registry"), + value: Some("https://registry.npmjs.org"), + json: false, + location: Some("global"), + pass_through_args: None, + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!( + result.args, + vec!["config", "set", "registry", "https://registry.npmjs.org", "--location", "global"] + ); + } + + #[test] + fn test_npm_config_set_global() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = pm.resolve_config_command(&ConfigCommandOptions { + subcommand: "set", + key: Some("registry"), + value: Some("https://registry.npmjs.org"), + json: false, + location: Some("global"), + pass_through_args: None, + }); + assert_eq!(result.bin_path, "npm"); + assert_eq!( + result.args, + vec!["config", "set", "registry", "https://registry.npmjs.org", "--location", "global"] + ); + } + + #[test] + fn test_yarn1_config_set_global() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.0"); + let result = pm.resolve_config_command(&ConfigCommandOptions { + subcommand: "set", + key: Some("registry"), + value: Some("https://registry.npmjs.org"), + json: false, + location: Some("global"), + pass_through_args: None, + }); + assert_eq!(result.bin_path, "yarn"); + assert_eq!( + result.args, + vec!["config", "set", "registry", "https://registry.npmjs.org", "--global"] + ); + } + + #[test] + fn test_pnpm_config_get() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = pm.resolve_config_command(&ConfigCommandOptions { + subcommand: "get", + key: Some("registry"), + value: None, + json: false, + location: None, + pass_through_args: None, + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["config", "get", "registry"]); + } + + #[test] + fn test_npm_config_delete() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = pm.resolve_config_command(&ConfigCommandOptions { + subcommand: "delete", + key: Some("registry"), + value: None, + json: false, + location: None, + pass_through_args: None, + }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["config", "delete", "registry"]); + } + + #[test] + fn test_yarn2_config_delete() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); + let result = pm.resolve_config_command(&ConfigCommandOptions { + subcommand: "delete", + key: Some("registry"), + value: None, + json: false, + location: None, + pass_through_args: None, + }); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["config", "unset", "registry"]); + } + + #[test] + fn test_yarn2_config_list() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); + let result = pm.resolve_config_command(&ConfigCommandOptions { + subcommand: "list", + key: None, + value: None, + json: false, + location: None, + pass_through_args: None, + }); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["config"]); + } +} diff --git a/crates/vite_install/src/commands/list.rs b/crates/vite_install/src/commands/list.rs new file mode 100644 index 0000000000..37823f339b --- /dev/null +++ b/crates/vite_install/src/commands/list.rs @@ -0,0 +1,698 @@ +use std::{collections::HashMap, process::ExitStatus}; + +use vite_command::run_command; +use vite_error::Error; +use vite_path::AbsolutePath; + +use crate::package_manager::{ + PackageManager, PackageManagerType, ResolveCommandResult, format_path_env, +}; + +/// Options for the list command. +#[derive(Debug, Default)] +pub struct ListCommandOptions<'a> { + pub pattern: Option<&'a str>, + pub depth: Option, + pub json: bool, + pub long: bool, + pub parseable: bool, + pub prod: bool, + pub dev: bool, + pub no_optional: bool, + pub exclude_peers: bool, + pub only_projects: bool, + pub find_by: Option<&'a str>, + pub recursive: bool, + pub filters: Option<&'a [String]>, + pub global: bool, + pub pass_through_args: Option<&'a [String]>, +} + +impl PackageManager { + /// Run the list command with the package manager. + /// Returns ExitStatus with success (0) if the command is not supported. + #[must_use] + pub async fn run_list_command( + &self, + options: &ListCommandOptions<'_>, + cwd: impl AsRef, + ) -> Result { + let Some(resolve_command) = self.resolve_list_command(options) else { + // Command not supported, return success + return Ok(ExitStatus::default()); + }; + run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) + .await + } + + /// Resolve the list command. + /// Returns None if the command is not supported by the package manager. + #[must_use] + pub fn resolve_list_command( + &self, + options: &ListCommandOptions, + ) -> Option { + // yarn@2+ does not support list command + if self.client == PackageManagerType::Yarn && !self.version.starts_with("1.") { + println!("Warning: yarn@2+ does not support 'list' command"); + return None; + } + + let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); + let mut args: Vec = Vec::new(); + + // Global packages should use npm cli only (since global installs use npm) + let bin_name: String; + if options.global { + bin_name = "npm".into(); + Self::format_npm_list_args(&mut args, options); + args.push("-g".into()); + + // Add pass-through args + if let Some(pass_through_args) = options.pass_through_args { + args.extend_from_slice(pass_through_args); + } + + return Some(ResolveCommandResult { bin_path: bin_name, args, envs }); + } + + bin_name = self.client.to_string(); + + match self.client { + PackageManagerType::Pnpm => { + // pnpm: --filter must come before command + if let Some(filters) = options.filters { + for filter in filters { + args.push("--filter".into()); + args.push(filter.clone()); + } + } + + args.push("list".into()); + + if let Some(pattern) = options.pattern { + args.push(pattern.to_string()); + } + + if let Some(depth) = options.depth { + args.push("--depth".into()); + args.push(depth.to_string()); + } + + if options.json { + args.push("--json".into()); + } + + if options.long { + args.push("--long".into()); + } + + if options.parseable { + args.push("--parseable".into()); + } + + if options.prod { + args.push("--prod".into()); + } + + if options.dev { + args.push("--dev".into()); + } + + if options.no_optional { + args.push("--no-optional".into()); + } + + if options.exclude_peers { + args.push("--exclude-peers".into()); + } + + if options.only_projects { + args.push("--only-projects".into()); + } + + if let Some(find_by) = options.find_by { + args.push("--find-by".into()); + args.push(find_by.to_string()); + } + + if options.recursive { + args.push("--recursive".into()); + } + } + PackageManagerType::Npm => { + Self::format_npm_list_args(&mut args, options); + } + PackageManagerType::Yarn => { + // yarn@1 only (yarn@2+ already filtered out earlier) + args.push("list".into()); + + if let Some(pattern) = options.pattern { + args.push(pattern.to_string()); + } + + if let Some(depth) = options.depth { + args.push("--depth".into()); + args.push(depth.to_string()); + } + + if options.json { + args.push("--json".into()); + } + + if options.prod { + println!("Warning: yarn@1 does not support --prod, ignoring --prod flag"); + } + + if options.dev { + println!("Warning: yarn@1 does not support --dev, ignoring --dev flag"); + } + + if options.no_optional { + println!( + "Warning: yarn@1 does not support --no-optional, ignoring --no-optional flag" + ); + } + + if options.exclude_peers { + println!("Warning: yarn@1 does not support --exclude-peers, ignoring flag"); + } + + if options.only_projects { + println!("Warning: yarn@1 does not support --only-projects, ignoring flag"); + } + + if options.find_by.is_some() { + println!("Warning: yarn@1 does not support --find-by, ignoring flag"); + } + + if options.recursive { + println!( + "Warning: yarn@1 does not support --recursive, ignoring --recursive flag" + ); + } + + // Check for filters (not supported by yarn@1) + if let Some(filters) = options.filters { + if !filters.is_empty() { + println!( + "Warning: yarn@1 does not support --filter, ignoring --filter flag" + ); + } + } + } + } + + // Add pass-through args + if let Some(pass_through_args) = options.pass_through_args { + args.extend_from_slice(pass_through_args); + } + + Some(ResolveCommandResult { bin_path: bin_name, args, envs }) + } + + fn format_npm_list_args(args: &mut Vec, options: &ListCommandOptions) { + args.push("list".into()); + + if let Some(pattern) = options.pattern { + args.push(pattern.to_string()); + } + + if let Some(depth) = options.depth { + args.push("--depth".into()); + args.push(depth.to_string()); + } + + if options.json { + args.push("--json".into()); + } + + if options.long { + args.push("--long".into()); + } + + if options.parseable { + args.push("--parseable".into()); + } + + if options.prod { + args.push("--include".into()); + args.push("prod".into()); + args.push("--include".into()); + args.push("peer".into()); + } + + if options.dev { + args.push("--include".into()); + args.push("dev".into()); + } + + if options.no_optional { + args.push("--omit".into()); + args.push("optional".into()); + } + + if options.exclude_peers { + args.push("--omit".into()); + args.push("peer".into()); + } + + if options.only_projects { + println!("Warning: --only-projects not supported by npm, ignoring flag"); + } + + if options.find_by.is_some() { + println!("Warning: --find-by not supported by npm, ignoring flag"); + } + + if options.recursive { + args.push("--workspaces".into()); + } + + // npm: --workspace comes after command (maps from --filter) + if let Some(filters) = options.filters { + for filter in filters { + args.push("--workspace".into()); + args.push(filter.clone()); + } + } + } +} + +#[cfg(test)] +mod tests { + use tempfile::{TempDir, tempdir}; + use vite_path::AbsolutePathBuf; + use vite_str::Str; + + use super::*; + + fn create_temp_dir() -> TempDir { + tempdir().expect("Failed to create temp directory") + } + + fn create_mock_package_manager(pm_type: PackageManagerType, version: &str) -> PackageManager { + let temp_dir = create_temp_dir(); + let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + let install_dir = temp_dir_path.join("install"); + + PackageManager { + client: pm_type, + package_name: pm_type.to_string().into(), + version: Str::from(version), + hash: None, + bin_name: pm_type.to_string().into(), + workspace_root: temp_dir_path.clone(), + install_dir, + } + } + + #[test] + fn test_pnpm_list_basic() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = pm.resolve_list_command(&ListCommandOptions::default()); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["list"]); + } + + #[test] + fn test_pnpm_list_recursive() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = + pm.resolve_list_command(&ListCommandOptions { recursive: true, ..Default::default() }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["list", "--recursive"]); + } + + #[test] + fn test_npm_list_basic() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = pm.resolve_list_command(&ListCommandOptions::default()); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["list"]); + } + + #[test] + fn test_npm_list_recursive() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = + pm.resolve_list_command(&ListCommandOptions { recursive: true, ..Default::default() }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["list", "--workspaces"]); + } + + #[test] + fn test_yarn1_list_basic() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.0"); + let result = pm.resolve_list_command(&ListCommandOptions::default()); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["list"]); + } + + #[test] + fn test_yarn1_list_recursive_ignored() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.0"); + let result = + pm.resolve_list_command(&ListCommandOptions { recursive: true, ..Default::default() }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["list"]); + } + + #[test] + fn test_yarn2_list_not_supported() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); + let result = pm.resolve_list_command(&ListCommandOptions::default()); + assert!(result.is_none()); + } + + #[test] + fn test_pnpm_list_global_uses_npm() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = + pm.resolve_list_command(&ListCommandOptions { global: true, ..Default::default() }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["list", "-g"]); + } + + #[test] + fn test_npm_list_global() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = + pm.resolve_list_command(&ListCommandOptions { global: true, ..Default::default() }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["list", "-g"]); + } + + #[test] + fn test_yarn1_list_global_uses_npm() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.0"); + let result = + pm.resolve_list_command(&ListCommandOptions { global: true, ..Default::default() }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["list", "-g"]); + } + + #[test] + fn test_global_list_with_depth() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = pm.resolve_list_command(&ListCommandOptions { + global: true, + depth: Some(0), + ..Default::default() + }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["list", "--depth", "0", "-g"]); + } + + #[test] + fn test_pnpm_list_with_filter() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let filters = vec!["app".to_string()]; + let result = pm.resolve_list_command(&ListCommandOptions { + filters: Some(&filters), + ..Default::default() + }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["--filter", "app", "list"]); + } + + #[test] + fn test_pnpm_list_with_multiple_filters() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let filters = vec!["app".to_string(), "web".to_string()]; + let result = pm.resolve_list_command(&ListCommandOptions { + filters: Some(&filters), + ..Default::default() + }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["--filter", "app", "--filter", "web", "list"]); + } + + #[test] + fn test_npm_list_with_filter() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let filters = vec!["app".to_string()]; + let result = pm.resolve_list_command(&ListCommandOptions { + filters: Some(&filters), + ..Default::default() + }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["list", "--workspace", "app"]); + } + + #[test] + fn test_yarn1_list_with_filter_ignored() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.0"); + let filters = vec!["app".to_string()]; + let result = pm.resolve_list_command(&ListCommandOptions { + filters: Some(&filters), + ..Default::default() + }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["list"]); + } + + #[test] + fn test_pnpm_list_prod() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = + pm.resolve_list_command(&ListCommandOptions { prod: true, ..Default::default() }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["list", "--prod"]); + } + + #[test] + fn test_npm_list_prod() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = + pm.resolve_list_command(&ListCommandOptions { prod: true, ..Default::default() }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["list", "--include", "prod", "--include", "peer"]); + } + + #[test] + fn test_yarn1_list_prod_ignored() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.0"); + let result = + pm.resolve_list_command(&ListCommandOptions { prod: true, ..Default::default() }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["list"]); + } + + #[test] + fn test_pnpm_list_dev() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = + pm.resolve_list_command(&ListCommandOptions { dev: true, ..Default::default() }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["list", "--dev"]); + } + + #[test] + fn test_npm_list_dev() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = + pm.resolve_list_command(&ListCommandOptions { dev: true, ..Default::default() }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["list", "--include", "dev"]); + } + + #[test] + fn test_yarn1_list_dev_ignored() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.0"); + let result = + pm.resolve_list_command(&ListCommandOptions { dev: true, ..Default::default() }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["list"]); + } + + #[test] + fn test_pnpm_list_no_optional() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = pm + .resolve_list_command(&ListCommandOptions { no_optional: true, ..Default::default() }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["list", "--no-optional"]); + } + + #[test] + fn test_npm_list_no_optional() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = pm + .resolve_list_command(&ListCommandOptions { no_optional: true, ..Default::default() }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["list", "--omit", "optional"]); + } + + #[test] + fn test_yarn1_list_no_optional_ignored() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.0"); + let result = pm + .resolve_list_command(&ListCommandOptions { no_optional: true, ..Default::default() }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["list"]); + } + + #[test] + fn test_pnpm_list_only_projects() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = pm.resolve_list_command(&ListCommandOptions { + only_projects: true, + ..Default::default() + }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["list", "--only-projects"]); + } + + #[test] + fn test_npm_list_only_projects_ignored() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = pm.resolve_list_command(&ListCommandOptions { + only_projects: true, + ..Default::default() + }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["list"]); + } + + #[test] + fn test_yarn1_list_only_projects_ignored() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.0"); + let result = pm.resolve_list_command(&ListCommandOptions { + only_projects: true, + ..Default::default() + }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["list"]); + } + + #[test] + fn test_pnpm_list_exclude_peers() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = pm.resolve_list_command(&ListCommandOptions { + exclude_peers: true, + ..Default::default() + }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["list", "--exclude-peers"]); + } + + #[test] + fn test_npm_list_exclude_peers() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = pm.resolve_list_command(&ListCommandOptions { + exclude_peers: true, + ..Default::default() + }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["list", "--omit", "peer"]); + } + + #[test] + fn test_yarn1_list_exclude_peers_ignored() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.0"); + let result = pm.resolve_list_command(&ListCommandOptions { + exclude_peers: true, + ..Default::default() + }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["list"]); + } + + #[test] + fn test_pnpm_list_find_by() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = pm.resolve_list_command(&ListCommandOptions { + find_by: Some("customFinder"), + ..Default::default() + }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["list", "--find-by", "customFinder"]); + } + + #[test] + fn test_npm_list_find_by_ignored() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = pm.resolve_list_command(&ListCommandOptions { + find_by: Some("customFinder"), + ..Default::default() + }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["list"]); + } + + #[test] + fn test_yarn1_list_find_by_ignored() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.0"); + let result = pm.resolve_list_command(&ListCommandOptions { + find_by: Some("customFinder"), + ..Default::default() + }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["list"]); + } +} diff --git a/crates/vite_install/src/commands/mod.rs b/crates/vite_install/src/commands/mod.rs index ca9b7e12ee..3651d33672 100644 --- a/crates/vite_install/src/commands/mod.rs +++ b/crates/vite_install/src/commands/mod.rs @@ -1,9 +1,17 @@ pub mod add; +pub mod cache; +pub mod config; pub mod dedupe; mod install; pub mod link; +pub mod list; pub mod outdated; +pub mod owner; +pub mod pack; +pub mod prune; +pub mod publish; pub mod remove; pub mod unlink; pub mod update; +pub mod view; pub mod why; diff --git a/crates/vite_install/src/commands/owner.rs b/crates/vite_install/src/commands/owner.rs new file mode 100644 index 0000000000..ad14daec13 --- /dev/null +++ b/crates/vite_install/src/commands/owner.rs @@ -0,0 +1,151 @@ +use std::{collections::HashMap, process::ExitStatus}; + +use vite_command::run_command; +use vite_error::Error; +use vite_path::AbsolutePath; + +use crate::package_manager::{PackageManager, ResolveCommandResult, format_path_env}; + +/// Owner subcommand type. +#[derive(Debug, Clone)] +pub enum OwnerSubcommand { + List { package: String, otp: Option }, + Add { user: String, package: String, otp: Option }, + Rm { user: String, package: String, otp: Option }, +} + +impl PackageManager { + /// Run the owner command with the package manager. + #[must_use] + pub async fn run_owner_command( + &self, + subcommand: &OwnerSubcommand, + cwd: impl AsRef, + ) -> Result { + let resolve_command = self.resolve_owner_command(subcommand); + run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) + .await + } + + /// Resolve the owner command. + /// All package managers delegate to npm owner. + #[must_use] + pub fn resolve_owner_command(&self, subcommand: &OwnerSubcommand) -> ResolveCommandResult { + let bin_name: String = "npm".to_string(); + let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); + let mut args: Vec = Vec::new(); + + args.push("owner".into()); + + match subcommand { + OwnerSubcommand::List { package, otp } => { + args.push("list".into()); + args.push(package.clone()); + + if let Some(otp_value) = otp { + args.push("--otp".into()); + args.push(otp_value.clone()); + } + } + OwnerSubcommand::Add { user, package, otp } => { + args.push("add".into()); + args.push(user.clone()); + args.push(package.clone()); + + if let Some(otp_value) = otp { + args.push("--otp".into()); + args.push(otp_value.clone()); + } + } + OwnerSubcommand::Rm { user, package, otp } => { + args.push("rm".into()); + args.push(user.clone()); + args.push(package.clone()); + + if let Some(otp_value) = otp { + args.push("--otp".into()); + args.push(otp_value.clone()); + } + } + } + + ResolveCommandResult { bin_path: bin_name, args, envs } + } +} + +#[cfg(test)] +mod tests { + use tempfile::{TempDir, tempdir}; + use vite_path::AbsolutePathBuf; + use vite_str::Str; + + use super::*; + use crate::package_manager::PackageManagerType; + + fn create_temp_dir() -> TempDir { + tempdir().expect("Failed to create temp directory") + } + + fn create_mock_package_manager(pm_type: PackageManagerType, version: &str) -> PackageManager { + let temp_dir = create_temp_dir(); + let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + let install_dir = temp_dir_path.join("install"); + + PackageManager { + client: pm_type, + package_name: pm_type.to_string().into(), + version: Str::from(version), + hash: None, + bin_name: pm_type.to_string().into(), + workspace_root: temp_dir_path.clone(), + install_dir, + } + } + + #[test] + fn test_pnpm_owner_list_uses_npm() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = pm.resolve_owner_command(&OwnerSubcommand::List { + package: "my-package".to_string(), + otp: None, + }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["owner", "list", "my-package"]); + } + + #[test] + fn test_npm_owner_add() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = pm.resolve_owner_command(&OwnerSubcommand::Add { + user: "username".to_string(), + package: "my-package".to_string(), + otp: None, + }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["owner", "add", "username", "my-package"]); + } + + #[test] + fn test_yarn_owner_rm_uses_npm() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); + let result = pm.resolve_owner_command(&OwnerSubcommand::Rm { + user: "username".to_string(), + package: "my-package".to_string(), + otp: None, + }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["owner", "rm", "username", "my-package"]); + } + + #[test] + fn test_owner_with_otp() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = pm.resolve_owner_command(&OwnerSubcommand::Add { + user: "username".to_string(), + package: "my-package".to_string(), + otp: Some("123456".to_string()), + }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["owner", "add", "username", "my-package", "--otp", "123456"]); + } +} diff --git a/crates/vite_install/src/commands/pack.rs b/crates/vite_install/src/commands/pack.rs new file mode 100644 index 0000000000..2ef11cf2d2 --- /dev/null +++ b/crates/vite_install/src/commands/pack.rs @@ -0,0 +1,551 @@ +use std::{collections::HashMap, process::ExitStatus}; + +use tokio::fs::create_dir_all; +use vite_command::run_command; +use vite_error::Error; +use vite_path::AbsolutePath; + +use crate::package_manager::{ + PackageManager, PackageManagerType, ResolveCommandResult, format_path_env, +}; + +/// Options for the pack command. +#[derive(Debug, Default)] +pub struct PackCommandOptions<'a> { + pub recursive: bool, + pub filters: Option<&'a [String]>, + pub out: Option<&'a str>, + pub pack_destination: Option<&'a str>, + pub pack_gzip_level: Option, + pub json: bool, + pub pass_through_args: Option<&'a [String]>, +} + +impl PackageManager { + /// Run the pack command with the package manager. + #[must_use] + pub async fn run_pack_command( + &self, + options: &PackCommandOptions<'_>, + cwd: impl AsRef, + ) -> Result { + // Special handling for npm: create pack-destination directory if it doesn't exist + if matches!(self.client, PackageManagerType::Npm) { + if let Some(pack_destination) = options.pack_destination { + let dest_path = cwd.as_ref().join(pack_destination); + if !dest_path.as_path().exists() { + create_dir_all(&dest_path) + .await + .map_err(|e| Error::IoWithPath { path: dest_path.into(), err: e })?; + } + } + } + + let resolve_command = self.resolve_pack_command(options); + run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) + .await + } + + /// Resolve the pack command. + #[must_use] + pub fn resolve_pack_command(&self, options: &PackCommandOptions) -> ResolveCommandResult { + let bin_name: String = self.client.to_string(); + let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); + let mut args: Vec = Vec::new(); + + match self.client { + PackageManagerType::Pnpm => { + // pnpm: --filter must come before command + if let Some(filters) = options.filters { + for filter in filters { + args.push("--filter".into()); + args.push(filter.clone()); + } + } + + args.push("pack".into()); + + if options.recursive { + args.push("--recursive".into()); + } + + if let Some(out) = options.out { + args.push("--out".into()); + args.push(out.to_string()); + } + + if let Some(dest) = options.pack_destination { + args.push("--pack-destination".into()); + args.push(dest.to_string()); + } + + if let Some(level) = options.pack_gzip_level { + args.push("--pack-gzip-level".into()); + args.push(level.to_string()); + } + + if options.json { + args.push("--json".into()); + } + } + PackageManagerType::Npm => { + args.push("pack".into()); + + if options.recursive { + args.push("--workspaces".into()); + } + + // npm: --workspace comes after command + if let Some(filters) = options.filters { + for filter in filters { + args.push("--workspace".into()); + args.push(filter.clone()); + } + } + + if options.out.is_some() { + println!("Warning: --out not supported by npm"); + } + + if let Some(dest) = options.pack_destination { + args.push("--pack-destination".into()); + args.push(dest.to_string()); + } + + if options.pack_gzip_level.is_some() { + println!("Warning: --pack-gzip-level not supported by npm"); + } + if options.json { + args.push("--json".into()); + } + } + PackageManagerType::Yarn => { + let is_yarn1 = self.version.starts_with("1."); + let has_filters = options.filters.is_some_and(|f| !f.is_empty()); + + // yarn@2+ uses 'workspaces foreach' for recursive or filters + if !is_yarn1 && (options.recursive || has_filters) { + args.push("workspaces".into()); + args.push("foreach".into()); + args.push("--all".into()); + + // Add --include for each filter + if let Some(filters) = options.filters { + for filter in filters { + args.push("--include".into()); + args.push(filter.clone()); + } + } + + args.push("pack".into()); + } else { + // yarn@1 or single package pack + if options.recursive && is_yarn1 { + println!( + "Warning: yarn@1 does not support recursive pack, ignoring --recursive flag" + ); + } + if has_filters && is_yarn1 { + println!( + "Warning: yarn@1 does not support --filter, ignoring --filter flag" + ); + } + args.push("pack".into()); + } + + if let Some(out) = options.out { + if is_yarn1 { + args.push("--filename".into()); + } else { + args.push("--out".into()); + } + args.push(out.to_string()); + } + + if options.pack_destination.is_some() { + println!("Warning: --pack-destination not supported by yarn"); + } + + if options.pack_gzip_level.is_some() { + println!("Warning: --pack-gzip-level not supported by yarn"); + } + + if options.json { + args.push("--json".into()); + } + } + } + + // Add pass-through args + if let Some(pass_through_args) = options.pass_through_args { + args.extend_from_slice(pass_through_args); + } + + ResolveCommandResult { bin_path: bin_name, args, envs } + } +} + +#[cfg(test)] +mod tests { + use tempfile::{TempDir, tempdir}; + use vite_path::AbsolutePathBuf; + use vite_str::Str; + + use super::*; + + fn create_temp_dir() -> TempDir { + tempdir().expect("Failed to create temp directory") + } + + fn create_mock_package_manager(pm_type: PackageManagerType, version: &str) -> PackageManager { + let temp_dir = create_temp_dir(); + let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + let install_dir = temp_dir_path.join("install"); + + PackageManager { + client: pm_type, + package_name: pm_type.to_string().into(), + version: Str::from(version), + hash: None, + bin_name: pm_type.to_string().into(), + workspace_root: temp_dir_path.clone(), + install_dir, + } + } + + #[test] + fn test_pnpm_pack_basic() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = pm.resolve_pack_command(&PackCommandOptions::default()); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["pack"]); + } + + #[test] + fn test_pnpm_pack_recursive() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = + pm.resolve_pack_command(&PackCommandOptions { recursive: true, ..Default::default() }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["pack", "--recursive"]); + } + + #[test] + fn test_pnpm_pack_with_out() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = pm.resolve_pack_command(&PackCommandOptions { + out: Some("./dist/package.tgz"), + ..Default::default() + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["pack", "--out", "./dist/package.tgz"]); + } + + #[test] + fn test_pnpm_pack_with_destination() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = pm.resolve_pack_command(&PackCommandOptions { + pack_destination: Some("./dist"), + ..Default::default() + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["pack", "--pack-destination", "./dist"]); + } + + #[test] + fn test_pnpm_pack_with_gzip_level() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = pm.resolve_pack_command(&PackCommandOptions { + pack_gzip_level: Some(9), + ..Default::default() + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["pack", "--pack-gzip-level", "9"]); + } + + #[test] + fn test_pnpm_pack_json() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = + pm.resolve_pack_command(&PackCommandOptions { json: true, ..Default::default() }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["pack", "--json"]); + } + + #[test] + fn test_pnpm_pack_with_filter() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let filters = vec!["app".to_string()]; + let result = pm.resolve_pack_command(&PackCommandOptions { + filters: Some(&filters), + ..Default::default() + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["--filter", "app", "pack"]); + } + + #[test] + fn test_pnpm_pack_with_multiple_filters() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let filters = vec!["app".to_string(), "web".to_string()]; + let result = pm.resolve_pack_command(&PackCommandOptions { + filters: Some(&filters), + ..Default::default() + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["--filter", "app", "--filter", "web", "pack"]); + } + + #[test] + fn test_npm_pack_basic() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = pm.resolve_pack_command(&PackCommandOptions::default()); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["pack"]); + } + + #[test] + fn test_npm_pack_recursive() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = + pm.resolve_pack_command(&PackCommandOptions { recursive: true, ..Default::default() }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["pack", "--workspaces"]); + } + + #[test] + fn test_npm_pack_with_destination() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = pm.resolve_pack_command(&PackCommandOptions { + pack_destination: Some("./dist"), + ..Default::default() + }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["pack", "--pack-destination", "./dist"]); + } + + #[test] + fn test_npm_pack_json() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = + pm.resolve_pack_command(&PackCommandOptions { json: true, ..Default::default() }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["pack", "--json"]); + } + + #[test] + fn test_npm_pack_with_filter() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let filters = vec!["app".to_string()]; + let result = pm.resolve_pack_command(&PackCommandOptions { + filters: Some(&filters), + ..Default::default() + }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["pack", "--workspace", "app"]); + } + + #[test] + fn test_npm_pack_with_multiple_filters() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let filters = vec!["app".to_string(), "web".to_string()]; + let result = pm.resolve_pack_command(&PackCommandOptions { + filters: Some(&filters), + ..Default::default() + }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["pack", "--workspace", "app", "--workspace", "web"]); + } + + #[test] + fn test_yarn1_pack_basic() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.0"); + let result = pm.resolve_pack_command(&PackCommandOptions::default()); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["pack"]); + } + + #[test] + fn test_yarn1_pack_recursive_ignored() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.0"); + let result = + pm.resolve_pack_command(&PackCommandOptions { recursive: true, ..Default::default() }); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["pack"]); + } + + #[test] + fn test_yarn1_pack_with_out() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.0"); + let result = pm.resolve_pack_command(&PackCommandOptions { + out: Some("./dist/package.tgz"), + ..Default::default() + }); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["pack", "--filename", "./dist/package.tgz"]); + } + + #[test] + fn test_yarn1_pack_json() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.0"); + let result = + pm.resolve_pack_command(&PackCommandOptions { json: true, ..Default::default() }); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["pack", "--json"]); + } + + #[test] + fn test_yarn1_pack_with_filter_ignored() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.0"); + let filters = vec!["app".to_string()]; + let result = pm.resolve_pack_command(&PackCommandOptions { + filters: Some(&filters), + ..Default::default() + }); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["pack"]); + } + + #[test] + fn test_yarn2_pack_basic() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); + let result = pm.resolve_pack_command(&PackCommandOptions::default()); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["pack"]); + } + + #[test] + fn test_yarn2_pack_recursive() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); + let result = + pm.resolve_pack_command(&PackCommandOptions { recursive: true, ..Default::default() }); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["workspaces", "foreach", "--all", "pack"]); + } + + #[test] + fn test_yarn2_pack_with_out() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); + let result = pm.resolve_pack_command(&PackCommandOptions { + out: Some("./dist/package.tgz"), + ..Default::default() + }); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["pack", "--out", "./dist/package.tgz"]); + } + + #[test] + fn test_yarn2_pack_json() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); + let result = + pm.resolve_pack_command(&PackCommandOptions { json: true, ..Default::default() }); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["pack", "--json"]); + } + + #[test] + fn test_yarn2_pack_with_filter() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); + let filters = vec!["app".to_string()]; + let result = pm.resolve_pack_command(&PackCommandOptions { + filters: Some(&filters), + ..Default::default() + }); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["workspaces", "foreach", "--all", "--include", "app", "pack"]); + } + + #[test] + fn test_yarn2_pack_with_multiple_filters() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); + let filters = vec!["app".to_string(), "web".to_string()]; + let result = pm.resolve_pack_command(&PackCommandOptions { + filters: Some(&filters), + ..Default::default() + }); + assert_eq!(result.bin_path, "yarn"); + assert_eq!( + result.args, + vec!["workspaces", "foreach", "--all", "--include", "app", "--include", "web", "pack"] + ); + } + + #[test] + fn test_yarn2_pack_with_filter_and_recursive() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); + let filters = vec!["app".to_string()]; + let result = pm.resolve_pack_command(&PackCommandOptions { + recursive: true, + filters: Some(&filters), + ..Default::default() + }); + assert_eq!(result.bin_path, "yarn"); + // Filter takes precedence, same command structure + assert_eq!(result.args, vec!["workspaces", "foreach", "--all", "--include", "app", "pack"]); + } + + #[tokio::test] + async fn test_npm_pack_destination_creates_directory() { + let temp_dir = create_temp_dir(); + let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + let install_dir = temp_dir_path.join("install"); + + let pm = PackageManager { + client: PackageManagerType::Npm, + package_name: "npm".into(), + version: Str::from("10.0.0"), + hash: None, + bin_name: "npm".into(), + workspace_root: temp_dir_path.clone(), + install_dir, + }; + + let dest_dir = "test-dest"; + let dest_path = temp_dir_path.join(dest_dir); + + // Ensure directory doesn't exist initially + assert!(!dest_path.as_path().exists()); + + // This would normally run npm pack but we're just testing directory creation + // The actual command will fail but directory should be created + let options = PackCommandOptions { pack_destination: Some(dest_dir), ..Default::default() }; + + // The command will fail because npm isn't actually available, but directory should be created + let _ = pm.run_pack_command(&options, &temp_dir_path).await; + + // Verify directory was created + assert!(dest_path.as_path().exists()); + assert!(dest_path.as_path().is_dir()); + } + + #[tokio::test] + async fn test_pnpm_pack_destination_no_directory_creation() { + let temp_dir = create_temp_dir(); + let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + let install_dir = temp_dir_path.join("install"); + + let pm = PackageManager { + client: PackageManagerType::Pnpm, + package_name: "pnpm".into(), + version: Str::from("10.0.0"), + hash: None, + bin_name: "pnpm".into(), + workspace_root: temp_dir_path.clone(), + install_dir, + }; + + let dest_dir = "test-dest"; + let dest_path = temp_dir_path.join(dest_dir); + + // Ensure directory doesn't exist initially + assert!(!dest_path.as_path().exists()); + + let options = PackCommandOptions { pack_destination: Some(dest_dir), ..Default::default() }; + + // The command will fail because pnpm isn't actually available, but directory should NOT be created + let _ = pm.run_pack_command(&options, &temp_dir_path).await; + + // Verify directory was NOT created (pnpm handles this itself) + assert!(!dest_path.as_path().exists()); + } +} diff --git a/crates/vite_install/src/commands/prune.rs b/crates/vite_install/src/commands/prune.rs new file mode 100644 index 0000000000..7972507c44 --- /dev/null +++ b/crates/vite_install/src/commands/prune.rs @@ -0,0 +1,198 @@ +use std::{collections::HashMap, process::ExitStatus}; + +use vite_command::run_command; +use vite_error::Error; +use vite_path::AbsolutePath; + +use crate::package_manager::{ + PackageManager, PackageManagerType, ResolveCommandResult, format_path_env, +}; + +/// Options for the prune command. +#[derive(Debug, Default)] +pub struct PruneCommandOptions<'a> { + pub prod: bool, + pub no_optional: bool, + pub pass_through_args: Option<&'a [String]>, +} + +impl PackageManager { + /// Run the prune command with the package manager. + /// Returns ExitStatus with success (0) if the command is not supported. + #[must_use] + pub async fn run_prune_command( + &self, + options: &PruneCommandOptions<'_>, + cwd: impl AsRef, + ) -> Result { + let Some(resolve_command) = self.resolve_prune_command(options) else { + // Command not supported, return success + return Ok(ExitStatus::default()); + }; + run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) + .await + } + + /// Resolve the prune command. + /// Returns None if the command is not supported by the package manager. + #[must_use] + pub fn resolve_prune_command( + &self, + options: &PruneCommandOptions, + ) -> Option { + let bin_name: String; + let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); + let mut args: Vec = Vec::new(); + + match self.client { + PackageManagerType::Pnpm => { + bin_name = "pnpm".into(); + args.push("prune".into()); + + if options.prod { + args.push("--prod".into()); + } + if options.no_optional { + args.push("--no-optional".into()); + } + } + PackageManagerType::Npm => { + bin_name = "npm".into(); + args.push("prune".into()); + + // npm uses --omit flags instead of --prod and --no-optional + if options.prod { + args.push("--omit=dev".into()); + } + if options.no_optional { + args.push("--omit=optional".into()); + } + } + PackageManagerType::Yarn => { + println!( + "Warning: yarn does not have 'prune' command. yarn install will prune extraneous packages automatically." + ); + return None; + } + } + + // Add pass-through args + if let Some(pass_through_args) = options.pass_through_args { + args.extend_from_slice(pass_through_args); + } + + Some(ResolveCommandResult { bin_path: bin_name, args, envs }) + } +} + +#[cfg(test)] +mod tests { + use tempfile::{TempDir, tempdir}; + use vite_path::AbsolutePathBuf; + use vite_str::Str; + + use super::*; + + fn create_temp_dir() -> TempDir { + tempdir().expect("Failed to create temp directory") + } + + fn create_mock_package_manager(pm_type: PackageManagerType, version: &str) -> PackageManager { + let temp_dir = create_temp_dir(); + let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + let install_dir = temp_dir_path.join("install"); + + PackageManager { + client: pm_type, + package_name: pm_type.to_string().into(), + version: Str::from(version), + hash: None, + bin_name: pm_type.to_string().into(), + workspace_root: temp_dir_path.clone(), + install_dir, + } + } + + #[test] + fn test_pnpm_prune() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = pm.resolve_prune_command(&PruneCommandOptions::default()); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["prune"]); + } + + #[test] + fn test_pnpm_prune_prod() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = + pm.resolve_prune_command(&PruneCommandOptions { prod: true, ..Default::default() }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["prune", "--prod"]); + } + + #[test] + fn test_npm_prune() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = pm.resolve_prune_command(&PruneCommandOptions::default()); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["prune"]); + } + + #[test] + fn test_npm_prune_prod() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = + pm.resolve_prune_command(&PruneCommandOptions { prod: true, ..Default::default() }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["prune", "--omit=dev"]); + } + + #[test] + fn test_npm_prune_no_optional() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = pm.resolve_prune_command(&PruneCommandOptions { + no_optional: true, + ..Default::default() + }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["prune", "--omit=optional"]); + } + + #[test] + fn test_npm_prune_both_flags() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = pm.resolve_prune_command(&PruneCommandOptions { + prod: true, + no_optional: true, + ..Default::default() + }); + assert!(result.is_some()); + let result = result.unwrap(); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["prune", "--omit=dev", "--omit=optional"]); + } + + #[test] + fn test_yarn1_prune_not_supported() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.0"); + let result = pm.resolve_prune_command(&PruneCommandOptions::default()); + assert!(result.is_none()); + } + + #[test] + fn test_yarn2_prune_not_supported() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); + let result = pm.resolve_prune_command(&PruneCommandOptions::default()); + assert!(result.is_none()); + } +} diff --git a/crates/vite_install/src/commands/publish.rs b/crates/vite_install/src/commands/publish.rs new file mode 100644 index 0000000000..22491147a5 --- /dev/null +++ b/crates/vite_install/src/commands/publish.rs @@ -0,0 +1,404 @@ +use std::{collections::HashMap, process::ExitStatus}; + +use vite_command::run_command; +use vite_error::Error; +use vite_path::AbsolutePath; + +use crate::package_manager::{ + PackageManager, PackageManagerType, ResolveCommandResult, format_path_env, +}; + +/// Options for the publish command. +#[derive(Debug, Default)] +pub struct PublishCommandOptions<'a> { + pub target: Option<&'a str>, + pub dry_run: bool, + pub tag: Option<&'a str>, + pub access: Option<&'a str>, + pub otp: Option<&'a str>, + pub no_git_checks: bool, + pub publish_branch: Option<&'a str>, + pub report_summary: bool, + pub force: bool, + pub json: bool, + pub recursive: bool, + pub filters: Option<&'a [String]>, + pub pass_through_args: Option<&'a [String]>, +} + +impl PackageManager { + /// Run the publish command with the package manager. + #[must_use] + pub async fn run_publish_command( + &self, + options: &PublishCommandOptions<'_>, + cwd: impl AsRef, + ) -> Result { + let resolve_command = self.resolve_publish_command(options); + run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) + .await + } + + /// Resolve the publish command. + /// All yarn versions delegate to npm publish. + #[must_use] + pub fn resolve_publish_command(&self, options: &PublishCommandOptions) -> ResolveCommandResult { + let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); + let mut args: Vec = Vec::new(); + + let bin_name: String; + + match self.client { + PackageManagerType::Pnpm => { + bin_name = "pnpm".into(); + + // pnpm: --filter must come before command + if let Some(filters) = options.filters { + for filter in filters { + args.push("--filter".into()); + args.push(filter.clone()); + } + } + + args.push("publish".into()); + + if let Some(target) = options.target { + args.push(target.to_string()); + } + + if options.dry_run { + args.push("--dry-run".into()); + } + + if let Some(tag) = options.tag { + args.push("--tag".into()); + args.push(tag.to_string()); + } + + if let Some(access) = options.access { + args.push("--access".into()); + args.push(access.to_string()); + } + + if let Some(otp) = options.otp { + args.push("--otp".into()); + args.push(otp.to_string()); + } + + if options.no_git_checks { + args.push("--no-git-checks".into()); + } + + if let Some(branch) = options.publish_branch { + args.push("--publish-branch".into()); + args.push(branch.to_string()); + } + + if options.report_summary { + args.push("--report-summary".into()); + } + + if options.force { + args.push("--force".into()); + } + + if options.json { + args.push("--json".into()); + } + + if options.recursive { + args.push("--recursive".into()); + } + } + PackageManagerType::Npm | PackageManagerType::Yarn => { + // Yarn always delegates to npm + bin_name = "npm".into(); + + args.push("publish".into()); + + if options.recursive { + args.push("--workspaces".into()); + } + + // npm: --workspace comes after command (maps from --filter) + if let Some(filters) = options.filters { + for filter in filters { + args.push("--workspace".into()); + args.push(filter.clone()); + } + } + + if let Some(target) = options.target { + args.push(target.to_string()); + } + + if options.dry_run { + args.push("--dry-run".into()); + } + + if let Some(tag) = options.tag { + args.push("--tag".into()); + args.push(tag.to_string()); + } + + if let Some(access) = options.access { + args.push("--access".into()); + args.push(access.to_string()); + } + + if let Some(otp) = options.otp { + args.push("--otp".into()); + args.push(otp.to_string()); + } + + if options.force { + args.push("--force".into()); + } + + if options.publish_branch.is_some() { + println!("Warning: --publish-branch not supported by npm, ignoring flag"); + } + + if options.report_summary { + println!("Warning: --report-summary not supported by npm, ignoring flag"); + } + + if options.json { + println!("Warning: --json not supported by npm, ignoring flag"); + } + } + } + + // Add pass-through args + if let Some(pass_through_args) = options.pass_through_args { + args.extend_from_slice(pass_through_args); + } + + ResolveCommandResult { bin_path: bin_name, args, envs } + } +} + +#[cfg(test)] +mod tests { + use tempfile::{TempDir, tempdir}; + use vite_path::AbsolutePathBuf; + use vite_str::Str; + + use super::*; + + fn create_temp_dir() -> TempDir { + tempdir().expect("Failed to create temp directory") + } + + fn create_mock_package_manager(pm_type: PackageManagerType, version: &str) -> PackageManager { + let temp_dir = create_temp_dir(); + let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + let install_dir = temp_dir_path.join("install"); + + PackageManager { + client: pm_type, + package_name: pm_type.to_string().into(), + version: Str::from(version), + hash: None, + bin_name: pm_type.to_string().into(), + workspace_root: temp_dir_path.clone(), + install_dir, + } + } + + #[test] + fn test_pnpm_publish() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = pm.resolve_publish_command(&PublishCommandOptions::default()); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["publish"]); + } + + #[test] + fn test_npm_publish() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = pm.resolve_publish_command(&PublishCommandOptions::default()); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["publish"]); + } + + #[test] + fn test_yarn1_publish_uses_npm() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.0"); + let result = pm.resolve_publish_command(&PublishCommandOptions::default()); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["publish"]); + } + + #[test] + fn test_yarn2_publish_uses_npm() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); + let result = pm.resolve_publish_command(&PublishCommandOptions::default()); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["publish"]); + } + + #[test] + fn test_yarn_publish_with_tag() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); + let result = pm.resolve_publish_command(&PublishCommandOptions { + tag: Some("beta"), + ..Default::default() + }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["publish", "--tag", "beta"]); + } + + #[test] + fn test_pnpm_publish_recursive() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = pm.resolve_publish_command(&PublishCommandOptions { + recursive: true, + ..Default::default() + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["publish", "--recursive"]); + } + + #[test] + fn test_npm_publish_recursive() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = pm.resolve_publish_command(&PublishCommandOptions { + recursive: true, + ..Default::default() + }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["publish", "--workspaces"]); + } + + #[test] + fn test_pnpm_publish_with_filter() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let filters = vec!["app".to_string()]; + let result = pm.resolve_publish_command(&PublishCommandOptions { + filters: Some(&filters), + ..Default::default() + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["--filter", "app", "publish"]); + } + + #[test] + fn test_npm_publish_with_filter() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let filters = vec!["app".to_string()]; + let result = pm.resolve_publish_command(&PublishCommandOptions { + filters: Some(&filters), + ..Default::default() + }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["publish", "--workspace", "app"]); + } + + #[test] + fn test_yarn_publish_with_filter() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.0"); + let filters = vec!["app".to_string()]; + let result = pm.resolve_publish_command(&PublishCommandOptions { + filters: Some(&filters), + ..Default::default() + }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["publish", "--workspace", "app"]); + } + + #[test] + fn test_pnpm_publish_json() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = + pm.resolve_publish_command(&PublishCommandOptions { json: true, ..Default::default() }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["publish", "--json"]); + } + + #[test] + fn test_npm_publish_json_ignored() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = + pm.resolve_publish_command(&PublishCommandOptions { json: true, ..Default::default() }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["publish"]); + } + + #[test] + fn test_pnpm_publish_branch() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = pm.resolve_publish_command(&PublishCommandOptions { + publish_branch: Some("main"), + ..Default::default() + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["publish", "--publish-branch", "main"]); + } + + #[test] + fn test_npm_publish_branch_ignored() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = pm.resolve_publish_command(&PublishCommandOptions { + publish_branch: Some("main"), + ..Default::default() + }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["publish"]); + } + + #[test] + fn test_pnpm_publish_report_summary() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = pm.resolve_publish_command(&PublishCommandOptions { + report_summary: true, + ..Default::default() + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["publish", "--report-summary"]); + } + + #[test] + fn test_npm_publish_report_summary_ignored() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = pm.resolve_publish_command(&PublishCommandOptions { + report_summary: true, + ..Default::default() + }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["publish"]); + } + + #[test] + fn test_pnpm_publish_otp() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = pm.resolve_publish_command(&PublishCommandOptions { + otp: Some("123456"), + ..Default::default() + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["publish", "--otp", "123456"]); + } + + #[test] + fn test_npm_publish_otp() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = pm.resolve_publish_command(&PublishCommandOptions { + otp: Some("654321"), + ..Default::default() + }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["publish", "--otp", "654321"]); + } + + #[test] + fn test_yarn_publish_otp() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.0"); + let result = pm.resolve_publish_command(&PublishCommandOptions { + otp: Some("999999"), + ..Default::default() + }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["publish", "--otp", "999999"]); + } +} diff --git a/crates/vite_install/src/commands/view.rs b/crates/vite_install/src/commands/view.rs new file mode 100644 index 0000000000..3b20aba3ce --- /dev/null +++ b/crates/vite_install/src/commands/view.rs @@ -0,0 +1,140 @@ +use std::{collections::HashMap, process::ExitStatus}; + +use vite_command::run_command; +use vite_error::Error; +use vite_path::AbsolutePath; + +use crate::package_manager::{PackageManager, ResolveCommandResult, format_path_env}; + +/// Options for the view command. +#[derive(Debug, Default)] +pub struct ViewCommandOptions<'a> { + pub package: &'a str, + pub field: Option<&'a str>, + pub json: bool, + pub pass_through_args: Option<&'a [String]>, +} + +impl PackageManager { + /// Run the view command with the package manager. + #[must_use] + pub async fn run_view_command( + &self, + options: &ViewCommandOptions<'_>, + cwd: impl AsRef, + ) -> Result { + let resolve_command = self.resolve_view_command(options); + run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) + .await + } + + /// Resolve the view command. + /// All package managers delegate to npm view (pnpm and yarn use npm internally). + #[must_use] + pub fn resolve_view_command(&self, options: &ViewCommandOptions) -> ResolveCommandResult { + let bin_name: String = "npm".to_string(); + let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); + let mut args: Vec = Vec::new(); + + args.push("view".into()); + + args.push(options.package.to_string()); + + if let Some(field) = options.field { + args.push(field.to_string()); + } + + if options.json { + args.push("--json".into()); + } + + // Add pass-through args + if let Some(pass_through_args) = options.pass_through_args { + args.extend_from_slice(pass_through_args); + } + + ResolveCommandResult { bin_path: bin_name, args, envs } + } +} + +#[cfg(test)] +mod tests { + use tempfile::{TempDir, tempdir}; + use vite_path::AbsolutePathBuf; + use vite_str::Str; + + use super::*; + use crate::package_manager::PackageManagerType; + + fn create_temp_dir() -> TempDir { + tempdir().expect("Failed to create temp directory") + } + + fn create_mock_package_manager(pm_type: PackageManagerType, version: &str) -> PackageManager { + let temp_dir = create_temp_dir(); + let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + let install_dir = temp_dir_path.join("install"); + + PackageManager { + client: pm_type, + package_name: pm_type.to_string().into(), + version: Str::from(version), + hash: None, + bin_name: pm_type.to_string().into(), + workspace_root: temp_dir_path.clone(), + install_dir, + } + } + + #[test] + fn test_pnpm_view_uses_npm() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = pm.resolve_view_command(&ViewCommandOptions { + package: "react", + field: None, + json: false, + pass_through_args: None, + }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["view", "react"]); + } + + #[test] + fn test_npm_view() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = pm.resolve_view_command(&ViewCommandOptions { + package: "react", + field: Some("version"), + json: false, + pass_through_args: None, + }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["view", "react", "version"]); + } + + #[test] + fn test_yarn_view_uses_npm() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "1.22.0"); + let result = pm.resolve_view_command(&ViewCommandOptions { + package: "lodash", + field: None, + json: true, + pass_through_args: None, + }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["view", "lodash", "--json"]); + } + + #[test] + fn test_view_with_nested_field() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = pm.resolve_view_command(&ViewCommandOptions { + package: "react", + field: Some("dist.tarball"), + json: false, + pass_through_args: None, + }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["view", "react", "dist.tarball"]); + } +} diff --git a/packages/cli/binding/src/cli.rs b/packages/cli/binding/src/cli.rs index a53b01a68e..cf363c6808 100644 --- a/packages/cli/binding/src/cli.rs +++ b/packages/cli/binding/src/cli.rs @@ -27,6 +27,7 @@ use crate::commands::{ link::LinkCommand, lint::{LintConfig, lint}, outdated::OutdatedCommand, + pm::PmCommand, remove::RemoveCommand, test::test, unlink::UnlinkCommand, @@ -437,6 +438,323 @@ pub enum Commands { #[arg(allow_hyphen_values = true, trailing_var_arg = true)] args: Vec, }, + /// Package manager utilities + #[command(subcommand)] + Pm(PmCommands), +} + +#[derive(Subcommand, Debug, Clone)] +pub enum PmCommands { + /// Remove unnecessary packages + Prune { + /// Remove devDependencies + #[arg(long)] + prod: bool, + + /// Remove optional dependencies + #[arg(long)] + no_optional: bool, + + /// Additional arguments to pass through to the package manager + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, + + /// Create a tarball of the package + Pack { + /// Pack all workspace packages + #[arg(short = 'r', long)] + recursive: bool, + + /// Filter packages to pack (can be used multiple times) + #[arg(long, value_name = "PATTERN")] + filter: Option>, + + /// Customizes the output path for the tarball. Use %s and %v to include the package name and version (pnpm and yarn@2+ only), e.g., %s.tgz or some-dir/%s-%v.tgz + #[arg(long)] + out: Option, + + /// Directory where the tarball will be saved (pnpm and npm only) + #[arg(long)] + pack_destination: Option, + + /// Gzip compression level (0-9) + #[arg(long)] + pack_gzip_level: Option, + + /// Output in JSON format + #[arg(long)] + json: bool, + + /// Additional arguments to pass through to the package manager + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, + + /// List installed packages + #[command(alias = "ls")] + List { + /// Package pattern to filter + pattern: Option, + + /// Maximum depth of dependency tree + #[arg(long)] + depth: Option, + + /// Output in JSON format + #[arg(long)] + json: bool, + + /// Show extended information + #[arg(long)] + long: bool, + + /// Parseable output format + #[arg(long)] + parseable: bool, + + /// Only production dependencies + #[arg(short = 'P', long)] + prod: bool, + + /// Only dev dependencies + #[arg(short = 'D', long)] + dev: bool, + + /// Exclude optional dependencies + #[arg(long)] + no_optional: bool, + + /// Exclude peer dependencies + #[arg(long)] + exclude_peers: bool, + + /// Show only project packages (pnpm-specific) + #[arg(long)] + only_projects: bool, + + /// Use a finder function defined in .pnpmfile.cjs (pnpm-specific) + #[arg(long, value_name = "FINDER_NAME")] + find_by: Option, + + /// List across all workspaces + #[arg(short = 'r', long)] + recursive: bool, + + /// Filter packages in monorepo (can be used multiple times) + #[arg(long, value_name = "PATTERN")] + filter: Vec, + + /// List global packages + #[arg(short = 'g', long)] + global: bool, + + /// Additional arguments to pass through to the package manager + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, + + /// View package information from registry + #[command(alias = "info", alias = "show")] + View { + /// Package name with optional version + #[arg(required = true)] + package: String, + + /// Specific field to view + field: Option, + + /// Output in JSON format + #[arg(long)] + json: bool, + + /// Additional arguments to pass through to the package manager + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, + + /// Publish package to registry + Publish { + /// Tarball or folder to publish + #[arg(value_name = "TARBALL|FOLDER")] + target: Option, + + /// Preview without publishing + #[arg(long)] + dry_run: bool, + + /// Publish tag (default: latest) + #[arg(long)] + tag: Option, + + /// Access level (public/restricted) + #[arg(long)] + access: Option, + + /// One-time password for authentication + #[arg(long, value_name = "OTP")] + otp: Option, + + /// Skip git checks (pnpm-specific) + #[arg(long)] + no_git_checks: bool, + + /// Set the branch name to publish from (pnpm-specific) + #[arg(long, value_name = "BRANCH")] + publish_branch: Option, + + /// Save publish summary to pnpm-publish-summary.json (pnpm-specific) + #[arg(long)] + report_summary: bool, + + /// Force publish + #[arg(long)] + force: bool, + + /// Output in JSON format (pnpm-specific) + #[arg(long)] + json: bool, + + /// Publish all workspace packages + #[arg(short = 'r', long)] + recursive: bool, + + /// Filter packages in monorepo (can be used multiple times) + #[arg(long, value_name = "PATTERN")] + filter: Option>, + + /// Additional arguments to pass through to the package manager + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, + + /// Manage package owners + #[command(subcommand, alias = "author")] + Owner(OwnerCommands), + + /// Manage package cache + Cache { + /// Subcommand: dir, path, clean + #[arg(required = true)] + subcommand: String, + + /// Additional arguments to pass through to the package manager + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, + + /// Manage package manager configuration + #[command(subcommand, alias = "c")] + Config(ConfigCommands), +} + +#[derive(Subcommand, Debug, Clone)] +pub enum ConfigCommands { + /// List all configuration + List { + /// Output in JSON format + #[arg(long)] + json: bool, + + /// Use global config + #[arg(short = 'g', long)] + global: bool, + + /// Config location: project (default) or global + #[arg(long, value_name = "LOCATION")] + location: Option, + }, + + /// Get configuration value + Get { + /// Config key + key: String, + + /// Output in JSON format + #[arg(long)] + json: bool, + + /// Use global config + #[arg(short = 'g', long)] + global: bool, + + /// Config location: project (default) or global + #[arg(long, value_name = "LOCATION")] + location: Option, + }, + + /// Set configuration value + Set { + /// Config key + key: String, + + /// Config value + value: String, + + /// Output in JSON format + #[arg(long)] + json: bool, + + /// Use global config + #[arg(short = 'g', long)] + global: bool, + + /// Config location: project (default) or global + #[arg(long, value_name = "LOCATION")] + location: Option, + }, + + /// Delete configuration key + Delete { + /// Config key + key: String, + + /// Use global config + #[arg(short = 'g', long)] + global: bool, + + /// Config location: project (default) or global + #[arg(long, value_name = "LOCATION")] + location: Option, + }, +} + +#[derive(Subcommand, Debug, Clone)] +pub enum OwnerCommands { + /// List package owners + #[command(alias = "ls")] + List { + /// Package name + package: String, + + /// One-time password for authentication + #[arg(long, value_name = "OTP")] + otp: Option, + }, + + /// Add package owner + Add { + /// Username + user: String, + /// Package name + package: String, + + /// One-time password for authentication + #[arg(long, value_name = "OTP")] + otp: Option, + }, + + /// Remove package owner + Rm { + /// Username + user: String, + /// Package name + package: String, + + /// One-time password for authentication + #[arg(long, value_name = "OTP")] + otp: Option, + }, } impl Commands { @@ -452,6 +770,7 @@ impl Commands { | Commands::Why { .. } | Commands::Link { .. } | Commands::Unlink { .. } + | Commands::Pm(..) ) } } @@ -960,6 +1279,10 @@ pub async fn main< .await?; return Ok(exit_status); } + Commands::Pm(pm_command) => { + let exit_status = PmCommand::new(cwd).execute(pm_command.clone()).await?; + return Ok(exit_status); + } }; let execution_summary_dir = EXECUTION_SUMMARY_DIR.as_path(); diff --git a/packages/cli/binding/src/commands/mod.rs b/packages/cli/binding/src/commands/mod.rs index 0164630fb9..4d1a9c8850 100644 --- a/packages/cli/binding/src/commands/mod.rs +++ b/packages/cli/binding/src/commands/mod.rs @@ -7,6 +7,7 @@ pub(crate) mod lib_cmd; pub(crate) mod link; pub(crate) mod lint; pub(crate) mod outdated; +pub(crate) mod pm; pub(crate) mod remove; pub(crate) mod test; pub(crate) mod unlink; diff --git a/packages/cli/binding/src/commands/pm.rs b/packages/cli/binding/src/commands/pm.rs new file mode 100644 index 0000000000..fc367145c9 --- /dev/null +++ b/packages/cli/binding/src/commands/pm.rs @@ -0,0 +1,225 @@ +use std::process::ExitStatus; + +use vite_install::{ + commands::{ + cache::CacheCommandOptions, config::ConfigCommandOptions, list::ListCommandOptions, + owner::OwnerSubcommand, pack::PackCommandOptions, prune::PruneCommandOptions, + publish::PublishCommandOptions, view::ViewCommandOptions, + }, + package_manager::PackageManager, +}; +use vite_path::AbsolutePathBuf; + +use crate::{ + Error, + cli::{ConfigCommands, OwnerCommands, PmCommands}, +}; + +/// Package manager utilities command. +/// +/// This command provides a unified interface to package manager utilities +/// across pnpm, npm, and yarn. +pub struct PmCommand { + cwd: AbsolutePathBuf, +} + +impl PmCommand { + pub fn new(cwd: AbsolutePathBuf) -> Self { + Self { cwd } + } + + pub async fn execute(self, command: PmCommands) -> Result { + // Detect package manager + let package_manager = PackageManager::builder(&self.cwd).build().await?; + + match command { + PmCommands::Prune { prod, no_optional, pass_through_args } => { + let options = PruneCommandOptions { + prod, + no_optional, + pass_through_args: pass_through_args.as_deref(), + }; + package_manager.run_prune_command(&options, &self.cwd).await + } + PmCommands::Pack { + recursive, + filter, + out, + pack_destination, + pack_gzip_level, + json, + pass_through_args, + } => { + let options = PackCommandOptions { + recursive, + filters: filter.as_deref(), + out: out.as_deref(), + pack_destination: pack_destination.as_deref(), + pack_gzip_level, + json, + pass_through_args: pass_through_args.as_deref(), + }; + package_manager.run_pack_command(&options, &self.cwd).await + } + PmCommands::List { + pattern, + depth, + json, + long, + parseable, + prod, + dev, + no_optional, + exclude_peers, + only_projects, + find_by, + recursive, + filter, + global, + pass_through_args, + } => { + let options = ListCommandOptions { + pattern: pattern.as_deref(), + depth, + json, + long, + parseable, + prod, + dev, + no_optional, + exclude_peers, + only_projects, + find_by: find_by.as_deref(), + recursive, + filters: if filter.is_empty() { None } else { Some(&filter) }, + global, + pass_through_args: pass_through_args.as_deref(), + }; + package_manager.run_list_command(&options, &self.cwd).await + } + PmCommands::View { package, field, json, pass_through_args } => { + let options = ViewCommandOptions { + package: &package, + field: field.as_deref(), + json, + pass_through_args: pass_through_args.as_deref(), + }; + package_manager.run_view_command(&options, &self.cwd).await + } + PmCommands::Publish { + target, + dry_run, + tag, + access, + otp, + no_git_checks, + publish_branch, + report_summary, + force, + json, + recursive, + filter, + pass_through_args, + } => { + let options = PublishCommandOptions { + target: target.as_deref(), + dry_run, + tag: tag.as_deref(), + access: access.as_deref(), + otp: otp.as_deref(), + no_git_checks, + publish_branch: publish_branch.as_deref(), + report_summary, + force, + json, + recursive, + filters: filter.as_deref(), + pass_through_args: pass_through_args.as_deref(), + }; + package_manager.run_publish_command(&options, &self.cwd).await + } + PmCommands::Owner(owner_command) => { + let subcommand = match owner_command { + OwnerCommands::List { package, otp } => OwnerSubcommand::List { package, otp }, + OwnerCommands::Add { user, package, otp } => { + OwnerSubcommand::Add { user, package, otp } + } + OwnerCommands::Rm { user, package, otp } => { + OwnerSubcommand::Rm { user, package, otp } + } + }; + package_manager.run_owner_command(&subcommand, &self.cwd).await + } + PmCommands::Cache { subcommand, pass_through_args } => { + let options = CacheCommandOptions { + subcommand: &subcommand, + pass_through_args: pass_through_args.as_deref(), + }; + package_manager.run_cache_command(&options, &self.cwd).await + } + PmCommands::Config(config_command) => match config_command { + ConfigCommands::List { json, global, location } => { + let options = ConfigCommandOptions { + subcommand: "list", + key: None, + value: None, + json, + location: if global { Some("global") } else { location.as_deref() }, + pass_through_args: None, + }; + package_manager.run_config_command(&options, &self.cwd).await + } + ConfigCommands::Get { key, json, global, location } => { + let options = ConfigCommandOptions { + subcommand: "get", + key: Some(key.as_str()), + value: None, + json, + location: if global { Some("global") } else { location.as_deref() }, + pass_through_args: None, + }; + package_manager.run_config_command(&options, &self.cwd).await + } + ConfigCommands::Set { key, value, json, global, location } => { + let options = ConfigCommandOptions { + subcommand: "set", + key: Some(key.as_str()), + value: Some(value.as_str()), + json, + location: if global { Some("global") } else { location.as_deref() }, + pass_through_args: None, + }; + package_manager.run_config_command(&options, &self.cwd).await + } + ConfigCommands::Delete { key, global, location } => { + let options = ConfigCommandOptions { + subcommand: "delete", + key: Some(key.as_str()), + value: None, + json: false, + location: if global { Some("global") } else { location.as_deref() }, + pass_through_args: None, + }; + package_manager.run_config_command(&options, &self.cwd).await + } + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_pm_command_new() { + let workspace_root = if cfg!(windows) { + AbsolutePathBuf::new("C:\\test".into()).unwrap() + } else { + AbsolutePathBuf::new("/test".into()).unwrap() + }; + + let cmd = PmCommand::new(workspace_root.clone()); + assert_eq!(cmd.cwd, workspace_root); + } +} diff --git a/packages/cli/snap-tests/exit-non-zero-on-cmd-not-exists/snap.txt b/packages/cli/snap-tests/exit-non-zero-on-cmd-not-exists/snap.txt index 18f00a9cf7..376dd9b0b2 100644 --- a/packages/cli/snap-tests/exit-non-zero-on-cmd-not-exists/snap.txt +++ b/packages/cli/snap-tests/exit-non-zero-on-cmd-not-exists/snap.txt @@ -1,6 +1,6 @@ [2]> vite command-not-exists # should exit with non-zero code error: 'vite' requires a subcommand but one was not provided - [subcommands: run, lint, fmt, build, test, lib, dev, doc, cache, install, i, add, remove, rm, un, uninstall, update, up, dedupe, ddp, outdated, why, explain, link, ln, unlink, help] + [subcommands: run, lint, fmt, build, test, lib, dev, doc, cache, install, i, add, remove, rm, un, uninstall, update, up, dedupe, ddp, outdated, why, explain, link, ln, unlink, pm, help] Usage: vite [OPTIONS] [TASK] [-- ...] diff --git a/packages/global/snap-tests/cli-helper-message/snap.txt b/packages/global/snap-tests/cli-helper-message/snap.txt index ba60359a24..e79df16411 100644 --- a/packages/global/snap-tests/cli-helper-message/snap.txt +++ b/packages/global/snap-tests/cli-helper-message/snap.txt @@ -20,6 +20,7 @@ Commands: why Show why a package is installed link Link packages for local development unlink Unlink packages + pm Package manager utilities help Print this message or the help of the given subcommand(s) Arguments: @@ -31,3 +32,22 @@ Options: --no-debug -h, --help Print help -V, --version Print version + +> vp pm -h # show pm help message +Package manager utilities + +Usage: vp pm + +Commands: + prune Remove unnecessary packages + pack Create a tarball of the package + list List installed packages + view View package information from registry + publish Publish package to registry + owner Manage package owners + cache Manage package cache + config Manage package manager configuration + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help Print help diff --git a/packages/global/snap-tests/cli-helper-message/steps.json b/packages/global/snap-tests/cli-helper-message/steps.json index c68d6aa874..17d47554ed 100644 --- a/packages/global/snap-tests/cli-helper-message/steps.json +++ b/packages/global/snap-tests/cli-helper-message/steps.json @@ -1,6 +1,7 @@ { "ignoredPlatforms": ["win32"], "commands": [ - "vp -h # show help message" + "vp -h # show help message", + "vp pm -h # show pm help message" ] } diff --git a/packages/global/snap-tests/command-cache-npm10/package.json b/packages/global/snap-tests/command-cache-npm10/package.json new file mode 100644 index 0000000000..7093626e6a --- /dev/null +++ b/packages/global/snap-tests/command-cache-npm10/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-cache-npm10", + "version": "1.0.0", + "packageManager": "npm@10.9.4" +} diff --git a/packages/global/snap-tests/command-cache-npm10/snap.txt b/packages/global/snap-tests/command-cache-npm10/snap.txt new file mode 100644 index 0000000000..2391920ce8 --- /dev/null +++ b/packages/global/snap-tests/command-cache-npm10/snap.txt @@ -0,0 +1,5 @@ +> vp pm cache dir # should show cache directory (uses npm config get cache) +/.npm + +> vp pm cache path # should show cache path (alias for dir, uses npm config get cache) +/.npm diff --git a/packages/global/snap-tests/command-cache-npm10/steps.json b/packages/global/snap-tests/command-cache-npm10/steps.json new file mode 100644 index 0000000000..0d528cd197 --- /dev/null +++ b/packages/global/snap-tests/command-cache-npm10/steps.json @@ -0,0 +1,10 @@ +{ + "ignoredPlatforms": ["win32"], + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp pm cache dir # should show cache directory (uses npm config get cache)", + "vp pm cache path # should show cache path (alias for dir, uses npm config get cache)" + ] +} diff --git a/packages/global/snap-tests/command-cache-pnpm10/package.json b/packages/global/snap-tests/command-cache-pnpm10/package.json new file mode 100644 index 0000000000..a31f5250e8 --- /dev/null +++ b/packages/global/snap-tests/command-cache-pnpm10/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-cache-pnpm10", + "version": "1.0.0", + "packageManager": "pnpm@10.20.0" +} diff --git a/packages/global/snap-tests/command-cache-pnpm10/snap.txt b/packages/global/snap-tests/command-cache-pnpm10/snap.txt new file mode 100644 index 0000000000..fc3e7568ef --- /dev/null +++ b/packages/global/snap-tests/command-cache-pnpm10/snap.txt @@ -0,0 +1,14 @@ +> vp pm cache --help # should show help +Manage package cache + +Usage: vp pm cache [-- ...] + +Arguments: + Subcommand: dir, path, clean + [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager + +Options: + -h, --help Print help + +> vp pm cache dir > /dev/null # should show cache directory (uses pnpm store path) +> vp pm cache path > /dev/null # should show cache path (alias for dir, uses pnpm store path) \ No newline at end of file diff --git a/packages/global/snap-tests/command-cache-pnpm10/steps.json b/packages/global/snap-tests/command-cache-pnpm10/steps.json new file mode 100644 index 0000000000..8416e89dc1 --- /dev/null +++ b/packages/global/snap-tests/command-cache-pnpm10/steps.json @@ -0,0 +1,10 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp pm cache --help # should show help", + "vp pm cache dir > /dev/null # should show cache directory (uses pnpm store path)", + "vp pm cache path > /dev/null # should show cache path (alias for dir, uses pnpm store path)" + ] +} diff --git a/packages/global/snap-tests/command-cache-yarn4/package.json b/packages/global/snap-tests/command-cache-yarn4/package.json new file mode 100644 index 0000000000..5c3fa1e7ac --- /dev/null +++ b/packages/global/snap-tests/command-cache-yarn4/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-cache-yarn4", + "version": "1.0.0", + "packageManager": "yarn@4.10.3" +} diff --git a/packages/global/snap-tests/command-cache-yarn4/snap.txt b/packages/global/snap-tests/command-cache-yarn4/snap.txt new file mode 100644 index 0000000000..35109064ee --- /dev/null +++ b/packages/global/snap-tests/command-cache-yarn4/snap.txt @@ -0,0 +1,5 @@ +> vp pm cache dir # should show cache directory (uses yarn config get cacheFolder) +/.yarn/berry/cache + +> vp pm cache path # should show cache path (alias for dir, uses yarn config get cacheFolder) +/.yarn/berry/cache diff --git a/packages/global/snap-tests/command-cache-yarn4/steps.json b/packages/global/snap-tests/command-cache-yarn4/steps.json new file mode 100644 index 0000000000..ba6ef8e1ae --- /dev/null +++ b/packages/global/snap-tests/command-cache-yarn4/steps.json @@ -0,0 +1,10 @@ +{ + "ignoredPlatforms": ["win32"], + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp pm cache dir # should show cache directory (uses yarn config get cacheFolder)", + "vp pm cache path # should show cache path (alias for dir, uses yarn config get cacheFolder)" + ] +} diff --git a/packages/global/snap-tests/command-config-npm10/.npmrc b/packages/global/snap-tests/command-config-npm10/.npmrc new file mode 100644 index 0000000000..80a2f5bed3 --- /dev/null +++ b/packages/global/snap-tests/command-config-npm10/.npmrc @@ -0,0 +1,2 @@ +vite-plus-pm-config-test-key = test-value +foo = bar diff --git a/packages/global/snap-tests/command-config-npm10/package.json b/packages/global/snap-tests/command-config-npm10/package.json new file mode 100644 index 0000000000..69e3b38ac8 --- /dev/null +++ b/packages/global/snap-tests/command-config-npm10/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-config-npm10", + "version": "1.0.0", + "packageManager": "npm@10.9.4" +} diff --git a/packages/global/snap-tests/command-config-npm10/snap.txt b/packages/global/snap-tests/command-config-npm10/snap.txt new file mode 100644 index 0000000000..f11ced1d74 --- /dev/null +++ b/packages/global/snap-tests/command-config-npm10/snap.txt @@ -0,0 +1,6 @@ +> # vp pm config set vite-plus-pm-config-test-key test-value --location project # npm set will check valid keys start from npm v9, see https://github.com/npm/cli/issues/5852 +> vp pm config get vite-plus-pm-config-test-key --location project # should get config value from project scope +test-value + +> vp pm config delete vite-plus-pm-config-test-key --location project && cat .npmrc # should delete config key from project scope +foo=bar diff --git a/packages/global/snap-tests/command-config-npm10/steps.json b/packages/global/snap-tests/command-config-npm10/steps.json new file mode 100644 index 0000000000..524b1ce5c2 --- /dev/null +++ b/packages/global/snap-tests/command-config-npm10/steps.json @@ -0,0 +1,10 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "# vp pm config set vite-plus-pm-config-test-key test-value --location project # npm set will check valid keys start from npm v9, see https://github.com/npm/cli/issues/5852", + "vp pm config get vite-plus-pm-config-test-key --location project # should get config value from project scope", + "vp pm config delete vite-plus-pm-config-test-key --location project && cat .npmrc # should delete config key from project scope" + ] +} diff --git a/packages/global/snap-tests/command-config-pnpm10/package.json b/packages/global/snap-tests/command-config-pnpm10/package.json new file mode 100644 index 0000000000..a8efae1725 --- /dev/null +++ b/packages/global/snap-tests/command-config-pnpm10/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-config-pnpm10", + "version": "1.0.0", + "packageManager": "pnpm@10.20.0" +} diff --git a/packages/global/snap-tests/command-config-pnpm10/snap.txt b/packages/global/snap-tests/command-config-pnpm10/snap.txt new file mode 100644 index 0000000000..46eff44df1 --- /dev/null +++ b/packages/global/snap-tests/command-config-pnpm10/snap.txt @@ -0,0 +1,21 @@ +> vp pm config --help # should show help +Manage package manager configuration + +Usage: vp pm config + +Commands: + list List all configuration + get Get configuration value + set Set configuration value + delete Delete configuration key + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help Print help + +> vp pm config list --location project > /dev/null # should list all project configuration +> vp pm config set vite-plus-pm-config-test-key test-value --location project # should set config value in project scope +> vp pm config get vite-plus-pm-config-test-key --location project # should get config value from project scope +test-value + +> vp pm config delete vite-plus-pm-config-test-key --location project # should delete config key from project scope \ No newline at end of file diff --git a/packages/global/snap-tests/command-config-pnpm10/steps.json b/packages/global/snap-tests/command-config-pnpm10/steps.json new file mode 100644 index 0000000000..32ef67bbdc --- /dev/null +++ b/packages/global/snap-tests/command-config-pnpm10/steps.json @@ -0,0 +1,12 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp pm config --help # should show help", + "vp pm config list --location project > /dev/null # should list all project configuration", + "vp pm config set vite-plus-pm-config-test-key test-value --location project # should set config value in project scope", + "vp pm config get vite-plus-pm-config-test-key --location project # should get config value from project scope", + "vp pm config delete vite-plus-pm-config-test-key --location project # should delete config key from project scope" + ] +} diff --git a/packages/global/snap-tests/command-config-yarn1/package.json b/packages/global/snap-tests/command-config-yarn1/package.json new file mode 100644 index 0000000000..d061eeb81f --- /dev/null +++ b/packages/global/snap-tests/command-config-yarn1/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-config-yarn1", + "version": "1.0.0", + "packageManager": "yarn@1.22.22" +} diff --git a/packages/global/snap-tests/command-config-yarn1/snap.txt b/packages/global/snap-tests/command-config-yarn1/snap.txt new file mode 100644 index 0000000000..b123c761a2 --- /dev/null +++ b/packages/global/snap-tests/command-config-yarn1/snap.txt @@ -0,0 +1,18 @@ +> vp pm config set vite-plus-pm-config-test-key test-value --location project # should set config value in project scope (shows warning for yarn@1) +Warning: yarn@1 does not support --location, ignoring flag +yarn config v +warning package.json: No license field +success Set "vite-plus-pm-config-test-key" to "test-value". +Done in ms. + +> vp pm config get vite-plus-pm-config-test-key --location project # should get config value from project scope (shows warning for yarn@1) +Warning: yarn@1 does not support --location, ignoring flag +warning package.json: No license field +test-value + +> vp pm config delete vite-plus-pm-config-test-key --location project # should delete config key from project scope (shows warning for yarn@1) +Warning: yarn@1 does not support --location, ignoring flag +yarn config v +warning package.json: No license field +success Deleted "vite-plus-pm-config-test-key". +Done in ms. diff --git a/packages/global/snap-tests/command-config-yarn1/steps.json b/packages/global/snap-tests/command-config-yarn1/steps.json new file mode 100644 index 0000000000..cb65aff829 --- /dev/null +++ b/packages/global/snap-tests/command-config-yarn1/steps.json @@ -0,0 +1,10 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp pm config set vite-plus-pm-config-test-key test-value --location project # should set config value in project scope (shows warning for yarn@1)", + "vp pm config get vite-plus-pm-config-test-key --location project # should get config value from project scope (shows warning for yarn@1)", + "vp pm config delete vite-plus-pm-config-test-key --location project # should delete config key from project scope (shows warning for yarn@1)" + ] +} diff --git a/packages/global/snap-tests/command-config-yarn4/package.json b/packages/global/snap-tests/command-config-yarn4/package.json new file mode 100644 index 0000000000..a5aaf04a9f --- /dev/null +++ b/packages/global/snap-tests/command-config-yarn4/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-config-yarn4", + "version": "1.0.0", + "packageManager": "yarn@4.10.3" +} diff --git a/packages/global/snap-tests/command-config-yarn4/snap.txt b/packages/global/snap-tests/command-config-yarn4/snap.txt new file mode 100644 index 0000000000..4653857497 --- /dev/null +++ b/packages/global/snap-tests/command-config-yarn4/snap.txt @@ -0,0 +1,14 @@ +[1]> vp pm config set vite-plus-pm-config-test-key test-value --location project # should set config value in project scope +Usage Error: Couldn't find a configuration settings named "vite-plus-pm-config-test-key" + +$ yarn config set [--json] [-H,--home] + +[1]> vp pm config get vite-plus-pm-config-test-key --location project # should get config value from project scope +Usage Error: Couldn't find a configuration settings named "vite-plus-pm-config-test-key" + +$ yarn config get [--why] [--json] [--no-redacted] + +[1]> vp pm config delete vite-plus-pm-config-test-key --location project # should delete config key from project scope (uses yarn config unset) +Usage Error: Couldn't find a configuration settings named "vite-plus-pm-config-test-key" + +$ yarn config unset [-H,--home] diff --git a/packages/global/snap-tests/command-config-yarn4/steps.json b/packages/global/snap-tests/command-config-yarn4/steps.json new file mode 100644 index 0000000000..931f5fc357 --- /dev/null +++ b/packages/global/snap-tests/command-config-yarn4/steps.json @@ -0,0 +1,10 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp pm config set vite-plus-pm-config-test-key test-value --location project # should set config value in project scope", + "vp pm config get vite-plus-pm-config-test-key --location project # should get config value from project scope", + "vp pm config delete vite-plus-pm-config-test-key --location project # should delete config key from project scope (uses yarn config unset)" + ] +} diff --git a/packages/global/snap-tests/command-list-npm10-with-workspace/package.json b/packages/global/snap-tests/command-list-npm10-with-workspace/package.json new file mode 100644 index 0000000000..69d1f5a061 --- /dev/null +++ b/packages/global/snap-tests/command-list-npm10-with-workspace/package.json @@ -0,0 +1,8 @@ +{ + "name": "command-list-npm10-with-workspace", + "version": "1.0.0", + "packageManager": "npm@10.8.0", + "workspaces": [ + "packages/*" + ] +} diff --git a/packages/global/snap-tests/command-list-npm10-with-workspace/packages/app/package.json b/packages/global/snap-tests/command-list-npm10-with-workspace/packages/app/package.json new file mode 100644 index 0000000000..3fe1338b13 --- /dev/null +++ b/packages/global/snap-tests/command-list-npm10-with-workspace/packages/app/package.json @@ -0,0 +1,10 @@ +{ + "name": "app", + "version": "1.0.0", + "dependencies": { + "testnpm2": "*" + }, + "peerDependencies": { + "test-vite-plus-package-optional": "1.0.0" + } +} diff --git a/packages/global/snap-tests/command-list-npm10-with-workspace/packages/utils/package.json b/packages/global/snap-tests/command-list-npm10-with-workspace/packages/utils/package.json new file mode 100644 index 0000000000..8e54c93d99 --- /dev/null +++ b/packages/global/snap-tests/command-list-npm10-with-workspace/packages/utils/package.json @@ -0,0 +1,8 @@ +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true, + "dependencies": { + "testnpm2": "*" + } +} diff --git a/packages/global/snap-tests/command-list-npm10-with-workspace/snap.txt b/packages/global/snap-tests/command-list-npm10-with-workspace/snap.txt new file mode 100644 index 0000000000..414c894c97 --- /dev/null +++ b/packages/global/snap-tests/command-list-npm10-with-workspace/snap.txt @@ -0,0 +1,169 @@ +> vp install # should install packages first + +added 4 packages in ms + + +> vp pm list --json # should list current workspace root dependencies +{ + "version": "1.0.0", + "name": "command-list-npm10-with-workspace", + "dependencies": { + "@vite-plus-test/utils": { + "version": "1.0.0", + "resolved": "file:../../packages/utils", + "overridden": false, + "dependencies": { + "testnpm2": { + "version": "1.0.1", + "resolved": "https://registry./testnpm2/-/testnpm2-1.0.1.tgz", + "overridden": false + } + } + }, + "app": { + "version": "1.0.0", + "resolved": "file:../packages/app", + "overridden": false, + "dependencies": { + "test-vite-plus-package-optional": { + "version": "1.0.0", + "resolved": "https://registry./test-vite-plus-package-optional/-/test-vite-plus-package-optional-1.0.0.tgz", + "overridden": false + }, + "testnpm2": { + "version": "1.0.1" + } + } + } + } +} + +> vp pm list --recursive --json # should list all packages in workspace (uses --workspaces) +{ + "version": "1.0.0", + "name": "command-list-npm10-with-workspace", + "dependencies": { + "@vite-plus-test/utils": { + "version": "1.0.0", + "resolved": "file:../../packages/utils", + "overridden": false, + "dependencies": { + "testnpm2": { + "version": "1.0.1", + "resolved": "https://registry./testnpm2/-/testnpm2-1.0.1.tgz", + "overridden": false + } + } + }, + "app": { + "version": "1.0.0", + "resolved": "file:../packages/app", + "overridden": false, + "dependencies": { + "test-vite-plus-package-optional": { + "version": "1.0.0", + "resolved": "https://registry./test-vite-plus-package-optional/-/test-vite-plus-package-optional-1.0.0.tgz", + "overridden": false + }, + "testnpm2": { + "version": "1.0.1" + } + } + } + } +} + +> vp pm list --filter app --json # should list specific workspace package (uses --workspace app) +{ + "version": "1.0.0", + "name": "command-list-npm10-with-workspace", + "dependencies": { + "app": { + "version": "1.0.0", + "resolved": "file:../packages/app", + "overridden": false, + "dependencies": { + "test-vite-plus-package-optional": { + "version": "1.0.0", + "resolved": "https://registry./test-vite-plus-package-optional/-/test-vite-plus-package-optional-1.0.0.tgz", + "overridden": false + }, + "testnpm2": { + "version": "1.0.1", + "resolved": "https://registry./testnpm2/-/testnpm2-1.0.1.tgz", + "overridden": false + } + } + } + } +} + +> vp pm list --filter app --filter @vite-plus-test/utils --json # should list multiple workspace packages +{ + "version": "1.0.0", + "name": "command-list-npm10-with-workspace", + "dependencies": { + "@vite-plus-test/utils": { + "version": "1.0.0", + "resolved": "file:../../packages/utils", + "overridden": false, + "dependencies": { + "testnpm2": { + "version": "1.0.1", + "resolved": "https://registry./testnpm2/-/testnpm2-1.0.1.tgz", + "overridden": false + } + } + }, + "app": { + "version": "1.0.0", + "resolved": "file:../packages/app", + "overridden": false, + "dependencies": { + "test-vite-plus-package-optional": { + "version": "1.0.0", + "resolved": "https://registry./test-vite-plus-package-optional/-/test-vite-plus-package-optional-1.0.0.tgz", + "overridden": false + }, + "testnpm2": { + "version": "1.0.1" + } + } + } + } +} + +> vp pm list --recursive --depth 0 --json # should list workspace packages with depth limit +{ + "version": "1.0.0", + "name": "command-list-npm10-with-workspace", + "dependencies": { + "@vite-plus-test/utils": { + "version": "1.0.0", + "resolved": "file:../../packages/utils", + "overridden": false, + "dependencies": { + "testnpm2": { + "version": "1.0.1", + "resolved": "https://registry./testnpm2/-/testnpm2-1.0.1.tgz", + "overridden": false + } + } + }, + "app": { + "version": "1.0.0", + "resolved": "file:../packages/app", + "overridden": false, + "dependencies": { + "test-vite-plus-package-optional": { + "version": "1.0.0", + "resolved": "https://registry./test-vite-plus-package-optional/-/test-vite-plus-package-optional-1.0.0.tgz", + "overridden": false + }, + "testnpm2": { + "version": "1.0.1" + } + } + } + } +} diff --git a/packages/global/snap-tests/command-list-npm10-with-workspace/steps.json b/packages/global/snap-tests/command-list-npm10-with-workspace/steps.json new file mode 100644 index 0000000000..6dfbfa29b8 --- /dev/null +++ b/packages/global/snap-tests/command-list-npm10-with-workspace/steps.json @@ -0,0 +1,13 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp install # should install packages first", + "vp pm list --json # should list current workspace root dependencies", + "vp pm list --recursive --json # should list all packages in workspace (uses --workspaces)", + "vp pm list --filter app --json # should list specific workspace package (uses --workspace app)", + "vp pm list --filter app --filter @vite-plus-test/utils --json # should list multiple workspace packages", + "vp pm list --recursive --depth 0 --json # should list workspace packages with depth limit" + ] +} diff --git a/packages/global/snap-tests/command-list-npm10/package.json b/packages/global/snap-tests/command-list-npm10/package.json new file mode 100644 index 0000000000..75fb3be426 --- /dev/null +++ b/packages/global/snap-tests/command-list-npm10/package.json @@ -0,0 +1,14 @@ +{ + "name": "command-list-npm10", + "version": "1.0.0", + "packageManager": "npm@10.8.0", + "dependencies": { + "testnpm2": "1.0.1" + }, + "devDependencies": { + "test-vite-plus-package": "1.0.0" + }, + "peerDependencies": { + "test-vite-plus-package-optional": "1.0.0" + } +} diff --git a/packages/global/snap-tests/command-list-npm10/snap.txt b/packages/global/snap-tests/command-list-npm10/snap.txt new file mode 100644 index 0000000000..48431ddfde --- /dev/null +++ b/packages/global/snap-tests/command-list-npm10/snap.txt @@ -0,0 +1,261 @@ +> vp install # should install packages first + +added 3 packages in ms + + +> vp pm list --json # should list installed packages +{ + "version": "1.0.0", + "name": "command-list-npm10", + "dependencies": { + "test-vite-plus-package-optional": { + "version": "1.0.0", + "resolved": "https://registry./test-vite-plus-package-optional/-/test-vite-plus-package-optional-1.0.0.tgz", + "overridden": false + }, + "test-vite-plus-package": { + "version": "1.0.0", + "resolved": "https://registry./test-vite-plus-package/-/test-vite-plus-package-1.0.0.tgz", + "overridden": false + }, + "testnpm2": { + "version": "1.0.1", + "resolved": "https://registry./testnpm2/-/testnpm2-1.0.1.tgz", + "overridden": false + } + } +} + +> vp pm list testnpm2 --json # should list specific package +{ + "version": "1.0.0", + "name": "command-list-npm10", + "dependencies": { + "testnpm2": { + "version": "1.0.1", + "resolved": "https://registry./testnpm2/-/testnpm2-1.0.1.tgz", + "overridden": false + } + } +} + +> vp pm list --depth 0 --json # should list packages with depth limit +{ + "version": "1.0.0", + "name": "command-list-npm10", + "dependencies": { + "test-vite-plus-package-optional": { + "version": "1.0.0", + "resolved": "https://registry./test-vite-plus-package-optional/-/test-vite-plus-package-optional-1.0.0.tgz", + "overridden": false + }, + "test-vite-plus-package": { + "version": "1.0.0", + "resolved": "https://registry./test-vite-plus-package/-/test-vite-plus-package-1.0.0.tgz", + "overridden": false + }, + "testnpm2": { + "version": "1.0.1", + "resolved": "https://registry./testnpm2/-/testnpm2-1.0.1.tgz", + "overridden": false + } + } +} + +> vp pm list --long --json # should list packages with extended info +{ + "version": "1.0.0", + "name": "command-list-npm10", + "packageManager": "npm@", + "devDependencies": { + "test-vite-plus-package": "1.0.0" + }, + "peerDependencies": { + "test-vite-plus-package-optional": "1.0.0" + }, + "_id": "command-list-npm10@", + "extraneous": false, + "path": "", + "_dependencies": { + "testnpm2": "1.0.1" + }, + "dependencies": { + "test-vite-plus-package-optional": { + "version": "1.0.0", + "resolved": "https://registry./test-vite-plus-package-optional/-/test-vite-plus-package-optional-1.0.0.tgz", + "overridden": false, + "name": "test-vite-plus-package-optional", + "integrity": "sha512-", + "license": "MIT", + "peer": true, + "_id": "test-vite-plus-package-optional@", + "extraneous": false, + "path": "/node_modules/test-vite-plus-package-optional", + "_dependencies": {}, + "devDependencies": {}, + "peerDependencies": {} + }, + "test-vite-plus-package": { + "version": "1.0.0", + "resolved": "https://registry./test-vite-plus-package/-/test-vite-plus-package-1.0.0.tgz", + "overridden": false, + "name": "test-vite-plus-package", + "integrity": "sha512-", + "dev": true, + "license": "MIT", + "_id": "test-vite-plus-package@", + "extraneous": false, + "path": "/node_modules/test-vite-plus-package", + "_dependencies": {}, + "devDependencies": {}, + "peerDependencies": {} + }, + "testnpm2": { + "version": "1.0.1", + "resolved": "https://registry./testnpm2/-/testnpm2-1.0.1.tgz", + "overridden": false, + "name": "testnpm2", + "integrity": "sha512-", + "license": "ISC", + "_id": "testnpm2@", + "extraneous": false, + "path": "/node_modules/testnpm2", + "_dependencies": {}, + "devDependencies": {}, + "peerDependencies": {} + } + } +} + +> vp pm list --parseable --json # should list packages in parseable format +{ + "version": "1.0.0", + "name": "command-list-npm10", + "dependencies": { + "test-vite-plus-package-optional": { + "version": "1.0.0", + "resolved": "https://registry./test-vite-plus-package-optional/-/test-vite-plus-package-optional-1.0.0.tgz", + "overridden": false + }, + "test-vite-plus-package": { + "version": "1.0.0", + "resolved": "https://registry./test-vite-plus-package/-/test-vite-plus-package-1.0.0.tgz", + "overridden": false + }, + "testnpm2": { + "version": "1.0.1", + "resolved": "https://registry./testnpm2/-/testnpm2-1.0.1.tgz", + "overridden": false + } + } +} + +> vp pm list --prod --json # should list production dependencies only (uses --include prod --include peer) +{ + "version": "1.0.0", + "name": "command-list-npm10", + "dependencies": { + "test-vite-plus-package-optional": { + "version": "1.0.0", + "resolved": "https://registry./test-vite-plus-package-optional/-/test-vite-plus-package-optional-1.0.0.tgz", + "overridden": false + }, + "test-vite-plus-package": { + "version": "1.0.0", + "resolved": "https://registry./test-vite-plus-package/-/test-vite-plus-package-1.0.0.tgz", + "overridden": false + }, + "testnpm2": { + "version": "1.0.1", + "resolved": "https://registry./testnpm2/-/testnpm2-1.0.1.tgz", + "overridden": false + } + } +} + +> vp pm list --dev --json # should list development dependencies only (uses --include dev) +{ + "version": "1.0.0", + "name": "command-list-npm10", + "dependencies": { + "test-vite-plus-package-optional": { + "version": "1.0.0", + "resolved": "https://registry./test-vite-plus-package-optional/-/test-vite-plus-package-optional-1.0.0.tgz", + "overridden": false + }, + "test-vite-plus-package": { + "version": "1.0.0", + "resolved": "https://registry./test-vite-plus-package/-/test-vite-plus-package-1.0.0.tgz", + "overridden": false + }, + "testnpm2": { + "version": "1.0.1", + "resolved": "https://registry./testnpm2/-/testnpm2-1.0.1.tgz", + "overridden": false + } + } +} + +> vp pm list --no-optional --json # should exclude optional dependencies (uses --omit optional) +{ + "version": "1.0.0", + "name": "command-list-npm10", + "dependencies": { + "test-vite-plus-package-optional": { + "version": "1.0.0", + "resolved": "https://registry./test-vite-plus-package-optional/-/test-vite-plus-package-optional-1.0.0.tgz", + "overridden": false + }, + "test-vite-plus-package": { + "version": "1.0.0", + "resolved": "https://registry./test-vite-plus-package/-/test-vite-plus-package-1.0.0.tgz", + "overridden": false + }, + "testnpm2": { + "version": "1.0.1", + "resolved": "https://registry./testnpm2/-/testnpm2-1.0.1.tgz", + "overridden": false + } + } +} + +> vp pm list --exclude-peers --json # should exclude peer dependencies (uses --omit peer) +{ + "version": "1.0.0", + "name": "command-list-npm10", + "dependencies": { + "test-vite-plus-package": { + "version": "1.0.0", + "resolved": "https://registry./test-vite-plus-package/-/test-vite-plus-package-1.0.0.tgz", + "overridden": false + }, + "testnpm2": { + "version": "1.0.1", + "resolved": "https://registry./testnpm2/-/testnpm2-1.0.1.tgz", + "overridden": false + } + } +} + +> vp pm list -- --loglevel=warn --json # should support pass through arguments +{ + "version": "1.0.0", + "name": "command-list-npm10", + "dependencies": { + "test-vite-plus-package-optional": { + "version": "1.0.0", + "resolved": "https://registry./test-vite-plus-package-optional/-/test-vite-plus-package-optional-1.0.0.tgz", + "overridden": false + }, + "test-vite-plus-package": { + "version": "1.0.0", + "resolved": "https://registry./test-vite-plus-package/-/test-vite-plus-package-1.0.0.tgz", + "overridden": false + }, + "testnpm2": { + "version": "1.0.1", + "resolved": "https://registry./testnpm2/-/testnpm2-1.0.1.tgz", + "overridden": false + } + } +} diff --git a/packages/global/snap-tests/command-list-npm10/steps.json b/packages/global/snap-tests/command-list-npm10/steps.json new file mode 100644 index 0000000000..91af807eec --- /dev/null +++ b/packages/global/snap-tests/command-list-npm10/steps.json @@ -0,0 +1,19 @@ +{ + "ignoredPlatforms": ["win32"], + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp install # should install packages first", + "vp pm list --json # should list installed packages", + "vp pm list testnpm2 --json # should list specific package", + "vp pm list --depth 0 --json # should list packages with depth limit", + "vp pm list --long --json # should list packages with extended info", + "vp pm list --parseable --json # should list packages in parseable format", + "vp pm list --prod --json # should list production dependencies only (uses --include prod --include peer)", + "vp pm list --dev --json # should list development dependencies only (uses --include dev)", + "vp pm list --no-optional --json # should exclude optional dependencies (uses --omit optional)", + "vp pm list --exclude-peers --json # should exclude peer dependencies (uses --omit peer)", + "vp pm list -- --loglevel=warn --json # should support pass through arguments" + ] +} diff --git a/packages/global/snap-tests/command-list-pnpm10-with-workspace/package.json b/packages/global/snap-tests/command-list-pnpm10-with-workspace/package.json new file mode 100644 index 0000000000..5496399cf1 --- /dev/null +++ b/packages/global/snap-tests/command-list-pnpm10-with-workspace/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-list-pnpm10-with-workspace", + "version": "1.0.0", + "packageManager": "pnpm@10.20.0" +} diff --git a/packages/global/snap-tests/command-list-pnpm10-with-workspace/packages/app/package.json b/packages/global/snap-tests/command-list-pnpm10-with-workspace/packages/app/package.json new file mode 100644 index 0000000000..8431ea2929 --- /dev/null +++ b/packages/global/snap-tests/command-list-pnpm10-with-workspace/packages/app/package.json @@ -0,0 +1,10 @@ +{ + "name": "app", + "version": "1.0.0", + "dependencies": { + "@vite-plus-test/utils": "workspace:*" + }, + "peerDependencies": { + "test-vite-plus-package-optional": "1.0.0" + } +} diff --git a/packages/global/snap-tests/command-list-pnpm10-with-workspace/packages/utils/package.json b/packages/global/snap-tests/command-list-pnpm10-with-workspace/packages/utils/package.json new file mode 100644 index 0000000000..8e54c93d99 --- /dev/null +++ b/packages/global/snap-tests/command-list-pnpm10-with-workspace/packages/utils/package.json @@ -0,0 +1,8 @@ +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true, + "dependencies": { + "testnpm2": "*" + } +} diff --git a/packages/global/snap-tests/command-list-pnpm10-with-workspace/pnpm-workspace.yaml b/packages/global/snap-tests/command-list-pnpm10-with-workspace/pnpm-workspace.yaml new file mode 100644 index 0000000000..924b55f42e --- /dev/null +++ b/packages/global/snap-tests/command-list-pnpm10-with-workspace/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - packages/* diff --git a/packages/global/snap-tests/command-list-pnpm10-with-workspace/snap.txt b/packages/global/snap-tests/command-list-pnpm10-with-workspace/snap.txt new file mode 100644 index 0000000000..c66972191f --- /dev/null +++ b/packages/global/snap-tests/command-list-pnpm10-with-workspace/snap.txt @@ -0,0 +1,139 @@ +> vp install # should install packages first +Scope: all workspace projects +Packages: + ++ +Progress: resolved , reused , downloaded , added , done + +Done in ms using pnpm v + + +> vp pm list # should list current workspace root dependencies +> vp pm list --recursive # should list all packages in workspace recursively +Legend: production dependency, optional only, dev only + +app@ /packages/app + +dependencies: +@vite-plus-test/utils link:../utils +test-vite-plus-package-optional + +@vite-plus-test/utils@ /packages/utils (PRIVATE) + +dependencies: +testnpm2 + +> vp pm list --filter app # should list specific workspace package (uses --filter app list) +Legend: production dependency, optional only, dev only + +app@ /packages/app + +dependencies: +@vite-plus-test/utils link:../utils +test-vite-plus-package-optional + +> vp pm list --filter app --filter @vite-plus-test/utils # should list multiple workspace packages +Legend: production dependency, optional only, dev only + +app@ /packages/app + +dependencies: +@vite-plus-test/utils link:../utils +test-vite-plus-package-optional + +@vite-plus-test/utils@ /packages/utils (PRIVATE) + +dependencies: +testnpm2 + +> vp pm list --recursive --json # should list all workspace packages in JSON format +[ + { + "name": "command-list-pnpm10-with-workspace", + "version": "1.0.0", + "path": "", + "private": false + }, + { + "name": "app", + "version": "1.0.0", + "path": "/packages/app", + "private": false, + "dependencies": { + "@vite-plus-test/utils": { + "from": "@vite-plus-test/utils", + "version": "link:../utils", + "path": "/packages/utils" + }, + "test-vite-plus-package-optional": { + "from": "test-vite-plus-package-optional", + "version": "1.0.0", + "resolved": "https://registry./test-vite-plus-package-optional/-/test-vite-plus-package-optional-1.0.0.tgz", + "path": "/node_modules/.pnpm/test-vite-plus-package-optional@/node_modules/test-vite-plus-package-optional" + } + } + }, + { + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "path": "/packages/utils", + "private": true, + "dependencies": { + "testnpm2": { + "from": "testnpm2", + "version": "1.0.1", + "resolved": "https://registry./testnpm2/-/testnpm2-1.0.1.tgz", + "path": "/node_modules/.pnpm/testnpm2@/node_modules/testnpm2" + } + } + } +] + +> vp pm list --recursive --depth 0 # should list workspace packages with depth limit +Legend: production dependency, optional only, dev only + +app@ /packages/app + +dependencies: +@vite-plus-test/utils link:../utils +test-vite-plus-package-optional + +@vite-plus-test/utils@ /packages/utils (PRIVATE) + +dependencies: +testnpm2 + +> vp pm list --recursive --only-projects # should list only workspace projects (pnpm-specific) +Legend: production dependency, optional only, dev only + +app@ /packages/app + +dependencies: +@vite-plus-test/utils link:../utils + +> vp pm list --recursive --exclude-peers # should exclude peer dependencies in workspace +Legend: production dependency, optional only, dev only + +app@ /packages/app + +dependencies: +@vite-plus-test/utils link:../utils +test-vite-plus-package-optional + +@vite-plus-test/utils@ /packages/utils (PRIVATE) + +dependencies: +testnpm2 + +> vp pm list --recursive --prod # should list production dependencies in workspace +Legend: production dependency, optional only, dev only + +app@ /packages/app + +dependencies: +@vite-plus-test/utils link:../utils +test-vite-plus-package-optional + +@vite-plus-test/utils@ /packages/utils (PRIVATE) + +dependencies: +testnpm2 diff --git a/packages/global/snap-tests/command-list-pnpm10-with-workspace/steps.json b/packages/global/snap-tests/command-list-pnpm10-with-workspace/steps.json new file mode 100644 index 0000000000..bae99ad138 --- /dev/null +++ b/packages/global/snap-tests/command-list-pnpm10-with-workspace/steps.json @@ -0,0 +1,18 @@ +{ + "ignoredPlatforms": ["win32"], + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp install # should install packages first", + "vp pm list # should list current workspace root dependencies", + "vp pm list --recursive # should list all packages in workspace recursively", + "vp pm list --filter app # should list specific workspace package (uses --filter app list)", + "vp pm list --filter app --filter @vite-plus-test/utils # should list multiple workspace packages", + "vp pm list --recursive --json # should list all workspace packages in JSON format", + "vp pm list --recursive --depth 0 # should list workspace packages with depth limit", + "vp pm list --recursive --only-projects # should list only workspace projects (pnpm-specific)", + "vp pm list --recursive --exclude-peers # should exclude peer dependencies in workspace", + "vp pm list --recursive --prod # should list production dependencies in workspace" + ] +} diff --git a/packages/global/snap-tests/command-list-pnpm10/package.json b/packages/global/snap-tests/command-list-pnpm10/package.json new file mode 100644 index 0000000000..5fa7f2cf16 --- /dev/null +++ b/packages/global/snap-tests/command-list-pnpm10/package.json @@ -0,0 +1,14 @@ +{ + "name": "command-list-pnpm10", + "version": "1.0.0", + "packageManager": "pnpm@10.20.0", + "dependencies": { + "testnpm2": "1.0.1" + }, + "devDependencies": { + "test-vite-plus-package": "1.0.0" + }, + "peerDependencies": { + "test-vite-plus-package-optional": "1.0.0" + } +} diff --git a/packages/global/snap-tests/command-list-pnpm10/snap.txt b/packages/global/snap-tests/command-list-pnpm10/snap.txt new file mode 100644 index 0000000000..92036972ae --- /dev/null +++ b/packages/global/snap-tests/command-list-pnpm10/snap.txt @@ -0,0 +1,195 @@ +> vp install # should install packages first +Packages: + ++ +Progress: resolved , reused , downloaded , added , done + +dependencies: ++ test-vite-plus-package-optional ++ testnpm2 + +devDependencies: ++ test-vite-plus-package + +Done in ms using pnpm v + + +> vp pm list --help # should show help +List installed packages + +Usage: vp pm list [OPTIONS] [PATTERN] [-- ...] + +Arguments: + [PATTERN] Package pattern to filter + [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager + +Options: + --depth Maximum depth of dependency tree + --json Output in JSON format + --long Show extended information + --parseable Parseable output format + -P, --prod Only production dependencies + -D, --dev Only dev dependencies + --no-optional Exclude optional dependencies + --exclude-peers Exclude peer dependencies + --only-projects Show only project packages (pnpm-specific) + --find-by Use a finder function defined in .pnpmfile.cjs (pnpm-specific) + -r, --recursive List across all workspaces + --filter Filter packages in monorepo (can be used multiple times) + -g, --global List global packages + -h, --help Print help + +> vp pm list # should list installed packages +Legend: production dependency, optional only, dev only + +command-list-pnpm10@ + +dependencies: +test-vite-plus-package-optional +testnpm2 + +devDependencies: +test-vite-plus-package + +> vp pm list testnpm2 # should list specific package +Legend: production dependency, optional only, dev only + +command-list-pnpm10@ + +dependencies: +testnpm2 + +> vp pm list --depth 0 # should list packages with depth limit +Legend: production dependency, optional only, dev only + +command-list-pnpm10@ + +dependencies: +test-vite-plus-package-optional +testnpm2 + +devDependencies: +test-vite-plus-package + +> vp pm list --json # should list packages in JSON format +[ + { + "name": "command-list-pnpm10", + "version": "1.0.0", + "path": "", + "private": false, + "dependencies": { + "test-vite-plus-package-optional": { + "from": "test-vite-plus-package-optional", + "version": "1.0.0", + "resolved": "https://registry./test-vite-plus-package-optional/-/test-vite-plus-package-optional-1.0.0.tgz", + "path": "/node_modules/.pnpm/test-vite-plus-package-optional@/node_modules/test-vite-plus-package-optional" + }, + "testnpm2": { + "from": "testnpm2", + "version": "1.0.1", + "resolved": "https://registry./testnpm2/-/testnpm2-1.0.1.tgz", + "path": "/node_modules/.pnpm/testnpm2@/node_modules/testnpm2" + } + }, + "devDependencies": { + "test-vite-plus-package": { + "from": "test-vite-plus-package", + "version": "1.0.0", + "resolved": "https://registry./test-vite-plus-package/-/test-vite-plus-package-1.0.0.tgz", + "path": "/node_modules/.pnpm/test-vite-plus-package@/node_modules/test-vite-plus-package" + } + } + } +] + +> vp pm list --long # should list packages with extended info +Legend: production dependency, optional only, dev only + +command-list-pnpm10@ + +dependencies: +test-vite-plus-package-optional + just for snap-test + /node_modules/.pnpm/test-vite-plus-package-optional@/node_modules/test-vite-plus-package-optional +testnpm2 + /node_modules/.pnpm/testnpm2@/node_modules/testnpm2 + +devDependencies: +test-vite-plus-package + just for snap-test + /node_modules/.pnpm/test-vite-plus-package@/node_modules/test-vite-plus-package + +> vp pm list --parseable # should list packages in parseable format + +/node_modules/.pnpm/test-vite-plus-package@/node_modules/test-vite-plus-package +/node_modules/.pnpm/test-vite-plus-package-optional@/node_modules/test-vite-plus-package-optional +/node_modules/.pnpm/testnpm2@/node_modules/testnpm2 + +> vp pm list --prod # should list production dependencies only +Legend: production dependency, optional only, dev only + +command-list-pnpm10@ + +dependencies: +test-vite-plus-package-optional +testnpm2 + +> vp pm list --dev # should list development dependencies only +Legend: production dependency, optional only, dev only + +command-list-pnpm10@ + +devDependencies: +test-vite-plus-package + +> vp pm list --no-optional # should exclude optional dependencies +Legend: production dependency, optional only, dev only + +command-list-pnpm10@ + +dependencies: +test-vite-plus-package-optional +testnpm2 + +devDependencies: +test-vite-plus-package + +> vp pm list --exclude-peers # should exclude peer dependencies +Legend: production dependency, optional only, dev only + +command-list-pnpm10@ + +dependencies: +test-vite-plus-package-optional +testnpm2 + +devDependencies: +test-vite-plus-package + +> vp pm list --only-projects # should list only workspace projects (pnpm-specific) +[1]> vp pm list --find-by customFinder # should use custom finder (pnpm-specific) + ERR_PNPM_FINDER_NOT_FOUND  No finder with name customFinder is found + +> vp pm list --recursive # should list packages recursively in workspace +Legend: production dependency, optional only, dev only + +command-list-pnpm10@ + +dependencies: +test-vite-plus-package-optional +testnpm2 + +devDependencies: +test-vite-plus-package + +> vp pm list -- --loglevel=warn # should support pass through arguments +Legend: production dependency, optional only, dev only + +command-list-pnpm10@ + +dependencies: +test-vite-plus-package-optional +testnpm2 + +devDependencies: +test-vite-plus-package diff --git a/packages/global/snap-tests/command-list-pnpm10/steps.json b/packages/global/snap-tests/command-list-pnpm10/steps.json new file mode 100644 index 0000000000..1e6f2ac116 --- /dev/null +++ b/packages/global/snap-tests/command-list-pnpm10/steps.json @@ -0,0 +1,24 @@ +{ + "ignoredPlatforms": ["win32"], + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp install # should install packages first", + "vp pm list --help # should show help", + "vp pm list # should list installed packages", + "vp pm list testnpm2 # should list specific package", + "vp pm list --depth 0 # should list packages with depth limit", + "vp pm list --json # should list packages in JSON format", + "vp pm list --long # should list packages with extended info", + "vp pm list --parseable # should list packages in parseable format", + "vp pm list --prod # should list production dependencies only", + "vp pm list --dev # should list development dependencies only", + "vp pm list --no-optional # should exclude optional dependencies", + "vp pm list --exclude-peers # should exclude peer dependencies", + "vp pm list --only-projects # should list only workspace projects (pnpm-specific)", + "vp pm list --find-by customFinder # should use custom finder (pnpm-specific)", + "vp pm list --recursive # should list packages recursively in workspace", + "vp pm list -- --loglevel=warn # should support pass through arguments" + ] +} diff --git a/packages/global/snap-tests/command-list-yarn1/package.json b/packages/global/snap-tests/command-list-yarn1/package.json new file mode 100644 index 0000000000..e190e582b5 --- /dev/null +++ b/packages/global/snap-tests/command-list-yarn1/package.json @@ -0,0 +1,11 @@ +{ + "name": "command-list-yarn1", + "version": "1.0.0", + "packageManager": "yarn@1.22.22", + "dependencies": { + "testnpm2": "1.0.1" + }, + "devDependencies": { + "test-vite-plus-package": "1.0.0" + } +} diff --git a/packages/global/snap-tests/command-list-yarn1/snap.txt b/packages/global/snap-tests/command-list-yarn1/snap.txt new file mode 100644 index 0000000000..f2639c4dbf --- /dev/null +++ b/packages/global/snap-tests/command-list-yarn1/snap.txt @@ -0,0 +1,121 @@ +> vp install # should install packages first +yarn install v +warning package.json: No license field +info No lockfile found. +warning command-list-yarn1@: No license field +[1/4] Resolving packages... +[2/4] Fetching packages... +[3/4] Linking dependencies... +[4/4] Building fresh packages... +success Saved lockfile. +Done in ms. + + +> vp pm list # should list installed packages +yarn list v +warning package.json: No license field +warning command-list-yarn1@: No license field +├─ test-vite-plus-package@ +└─ testnpm2@ +Done in ms. + +> vp pm list testnpm2 # should list specific package +yarn list v +warning package.json: No license field +warning command-list-yarn1@: No license field +warning Filtering by arguments is deprecated. Please use the pattern option instead. +└─ testnpm2@ +Done in ms. + +> vp pm list --depth 0 # should list packages with depth limit +yarn list v +warning package.json: No license field +warning command-list-yarn1@: No license field +├─ test-vite-plus-package@ +└─ testnpm2@ +Done in ms. + +> vp pm list --json # should list packages in JSON format +{"type":"warning","data":"package.json: No license field"} +{"type":"warning","data":"command-list-yarn1@: No license field"} +{"type":"tree","data":{"type":"list","trees":[{"name":"testnpm2@","children":[],"hint":null,"color":"bold","depth":0},{"name":"test-vite-plus-package@","children":[],"hint":null,"color":"bold","depth":0}]}} + +> vp pm list --prod # should show warning that --prod not supported by yarn@1 +Warning: yarn@1 does not support --prod, ignoring --prod flag +yarn list v +warning package.json: No license field +warning command-list-yarn1@: No license field +├─ test-vite-plus-package@ +└─ testnpm2@ +Done in ms. + +> vp pm list --dev # should show warning that --dev not supported by yarn@1 +Warning: yarn@1 does not support --dev, ignoring --dev flag +yarn list v +warning package.json: No license field +warning command-list-yarn1@: No license field +├─ test-vite-plus-package@ +└─ testnpm2@ +Done in ms. + +> vp pm list --no-optional # should show warning that --no-optional not supported by yarn@1 +Warning: yarn@1 does not support --no-optional, ignoring --no-optional flag +yarn list v +warning package.json: No license field +warning command-list-yarn1@: No license field +├─ test-vite-plus-package@ +└─ testnpm2@ +Done in ms. + +> vp pm list --exclude-peers # should show warning that --exclude-peers not supported by yarn@1 +Warning: yarn@1 does not support --exclude-peers, ignoring flag +yarn list v +warning package.json: No license field +warning command-list-yarn1@: No license field +├─ test-vite-plus-package@ +└─ testnpm2@ +Done in ms. + +> vp pm list --only-projects # should show warning that --only-projects not supported by yarn@1 +Warning: yarn@1 does not support --only-projects, ignoring flag +yarn list v +warning package.json: No license field +warning command-list-yarn1@: No license field +├─ test-vite-plus-package@ +└─ testnpm2@ +Done in ms. + +> vp pm list --find-by customFinder # should show warning that --find-by not supported by yarn@1 +Warning: yarn@1 does not support --find-by, ignoring flag +yarn list v +warning package.json: No license field +warning command-list-yarn1@: No license field +├─ test-vite-plus-package@ +└─ testnpm2@ +Done in ms. + +> vp pm list --recursive # should show warning that --recursive not supported by yarn@1 +Warning: yarn@1 does not support --recursive, ignoring --recursive flag +yarn list v +warning package.json: No license field +warning command-list-yarn1@: No license field +├─ test-vite-plus-package@ +└─ testnpm2@ +Done in ms. + +> vp pm list --filter app # should show warning that --filter not supported by yarn@1 +Warning: yarn@1 does not support --filter, ignoring --filter flag +yarn list v +warning package.json: No license field +warning command-list-yarn1@: No license field +├─ test-vite-plus-package@ +└─ testnpm2@ +Done in ms. + +> vp pm list -- --loglevel=warn # should support pass through arguments +yarn list v +warning package.json: No license field +warning command-list-yarn1@: No license field +├─ test-vite-plus-package@ +└─ testnpm2@ +Done in ms. diff --git a/packages/global/snap-tests/command-list-yarn1/steps.json b/packages/global/snap-tests/command-list-yarn1/steps.json new file mode 100644 index 0000000000..3499fdef76 --- /dev/null +++ b/packages/global/snap-tests/command-list-yarn1/steps.json @@ -0,0 +1,21 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp install # should install packages first", + "vp pm list # should list installed packages", + "vp pm list testnpm2 # should list specific package", + "vp pm list --depth 0 # should list packages with depth limit", + "vp pm list --json # should list packages in JSON format", + "vp pm list --prod # should show warning that --prod not supported by yarn@1", + "vp pm list --dev # should show warning that --dev not supported by yarn@1", + "vp pm list --no-optional # should show warning that --no-optional not supported by yarn@1", + "vp pm list --exclude-peers # should show warning that --exclude-peers not supported by yarn@1", + "vp pm list --only-projects # should show warning that --only-projects not supported by yarn@1", + "vp pm list --find-by customFinder # should show warning that --find-by not supported by yarn@1", + "vp pm list --recursive # should show warning that --recursive not supported by yarn@1", + "vp pm list --filter app # should show warning that --filter not supported by yarn@1", + "vp pm list -- --loglevel=warn # should support pass through arguments" + ] +} diff --git a/packages/global/snap-tests/command-list-yarn4/package.json b/packages/global/snap-tests/command-list-yarn4/package.json new file mode 100644 index 0000000000..6f6bd50076 --- /dev/null +++ b/packages/global/snap-tests/command-list-yarn4/package.json @@ -0,0 +1,11 @@ +{ + "name": "command-list-yarn4", + "version": "1.0.0", + "packageManager": "yarn@4.10.3", + "dependencies": { + "testnpm2": "1.0.1" + }, + "devDependencies": { + "test-vite-plus-package": "1.0.0" + } +} diff --git a/packages/global/snap-tests/command-list-yarn4/snap.txt b/packages/global/snap-tests/command-list-yarn4/snap.txt new file mode 100644 index 0000000000..8626bc0a8a --- /dev/null +++ b/packages/global/snap-tests/command-list-yarn4/snap.txt @@ -0,0 +1,2 @@ +> vp pm list # should show warning that yarn@2+ does not support list command +Warning: yarn@2+ does not support 'list' command diff --git a/packages/global/snap-tests/command-list-yarn4/steps.json b/packages/global/snap-tests/command-list-yarn4/steps.json new file mode 100644 index 0000000000..194f226678 --- /dev/null +++ b/packages/global/snap-tests/command-list-yarn4/steps.json @@ -0,0 +1,8 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp pm list # should show warning that yarn@2+ does not support list command" + ] +} diff --git a/packages/global/snap-tests/command-owner-npm10/package.json b/packages/global/snap-tests/command-owner-npm10/package.json new file mode 100644 index 0000000000..b8babc8f5b --- /dev/null +++ b/packages/global/snap-tests/command-owner-npm10/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-owner-npm10", + "version": "1.0.0", + "packageManager": "npm@10.9.4" +} diff --git a/packages/global/snap-tests/command-owner-npm10/snap.txt b/packages/global/snap-tests/command-owner-npm10/snap.txt new file mode 100644 index 0000000000..4705598b5b --- /dev/null +++ b/packages/global/snap-tests/command-owner-npm10/snap.txt @@ -0,0 +1,2 @@ +> vp pm owner list testnpm2 # should list package owners +fengmk2 diff --git a/packages/global/snap-tests/command-owner-npm10/steps.json b/packages/global/snap-tests/command-owner-npm10/steps.json new file mode 100644 index 0000000000..8ebbcac5e6 --- /dev/null +++ b/packages/global/snap-tests/command-owner-npm10/steps.json @@ -0,0 +1,8 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp pm owner list testnpm2 # should list package owners" + ] +} diff --git a/packages/global/snap-tests/command-owner-pnpm10/package.json b/packages/global/snap-tests/command-owner-pnpm10/package.json new file mode 100644 index 0000000000..0378db825d --- /dev/null +++ b/packages/global/snap-tests/command-owner-pnpm10/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-owner-pnpm10", + "version": "1.0.0", + "packageManager": "pnpm@10.20.0" +} diff --git a/packages/global/snap-tests/command-owner-pnpm10/snap.txt b/packages/global/snap-tests/command-owner-pnpm10/snap.txt new file mode 100644 index 0000000000..a057f660cb --- /dev/null +++ b/packages/global/snap-tests/command-owner-pnpm10/snap.txt @@ -0,0 +1,16 @@ +> vp pm owner --help # should show help +Manage package owners + +Usage: vp pm owner + +Commands: + list List package owners + add Add package owner + rm Remove package owner + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help Print help + +> vp pm owner list testnpm2 # should list package owners (uses npm owner) +fengmk2 diff --git a/packages/global/snap-tests/command-owner-pnpm10/steps.json b/packages/global/snap-tests/command-owner-pnpm10/steps.json new file mode 100644 index 0000000000..e50961cdd6 --- /dev/null +++ b/packages/global/snap-tests/command-owner-pnpm10/steps.json @@ -0,0 +1,9 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp pm owner --help # should show help", + "vp pm owner list testnpm2 # should list package owners (uses npm owner)" + ] +} diff --git a/packages/global/snap-tests/command-owner-yarn1/package.json b/packages/global/snap-tests/command-owner-yarn1/package.json new file mode 100644 index 0000000000..a745d0f041 --- /dev/null +++ b/packages/global/snap-tests/command-owner-yarn1/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-owner-yarn1", + "version": "1.0.0", + "packageManager": "yarn@1.22.22" +} diff --git a/packages/global/snap-tests/command-owner-yarn1/snap.txt b/packages/global/snap-tests/command-owner-yarn1/snap.txt new file mode 100644 index 0000000000..f1f1719d7e --- /dev/null +++ b/packages/global/snap-tests/command-owner-yarn1/snap.txt @@ -0,0 +1,2 @@ +> vp pm owner list testnpm2 # should list package owners (uses npm owner) +fengmk2 diff --git a/packages/global/snap-tests/command-owner-yarn1/steps.json b/packages/global/snap-tests/command-owner-yarn1/steps.json new file mode 100644 index 0000000000..ce07cfded5 --- /dev/null +++ b/packages/global/snap-tests/command-owner-yarn1/steps.json @@ -0,0 +1,8 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp pm owner list testnpm2 # should list package owners (uses npm owner)" + ] +} diff --git a/packages/global/snap-tests/command-owner-yarn4/package.json b/packages/global/snap-tests/command-owner-yarn4/package.json new file mode 100644 index 0000000000..ee18dacd08 --- /dev/null +++ b/packages/global/snap-tests/command-owner-yarn4/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-owner-yarn4", + "version": "1.0.0", + "packageManager": "yarn@4.10.3" +} diff --git a/packages/global/snap-tests/command-owner-yarn4/snap.txt b/packages/global/snap-tests/command-owner-yarn4/snap.txt new file mode 100644 index 0000000000..f1f1719d7e --- /dev/null +++ b/packages/global/snap-tests/command-owner-yarn4/snap.txt @@ -0,0 +1,2 @@ +> vp pm owner list testnpm2 # should list package owners (uses npm owner) +fengmk2 diff --git a/packages/global/snap-tests/command-owner-yarn4/steps.json b/packages/global/snap-tests/command-owner-yarn4/steps.json new file mode 100644 index 0000000000..ce07cfded5 --- /dev/null +++ b/packages/global/snap-tests/command-owner-yarn4/steps.json @@ -0,0 +1,8 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp pm owner list testnpm2 # should list package owners (uses npm owner)" + ] +} diff --git a/packages/global/snap-tests/command-pack-npm10-with-workspace/package.json b/packages/global/snap-tests/command-pack-npm10-with-workspace/package.json new file mode 100644 index 0000000000..dbb16f69be --- /dev/null +++ b/packages/global/snap-tests/command-pack-npm10-with-workspace/package.json @@ -0,0 +1,8 @@ +{ + "name": "command-pack-npm10-with-workspace", + "version": "1.0.0", + "packageManager": "npm@10.9.4", + "workspaces": [ + "packages/*" + ] +} diff --git a/packages/global/snap-tests/command-pack-npm10-with-workspace/packages/app/package.json b/packages/global/snap-tests/command-pack-npm10-with-workspace/packages/app/package.json new file mode 100644 index 0000000000..33b9c5952a --- /dev/null +++ b/packages/global/snap-tests/command-pack-npm10-with-workspace/packages/app/package.json @@ -0,0 +1,4 @@ +{ + "name": "app", + "version": "1.0.0" +} diff --git a/packages/global/snap-tests/command-pack-npm10-with-workspace/packages/utils/package.json b/packages/global/snap-tests/command-pack-npm10-with-workspace/packages/utils/package.json new file mode 100644 index 0000000000..d063b255a7 --- /dev/null +++ b/packages/global/snap-tests/command-pack-npm10-with-workspace/packages/utils/package.json @@ -0,0 +1,5 @@ +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true +} diff --git a/packages/global/snap-tests/command-pack-npm10-with-workspace/snap.txt b/packages/global/snap-tests/command-pack-npm10-with-workspace/snap.txt new file mode 100644 index 0000000000..461f800722 --- /dev/null +++ b/packages/global/snap-tests/command-pack-npm10-with-workspace/snap.txt @@ -0,0 +1,217 @@ +> vp pm pack --json # should pack current workspace root +[ + { + "id": "command-pack-npm10-with-workspace@", + "name": "command-pack-npm10-with-workspace", + "version": "1.0.0", + "size": , + "unpackedSize": , + "shasum": "", + "integrity": "sha512-", + "filename": "command-pack-npm10-with-workspace-1.0.0.tgz", + "files": [ + { + "path": "output.log", + "size": , + "mode": 420 + }, + { + "path": "package.json", + "size": , + "mode": 420 + }, + { + "path": "packages/app/package.json", + "size": , + "mode": 420 + }, + { + "path": "packages/utils/package.json", + "size": , + "mode": 420 + }, + { + "path": "snap.txt", + "size": , + "mode": 420 + }, + { + "path": "steps.json", + "size": , + "mode": 420 + } + ], + "entryCount": 6, + "bundled": [] + } +] + +> vp pm pack --recursive --json && rm -rf *.tgz # should pack all packages in workspace (uses --workspaces) +[ + { + "id": "app@", + "name": "app", + "version": "1.0.0", + "size": , + "unpackedSize": , + "shasum": "", + "integrity": "sha512-", + "filename": "app-1.0.0.tgz", + "files": [ + { + "path": "package.json", + "size": , + "mode": 420 + } + ], + "entryCount": 1, + "bundled": [] + }, + { + "id": "@vite-plus-test/utils@", + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "size": , + "unpackedSize": , + "shasum": "", + "integrity": "sha512-", + "filename": "vite-plus-test-utils-1.0.0.tgz", + "files": [ + { + "path": "package.json", + "size": , + "mode": 420 + } + ], + "entryCount": 1, + "bundled": [] + } +] + +> vp pm pack --filter app --json && rm -rf *.tgz # should pack specific package (uses --workspace app) +[ + { + "id": "app@", + "name": "app", + "version": "1.0.0", + "size": , + "unpackedSize": , + "shasum": "", + "integrity": "sha512-", + "filename": "app-1.0.0.tgz", + "files": [ + { + "path": "package.json", + "size": , + "mode": 420 + } + ], + "entryCount": 1, + "bundled": [] + } +] + +> vp pm pack --filter app --filter @vite-plus-test/utils --json && rm -rf *.tgz # should pack multiple packages +[ + { + "id": "app@", + "name": "app", + "version": "1.0.0", + "size": , + "unpackedSize": , + "shasum": "", + "integrity": "sha512-", + "filename": "app-1.0.0.tgz", + "files": [ + { + "path": "package.json", + "size": , + "mode": 420 + } + ], + "entryCount": 1, + "bundled": [] + }, + { + "id": "@vite-plus-test/utils@", + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "size": , + "unpackedSize": , + "shasum": "", + "integrity": "sha512-", + "filename": "vite-plus-test-utils-1.0.0.tgz", + "files": [ + { + "path": "package.json", + "size": , + "mode": 420 + } + ], + "entryCount": 1, + "bundled": [] + } +] + +> vp pm pack --pack-destination ./dist --json && rm -rf ./dist # should pack with destination +[ + { + "id": "command-pack-npm10-with-workspace@", + "name": "command-pack-npm10-with-workspace", + "version": "1.0.0", + "size": , + "unpackedSize": , + "shasum": "", + "integrity": "sha512-", + "filename": "command-pack-npm10-with-workspace-1.0.0.tgz", + "files": [ + { + "path": "app-1.0.0.tgz", + "size": , + "mode": 420 + }, + { + "path": "command-pack-npm10-with-workspace-1.0.0.tgz", + "size": , + "mode": 420 + }, + { + "path": "output.log", + "size": , + "mode": 420 + }, + { + "path": "package.json", + "size": , + "mode": 420 + }, + { + "path": "packages/app/package.json", + "size": , + "mode": 420 + }, + { + "path": "packages/utils/package.json", + "size": , + "mode": 420 + }, + { + "path": "snap.txt", + "size": , + "mode": 420 + }, + { + "path": "steps.json", + "size": , + "mode": 420 + }, + { + "path": "vite-plus-test-utils-1.0.0.tgz", + "size": , + "mode": 420 + } + ], + "entryCount": 9, + "bundled": [] + } +] diff --git a/packages/global/snap-tests/command-pack-npm10-with-workspace/steps.json b/packages/global/snap-tests/command-pack-npm10-with-workspace/steps.json new file mode 100644 index 0000000000..5288aebc3a --- /dev/null +++ b/packages/global/snap-tests/command-pack-npm10-with-workspace/steps.json @@ -0,0 +1,13 @@ +{ + "ignoredPlatforms": ["win32"], + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp pm pack --json # should pack current workspace root", + "vp pm pack --recursive --json && rm -rf *.tgz # should pack all packages in workspace (uses --workspaces)", + "vp pm pack --filter app --json && rm -rf *.tgz # should pack specific package (uses --workspace app)", + "vp pm pack --filter app --filter @vite-plus-test/utils --json && rm -rf *.tgz # should pack multiple packages", + "vp pm pack --pack-destination ./dist --json && rm -rf ./dist # should pack with destination" + ] +} diff --git a/packages/global/snap-tests/command-pack-npm10/package.json b/packages/global/snap-tests/command-pack-npm10/package.json new file mode 100644 index 0000000000..93063d0891 --- /dev/null +++ b/packages/global/snap-tests/command-pack-npm10/package.json @@ -0,0 +1,11 @@ +{ + "name": "command-pack-npm10", + "version": "1.0.0", + "packageManager": "npm@10.9.4", + "dependencies": { + "testnpm2": "1.0.1" + }, + "devDependencies": { + "test-vite-plus-package": "1.0.0" + } +} diff --git a/packages/global/snap-tests/command-pack-npm10/snap.txt b/packages/global/snap-tests/command-pack-npm10/snap.txt new file mode 100644 index 0000000000..1835d19aae --- /dev/null +++ b/packages/global/snap-tests/command-pack-npm10/snap.txt @@ -0,0 +1,123 @@ +> vp pm pack --json && rm -rf *.tgz # should pack current package +[ + { + "id": "command-pack-npm10@", + "name": "command-pack-npm10", + "version": "1.0.0", + "size": , + "unpackedSize": , + "shasum": "", + "integrity": "sha512-", + "filename": "command-pack-npm10-1.0.0.tgz", + "files": [ + { + "path": "output.log", + "size": , + "mode": 420 + }, + { + "path": "package.json", + "size": , + "mode": 420 + }, + { + "path": "snap.txt", + "size": , + "mode": 420 + }, + { + "path": "steps.json", + "size": , + "mode": 420 + } + ], + "entryCount": 4, + "bundled": [] + } +] + +> vp pm pack --pack-destination ./dist --json && rm -rf ./dist # should pack with destination +[ + { + "id": "command-pack-npm10@", + "name": "command-pack-npm10", + "version": "1.0.0", + "size": , + "unpackedSize": , + "shasum": "", + "integrity": "sha512-", + "filename": "command-pack-npm10-1.0.0.tgz", + "files": [ + { + "path": "command-pack-npm10-1.0.0.tgz", + "size": , + "mode": 420 + }, + { + "path": "output.log", + "size": , + "mode": 420 + }, + { + "path": "package.json", + "size": , + "mode": 420 + }, + { + "path": "snap.txt", + "size": , + "mode": 420 + }, + { + "path": "steps.json", + "size": , + "mode": 420 + } + ], + "entryCount": 5, + "bundled": [] + } +] + +> vp pm pack --json -- --loglevel=warn && rm -rf *.tgz # should support pass through arguments +[ + { + "id": "command-pack-npm10@", + "name": "command-pack-npm10", + "version": "1.0.0", + "size": , + "unpackedSize": , + "shasum": "", + "integrity": "sha512-", + "filename": "command-pack-npm10-1.0.0.tgz", + "files": [ + { + "path": "command-pack-npm10-1.0.0.tgz", + "size": , + "mode": 420 + }, + { + "path": "output.log", + "size": , + "mode": 420 + }, + { + "path": "package.json", + "size": , + "mode": 420 + }, + { + "path": "snap.txt", + "size": , + "mode": 420 + }, + { + "path": "steps.json", + "size": , + "mode": 420 + } + ], + "entryCount": 5, + "bundled": [] + } +] diff --git a/packages/global/snap-tests/command-pack-npm10/steps.json b/packages/global/snap-tests/command-pack-npm10/steps.json new file mode 100644 index 0000000000..2b09f6b231 --- /dev/null +++ b/packages/global/snap-tests/command-pack-npm10/steps.json @@ -0,0 +1,11 @@ +{ + "ignoredPlatforms": ["win32"], + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp pm pack --json && rm -rf *.tgz # should pack current package", + "vp pm pack --pack-destination ./dist --json && rm -rf ./dist # should pack with destination", + "vp pm pack --json -- --loglevel=warn && rm -rf *.tgz # should support pass through arguments" + ] +} diff --git a/packages/global/snap-tests/command-pack-pnpm10-with-workspace/package.json b/packages/global/snap-tests/command-pack-pnpm10-with-workspace/package.json new file mode 100644 index 0000000000..86c2e4ba2b --- /dev/null +++ b/packages/global/snap-tests/command-pack-pnpm10-with-workspace/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-pack-pnpm10-with-workspace", + "version": "1.0.0", + "packageManager": "pnpm@10.20.0" +} diff --git a/packages/global/snap-tests/command-pack-pnpm10-with-workspace/packages/app/package.json b/packages/global/snap-tests/command-pack-pnpm10-with-workspace/packages/app/package.json new file mode 100644 index 0000000000..33b9c5952a --- /dev/null +++ b/packages/global/snap-tests/command-pack-pnpm10-with-workspace/packages/app/package.json @@ -0,0 +1,4 @@ +{ + "name": "app", + "version": "1.0.0" +} diff --git a/packages/global/snap-tests/command-pack-pnpm10-with-workspace/packages/utils/package.json b/packages/global/snap-tests/command-pack-pnpm10-with-workspace/packages/utils/package.json new file mode 100644 index 0000000000..d063b255a7 --- /dev/null +++ b/packages/global/snap-tests/command-pack-pnpm10-with-workspace/packages/utils/package.json @@ -0,0 +1,5 @@ +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true +} diff --git a/packages/global/snap-tests/command-pack-pnpm10-with-workspace/pnpm-workspace.yaml b/packages/global/snap-tests/command-pack-pnpm10-with-workspace/pnpm-workspace.yaml new file mode 100644 index 0000000000..924b55f42e --- /dev/null +++ b/packages/global/snap-tests/command-pack-pnpm10-with-workspace/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - packages/* diff --git a/packages/global/snap-tests/command-pack-pnpm10-with-workspace/snap.txt b/packages/global/snap-tests/command-pack-pnpm10-with-workspace/snap.txt new file mode 100644 index 0000000000..c7817703ac --- /dev/null +++ b/packages/global/snap-tests/command-pack-pnpm10-with-workspace/snap.txt @@ -0,0 +1,194 @@ +> vp pm pack && rm -rf *.tgz # should pack current workspace root +📦 command-pack-pnpm10-with-workspace@ +Tarball Contents +output.log +package.json +packages/app/package.json +packages/utils/package.json +pnpm-workspace.yaml +snap.txt +steps.json +Tarball Details +command-pack-pnpm10-with-workspace-1.0.0.tgz + +> vp pm pack --recursive --json > out.json && tool json-sort out.json '_.name' && cat out.json && rm -rf *.tgz # should pack all packages in workspace +[ + { + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "filename": "/vite-plus-test-utils-1.0.0.tgz", + "files": [ + { + "path": "package.json" + } + ] + }, + { + "name": "app", + "version": "1.0.0", + "filename": "/app-1.0.0.tgz", + "files": [ + { + "path": "package.json" + } + ] + }, + { + "name": "command-pack-pnpm10-with-workspace", + "version": "1.0.0", + "filename": "command-pack-pnpm10-with-workspace-1.0.0.tgz", + "files": [ + { + "path": "command-pack-pnpm10-with-workspace-1.0.0.tgz" + }, + { + "path": "out.json" + }, + { + "path": "output.log" + }, + { + "path": "package.json" + }, + { + "path": "packages/app/package.json" + }, + { + "path": "packages/utils/package.json" + }, + { + "path": "pnpm-workspace.yaml" + }, + { + "path": "snap.txt" + }, + { + "path": "steps.json" + } + ] + } +] + +> vp pm pack --filter app && rm -rf *.tgz # should pack specific package (uses --filter app pack) +📦 app@ +Tarball Contents +package.json +Tarball Details +/app-1.0.0.tgz + +> vp pm pack --filter app --filter @vite-plus-test/utils --json > out.json && tool json-sort out.json '_.name' && cat out.json && rm -rf *.tgz # should pack multiple packages +[ + { + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "filename": "/vite-plus-test-utils-1.0.0.tgz", + "files": [ + { + "path": "package.json" + } + ] + }, + { + "name": "app", + "version": "1.0.0", + "filename": "/app-1.0.0.tgz", + "files": [ + { + "path": "package.json" + } + ] + } +] + +> vp pm pack --out ./dist/package.tgz && rm -rf ./dist # should pack with output file +📦 command-pack-pnpm10-with-workspace@ +Tarball Contents +app-1.0.0.tgz +command-pack-pnpm10-with-workspace-1.0.0.tgz +out.json +output.log +package.json +packages/app/package.json +packages/utils/package.json +pnpm-workspace.yaml +snap.txt +steps.json +vite-plus-test-utils-1.0.0.tgz +Tarball Details +/dist/package.tgz + +> vp pm pack --pack-destination ./dist && rm -rf ./dist # should pack with destination +📦 command-pack-pnpm10-with-workspace@ +Tarball Contents +app-1.0.0.tgz +command-pack-pnpm10-with-workspace-1.0.0.tgz +out.json +output.log +package.json +packages/app/package.json +packages/utils/package.json +pnpm-workspace.yaml +snap.txt +steps.json +vite-plus-test-utils-1.0.0.tgz +Tarball Details +/dist/command-pack-pnpm10-with-workspace-1.0.0.tgz + +> vp pm pack --pack-gzip-level 9 && rm -rf *.tgz # should pack with gzip compression level +📦 command-pack-pnpm10-with-workspace@ +Tarball Contents +app-1.0.0.tgz +command-pack-pnpm10-with-workspace-1.0.0.tgz +out.json +output.log +package.json +packages/app/package.json +packages/utils/package.json +pnpm-workspace.yaml +snap.txt +steps.json +vite-plus-test-utils-1.0.0.tgz +Tarball Details +command-pack-pnpm10-with-workspace-1.0.0.tgz + +> vp pm pack --json --out 'foo-%s-%v.tgz' && rm -rf *.tgz # should pack with json output +{ + "name": "command-pack-pnpm10-with-workspace", + "version": "1.0.0", + "filename": "foo-command-pack-pnpm10-with-workspace-1.0.0.tgz", + "files": [ + { + "path": "app-1.0.0.tgz" + }, + { + "path": "command-pack-pnpm10-with-workspace-1.0.0.tgz" + }, + { + "path": "out.json" + }, + { + "path": "output.log" + }, + { + "path": "package.json" + }, + { + "path": "packages/app/package.json" + }, + { + "path": "packages/utils/package.json" + }, + { + "path": "pnpm-workspace.yaml" + }, + { + "path": "snap.txt" + }, + { + "path": "steps.json" + }, + { + "path": "vite-plus-test-utils-1.0.0.tgz" + } + ] +} diff --git a/packages/global/snap-tests/command-pack-pnpm10-with-workspace/steps.json b/packages/global/snap-tests/command-pack-pnpm10-with-workspace/steps.json new file mode 100644 index 0000000000..8c69ae20b7 --- /dev/null +++ b/packages/global/snap-tests/command-pack-pnpm10-with-workspace/steps.json @@ -0,0 +1,16 @@ +{ + "ignoredPlatforms": ["win32"], + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp pm pack && rm -rf *.tgz # should pack current workspace root", + "vp pm pack --recursive --json > out.json && tool json-sort out.json '_.name' && cat out.json && rm -rf *.tgz # should pack all packages in workspace", + "vp pm pack --filter app && rm -rf *.tgz # should pack specific package (uses --filter app pack)", + "vp pm pack --filter app --filter @vite-plus-test/utils --json > out.json && tool json-sort out.json '_.name' && cat out.json && rm -rf *.tgz # should pack multiple packages", + "vp pm pack --out ./dist/package.tgz && rm -rf ./dist # should pack with output file", + "vp pm pack --pack-destination ./dist && rm -rf ./dist # should pack with destination", + "vp pm pack --pack-gzip-level 9 && rm -rf *.tgz # should pack with gzip compression level", + "vp pm pack --json --out 'foo-%s-%v.tgz' && rm -rf *.tgz # should pack with json output" + ] +} diff --git a/packages/global/snap-tests/command-pack-pnpm10/package.json b/packages/global/snap-tests/command-pack-pnpm10/package.json new file mode 100644 index 0000000000..ee71f82ed5 --- /dev/null +++ b/packages/global/snap-tests/command-pack-pnpm10/package.json @@ -0,0 +1,11 @@ +{ + "name": "command-pack-pnpm10", + "version": "1.0.0", + "packageManager": "pnpm@10.20.0", + "dependencies": { + "testnpm2": "1.0.1" + }, + "devDependencies": { + "test-vite-plus-package": "1.0.0" + } +} diff --git a/packages/global/snap-tests/command-pack-pnpm10/snap.txt b/packages/global/snap-tests/command-pack-pnpm10/snap.txt new file mode 100644 index 0000000000..85e038a219 --- /dev/null +++ b/packages/global/snap-tests/command-pack-pnpm10/snap.txt @@ -0,0 +1,114 @@ +> vp pm pack --help # should show help +Create a tarball of the package + +Usage: vp pm pack [OPTIONS] [-- ...] + +Arguments: + [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager + +Options: + -r, --recursive + Pack all workspace packages + --filter + Filter packages to pack (can be used multiple times) + --out + Customizes the output path for the tarball. Use %s and %v to include the package name and version (pnpm and yarn@2+ only), e.g., %s.tgz or some-dir/%s-%v.tgz + --pack-destination + Directory where the tarball will be saved (pnpm and npm only) + --pack-gzip-level + Gzip compression level (0-9) + --json + Output in JSON format + -h, --help + Print help + +> vp pm pack && rm -rf *.tgz # should pack current package +📦 command-pack-pnpm10@ +Tarball Contents +output.log +package.json +snap.txt +steps.json +Tarball Details +command-pack-pnpm10-1.0.0.tgz + +> vp pm pack --out ./dist/package.tgz && rm -rf ./dist # should pack with output file +📦 command-pack-pnpm10@ +Tarball Contents +command-pack-pnpm10-1.0.0.tgz +output.log +package.json +snap.txt +steps.json +Tarball Details +/dist/package.tgz + +> vp pm pack --pack-destination ./dist && rm -rf ./dist # should pack with destination +📦 command-pack-pnpm10@ +Tarball Contents +command-pack-pnpm10-1.0.0.tgz +output.log +package.json +snap.txt +steps.json +Tarball Details +/dist/command-pack-pnpm10-1.0.0.tgz + +> vp pm pack --json --pack-gzip-level 9 && rm -rf *.tgz # should pack with gzip compression level +{ + "name": "command-pack-pnpm10", + "version": "1.0.0", + "filename": "command-pack-pnpm10-1.0.0.tgz", + "files": [ + { + "path": "command-pack-pnpm10-1.0.0.tgz" + }, + { + "path": "output.log" + }, + { + "path": "package.json" + }, + { + "path": "snap.txt" + }, + { + "path": "steps.json" + } + ] +} + +> vp pm pack --json && rm -rf *.tgz # should pack with json output +{ + "name": "command-pack-pnpm10", + "version": "1.0.0", + "filename": "command-pack-pnpm10-1.0.0.tgz", + "files": [ + { + "path": "command-pack-pnpm10-1.0.0.tgz" + }, + { + "path": "output.log" + }, + { + "path": "package.json" + }, + { + "path": "snap.txt" + }, + { + "path": "steps.json" + } + ] +} + +> vp pm pack -- --loglevel=warn && rm -rf *.tgz # should support pass through arguments +📦 command-pack-pnpm10@ +Tarball Contents +command-pack-pnpm10-1.0.0.tgz +output.log +package.json +snap.txt +steps.json +Tarball Details +command-pack-pnpm10-1.0.0.tgz diff --git a/packages/global/snap-tests/command-pack-pnpm10/steps.json b/packages/global/snap-tests/command-pack-pnpm10/steps.json new file mode 100644 index 0000000000..27ece87f4e --- /dev/null +++ b/packages/global/snap-tests/command-pack-pnpm10/steps.json @@ -0,0 +1,15 @@ +{ + "ignoredPlatforms": ["win32"], + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp pm pack --help # should show help", + "vp pm pack && rm -rf *.tgz # should pack current package", + "vp pm pack --out ./dist/package.tgz && rm -rf ./dist # should pack with output file", + "vp pm pack --pack-destination ./dist && rm -rf ./dist # should pack with destination", + "vp pm pack --json --pack-gzip-level 9 && rm -rf *.tgz # should pack with gzip compression level", + "vp pm pack --json && rm -rf *.tgz # should pack with json output", + "vp pm pack -- --loglevel=warn && rm -rf *.tgz # should support pass through arguments" + ] +} diff --git a/packages/global/snap-tests/command-pack-yarn4-with-workspace/package.json b/packages/global/snap-tests/command-pack-yarn4-with-workspace/package.json new file mode 100644 index 0000000000..e9b9d29b8f --- /dev/null +++ b/packages/global/snap-tests/command-pack-yarn4-with-workspace/package.json @@ -0,0 +1,8 @@ +{ + "name": "command-pack-yarn4-with-workspace", + "version": "1.0.0", + "packageManager": "yarn@4.10.3", + "workspaces": [ + "packages/*" + ] +} diff --git a/packages/global/snap-tests/command-pack-yarn4-with-workspace/packages/app/package.json b/packages/global/snap-tests/command-pack-yarn4-with-workspace/packages/app/package.json new file mode 100644 index 0000000000..33b9c5952a --- /dev/null +++ b/packages/global/snap-tests/command-pack-yarn4-with-workspace/packages/app/package.json @@ -0,0 +1,4 @@ +{ + "name": "app", + "version": "1.0.0" +} diff --git a/packages/global/snap-tests/command-pack-yarn4-with-workspace/packages/utils/package.json b/packages/global/snap-tests/command-pack-yarn4-with-workspace/packages/utils/package.json new file mode 100644 index 0000000000..d063b255a7 --- /dev/null +++ b/packages/global/snap-tests/command-pack-yarn4-with-workspace/packages/utils/package.json @@ -0,0 +1,5 @@ +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true +} diff --git a/packages/global/snap-tests/command-pack-yarn4-with-workspace/snap.txt b/packages/global/snap-tests/command-pack-yarn4-with-workspace/snap.txt new file mode 100644 index 0000000000..61d159c8a5 --- /dev/null +++ b/packages/global/snap-tests/command-pack-yarn4-with-workspace/snap.txt @@ -0,0 +1,66 @@ +> vp install --mode=update-lockfile # should install packages first +➤ YN0000: · Yarn +➤ YN0000: ┌ Resolution step +➤ YN0000: └ Completed +➤ YN0000: ┌ Fetch step +➤ YN0000: └ Completed +➤ YN0000: ┌ Link step +➤ YN0073: │ Skipped due to mode=update-lockfile +➤ YN0000: └ Completed +➤ YN0000: · Done with warnings in ms ms + + +> vp pm pack # should pack current workspace root +➤ YN0000: output.log +➤ YN0000: package.json +➤ YN0000: snap.txt +➤ YN0000: steps.json +➤ YN0000: Package archive generated in /package.tgz +➤ YN0000: Done in ms ms + +> vp pm pack --recursive # should pack all packages in workspace (uses workspaces foreach --all pack) +➤ YN0000: output.log +➤ YN0000: package.json +➤ YN0000: snap.txt +➤ YN0000: steps.json +➤ YN0000: Package archive generated in /package.tgz +➤ YN0000: Done in ms ms +➤ YN0000: package.json +➤ YN0000: Package archive generated in /packages/app/package.tgz +➤ YN0000: Done in ms ms +➤ YN0000: package.json +➤ YN0000: Package archive generated in /packages/utils/package.tgz +➤ YN0000: Done in ms ms +Done in ms ms + +> vp pm pack --filter app # should pack specific package (uses workspaces foreach --all --include app pack) +➤ YN0000: package.json +➤ YN0000: Package archive generated in /packages/app/package.tgz +➤ YN0000: Done in ms ms +Done in ms ms + +> vp pm pack --filter app --filter @vite-plus-test/utils # should pack multiple packages +➤ YN0000: package.json +➤ YN0000: Package archive generated in /packages/app/package.tgz +➤ YN0000: Done in ms ms +➤ YN0000: package.json +➤ YN0000: Package archive generated in /packages/utils/package.tgz +➤ YN0000: Done in ms ms +Done in ms ms + +> vp pm pack --out ./dist/package.tgz # should pack with output file +➤ YN0000: output.log +➤ YN0000: package.json +➤ YN0000: snap.txt +➤ YN0000: steps.json +➤ YN0000: Package archive generated in /dist/package.tgz +➤ YN0000: Done in ms ms + +> vp pm pack --json # should pack with json output +{"base":""} +{"location":"dist/package.tgz"} +{"location":"output.log"} +{"location":"package.json"} +{"location":"snap.txt"} +{"location":"steps.json"} +{"output":"/package.tgz"} diff --git a/packages/global/snap-tests/command-pack-yarn4-with-workspace/steps.json b/packages/global/snap-tests/command-pack-yarn4-with-workspace/steps.json new file mode 100644 index 0000000000..ac56a62b5c --- /dev/null +++ b/packages/global/snap-tests/command-pack-yarn4-with-workspace/steps.json @@ -0,0 +1,15 @@ +{ + "ignoredPlatforms": ["win32"], + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp install --mode=update-lockfile # should install packages first", + "vp pm pack # should pack current workspace root", + "vp pm pack --recursive # should pack all packages in workspace (uses workspaces foreach --all pack)", + "vp pm pack --filter app # should pack specific package (uses workspaces foreach --all --include app pack)", + "vp pm pack --filter app --filter @vite-plus-test/utils # should pack multiple packages", + "vp pm pack --out ./dist/package.tgz # should pack with output file", + "vp pm pack --json # should pack with json output" + ] +} diff --git a/packages/global/snap-tests/command-pack-yarn4/package.json b/packages/global/snap-tests/command-pack-yarn4/package.json new file mode 100644 index 0000000000..ce511d3c72 --- /dev/null +++ b/packages/global/snap-tests/command-pack-yarn4/package.json @@ -0,0 +1,11 @@ +{ + "name": "command-pack-yarn4", + "version": "1.0.0", + "packageManager": "yarn@4.10.3", + "dependencies": { + "testnpm2": "1.0.1" + }, + "devDependencies": { + "test-vite-plus-package": "1.0.0" + } +} diff --git a/packages/global/snap-tests/command-pack-yarn4/snap.txt b/packages/global/snap-tests/command-pack-yarn4/snap.txt new file mode 100644 index 0000000000..ba931513ab --- /dev/null +++ b/packages/global/snap-tests/command-pack-yarn4/snap.txt @@ -0,0 +1,32 @@ +> vp pm pack # should pack current package +➤ YN0000: output.log +➤ YN0000: package.json +➤ YN0000: snap.txt +➤ YN0000: steps.json +➤ YN0000: Package archive generated in /package.tgz +➤ YN0000: Done in ms ms + +> vp pm pack --out ./dist/package.tgz # should pack with output file +➤ YN0000: output.log +➤ YN0000: package.json +➤ YN0000: snap.txt +➤ YN0000: steps.json +➤ YN0000: Package archive generated in /dist/package.tgz +➤ YN0000: Done in ms ms + +> vp pm pack --json # should pack with json output +{"base":""} +{"location":"dist/package.tgz"} +{"location":"output.log"} +{"location":"package.json"} +{"location":"snap.txt"} +{"location":"steps.json"} +{"output":"/package.tgz"} + +> vp pm pack -- --dry-run # should support pass through arguments +➤ YN0000: dist/package.tgz +➤ YN0000: output.log +➤ YN0000: package.json +➤ YN0000: snap.txt +➤ YN0000: steps.json +➤ YN0000: Done in ms ms diff --git a/packages/global/snap-tests/command-pack-yarn4/steps.json b/packages/global/snap-tests/command-pack-yarn4/steps.json new file mode 100644 index 0000000000..20de1ec936 --- /dev/null +++ b/packages/global/snap-tests/command-pack-yarn4/steps.json @@ -0,0 +1,12 @@ +{ + "ignoredPlatforms": ["win32"], + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp pm pack # should pack current package", + "vp pm pack --out ./dist/package.tgz # should pack with output file", + "vp pm pack --json # should pack with json output", + "vp pm pack -- --dry-run # should support pass through arguments" + ] +} diff --git a/packages/global/snap-tests/command-prune-npm10/package.json b/packages/global/snap-tests/command-prune-npm10/package.json new file mode 100644 index 0000000000..2f54927e9c --- /dev/null +++ b/packages/global/snap-tests/command-prune-npm10/package.json @@ -0,0 +1,14 @@ +{ + "name": "command-prune-npm10", + "version": "1.0.0", + "packageManager": "npm@10.9.4", + "dependencies": { + "testnpm2": "1.0.1" + }, + "devDependencies": { + "test-vite-plus-package": "1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "1.0.0" + } +} diff --git a/packages/global/snap-tests/command-prune-npm10/snap.txt b/packages/global/snap-tests/command-prune-npm10/snap.txt new file mode 100644 index 0000000000..14cfebfa9a --- /dev/null +++ b/packages/global/snap-tests/command-prune-npm10/snap.txt @@ -0,0 +1,37 @@ +> vp install # should install packages first + +added 3 packages in ms + + +> vp pm prune --help # should show help +Remove unnecessary packages + +Usage: vp pm prune [OPTIONS] [-- ...] + +Arguments: + [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager + +Options: + --prod Remove devDependencies + --no-optional Remove optional dependencies + -h, --help Print help + +> vp pm prune # should prune extraneous dependencies + +up to date in ms + +> vp pm prune --prod # should prune dev dependencies (uses --omit=dev) + +up to date in ms + +> vp pm prune --no-optional # should prune optional dependencies (uses --omit=optional) + +added 1 package in ms + +> vp pm prune --prod --no-optional # should prune both dev and optional dependencies + +up to date in ms + +> vp pm prune -- --loglevel=warn # should support pass through arguments + +added 2 packages in ms diff --git a/packages/global/snap-tests/command-prune-npm10/steps.json b/packages/global/snap-tests/command-prune-npm10/steps.json new file mode 100644 index 0000000000..360dd84fa5 --- /dev/null +++ b/packages/global/snap-tests/command-prune-npm10/steps.json @@ -0,0 +1,14 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp install # should install packages first", + "vp pm prune --help # should show help", + "vp pm prune # should prune extraneous dependencies", + "vp pm prune --prod # should prune dev dependencies (uses --omit=dev)", + "vp pm prune --no-optional # should prune optional dependencies (uses --omit=optional)", + "vp pm prune --prod --no-optional # should prune both dev and optional dependencies", + "vp pm prune -- --loglevel=warn # should support pass through arguments" + ] +} diff --git a/packages/global/snap-tests/command-prune-pnpm10/package.json b/packages/global/snap-tests/command-prune-pnpm10/package.json new file mode 100644 index 0000000000..a08e6f2a6a --- /dev/null +++ b/packages/global/snap-tests/command-prune-pnpm10/package.json @@ -0,0 +1,14 @@ +{ + "name": "command-prune-pnpm10", + "version": "1.0.0", + "packageManager": "pnpm@10.20.0", + "dependencies": { + "testnpm2": "1.0.1" + }, + "devDependencies": { + "test-vite-plus-package": "1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "1.0.0" + } +} diff --git a/packages/global/snap-tests/command-prune-pnpm10/snap.txt b/packages/global/snap-tests/command-prune-pnpm10/snap.txt new file mode 100644 index 0000000000..86f1388b16 --- /dev/null +++ b/packages/global/snap-tests/command-prune-pnpm10/snap.txt @@ -0,0 +1,139 @@ +> vp install # should install packages first +Packages: + ++ +Progress: resolved , reused , downloaded , added , done + +dependencies: ++ testnpm2 + +optionalDependencies: ++ test-vite-plus-package-optional + +devDependencies: ++ test-vite-plus-package + +Done in ms using pnpm v + + +> vp pm prune --help # should show help +Remove unnecessary packages + +Usage: vp pm prune [OPTIONS] [-- ...] + +Arguments: + [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager + +Options: + --prod Remove devDependencies + --no-optional Remove optional dependencies + -h, --help Print help + +> vp pm prune && cat package.json # should prune extraneous dependencies +Lockfile is up to date, resolution step is skipped +Already up to date + +{ + "name": "command-prune-pnpm10", + "version": "1.0.0", + "packageManager": "pnpm@", + "dependencies": { + "testnpm2": "1.0.1" + }, + "devDependencies": { + "test-vite-plus-package": "1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "1.0.0" + } +} + +> vp pm prune --prod && cat package.json # should prune dev dependencies +Lockfile is up to date, resolution step is skipped +Packages: -1 +- + +devDependencies: +- test-vite-plus-package + +{ + "name": "command-prune-pnpm10", + "version": "1.0.0", + "packageManager": "pnpm@", + "dependencies": { + "testnpm2": "1.0.1" + }, + "devDependencies": { + "test-vite-plus-package": "1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "1.0.0" + } +} + +> vp pm prune --no-optional && cat package.json # should prune optional dependencies +Lockfile is up to date, resolution step is skipped +Packages: -1 +- +Progress: resolved , reused , downloaded , added , done + +optionalDependencies: +- test-vite-plus-package-optional + +devDependencies: ++ test-vite-plus-package + +{ + "name": "command-prune-pnpm10", + "version": "1.0.0", + "packageManager": "pnpm@", + "dependencies": { + "testnpm2": "1.0.1" + }, + "devDependencies": { + "test-vite-plus-package": "1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "1.0.0" + } +} + +> vp pm prune --prod --no-optional && cat package.json # should prune both dev and optional dependencies +Lockfile is up to date, resolution step is skipped +Packages: -1 +- + +optionalDependencies: skipped + +devDependencies: +- test-vite-plus-package + +{ + "name": "command-prune-pnpm10", + "version": "1.0.0", + "packageManager": "pnpm@", + "dependencies": { + "testnpm2": "1.0.1" + }, + "devDependencies": { + "test-vite-plus-package": "1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "1.0.0" + } +} + +> vp pm prune -- --loglevel=warn && cat package.json # should support pass through arguments +{ + "name": "command-prune-pnpm10", + "version": "1.0.0", + "packageManager": "pnpm@", + "dependencies": { + "testnpm2": "1.0.1" + }, + "devDependencies": { + "test-vite-plus-package": "1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "1.0.0" + } +} diff --git a/packages/global/snap-tests/command-prune-pnpm10/steps.json b/packages/global/snap-tests/command-prune-pnpm10/steps.json new file mode 100644 index 0000000000..3e624e2aa5 --- /dev/null +++ b/packages/global/snap-tests/command-prune-pnpm10/steps.json @@ -0,0 +1,14 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp install # should install packages first", + "vp pm prune --help # should show help", + "vp pm prune && cat package.json # should prune extraneous dependencies", + "vp pm prune --prod && cat package.json # should prune dev dependencies", + "vp pm prune --no-optional && cat package.json # should prune optional dependencies", + "vp pm prune --prod --no-optional && cat package.json # should prune both dev and optional dependencies", + "vp pm prune -- --loglevel=warn && cat package.json # should support pass through arguments" + ] +} diff --git a/packages/global/snap-tests/command-prune-yarn4/package.json b/packages/global/snap-tests/command-prune-yarn4/package.json new file mode 100644 index 0000000000..e88d817272 --- /dev/null +++ b/packages/global/snap-tests/command-prune-yarn4/package.json @@ -0,0 +1,14 @@ +{ + "name": "command-prune-yarn4", + "version": "1.0.0", + "packageManager": "yarn@4.10.3", + "dependencies": { + "testnpm2": "1.0.1" + }, + "devDependencies": { + "test-vite-plus-package": "1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "1.0.0" + } +} diff --git a/packages/global/snap-tests/command-prune-yarn4/snap.txt b/packages/global/snap-tests/command-prune-yarn4/snap.txt new file mode 100644 index 0000000000..a9abb89da6 --- /dev/null +++ b/packages/global/snap-tests/command-prune-yarn4/snap.txt @@ -0,0 +1,15 @@ +> vp pm prune --help # should show help +Remove unnecessary packages + +Usage: vp pm prune [OPTIONS] [-- ...] + +Arguments: + [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager + +Options: + --prod Remove devDependencies + --no-optional Remove optional dependencies + -h, --help Print help + +> vp pm prune # should show warning that yarn does not support prune command +Warning: yarn does not have 'prune' command. yarn install will prune extraneous packages automatically. diff --git a/packages/global/snap-tests/command-prune-yarn4/steps.json b/packages/global/snap-tests/command-prune-yarn4/steps.json new file mode 100644 index 0000000000..244a4c4505 --- /dev/null +++ b/packages/global/snap-tests/command-prune-yarn4/steps.json @@ -0,0 +1,9 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp pm prune --help # should show help", + "vp pm prune # should show warning that yarn does not support prune command" + ] +} diff --git a/packages/global/snap-tests/command-publish-npm10/package.json b/packages/global/snap-tests/command-publish-npm10/package.json new file mode 100644 index 0000000000..fe646356eb --- /dev/null +++ b/packages/global/snap-tests/command-publish-npm10/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-publish-npm10", + "version": "1.0.0", + "packageManager": "npm@10.9.4" +} diff --git a/packages/global/snap-tests/command-publish-npm10/snap.txt b/packages/global/snap-tests/command-publish-npm10/snap.txt new file mode 100644 index 0000000000..6c0d0ca7ec --- /dev/null +++ b/packages/global/snap-tests/command-publish-npm10/snap.txt @@ -0,0 +1,2 @@ +> vp pm publish --dry-run -- --loglevel error # should preview publish without actually publishing (uses npm publish --dry-run) ++ command-publish-npm10@ diff --git a/packages/global/snap-tests/command-publish-npm10/steps.json b/packages/global/snap-tests/command-publish-npm10/steps.json new file mode 100644 index 0000000000..748e0719e2 --- /dev/null +++ b/packages/global/snap-tests/command-publish-npm10/steps.json @@ -0,0 +1,8 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp pm publish --dry-run -- --loglevel error # should preview publish without actually publishing (uses npm publish --dry-run)" + ] +} diff --git a/packages/global/snap-tests/command-publish-pnpm10/package.json b/packages/global/snap-tests/command-publish-pnpm10/package.json new file mode 100644 index 0000000000..f5a5683819 --- /dev/null +++ b/packages/global/snap-tests/command-publish-pnpm10/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-publish-pnpm10", + "version": "1.0.0", + "packageManager": "pnpm@10.20.0" +} diff --git a/packages/global/snap-tests/command-publish-pnpm10/snap.txt b/packages/global/snap-tests/command-publish-pnpm10/snap.txt new file mode 100644 index 0000000000..b0248f326c --- /dev/null +++ b/packages/global/snap-tests/command-publish-pnpm10/snap.txt @@ -0,0 +1,25 @@ +> vp pm publish --help # should show help +Publish package to registry + +Usage: vp pm publish [OPTIONS] [TARBALL|FOLDER] [-- ...] + +Arguments: + [TARBALL|FOLDER] Tarball or folder to publish + [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager + +Options: + --dry-run Preview without publishing + --tag Publish tag (default: latest) + --access Access level (public/restricted) + --otp One-time password for authentication + --no-git-checks Skip git checks (pnpm-specific) + --publish-branch Set the branch name to publish from (pnpm-specific) + --report-summary Save publish summary to pnpm-publish-summary.json (pnpm-specific) + --force Force publish + --json Output in JSON format (pnpm-specific) + -r, --recursive Publish all workspace packages + --filter Filter packages in monorepo (can be used multiple times) + -h, --help Print help + +> vp pm publish --dry-run -- --loglevel error # should preview publish without actually publishing (uses pnpm publish --dry-run) ++ command-publish-pnpm10@ diff --git a/packages/global/snap-tests/command-publish-pnpm10/steps.json b/packages/global/snap-tests/command-publish-pnpm10/steps.json new file mode 100644 index 0000000000..d1d096114c --- /dev/null +++ b/packages/global/snap-tests/command-publish-pnpm10/steps.json @@ -0,0 +1,9 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp pm publish --help # should show help", + "vp pm publish --dry-run -- --loglevel error # should preview publish without actually publishing (uses pnpm publish --dry-run)" + ] +} diff --git a/packages/global/snap-tests/command-publish-yarn1/package.json b/packages/global/snap-tests/command-publish-yarn1/package.json new file mode 100644 index 0000000000..57f07bef32 --- /dev/null +++ b/packages/global/snap-tests/command-publish-yarn1/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-publish-yarn1", + "version": "1.0.0", + "packageManager": "yarn@1.22.22" +} diff --git a/packages/global/snap-tests/command-publish-yarn1/snap.txt b/packages/global/snap-tests/command-publish-yarn1/snap.txt new file mode 100644 index 0000000000..c0bbc9a3b4 --- /dev/null +++ b/packages/global/snap-tests/command-publish-yarn1/snap.txt @@ -0,0 +1,2 @@ +> vp pm publish --dry-run -- --loglevel error # should preview publish without actually publishing (uses npm publish --dry-run) ++ command-publish-yarn1@ diff --git a/packages/global/snap-tests/command-publish-yarn1/steps.json b/packages/global/snap-tests/command-publish-yarn1/steps.json new file mode 100644 index 0000000000..748e0719e2 --- /dev/null +++ b/packages/global/snap-tests/command-publish-yarn1/steps.json @@ -0,0 +1,8 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp pm publish --dry-run -- --loglevel error # should preview publish without actually publishing (uses npm publish --dry-run)" + ] +} diff --git a/packages/global/snap-tests/command-publish-yarn4/package.json b/packages/global/snap-tests/command-publish-yarn4/package.json new file mode 100644 index 0000000000..556f0342cb --- /dev/null +++ b/packages/global/snap-tests/command-publish-yarn4/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-publish-yarn4", + "version": "1.0.0", + "packageManager": "yarn@4.10.3" +} diff --git a/packages/global/snap-tests/command-publish-yarn4/snap.txt b/packages/global/snap-tests/command-publish-yarn4/snap.txt new file mode 100644 index 0000000000..d5143b5500 --- /dev/null +++ b/packages/global/snap-tests/command-publish-yarn4/snap.txt @@ -0,0 +1,2 @@ +> vp pm publish --dry-run -- --loglevel error # should preview publish without actually publishing (uses npm publish --dry-run) ++ command-publish-yarn4@ diff --git a/packages/global/snap-tests/command-publish-yarn4/steps.json b/packages/global/snap-tests/command-publish-yarn4/steps.json new file mode 100644 index 0000000000..748e0719e2 --- /dev/null +++ b/packages/global/snap-tests/command-publish-yarn4/steps.json @@ -0,0 +1,8 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp pm publish --dry-run -- --loglevel error # should preview publish without actually publishing (uses npm publish --dry-run)" + ] +} diff --git a/packages/global/snap-tests/command-view-npm10/package.json b/packages/global/snap-tests/command-view-npm10/package.json new file mode 100644 index 0000000000..91240e9be3 --- /dev/null +++ b/packages/global/snap-tests/command-view-npm10/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-view-npm10", + "version": "1.0.0", + "packageManager": "npm@10.9.4" +} diff --git a/packages/global/snap-tests/command-view-npm10/snap.txt b/packages/global/snap-tests/command-view-npm10/snap.txt new file mode 100644 index 0000000000..ce0ee661f1 --- /dev/null +++ b/packages/global/snap-tests/command-view-npm10/snap.txt @@ -0,0 +1,8 @@ +> vp pm view testnpm2 dist.tarball # should view testnpm2 package information +https://registry./testnpm2/-/testnpm2-1.0.1.tgz + +> vp pm info testnpm2 dist.tarball # should info alias to view +https://registry./testnpm2/-/testnpm2-1.0.1.tgz + +> vp pm show testnpm2 dist.tarball # should show alias to view +https://registry./testnpm2/-/testnpm2-1.0.1.tgz diff --git a/packages/global/snap-tests/command-view-npm10/steps.json b/packages/global/snap-tests/command-view-npm10/steps.json new file mode 100644 index 0000000000..4562d0e988 --- /dev/null +++ b/packages/global/snap-tests/command-view-npm10/steps.json @@ -0,0 +1,10 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp pm view testnpm2 dist.tarball # should view testnpm2 package information", + "vp pm info testnpm2 dist.tarball # should info alias to view", + "vp pm show testnpm2 dist.tarball # should show alias to view" + ] +} diff --git a/packages/global/snap-tests/command-view-pnpm10/package.json b/packages/global/snap-tests/command-view-pnpm10/package.json new file mode 100644 index 0000000000..19f92148ee --- /dev/null +++ b/packages/global/snap-tests/command-view-pnpm10/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-view-pnpm10", + "version": "1.0.0", + "packageManager": "pnpm@10.20.0" +} diff --git a/packages/global/snap-tests/command-view-pnpm10/snap.txt b/packages/global/snap-tests/command-view-pnpm10/snap.txt new file mode 100644 index 0000000000..45ddbeb9da --- /dev/null +++ b/packages/global/snap-tests/command-view-pnpm10/snap.txt @@ -0,0 +1,80 @@ +> vp pm view --help # should show help +View package information from registry + +Usage: vp pm view [OPTIONS] [FIELD] [-- ...] + +Arguments: + Package name with optional version + [FIELD] Specific field to view + [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager + +Options: + --json Output in JSON format + -h, --help Print help + +> vp pm view testnpm2 # should view lodash package information (uses npm view) + +testnpm2@ | ISC | deps: none | versions: 2 + +dist +.tarball: https://registry./testnpm2/-/testnpm2-1.0.1.tgz +.shasum: +.integrity: sha512- + +maintainers: +- fengmk2 + +dist-tags: +latest: +release-1: + +published over a year ago by fengmk2 + +> vp pm view testnpm2 version # should view lodash version field (uses npm view) +1.0.1 + +> vp pm view testnpm2@1.0.0 # should view specific version of lodash (uses npm view) + +testnpm2@ | ISC | deps: none | versions: 2 + +dist +.tarball: https://registry./testnpm2/-/testnpm2-1.0.0.tgz +.shasum: +.integrity: sha512- + +maintainers: +- fengmk2 + +dist-tags: +latest: +release-1: + +published over a year ago by fengmk2 + +> vp pm view testnpm2 dist.tarball # should view nested field (uses npm view) +https://registry./testnpm2/-/testnpm2-1.0.1.tgz + +> vp pm view testnpm2 dependencies # should view dependencies object (uses npm view) +> vp pm view testnpm2 dist.tarball --json # should view package.dist.tarball info in JSON format (uses npm view) +"https://registry./testnpm2/-/testnpm2-1.0.1.tgz" + +> vp pm view testnpm2 version --json # should view field in JSON format (uses npm view) +"1.0.1" + +> vp pm view testnpm2 -- --loglevel=warn # should support pass through arguments (uses npm view) + +testnpm2@ | ISC | deps: none | versions: 2 + +dist +.tarball: https://registry./testnpm2/-/testnpm2-1.0.1.tgz +.shasum: +.integrity: sha512- + +maintainers: +- fengmk2 + +dist-tags: +latest: +release-1: + +published over a year ago by fengmk2 diff --git a/packages/global/snap-tests/command-view-pnpm10/steps.json b/packages/global/snap-tests/command-view-pnpm10/steps.json new file mode 100644 index 0000000000..1998548d94 --- /dev/null +++ b/packages/global/snap-tests/command-view-pnpm10/steps.json @@ -0,0 +1,16 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp pm view --help # should show help", + "vp pm view testnpm2 # should view lodash package information (uses npm view)", + "vp pm view testnpm2 version # should view lodash version field (uses npm view)", + "vp pm view testnpm2@1.0.0 # should view specific version of lodash (uses npm view)", + "vp pm view testnpm2 dist.tarball # should view nested field (uses npm view)", + "vp pm view testnpm2 dependencies # should view dependencies object (uses npm view)", + "vp pm view testnpm2 dist.tarball --json # should view package.dist.tarball info in JSON format (uses npm view)", + "vp pm view testnpm2 version --json # should view field in JSON format (uses npm view)", + "vp pm view testnpm2 -- --loglevel=warn # should support pass through arguments (uses npm view)" + ] +} diff --git a/packages/global/snap-tests/command-view-yarn1/package.json b/packages/global/snap-tests/command-view-yarn1/package.json new file mode 100644 index 0000000000..4eca2edb72 --- /dev/null +++ b/packages/global/snap-tests/command-view-yarn1/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-view-yarn1", + "version": "1.0.0", + "packageManager": "yarn@1.22.22" +} diff --git a/packages/global/snap-tests/command-view-yarn1/snap.txt b/packages/global/snap-tests/command-view-yarn1/snap.txt new file mode 100644 index 0000000000..5896cadbfd --- /dev/null +++ b/packages/global/snap-tests/command-view-yarn1/snap.txt @@ -0,0 +1,20 @@ +> vp pm view testnpm2 # should view testnpm2 package information (uses npm view) + +testnpm2@ | ISC | deps: none | versions: 2 + +dist +.tarball: https://registry./testnpm2/-/testnpm2-1.0.1.tgz +.shasum: +.integrity: sha512- + +maintainers: +- fengmk2 + +dist-tags: +latest: +release-1: + +published over a year ago by fengmk2 + +> vp pm view testnpm2 version # should view testnpm2 version field (uses npm view) +1.0.1 diff --git a/packages/global/snap-tests/command-view-yarn1/steps.json b/packages/global/snap-tests/command-view-yarn1/steps.json new file mode 100644 index 0000000000..a554e4a81d --- /dev/null +++ b/packages/global/snap-tests/command-view-yarn1/steps.json @@ -0,0 +1,9 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp pm view testnpm2 # should view testnpm2 package information (uses npm view)", + "vp pm view testnpm2 version # should view testnpm2 version field (uses npm view)" + ] +} diff --git a/packages/global/snap-tests/command-view-yarn4/package.json b/packages/global/snap-tests/command-view-yarn4/package.json new file mode 100644 index 0000000000..79d915b5d9 --- /dev/null +++ b/packages/global/snap-tests/command-view-yarn4/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-view-yarn4", + "version": "1.0.0", + "packageManager": "yarn@4.10.3" +} diff --git a/packages/global/snap-tests/command-view-yarn4/snap.txt b/packages/global/snap-tests/command-view-yarn4/snap.txt new file mode 100644 index 0000000000..5896cadbfd --- /dev/null +++ b/packages/global/snap-tests/command-view-yarn4/snap.txt @@ -0,0 +1,20 @@ +> vp pm view testnpm2 # should view testnpm2 package information (uses npm view) + +testnpm2@ | ISC | deps: none | versions: 2 + +dist +.tarball: https://registry./testnpm2/-/testnpm2-1.0.1.tgz +.shasum: +.integrity: sha512- + +maintainers: +- fengmk2 + +dist-tags: +latest: +release-1: + +published over a year ago by fengmk2 + +> vp pm view testnpm2 version # should view testnpm2 version field (uses npm view) +1.0.1 diff --git a/packages/global/snap-tests/command-view-yarn4/steps.json b/packages/global/snap-tests/command-view-yarn4/steps.json new file mode 100644 index 0000000000..a554e4a81d --- /dev/null +++ b/packages/global/snap-tests/command-view-yarn4/steps.json @@ -0,0 +1,9 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp pm view testnpm2 # should view testnpm2 package information (uses npm view)", + "vp pm view testnpm2 version # should view testnpm2 version field (uses npm view)" + ] +} diff --git a/packages/tools/src/__tests__/__snapshots__/utils.spec.ts.snap b/packages/tools/src/__tests__/__snapshots__/utils.spec.ts.snap index 18dee841cf..b639c964d4 100644 --- a/packages/tools/src/__tests__/__snapshots__/utils.spec.ts.snap +++ b/packages/tools/src/__tests__/__snapshots__/utils.spec.ts.snap @@ -5,6 +5,13 @@ exports[`replaceUnstableOutput() > replace date 1`] = ` " `; +exports[`replaceUnstableOutput() > replace hash values 1`] = ` +"npm notice shasum: +npm notice integrity: sha512- +"shasum": "", +"integrity": "sha512-"," +`; + exports[`replaceUnstableOutput() > replace ignore npm audited packages log 1`] = ` "removed 1 package in ms up to date in ms @@ -26,6 +33,11 @@ exports[`replaceUnstableOutput() > replace ignore pnpm request warning log 1`] = Packages:" `; +exports[`replaceUnstableOutput() > replace ignore tarball download average speed warning log 1`] = ` +"WARN  Tarball download average speed 29 KiB/s (size 56 KiB) is below 50 KiB/s: https://registry./qs/-/qs-6.14.0.tgz (GET) + WARN  Tarball download average speed 34 KiB/s (size 347 KiB) is below 50 KiB/s: https://registry./undici/-/undici-7.16.0.tgz (GET)" +`; + exports[`replaceUnstableOutput() > replace pnpm registry request error warning log 1`] = `"Progress: resolved"`; exports[`replaceUnstableOutput() > replace tsdown output 1`] = ` diff --git a/packages/tools/src/__tests__/utils.spec.ts b/packages/tools/src/__tests__/utils.spec.ts index ffe182dc1a..ec99c11e46 100644 --- a/packages/tools/src/__tests__/utils.spec.ts +++ b/packages/tools/src/__tests__/utils.spec.ts @@ -152,6 +152,24 @@ Progress: resolved `; expect(replaceUnstableOutput(output.trim())).toMatchSnapshot(); }); + + test('replace ignore tarball download average speed warning log', () => { + const output = ` + WARN  Tarball download average speed 29 KiB/s (size 56 KiB) is below 50 KiB/s: https://registry.npmjs.org/qs/-/qs-6.14.0.tgz (GET) + WARN  Tarball download average speed 34 KiB/s (size 347 KiB) is below 50 KiB/s: https://registry.npmjs.org/undici/-/undici-7.16.0.tgz (GET) +`; + expect(replaceUnstableOutput(output.trim())).toMatchSnapshot(); + }); + + test('replace hash values', () => { + const output = ` +npm notice shasum: 65c35f9599054722ecde040abd4a19682a723cdc +npm notice integrity: sha512-qugLL42iCblSD[...]Gfk6HJodp2ZOQ== +"shasum": "65c35f9599054722ecde040abd4a19682a723cdc", +"integrity": "sha512-qugLL42iCblSDO0Vwic9xYkKYNtf+MwPW4cQSppKbGtQ/xswl1gXyu/DF5b7I/WbsVi02DJIHGfk6HJodp2ZOQ==", + `; + expect(replaceUnstableOutput(output.trim())).toMatchSnapshot(); + }); }); describe('isPassThroughEnv()', () => { diff --git a/packages/tools/src/utils.ts b/packages/tools/src/utils.ts index 04b69cd1a1..153c1e5221 100644 --- a/packages/tools/src/utils.ts +++ b/packages/tools/src/utils.ts @@ -61,9 +61,23 @@ export function replaceUnstableOutput(output: string, cwd?: string) { .replaceAll(/(added \d+ packages?), and audited \d+ packages( in (?:s|ms|µs))\n/g, '$1$2\n') .replaceAll(/\nfound \d+ vulnerabilities\n/g, '') // replace size for tsdown - .replaceAll(/ \d+(\.\d+)? ([km]B)/g, ' $2') + .replaceAll(/ \d+(\.\d+)? ([km]?B)/g, ' $2') + // replace npm notice size: + // "npm notice 5.6kB snap.txt" + // "npm notice 619B steps.json" + .replaceAll(/ \d+(\.\d+)?([km]?B) /g, ' $2 ') + // '"size": 821' => '"size": ' + // '"unpackedSize": 2720' => '"unpackedSize": ' + .replaceAll(/"(size|unpackedSize)": \d+/g, '"$1": ') // ignore npm registry domain - .replaceAll(/(https?:\/\/registry\.)[^/]+?\//g, '$1/') + .replaceAll(/(https?:\/\/registry\.)[^/\s]+(\/?)/g, '$1$2') + // ignore pnpm tarball download average speed warning log + .replaceAll(/ WARN  Tarball download average speed .+?\n/g, '') + // ignore npm hash values + .replaceAll(/shasum: .+?\n/g, 'shasum: \n') + .replaceAll(/integrity: ([\w-]+)-.+?\n/g, 'integrity: $1-\n') + .replaceAll(/"shasum": ".+?"/g, '"shasum": ""') + .replaceAll(/"integrity": "(\w+)-.+?"/g, '"integrity": "$1-"') // replace homedir; e.g.: /Users/foo/Library/pnpm/global/5/node_modules/testnpm2 => /Library/pnpm/global/5/node_modules/testnpm2 .replaceAll(homedir(), ''); } diff --git a/rfcs/pm-command-group.md b/rfcs/pm-command-group.md new file mode 100644 index 0000000000..4dc531dc8a --- /dev/null +++ b/rfcs/pm-command-group.md @@ -0,0 +1,1738 @@ +# RFC: Vite+ Package Manager Utilities Command Group + +## Summary + +Add `vite pm` command group that provides a set of utilities for working with package managers. The `pm` command group offers direct access to package manager utilities like cache management, package publishing, configuration, and more. These are pass-through commands that delegate to the detected package manager (pnpm/npm/yarn) with minimal processing, providing a unified interface across different package managers. + +## Motivation + +Currently, developers must use package manager-specific commands for various utilities: + +```bash +# Cache management +pnpm store path +npm cache dir +yarn cache dir + +# Package publishing +pnpm publish +npm publish +yarn publish + +# Package information +pnpm list +npm list +yarn list + +# Configuration +pnpm config get +npm config get +yarn config get +``` + +This creates several issues: + +1. **Cognitive Load**: Developers must remember different commands and flags for each package manager +2. **Context Switching**: When working across projects with different package managers, developers need to switch mental models +3. **Script Portability**: Scripts that use package manager utilities are tied to a specific package manager +4. **Missing Abstraction**: While vite+ provides abstractions for install/add/remove/update, it lacks utilities for cache, publish, config, etc. + +### Current Pain Points + +```bash +# Developer needs to know which package manager is used +pnpm store path # pnpm project +npm cache dir # npm project +yarn cache dir # yarn project + +# Different command names +pnpm list --depth 0 # pnpm - list packages +npm list --depth 0 # npm - list packages +yarn list --depth 0 # yarn - list packages + +# Different config commands +pnpm config get registry # pnpm +npm config get registry # npm +yarn config get registry # yarn + +# Different cache cleaning +pnpm store prune # pnpm +npm cache clean --force # npm +yarn cache clean # yarn +``` + +### Proposed Solution + +```bash +# Works for all package managers +vite pm cache # Show cache directory +vite pm cache clean # Clean cache +vite pm list # List installed packages +vite pm config get registry # Get config value +vite pm publish # Publish package +vite pm pack # Create package tarball +vite pm prune # Remove unnecessary packages +vite pm owner list # List package owners +vite pm view # View package information +``` + +## Proposed Solution + +### Command Syntax + +```bash +vite pm [OPTIONS] [ARGS...] +``` + +**Subcommands:** + +1. **prune**: Remove unnecessary packages +2. **pack**: Create a tarball of the package +3. **list** (alias: **ls**): List installed packages +4. **view**: View package information from registry +5. **publish**: Publish package to registry +6. **owner**: Manage package owners +7. **cache**: Manage package cache +8. **config**: Manage package manager configuration + +### Subcommand Details + +#### 1. vite pm prune + +Remove unnecessary packages from node_modules. + +```bash +vite pm prune [OPTIONS] +``` + +**Examples:** + +```bash +# Remove all extraneous packages +vite pm prune + +# Remove devDependencies (production only) +vite pm prune --prod + +# Remove optional dependencies +vite pm prune --no-optional +``` + +**Options:** + +- `--prod`: Remove devDependencies +- `--no-optional`: Remove optional dependencies + +#### 2. vite pm pack + +Create a tarball archive of the package. + +```bash +vite pm pack [OPTIONS] +``` + +**Examples:** + +```bash +# Create tarball in current directory +vite pm pack + +# Specify output file path +vite pm pack --out ./dist/package.tgz + +# Use placeholders for package name and version (pnpm/yarn@2+ only) +vite pm pack --out ./dist/%s-%v.tgz + +# Specify output directory +vite pm pack --pack-destination ./dist + +# Custom gzip compression level +vite pm pack --pack-gzip-level 9 + +# Pack all workspace packages +vite pm pack -r + +# Pack specific workspace packages +vite pm pack --filter app --filter web +``` + +**Options:** + +- `-r, --recursive`: Pack all workspace packages +- `--filter `: Filter packages to pack (can be used multiple times) +- `--out `: Customizes the output path for the tarball. Use `%s` and `%v` to include the package name and version (pnpm and yarn@2+ only), e.g., `%s.tgz` or `some-dir/%s-%v.tgz`. By default, the tarball is saved in the current working directory with the name `-.tgz` +- `--pack-destination `: Directory where the tarball will be saved (pnpm and npm only) +- `--pack-gzip-level `: Gzip compression level 0-9 (pnpm only) +- `--json`: Output in JSON format + +#### 3. vite pm list / vite pm ls + +List installed packages. + +```bash +vite pm list [PATTERN] [OPTIONS] +vite pm ls [PATTERN] [OPTIONS] +``` + +**Examples:** + +```bash +# List all direct dependencies +vite pm list + +# List dependencies matching pattern +vite pm list react + +# Show dependency tree +vite pm list --depth 2 + +# JSON output +vite pm list --json + +# List in specific workspace +vite pm list --filter app + +# List across all workspaces +vite pm list -r + +# List only production dependencies +vite pm list --prod + +# List globally installed packages +vite pm list -g +``` + +**Options:** + +- `--depth `: Maximum depth of dependency tree +- `--json`: JSON output format +- `--long`: Extended information +- `--parseable`: Parseable output +- `--prod`: Only production dependencies +- `--dev`: Only dev dependencies +- `-r, --recursive`: List across all workspaces +- `--filter `: Filter by workspace (can be used multiple times) +- `-g, --global`: List global packages + +#### 4. vite pm view / vite pm info / vite pm show + +View package information from the registry. + +```bash +vite pm view [] [[.subfield]...] [OPTIONS] +vite pm info [] [[.subfield]...] [OPTIONS] +vite pm show [] [[.subfield]...] [OPTIONS] +``` + +**Examples:** + +```bash +# View package information +vite pm view react + +# View specific version +vite pm view react@18.3.1 + +# View specific field +vite pm view react version +vite pm view react dist.tarball + +# View nested field +vite pm view react dependencies.prop-types + +# JSON output +vite pm view react --json + +# Use aliases +vite pm info lodash +vite pm show express +``` + +**Options:** + +- `--json`: JSON output format + +#### 5. vite pm publish + +Publish package to the registry. + +```bash +vite pm publish [TARBALL|FOLDER] [OPTIONS] +``` + +**Examples:** + +```bash +# Publish current package +vite pm publish + +# Publish specific tarball +vite pm publish package.tgz + +# Dry run +vite pm publish --dry-run + +# Set tag +vite pm publish --tag beta + +# Set access level +vite pm publish --access public + +# Recursive publish in monorepo +vite pm publish -r + +# Publish with filter +vite pm publish --filter app +``` + +**Options:** + +- `--dry-run`: Preview without actually publishing +- `--tag `: Publish with specific tag (default: latest) +- `--access `: Access level +- `--no-git-checks`: Skip git checks +- `--force`: Force publish even if already exists +- `-r, --recursive`: Publish all workspace packages +- `--filter `: Filter workspaces (pnpm) +- `--workspace `: Specific workspace (npm) + +#### 6. vite pm owner + +Manage package owners. + +```bash +vite pm owner +``` + +**Subcommands:** + +- `list `: List package owners +- `add `: Add owner +- `rm `: Remove owner + +**Examples:** + +```bash +# List package owners +vite pm owner list my-package + +# Add owner +vite pm owner add username my-package + +# Remove owner +vite pm owner rm username my-package +``` + +#### 7. vite pm cache + +Manage package cache. + +```bash +vite pm cache [SUBCOMMAND] [OPTIONS] +``` + +**Subcommands:** + +- `dir` / `path`: Show cache directory +- `clean` / `clear`: Clean cache +- `verify`: Verify cache integrity (npm) +- `list`: List cached packages (pnpm) + +**Examples:** + +```bash +# Show cache directory +vite pm cache dir +vite pm cache path + +# Clean cache +vite pm cache clean +vite pm cache clear + +# Force clean (npm) +vite pm cache clean --force + +# Verify cache (npm) +vite pm cache verify + +# List cached packages (pnpm) +vite pm cache list +``` + +**Options:** + +- `--force`: Force cache clean (npm) + +#### 8. vite pm config / vite pm c + +Manage package manager configuration. + +```bash +vite pm config [key] [value] [OPTIONS] +vite pm c [key] [value] [OPTIONS] +``` + +**Subcommands:** + +- `list`: List all configuration +- `get `: Get configuration value +- `set `: Set configuration value +- `delete `: Delete configuration key + +**Examples:** + +```bash +# List all config +vite pm config list + +# Get config value +vite pm config get registry + +# Set config value +vite pm config set registry https://registry.npmjs.org + +# Set with JSON format (pnpm/yarn@2+) +vite pm config set registry https://registry.npmjs.org --json + +# Set global config +vite pm config set registry https://registry.npmjs.org --global + +# Set global config with location parameter (alternative) +vite pm config set registry https://registry.npmjs.org --location global + +# Delete config key +vite pm config delete registry + +# Use alias +vite pm c get registry +``` + +**Options:** + +- `--json`: JSON output format (pnpm/yarn@2+) +- `-g, --global`: Use global config (shorthand for `--location global`) +- `--location `: Config location: project (default) or global + +### Command Mapping + +#### Prune Command + +**pnpm references:** + +- https://pnpm.io/cli/prune + +**npm references:** + +- https://docs.npmjs.com/cli/v11/commands/npm-prune + +**yarn references:** + +- https://classic.yarnpkg.com/en/docs/cli/prune +- The prune command isn't necessary. yarn install will prune extraneous packages. + +| Vite+ Flag | pnpm | npm | yarn | Description | +| --------------- | --------------- | ----------------- | ---- | --------------------------- | +| `vite pm prune` | `pnpm prune` | `npm prune` | N/A | Remove unnecessary packages | +| `--prod` | `--prod` | `--omit=dev` | N/A | Remove devDependencies | +| `--no-optional` | `--no-optional` | `--omit=optional` | N/A | Remove optional deps | + +**Note:** + +- npm supports prune with `--omit=dev` (for prod) and `--omit=optional` (for no-optional) +- yarn doesn't have a prune command (automatic during install) + +#### Pack Command + +**pnpm references:** + +- https://pnpm.io/cli/pack + +**npm references:** + +- https://docs.npmjs.com/cli/v11/commands/npm-pack + +**yarn references:** + +- https://classic.yarnpkg.com/en/docs/cli/pack +- https://yarnpkg.com/cli/pack +- https://yarnpkg.com/cli/workspaces/foreach (for yarn@2+ recursive packing) + +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | +| --------------------------- | -------------------- | -------------------- | ------------ | --------------------------------------------- | --------------------------------- | +| `vite pm pack` | `pnpm pack` | `npm pack` | `yarn pack` | `yarn pack` | Create package tarball | +| `-r, --recursive` | `--recursive` | `--workspaces` | N/A | `workspaces foreach --all pack` | Pack all workspace packages | +| `--filter ` | `--filter` | `--workspace` | N/A | `workspaces foreach --include pack` | Filter packages to pack | +| `--out ` | `--out` | N/A | `--filename` | `--out` | Output file path (supports %s/%v) | +| `--pack-destination ` | `--pack-destination` | `--pack-destination` | N/A | N/A | Output directory | +| `--pack-gzip-level ` | `--pack-gzip-level` | N/A | N/A | N/A | Gzip compression level (0-9) | +| `--json` | `--json` | `--json` | `--json` | `--json` | JSON output | + +**Note:** + +- `-r, --recursive`: Pack all workspace packages + - pnpm uses `--recursive` + - npm uses `--workspaces` + - yarn@1 does not support (prints warning and ignores) + - yarn@2+ uses `yarn workspaces foreach --all pack` +- `--filter `: Filter packages to pack (can be used multiple times) + - pnpm uses `--filter ` + - npm uses `--workspace ` + - yarn@1 does not support (prints warning and ignores) + - yarn@2+ always uses `yarn workspaces foreach --all --include pack` +- `--out `: Specifies the full output file path + - pnpm and yarn@2+ support `%s` (package name) and `%v` (version) placeholders + - yarn@1 uses `--filename` (does not support placeholders) + - npm does not support this option +- `--pack-destination `: Specifies the output directory (file name auto-generated) + - Supported by pnpm and npm + - yarn does not support this option (prints warning and ignores) +- `--pack-gzip-level `: Gzip compression level (0-9) + - Only supported by pnpm + - npm and yarn do not support this option (prints warning and ignores) + +#### List Command + +**pnpm references:** + +- https://pnpm.io/cli/list + +**npm references:** + +- https://docs.npmjs.com/cli/v11/commands/npm-ls + +**yarn references:** + +- https://classic.yarnpkg.com/en/docs/cli/list + +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | +| -------------------- | ----------------- | ------------------------------- | ------------- | ------------- | --------------------------------------------- | +| `vite pm list` | `pnpm list` | `npm list` | `yarn list` | N/A | List installed packages | +| `--depth ` | `--depth ` | `--depth ` | `--depth ` | N/A | Limit tree depth | +| `--json` | `--json` | `--json` | `--json` | N/A | JSON output | +| `--long` | `--long` | `--long` | N/A | N/A | Extended info | +| `--parseable` | `--parseable` | `--parseable` | N/A | N/A | Parseable format | +| `-P, --prod` | `--prod` | `--include prod --include peer` | N/A | N/A | Production deps only | +| `-D, --dev` | `--dev` | `--include dev` | N/A | N/A | Dev deps only | +| `--no-optional` | `--no-optional` | `--omit optional` | N/A | N/A | Exclude optional deps | +| `--exclude-peers` | `--exclude-peers` | `--omit peer` | N/A | N/A | Exclude peer deps | +| `--only-projects` | `--only-projects` | N/A | N/A | N/A | Show only project packages (pnpm) | +| `--find-by ` | `--find-by` | N/A | N/A | N/A | Use finder function from .pnpmfile.cjs (pnpm) | +| `-r, --recursive` | `--recursive` | `--workspaces` | N/A | N/A | List across workspaces | +| `--filter ` | `--filter` | `--workspace` | N/A | N/A | Filter workspace | +| `-g, --global` | `npm list -g` | `npm list -g` | `npm list -g` | `npm list -g` | List global packages | + +**Note:** + +- yarn@2+ does not support the `list` command (command is ignored) +- `-r, --recursive`: List across all workspaces + - pnpm uses `--recursive` + - npm uses `--workspaces` + - yarn@1 does not support (prints warning and ignores) + - yarn@2+ does not support list command at all +- `--filter `: Filter by workspace (can be used multiple times) + - pnpm uses `--filter ` (comes before `list` command) + - npm uses `--workspace ` (comes after `list` command) + - yarn@1 does not support (prints warning and ignores) + - yarn@2+ does not support list command at all +- `-P, --prod`: Show only production dependencies (and peer dependencies) + - pnpm uses `--prod` + - npm uses `--include prod --include peer` + - yarn@1 does not support (prints warning and ignores) + - yarn@2+ does not support list command at all +- `-D, --dev`: Show only dev dependencies + - pnpm uses `--dev` + - npm uses `--include dev` + - yarn@1 does not support (prints warning and ignores) + - yarn@2+ does not support list command at all +- `--no-optional`: Exclude optional dependencies + - pnpm uses `--no-optional` + - npm uses `--omit optional` + - yarn@1 does not support (prints warning and ignores) + - yarn@2+ does not support list command at all +- `--exclude-peers`: Exclude peer dependencies + - pnpm uses `--exclude-peers` + - npm uses `--omit peer` + - yarn@1 does not support (prints warning and ignores) + - yarn@2+ does not support list command at all +- `--only-projects`: Show only project packages (workspace packages only, no dependencies) + - Only supported by pnpm + - npm does not support (prints warning and ignores) + - yarn@1 does not support (prints warning and ignores) + - yarn@2+ does not support list command at all +- `--find-by `: Use a finder function defined in .pnpmfile.cjs to match dependencies by properties other than name + - Only supported by pnpm (pnpm-specific feature) + - npm does not support (prints warning and ignores) + - yarn@1 does not support (prints warning and ignores) + - yarn@2+ does not support list command at all +- `-g, --global`: List globally installed packages + - All package managers delegate to `npm list -g` (since global installs use npm) + - Uses npm regardless of the detected package manager + +#### View Command + +**pnpm references:** + +- https://pnpm.io/cli/view (delegates to npm view) + +**npm references:** + +- https://docs.npmjs.com/cli/v11/commands/npm-view + +**yarn references:** + +- https://classic.yarnpkg.com/en/docs/cli/info (delegates to npm view) +- https://yarnpkg.com/cli/npm/info (delegates to npm view) + +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | +| -------------- | ---------- | ---------- | ---------- | ---------- | ----------------- | +| `vite pm view` | `npm view` | `npm view` | `npm view` | `npm view` | View package info | +| `--json` | `--json` | `--json` | `--json` | `--json` | JSON output | + +**Note:** + +- All package managers delegate to `npm view` for viewing package information +- pnpm and yarn both use npm's view/info functionality internally +- Aliases: `vite pm info` and `vite pm show` work the same as `vite pm view` + +#### Publish Command + +**pnpm references:** + +- https://pnpm.io/cli/publish + +**npm references:** + +- https://docs.npmjs.com/cli/v11/commands/npm-publish + +**yarn references:** + +- https://classic.yarnpkg.com/en/docs/cli/publish (delegates to npm publish) +- https://yarnpkg.com/cli/npm/publish (delegates to npm publish) + +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | +| --------------------------- | ------------------ | ------------------ | ------------------ | ------------------ | --------------------------- | +| `vite pm publish` | `pnpm publish` | `npm publish` | `npm publish` | `npm publish` | Publish package | +| `--dry-run` | `--dry-run` | `--dry-run` | `--dry-run` | `--dry-run` | Preview without publishing | +| `--tag ` | `--tag ` | `--tag ` | `--tag ` | `--tag ` | Publish tag | +| `--access ` | `--access ` | `--access ` | `--access ` | `--access ` | Public/restricted | +| `--otp ` | `--otp` | `--otp` | `--otp` | `--otp` | One-time password | +| `--no-git-checks` | `--no-git-checks` | N/A | N/A | N/A | Skip git checks (pnpm) | +| `--publish-branch ` | `--publish-branch` | N/A | N/A | N/A | Set publish branch (pnpm) | +| `--report-summary` | `--report-summary` | N/A | N/A | N/A | Save publish summary (pnpm) | +| `--force` | `--force` | `--force` | `--force` | `--force` | Force publish | +| `--json` | `--json` | N/A | N/A | N/A | JSON output (pnpm) | +| `-r, --recursive` | `--recursive` | `--workspaces` | N/A | N/A | Publish workspaces | +| `--filter ` | `--filter` | `--workspace` | N/A | N/A | Filter workspace | + +**Note:** + +- All yarn versions delegate to `npm publish` for publishing packages +- yarn@1 and yarn@2+ both use npm's publish functionality internally +- `-r, --recursive`: Publish all workspace packages + - pnpm uses `--recursive` + - npm uses `--workspaces` + - yarn does not support (delegates to npm which doesn't support this in single publish mode) +- `--filter `: Filter workspace packages to publish (can be used multiple times) + - pnpm uses `--filter ` (comes before `publish` command) + - npm uses `--workspace ` (comes after `publish` command) + - yarn does not support (delegates to npm, can use --workspace) +- `--no-git-checks`: Skip git checks before publishing + - Only supported by pnpm (pnpm-specific feature) + - npm does not support (prints warning and ignores) + - yarn does not support (delegates to npm which doesn't support it) +- `--publish-branch `: Set the branch name to publish from + - Only supported by pnpm (pnpm-specific feature) + - npm does not support (prints warning and ignores) + - yarn does not support (delegates to npm which doesn't support it) +- `--report-summary`: Save publish summary to pnpm-publish-summary.json + - Only supported by pnpm (pnpm-specific feature) + - npm does not support (prints warning and ignores) + - yarn does not support (delegates to npm which doesn't support it) +- `--json`: JSON output + - Only supported by pnpm (pnpm-specific feature) + - npm does not support (prints warning and ignores) + - yarn does not support (delegates to npm which doesn't support it) +- pnpm-specific features: `--no-git-checks`, `--publish-branch`, `--report-summary`, `--json` + +#### Owner Command + +**pnpm references:** + +- https://pnpm.io/cli/owner (delegates to npm owner) + +**npm references:** + +- https://docs.npmjs.com/cli/v11/commands/npm-owner + +**yarn references:** + +- https://classic.yarnpkg.com/en/docs/cli/owner (delegates to npm owner) +- https://yarnpkg.com/cli/npm/owner (delegates to npm owner) + +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | +| --------------------------- | ---------------- | ---------------- | ---------------- | ---------------- | ------------------- | +| `vite pm owner list ` | `npm owner list` | `npm owner list` | `npm owner list` | `npm owner list` | List package owners | +| `vite pm owner add

` | `npm owner add` | `npm owner add` | `npm owner add` | `npm owner add` | Add owner | +| `vite pm owner rm

` | `npm owner rm` | `npm owner rm` | `npm owner rm` | `npm owner rm` | Remove owner | +| `--otp ` | `--otp` | `--otp` | `--otp` | `--otp` | One-time password | + +**Note:** + +- All package managers delegate to `npm owner` for managing package ownership +- pnpm and yarn both use npm's owner functionality internally +- Alias: `vite pm author` works the same as `vite pm owner` + +#### Cache Command + +**pnpm references:** + +- https://pnpm.io/cli/store + +**npm references:** + +- https://docs.npmjs.com/cli/v11/commands/npm-cache + +**yarn references:** + +- https://classic.yarnpkg.com/en/docs/cli/cache +- https://yarnpkg.com/cli/cache + +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | +| --------------------- | ------------------ | ---------------------- | ------------------ | ----------------------------- | -------------------- | +| `vite pm cache dir` | `pnpm store path` | `npm config get cache` | `yarn cache dir` | `yarn config get cacheFolder` | Show cache directory | +| `vite pm cache path` | Alias for `dir` | Alias for `dir` | Alias for `dir` | Alias for `dir` | Alias for dir | +| `vite pm cache clean` | `pnpm store prune` | `npm cache clean` | `yarn cache clean` | `yarn cache clean` | Clean cache | + +**Note:** + +- `cache dir` / `cache path`: Show cache directory location + - pnpm uses `pnpm store path` + - npm uses `npm config get cache` (not `npm cache dir` which doesn't exist in modern npm) + - yarn@1 uses `yarn cache dir` + - yarn@2+ uses `yarn config get cacheFolder` +- Subcommand aliases: `path` is an alias for `dir` + +#### Config Command + +**pnpm references:** + +- https://pnpm.io/cli/config + +**npm references:** + +- https://docs.npmjs.com/cli/v11/commands/npm-config + +**yarn references:** + +- https://classic.yarnpkg.com/en/docs/cli/config +- https://yarnpkg.com/cli/config + +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | +| ----------------------------- | -------------------- | ------------------- | -------------------- | --------------------------- | ------------------ | +| `vite pm config list` | `pnpm config list` | `npm config list` | `yarn config list` | `yarn config` | List configuration | +| `vite pm config get ` | `pnpm config get` | `npm config get` | `yarn config get` | `yarn config get` | Get config value | +| `vite pm config set ` | `pnpm config set` | `npm config set` | `yarn config set` | `yarn config set` | Set config value | +| `vite pm config delete ` | `pnpm config delete` | `npm config delete` | `yarn config delete` | `yarn config unset` | Delete config key | +| `--json` | `--json` | `--json` | `--json` | `--json` | JSON output | +| `-g, --global` | `--global` | `--global` | `--global` | `--home` | Global config | +| `--location ` | `--location` | `--location` | N/A | Maps to `--home` for global | Config location | + +**Note:** + +- Alias: `vite pm c` works the same as `vite pm config` +- `-g, --global`: Shorthand for setting global configuration + - pnpm uses `--global` + - npm uses `--global` + - yarn@1 uses `--global` + - yarn@2+ uses `--home` + - Equivalent to `--location global` +- `--location`: Specify config location (default: global) + - pnpm supports: `project`, `global` (default) + - npm supports: `project`, `user`, `global` (default), etc. + - yarn@1 does not support (prints warning and ignores, uses global by default) + - yarn@2+ maps `global` to `--home` flag; `project` is default when no flag specified +- `--json`: JSON output format + - Supported by all package managers for output formatting (list/get commands) + - For `set` command with JSON value: pnpm, npm, yarn@2+ support; yarn@1 does not support + +### Implementation Architecture + +#### 1. Command Structure + +**File**: `crates/vite_task/src/lib.rs` + +Add new command group: + +```rust +#[derive(Subcommand, Debug)] +pub enum Commands { + // ... existing commands + + /// Package manager utilities + #[command(disable_help_flag = true, subcommand)] + Pm(PmCommands), +} + +#[derive(Subcommand, Debug)] +pub enum PmCommands { + /// Remove unnecessary packages + Prune { + /// Remove devDependencies + #[arg(long)] + prod: bool, + + /// Remove optional dependencies + #[arg(long)] + no_optional: bool, + + /// Arguments to pass to package manager + #[arg(allow_hyphen_values = true, trailing_var_arg = true)] + args: Vec, + }, + + /// Create a tarball of the package + Pack { + /// Preview without creating tarball + #[arg(long)] + dry_run: bool, + + /// Output directory for tarball + #[arg(long)] + pack_destination: Option, + + /// Gzip compression level (0-9) + #[arg(long)] + pack_gzip_level: Option, + + /// Output in JSON format + #[arg(long)] + json: bool, + + /// Arguments to pass to package manager + #[arg(allow_hyphen_values = true, trailing_var_arg = true)] + args: Vec, + }, + + /// List installed packages + #[command(alias = "ls")] + List { + /// Package pattern to filter + pattern: Option, + + /// Maximum depth of dependency tree + #[arg(long)] + depth: Option, + + /// Output in JSON format + #[arg(long)] + json: bool, + + /// Show extended information + #[arg(long)] + long: bool, + + /// Parseable output format + #[arg(long)] + parseable: bool, + + /// Only production dependencies + #[arg(long)] + prod: bool, + + /// Only dev dependencies + #[arg(long)] + dev: bool, + + /// List across all workspaces + #[arg(short = 'r', long)] + recursive: bool, + + /// Filter packages in monorepo (pnpm) + #[arg(long)] + filter: Vec, + + /// Target specific workspace (npm) + #[arg(long)] + workspace: Vec, + + /// List global packages + #[arg(short = 'g', long)] + global: bool, + + /// Arguments to pass to package manager + #[arg(allow_hyphen_values = true, trailing_var_arg = true)] + args: Vec, + }, + + /// View package information from registry + View { + /// Package name with optional version + package: String, + + /// Specific field to view + field: Option, + + /// Output in JSON format + #[arg(long)] + json: bool, + + /// Arguments to pass to package manager + #[arg(allow_hyphen_values = true, trailing_var_arg = true)] + args: Vec, + }, + + /// Publish package to registry + Publish { + /// Tarball or folder to publish + target: Option, + + /// Preview without publishing + #[arg(long)] + dry_run: bool, + + /// Publish tag (default: latest) + #[arg(long)] + tag: Option, + + /// Access level (public/restricted) + #[arg(long)] + access: Option, + + /// Skip git checks (pnpm) + #[arg(long)] + no_git_checks: bool, + + /// Force publish + #[arg(long)] + force: bool, + + /// Publish all workspace packages + #[arg(short = 'r', long)] + recursive: bool, + + /// Filter packages in monorepo (pnpm) + #[arg(long)] + filter: Vec, + + /// Target specific workspace (npm) + #[arg(long)] + workspace: Vec, + + /// Arguments to pass to package manager + #[arg(allow_hyphen_values = true, trailing_var_arg = true)] + args: Vec, + }, + + /// Manage package owners + Owner { + /// Subcommand: list, add, rm + #[command(subcommand)] + command: OwnerCommands, + }, + + /// Manage package cache + Cache { + /// Subcommand: dir, path, clean, clear, verify, list + subcommand: Option, + + /// Force clean (npm) + #[arg(long)] + force: bool, + + /// Arguments to pass to package manager + #[arg(allow_hyphen_values = true, trailing_var_arg = true)] + args: Vec, + }, + + /// Manage package manager configuration + Config { + /// Subcommand: list, get, set, delete + subcommand: Option, + + /// Config key + key: Option, + + /// Config value (for set) + value: Option, + + /// Output in JSON format + #[arg(long)] + json: bool, + + /// Use global config + #[arg(long)] + global: bool, + + /// Arguments to pass to package manager + #[arg(allow_hyphen_values = true, trailing_var_arg = true)] + args: Vec, + }, +} + +#[derive(Subcommand, Debug)] +pub enum OwnerCommands { + /// List package owners + List { + /// Package name + package: String, + }, + + /// Add package owner + Add { + /// Username + user: String, + /// Package name + package: String, + }, + + /// Remove package owner + Rm { + /// Username + user: String, + /// Package name + package: String, + }, +} +``` + +#### 2. Package Manager Adapter + +**File**: `crates/vite_package_manager/src/commands/pm.rs` (new file) + +```rust +use std::{collections::HashMap, process::ExitStatus}; + +use vite_error::Error; +use vite_path::AbsolutePath; + +use crate::package_manager::{ + PackageManager, PackageManagerType, ResolveCommandResult, format_path_env, run_command, +}; + +impl PackageManager { + /// Run a pm subcommand with pass-through arguments. + #[must_use] + pub async fn run_pm_command( + &self, + subcommand: &str, + args: &[String], + cwd: impl AsRef, + ) -> Result { + let resolve_command = self.resolve_pm_command(subcommand, args); + run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) + .await + } + + /// Resolve pm command with minimal processing. + /// Most arguments are passed through directly to the package manager. + #[must_use] + pub fn resolve_pm_command(&self, subcommand: &str, args: &[String]) -> ResolveCommandResult { + let bin_name: String; + let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); + let mut cmd_args: Vec = Vec::new(); + + match self.client { + PackageManagerType::Pnpm => { + bin_name = "pnpm".into(); + + // Map vite pm commands to pnpm commands + match subcommand { + "prune" => cmd_args.push("prune".into()), + "pack" => cmd_args.push("pack".into()), + "list" | "ls" => cmd_args.push("list".into()), + "view" => cmd_args.push("view".into()), + "publish" => cmd_args.push("publish".into()), + "owner" => cmd_args.push("owner".into()), + "cache" => { + // Map cache subcommands + if !args.is_empty() { + match args[0].as_str() { + "dir" | "path" => cmd_args.push("store".into()), + "clean" | "clear" => { + cmd_args.push("store".into()); + cmd_args.push("prune".into()); + return ResolveCommandResult { bin_path: bin_name, args: cmd_args, envs }; + } + "list" => { + cmd_args.push("store".into()); + cmd_args.push("list".into()); + return ResolveCommandResult { bin_path: bin_name, args: cmd_args, envs }; + } + _ => cmd_args.push("store".into()), + } + } else { + cmd_args.push("store".into()); + cmd_args.push("path".into()); + return ResolveCommandResult { bin_path: bin_name, args: cmd_args, envs }; + } + } + "config" => cmd_args.push("config".into()), + _ => cmd_args.push(subcommand.into()), + } + } + PackageManagerType::Npm => { + bin_name = "npm".into(); + + match subcommand { + "prune" => { + eprintln!("Warning: npm removed 'prune' command in v6. Use 'vite install --prod' instead."); + return ResolveCommandResult { + bin_path: "echo".into(), + args: vec!["npm prune is deprecated".into()], + envs, + }; + } + "pack" => cmd_args.push("pack".into()), + "list" | "ls" => cmd_args.push("list".into()), + "view" => cmd_args.push("view".into()), + "publish" => cmd_args.push("publish".into()), + "owner" => cmd_args.push("owner".into()), + "cache" => { + cmd_args.push("cache".into()); + if !args.is_empty() { + match args[0].as_str() { + "path" => { + // npm uses 'dir' not 'path' + cmd_args.push("dir".into()); + return ResolveCommandResult { bin_path: bin_name, args: cmd_args, envs }; + } + "clear" => { + // npm uses 'clean' not 'clear' + cmd_args.push("clean".into()); + } + _ => {} + } + } + } + "config" => cmd_args.push("config".into()), + _ => cmd_args.push(subcommand.into()), + } + } + PackageManagerType::Yarn => { + bin_name = "yarn".into(); + + match subcommand { + "prune" => { + if self.version.starts_with("1.") { + cmd_args.push("prune".into()); + } else { + eprintln!("Warning: yarn@2+ does not have 'prune' command"); + return ResolveCommandResult { + bin_path: "echo".into(), + args: vec!["yarn@2+ does not support prune".into()], + envs, + }; + } + } + "pack" => cmd_args.push("pack".into()), + "list" | "ls" => cmd_args.push("list".into()), + "view" => { + // yarn uses 'info' instead of 'view' + cmd_args.push("info".into()); + } + "publish" => { + if self.version.starts_with("1.") { + cmd_args.push("publish".into()); + } else { + cmd_args.push("npm".into()); + cmd_args.push("publish".into()); + } + } + "owner" => { + if self.version.starts_with("1.") { + cmd_args.push("owner".into()); + } else { + cmd_args.push("npm".into()); + cmd_args.push("owner".into()); + } + } + "cache" => { + cmd_args.push("cache".into()); + if !args.is_empty() { + match args[0].as_str() { + "path" => { + // yarn uses 'dir' not 'path' + cmd_args.push("dir".into()); + return ResolveCommandResult { bin_path: bin_name, args: cmd_args, envs }; + } + "clear" => { + // yarn uses 'clean' not 'clear' + cmd_args.push("clean".into()); + } + "verify" => { + eprintln!("Warning: yarn does not support 'cache verify'"); + return ResolveCommandResult { + bin_path: "echo".into(), + args: vec!["yarn does not support cache verify".into()], + envs, + }; + } + _ => {} + } + } + } + "config" => { + cmd_args.push("config".into()); + // yarn@2+ uses different config commands + if !self.version.starts_with("1.") && !args.is_empty() && args[0] == "delete" { + cmd_args.push("unset".into()); + cmd_args.extend_from_slice(&args[1..]); + return ResolveCommandResult { bin_path: bin_name, args: cmd_args, envs }; + } + } + _ => cmd_args.push(subcommand.into()), + } + } + } + + // Pass through all remaining arguments + cmd_args.extend_from_slice(args); + + ResolveCommandResult { bin_path: bin_name, args: cmd_args, envs } + } +} +``` + +**File**: `crates/vite_package_manager/src/commands/mod.rs` + +Update to include pm module: + +```rust +pub mod add; +mod install; +pub mod remove; +pub mod update; +pub mod link; +pub mod unlink; +pub mod dedupe; +pub mod why; +pub mod outdated; +pub mod pm; // Add this line +``` + +#### 3. PM Command Implementation + +**File**: `crates/vite_task/src/pm.rs` (new file) + +```rust +use vite_error::Error; +use vite_path::AbsolutePathBuf; +use vite_package_manager::PackageManager; +use vite_workspace::Workspace; + +pub struct PmCommand { + workspace_root: AbsolutePathBuf, +} + +impl PmCommand { + pub fn new(workspace_root: AbsolutePathBuf) -> Self { + Self { workspace_root } + } + + pub async fn execute( + self, + subcommand: String, + args: Vec, + ) -> Result { + let package_manager = PackageManager::builder(&self.workspace_root).build().await?; + let workspace = Workspace::partial_load(self.workspace_root)?; + + let exit_status = package_manager + .run_pm_command(&subcommand, &args, &workspace.root) + .await?; + + if !exit_status.success() { + return Err(Error::CommandFailed { + command: format!("pm {}", subcommand), + exit_code: exit_status.code(), + }); + } + + workspace.unload().await?; + + Ok(ExecutionSummary::default()) + } +} +``` + +## Design Decisions + +### 1. Pass-Through Architecture + +**Decision**: Use minimal processing and pass most arguments directly to package managers. + +**Rationale**: + +- Package managers have many flags and options that change frequently +- Trying to map every option is maintenance-intensive and error-prone +- Pass-through allows users to use any package manager feature +- Vite+ provides the abstraction of which PM to use, not feature mapping +- Users can reference their package manager docs for advanced options + +### 2. Command Name Mapping + +**Decision**: Map common command name differences (e.g., `view` → `info` for yarn). + +**Rationale**: + +- Some commands have different names across package managers +- Basic name mapping provides better UX +- Keeps common cases simple +- Advanced users can still use native commands directly + +### 3. Cache Command Special Handling + +**Decision**: Provide subcommands for cache (dir, clean, verify, list). + +**Rationale**: + +- Cache commands have very different syntax across package managers +- pnpm uses `store`, npm uses `cache`, yarn uses `cache` +- Unified interface makes cache management easier +- Common operation that benefits from abstraction + +### 4. No Caching + +**Decision**: Don't cache any pm command results. + +**Rationale**: + +- PM utilities query current state or modify configuration +- Caching would provide stale data +- Operations are fast enough without caching +- Real-time data is expected + +### 5. Deprecation Warnings + +**Decision**: Warn users when commands aren't available in their package manager. + +**Rationale**: + +- npm removed `prune` in v6 +- yarn@2+ doesn't have `prune` +- Helpful to educate users about alternatives +- Better than silent failure + +### 6. Subcommand Groups + +**Decision**: Group related commands under `pm` rather than top-level commands. + +**Rationale**: + +- Keeps vite+ CLI namespace clean +- Clear categorization (pm utilities vs task running) +- Matches Bun's design pattern +- Extensible for future utilities + +## Error Handling + +### No Package Manager Detected + +```bash +$ vite pm list +Error: No package manager detected +Please run one of: + - vite install (to set up package manager) + - Add packageManager field to package.json +``` + +### Unsupported Command + +```bash +$ vite pm prune +Detected package manager: yarn@4.0.0 +Warning: yarn does not have 'prune' command. yarn install will prune extraneous packages automatically. +$ echo $? +0 +``` + +### Command Failed + +```bash +$ vite pm publish +Detected package manager: pnpm@10.15.0 +Running: pnpm publish +Error: You must be logged in to publish packages +Exit code: 1 +``` + +## User Experience + +### Prune Packages + +```bash +$ vite pm prune +Detected package manager: pnpm@10.15.0 +Running: pnpm prune +Packages: -12 + +$ vite pm prune --prod +Detected package manager: npm@11.0.0 +Running: npm prune --omit=dev +removed 45 packages +``` + +### Cache Management + +```bash +$ vite pm cache dir +Detected package manager: pnpm@10.15.0 +Running: pnpm store path +/Users/user/Library/pnpm/store + +$ vite pm cache clean +Detected package manager: pnpm@10.15.0 +Running: pnpm store prune +Removed 145 packages +``` + +### List Packages + +```bash +$ vite pm list --depth 0 +Detected package manager: pnpm@10.15.0 +Running: pnpm list --depth 0 + +my-app@1.0.0 +├── react@18.3.1 +├── react-dom@18.3.1 +└── lodash@4.17.21 +``` + +### View Package + +```bash +$ vite pm view react version +Detected package manager: npm@11.0.0 +Running: npm view react version +18.3.1 +``` + +### Publish Package + +```bash +$ vite pm publish --dry-run +Detected package manager: pnpm@10.15.0 +Running: pnpm publish --dry-run + +npm notice +npm notice package: my-package@1.0.0 +npm notice === Tarball Contents === +npm notice 1.2kB package.json +npm notice 2.3kB README.md +npm notice === Tarball Details === +npm notice name: my-package +npm notice version: 1.0.0 +``` + +### Configuration + +```bash +$ vite pm config get registry +Detected package manager: pnpm@10.15.0 +Running: pnpm config get registry +https://registry.npmjs.org + +$ vite pm config set registry https://custom-registry.com +Detected package manager: pnpm@10.15.0 +Running: pnpm config set registry https://custom-registry.com +``` + +## Alternative Designs Considered + +### Alternative 1: Individual Top-Level Commands + +```bash +vite cache dir +vite publish +vite pack +``` + +**Rejected because**: + +- Clutters top-level namespace +- Mixes task running with PM utilities +- Less clear categorization +- Harder to discover related commands + +### Alternative 2: Full Flag Mapping + +```bash +# Try to map all package manager flags +vite pm list --production # Map to --prod (pnpm), --production (npm) +``` + +**Rejected because**: + +- Maintenance burden as PMs add/change flags +- Incomplete mapping would be confusing +- Pass-through is more flexible +- Users can refer to PM docs for advanced usage + +### Alternative 3: Single Pass-Through Command + +```bash +vite pm -- pnpm store path +vite pm -- npm cache dir +``` + +**Rejected because**: + +- Loses abstraction benefit +- User must know package manager +- No command name translation +- Defeats purpose of unified interface + +## Implementation Plan + +### Phase 1: Core Infrastructure + +1. Add `Pm` command group to `Commands` enum +2. Create `pm.rs` module in vite_package_manager +3. Implement basic pass-through for each subcommand +4. Add command name mapping (view → info, etc.) + +### Phase 2: Subcommands + +1. Implement `prune` with deprecation warnings +2. Implement `pack` with options +3. Implement `list/ls` with filtering +4. Implement `view` with field selection +5. Implement `publish` with workspace support +6. Implement `owner` subcommands +7. Implement `cache` with subcommands +8. Implement `config` with subcommands + +### Phase 3: Testing + +1. Unit tests for command resolution +2. Test pass-through arguments +3. Test command name mapping +4. Test deprecation warnings +5. Integration tests with mock package managers +6. Test workspace operations + +### Phase 4: Documentation + +1. Update CLI documentation +2. Add examples for each subcommand +3. Document package manager compatibility +4. Add troubleshooting guide + +## Testing Strategy + +### Unit Tests + +```rust +#[test] +fn test_pnpm_cache_dir() { + let pm = PackageManager::mock(PackageManagerType::Pnpm); + let result = pm.resolve_pm_command("cache", &["dir".to_string()]); + assert_eq!(result.args, vec!["store", "path"]); +} + +#[test] +fn test_npm_cache_dir() { + let pm = PackageManager::mock(PackageManagerType::Npm); + let result = pm.resolve_pm_command("cache", &["dir".to_string()]); + assert_eq!(result.args, vec!["cache", "dir"]); +} + +#[test] +fn test_yarn_view_maps_to_info() { + let pm = PackageManager::mock(PackageManagerType::Yarn); + let result = pm.resolve_pm_command("view", &["react".to_string()]); + assert_eq!(result.args, vec!["info", "react"]); +} + +#[test] +fn test_pass_through_args() { + let pm = PackageManager::mock(PackageManagerType::Pnpm); + let result = pm.resolve_pm_command("list", &["--depth".to_string(), "0".to_string()]); + assert_eq!(result.args, vec!["list", "--depth", "0"]); +} +``` + +## CLI Help Output + +```bash +$ vite pm --help +Package manager utilities + +Usage: vite pm + +Commands: + prune Remove unnecessary packages + pack Create a tarball of the package + list List installed packages (alias: ls) + view View package information from registry + publish Publish package to registry + owner Manage package owners + cache Manage package cache + config Manage package manager configuration + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help Print help + +$ vite pm cache --help +Manage package cache + +Usage: vite pm cache [SUBCOMMAND] [OPTIONS] + +Subcommands: + dir Show cache directory (alias: path) + path Alias for dir + clean Clean cache (alias: clear) + clear Alias for clean + verify Verify cache integrity (npm only) + list List cached packages (pnpm only) + +Options: + --force Force cache clean (npm only) + -h, --help Print help + +Examples: + vite pm cache dir # Show cache directory + vite pm cache clean # Clean cache + vite pm cache clean --force # Force clean (npm) + vite pm cache verify # Verify cache (npm) + vite pm cache list # List cached packages (pnpm) +``` + +## Package Manager Compatibility + +| Subcommand | pnpm | npm | yarn@1 | yarn@2+ | Notes | +| ---------- | --------- | ------- | --------- | --------------- | --------------------------------------- | +| prune | ✅ Full | ✅ Full | ❌ N/A | ❌ N/A | npm uses --omit flags, yarn auto-prunes | +| pack | ✅ Full | ✅ Full | ✅ Full | ✅ Full | All supported | +| list/ls | ✅ Full | ✅ Full | ⚠️ Limited | ❌ N/A | yarn@1 no -r, yarn@2+ not supported | +| view | ✅ Full | ✅ Full | ⚠️ `info` | ⚠️ `info` | yarn uses different name | +| publish | ✅ Full | ✅ Full | ✅ Full | ⚠️ `npm publish` | yarn@2+ uses npm plugin | +| owner | ✅ Full | ✅ Full | ✅ Full | ⚠️ `npm owner` | yarn@2+ uses npm plugin | +| cache | ⚠️ `store` | ✅ Full | ✅ Full | ✅ Full | pnpm uses different command | +| config | ✅ Full | ✅ Full | ✅ Full | ⚠️ Different | yarn@2+ has different API | + +## Future Enhancements + +### 1. Interactive Cache Management + +```bash +vite pm cache --interactive +# Shows cache size, allows selective cleaning +``` + +### 2. Publish Dry-Run Summary + +```bash +vite pm publish --dry-run --summary +# Shows what would be published with sizes +``` + +### 3. Config Validation + +```bash +vite pm config validate +# Checks configuration for issues +``` + +### 4. Owner Management UI + +```bash +vite pm owner --interactive my-package +# Interactive UI for adding/removing owners +``` + +### 5. Cache Analytics + +```bash +vite pm cache stats +# Shows cache usage statistics, size breakdown +``` + +## Security Considerations + +1. **Publish Safety**: Dry-run option allows preview before publishing +2. **Config Isolation**: Respects package manager's configuration hierarchy +3. **Owner Management**: Delegates to package manager's authentication +4. **Cache Integrity**: Verify option (npm) checks for corruption +5. **Pass-Through Safety**: Arguments are passed through shell-escaped + +## Backward Compatibility + +This is a new feature with no breaking changes: + +- Existing commands unaffected +- New command group is additive +- No changes to task configuration +- No changes to caching behavior + +## Real-World Usage Examples + +### Cache Management in CI + +```yaml +# Clean cache before build +- run: vite pm cache clean --force + +# Show cache location for debugging +- run: vite pm cache dir +``` + +### Publishing Workflow + +```bash +# Build packages +vite build -r + +# Dry run to verify +vite pm publish --dry-run -r + +# Publish with beta tag +vite pm publish --tag beta -r + +# Publish only specific packages +vite pm publish --filter app +``` + +### Configuration Management + +```bash +# Set custom registry +vite pm config set registry https://custom-registry.com + +# Verify configuration +vite pm config get registry + +# List all configuration +vite pm config list +``` + +### Dependency Auditing + +```bash +# List dependencies to JSON file +vite pm list --json > deps.json + +# List production dependencies +vite pm list --prod + +# List specific workspace +vite pm list --filter app +``` + +## Conclusion + +This RFC proposes adding `vite pm` command group to provide unified access to package manager utilities across pnpm/npm/yarn. The design: + +- ✅ Pass-through architecture for maximum flexibility +- ✅ Command name translation for common operations +- ✅ Unified cache management interface +- ✅ Support for all major package managers +- ✅ Workspace-aware operations +- ✅ Deprecation warnings for removed commands +- ✅ Extensible for future enhancements +- ✅ Simple implementation leveraging existing infrastructure +- ✅ Matches Bun's pm command design pattern + +The implementation follows the same patterns as other package management commands while providing direct access to package manager utilities that developers need for publishing, cache management, configuration, and more.