From 4ed0c728929a397f6ad872da36492f3f303caecc Mon Sep 17 00:00:00 2001 From: MK Date: Fri, 24 Oct 2025 11:30:59 +0800 Subject: [PATCH] feat(pm): add pm command group --- .../src/commands/cache.rs | 308 ++++ .../src/commands/config.rs | 340 ++++ .../vite_package_manager/src/commands/list.rs | 363 +++++ .../vite_package_manager/src/commands/mod.rs | 8 + .../src/commands/owner.rs | 203 +++ .../vite_package_manager/src/commands/pack.rs | 229 +++ .../src/commands/prune.rs | 207 +++ .../src/commands/publish.rs | 321 ++++ .../vite_package_manager/src/commands/view.rs | 189 +++ packages/cli/binding/src/cli.rs | 391 +++++ .../exit-non-zero-on-cmd-not-exists/snap.txt | 2 +- .../snap-tests/cli-helper-message/snap.txt | 1 + rfcs/pm-command-group.md | 1448 +++++++++++++++++ 13 files changed, 4009 insertions(+), 1 deletion(-) create mode 100644 crates/vite_package_manager/src/commands/cache.rs create mode 100644 crates/vite_package_manager/src/commands/config.rs create mode 100644 crates/vite_package_manager/src/commands/list.rs create mode 100644 crates/vite_package_manager/src/commands/owner.rs create mode 100644 crates/vite_package_manager/src/commands/pack.rs create mode 100644 crates/vite_package_manager/src/commands/prune.rs create mode 100644 crates/vite_package_manager/src/commands/publish.rs create mode 100644 crates/vite_package_manager/src/commands/view.rs create mode 100644 rfcs/pm-command-group.md diff --git a/crates/vite_package_manager/src/commands/cache.rs b/crates/vite_package_manager/src/commands/cache.rs new file mode 100644 index 0000000000..0614113912 --- /dev/null +++ b/crates/vite_package_manager/src/commands/cache.rs @@ -0,0 +1,308 @@ +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, +}; + +#[derive(Debug)] +pub enum CacheSubcommand { + Dir, + Path, + Clean, + Clear, + Verify, + List, +} + +#[derive(Debug, Default)] +pub struct CacheCommandOptions<'a> { + pub subcommand: Option, + pub force: bool, + pub pass_through_args: Option<&'a [String]>, +} + +impl PackageManager { + /// Run the cache command with the package manager. + #[must_use] + pub async fn run_cache_command( + &self, + options: &CacheCommandOptions<'_>, + cwd: impl AsRef, + ) -> Result { + let resolve_command = self.resolve_cache_command(options); + run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) + .await + } + + /// Resolve the cache command. + #[must_use] + pub fn resolve_cache_command(&self, options: &CacheCommandOptions) -> ResolveCommandResult { + 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("store".into()); + + match options.subcommand { + Some(CacheSubcommand::Dir) | Some(CacheSubcommand::Path) | None => { + args.push("path".into()); + } + Some(CacheSubcommand::Clean) | Some(CacheSubcommand::Clear) => { + args.push("prune".into()); + } + Some(CacheSubcommand::List) => { + args.push("list".into()); + } + Some(CacheSubcommand::Verify) => { + eprintln!("Warning: pnpm does not support 'cache verify'"); + return ResolveCommandResult { + bin_path: "echo".into(), + args: vec!["pnpm does not support cache verify".into()], + envs, + }; + } + } + + if options.force { + eprintln!("Warning: --force not supported by pnpm store"); + } + } + PackageManagerType::Npm => { + bin_name = "npm".into(); + args.push("cache".into()); + + match options.subcommand { + Some(CacheSubcommand::Dir) | Some(CacheSubcommand::Path) | None => { + args.push("dir".into()); + } + Some(CacheSubcommand::Clean) | Some(CacheSubcommand::Clear) => { + args.push("clean".into()); + if options.force { + args.push("--force".into()); + } + } + Some(CacheSubcommand::Verify) => { + args.push("verify".into()); + } + Some(CacheSubcommand::List) => { + eprintln!("Warning: npm does not support 'cache list'"); + return ResolveCommandResult { + bin_path: "echo".into(), + args: vec!["npm does not support cache list".into()], + envs, + }; + } + } + } + PackageManagerType::Yarn => { + bin_name = "yarn".into(); + args.push("cache".into()); + + match options.subcommand { + Some(CacheSubcommand::Dir) | Some(CacheSubcommand::Path) | None => { + args.push("dir".into()); + } + Some(CacheSubcommand::Clean) | Some(CacheSubcommand::Clear) => { + args.push("clean".into()); + } + Some(CacheSubcommand::List) => { + if self.version.starts_with("1.") { + args.push("list".into()); + } else { + eprintln!("Warning: yarn@2+ does not support 'cache list'"); + return ResolveCommandResult { + bin_path: "echo".into(), + args: vec!["yarn@2+ does not support cache list".into()], + envs, + }; + } + } + Some(CacheSubcommand::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, + }; + } + } + + if options.force { + eprintln!("Warning: --force not supported by yarn cache"); + } + } + } + + // 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) -> 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("1.0.0"), + 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); + let result = pm.resolve_cache_command(&CacheCommandOptions { + subcommand: Some(CacheSubcommand::Dir), + ..Default::default() + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["store", "path"]); + } + + #[test] + fn test_pnpm_cache_path() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_cache_command(&CacheCommandOptions { + subcommand: Some(CacheSubcommand::Path), + ..Default::default() + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["store", "path"]); + } + + #[test] + fn test_pnpm_cache_clean() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_cache_command(&CacheCommandOptions { + subcommand: Some(CacheSubcommand::Clean), + ..Default::default() + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["store", "prune"]); + } + + #[test] + fn test_pnpm_cache_list() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_cache_command(&CacheCommandOptions { + subcommand: Some(CacheSubcommand::List), + ..Default::default() + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["store", "list"]); + } + + #[test] + fn test_pnpm_cache_default_is_path() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_cache_command(&CacheCommandOptions::default()); + 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); + let result = pm.resolve_cache_command(&CacheCommandOptions { + subcommand: Some(CacheSubcommand::Dir), + ..Default::default() + }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["cache", "dir"]); + } + + #[test] + fn test_npm_cache_clean() { + let pm = create_mock_package_manager(PackageManagerType::Npm); + let result = pm.resolve_cache_command(&CacheCommandOptions { + subcommand: Some(CacheSubcommand::Clean), + ..Default::default() + }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["cache", "clean"]); + } + + #[test] + fn test_npm_cache_clean_with_force() { + let pm = create_mock_package_manager(PackageManagerType::Npm); + let result = pm.resolve_cache_command(&CacheCommandOptions { + subcommand: Some(CacheSubcommand::Clean), + force: true, + ..Default::default() + }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["cache", "clean", "--force"]); + } + + #[test] + fn test_npm_cache_verify() { + let pm = create_mock_package_manager(PackageManagerType::Npm); + let result = pm.resolve_cache_command(&CacheCommandOptions { + subcommand: Some(CacheSubcommand::Verify), + ..Default::default() + }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["cache", "verify"]); + } + + #[test] + fn test_yarn_cache_dir() { + let pm = create_mock_package_manager(PackageManagerType::Yarn); + let result = pm.resolve_cache_command(&CacheCommandOptions { + subcommand: Some(CacheSubcommand::Dir), + ..Default::default() + }); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["cache", "dir"]); + } + + #[test] + fn test_yarn_cache_clean() { + let pm = create_mock_package_manager(PackageManagerType::Yarn); + let result = pm.resolve_cache_command(&CacheCommandOptions { + subcommand: Some(CacheSubcommand::Clean), + ..Default::default() + }); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["cache", "clean"]); + } + + #[test] + fn test_yarn1_cache_list() { + let pm = create_mock_package_manager(PackageManagerType::Yarn); + let result = pm.resolve_cache_command(&CacheCommandOptions { + subcommand: Some(CacheSubcommand::List), + ..Default::default() + }); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["cache", "list"]); + } +} diff --git a/crates/vite_package_manager/src/commands/config.rs b/crates/vite_package_manager/src/commands/config.rs new file mode 100644 index 0000000000..0b12fd6fc5 --- /dev/null +++ b/crates/vite_package_manager/src/commands/config.rs @@ -0,0 +1,340 @@ +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, +}; + +#[derive(Debug)] +pub enum ConfigSubcommand<'a> { + List, + Get { key: &'a str }, + Set { key: &'a str, value: &'a str }, + Delete { key: &'a str }, +} + +#[derive(Debug, Default)] +pub struct ConfigCommandOptions<'a> { + pub subcommand: Option>, + pub json: bool, + pub global: bool, + 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; + 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("config".into()); + + match &options.subcommand { + Some(ConfigSubcommand::List) | None => { + args.push("list".into()); + } + Some(ConfigSubcommand::Get { key }) => { + args.push("get".into()); + args.push((*key).into()); + } + Some(ConfigSubcommand::Set { key, value }) => { + args.push("set".into()); + args.push((*key).into()); + args.push((*value).into()); + } + Some(ConfigSubcommand::Delete { key }) => { + args.push("delete".into()); + args.push((*key).into()); + } + } + + if options.global { + args.push("--global".into()); + } + + if options.json { + eprintln!("Warning: --json not supported by pnpm config"); + } + } + PackageManagerType::Npm => { + bin_name = "npm".into(); + args.push("config".into()); + + match &options.subcommand { + Some(ConfigSubcommand::List) | None => { + args.push("list".into()); + } + Some(ConfigSubcommand::Get { key }) => { + args.push("get".into()); + args.push((*key).into()); + } + Some(ConfigSubcommand::Set { key, value }) => { + args.push("set".into()); + args.push((*key).into()); + args.push((*value).into()); + } + Some(ConfigSubcommand::Delete { key }) => { + args.push("delete".into()); + args.push((*key).into()); + } + } + + if options.global { + args.push("--global".into()); + } + if options.json { + args.push("--json".into()); + } + } + PackageManagerType::Yarn => { + bin_name = "yarn".into(); + args.push("config".into()); + + match &options.subcommand { + Some(ConfigSubcommand::List) | None => { + if self.version.starts_with("1.") { + args.push("list".into()); + } + // yarn@2+ just uses 'yarn config' for list + } + Some(ConfigSubcommand::Get { key }) => { + args.push("get".into()); + args.push((*key).into()); + } + Some(ConfigSubcommand::Set { key, value }) => { + args.push("set".into()); + args.push((*key).into()); + args.push((*value).into()); + } + Some(ConfigSubcommand::Delete { key }) => { + if self.version.starts_with("1.") { + args.push("delete".into()); + } else { + args.push("unset".into()); + } + args.push((*key).into()); + } + } + + if options.global { + if !self.version.starts_with("1.") { + eprintln!("Warning: --global not supported by yarn@2+"); + } else { + args.push("--global".into()); + } + } + + if options.json { + eprintln!("Warning: --json not supported by yarn config"); + } + } + } + + // 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) -> 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("1.0.0"), + hash: None, + bin_name: pm_type.to_string().into(), + workspace_root: temp_dir_path.clone(), + install_dir, + } + } + + #[test] + fn test_pnpm_config_list() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_config_command(&ConfigCommandOptions { + subcommand: Some(ConfigSubcommand::List), + ..Default::default() + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["config", "list"]); + } + + #[test] + fn test_pnpm_config_get() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_config_command(&ConfigCommandOptions { + subcommand: Some(ConfigSubcommand::Get { key: "registry" }), + ..Default::default() + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["config", "get", "registry"]); + } + + #[test] + fn test_pnpm_config_set() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_config_command(&ConfigCommandOptions { + subcommand: Some(ConfigSubcommand::Set { + key: "registry", + value: "https://registry.npmjs.org", + }), + ..Default::default() + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["config", "set", "registry", "https://registry.npmjs.org"]); + } + + #[test] + fn test_pnpm_config_delete() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_config_command(&ConfigCommandOptions { + subcommand: Some(ConfigSubcommand::Delete { key: "registry" }), + ..Default::default() + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["config", "delete", "registry"]); + } + + #[test] + fn test_pnpm_config_with_global() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_config_command(&ConfigCommandOptions { + subcommand: Some(ConfigSubcommand::Get { key: "registry" }), + global: true, + ..Default::default() + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["config", "get", "registry", "--global"]); + } + + #[test] + fn test_npm_config_list() { + let pm = create_mock_package_manager(PackageManagerType::Npm); + let result = pm.resolve_config_command(&ConfigCommandOptions { + subcommand: Some(ConfigSubcommand::List), + ..Default::default() + }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["config", "list"]); + } + + #[test] + fn test_npm_config_with_json() { + let pm = create_mock_package_manager(PackageManagerType::Npm); + let result = pm.resolve_config_command(&ConfigCommandOptions { + subcommand: Some(ConfigSubcommand::List), + json: true, + ..Default::default() + }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["config", "list", "--json"]); + } + + #[test] + fn test_yarn1_config_list() { + let pm = create_mock_package_manager(PackageManagerType::Yarn); + let result = pm.resolve_config_command(&ConfigCommandOptions { + subcommand: Some(ConfigSubcommand::List), + ..Default::default() + }); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["config", "list"]); + } + + #[test] + fn test_yarn1_config_delete() { + let pm = create_mock_package_manager(PackageManagerType::Yarn); + let result = pm.resolve_config_command(&ConfigCommandOptions { + subcommand: Some(ConfigSubcommand::Delete { key: "registry" }), + ..Default::default() + }); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["config", "delete", "registry"]); + } + + #[test] + fn test_yarn2_config_delete_uses_unset() { + 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::Yarn, + package_name: "yarn".into(), + version: Str::from("4.0.0"), + hash: None, + bin_name: "yarn".into(), + workspace_root: temp_dir_path.clone(), + install_dir, + }; + + let result = pm.resolve_config_command(&ConfigCommandOptions { + subcommand: Some(ConfigSubcommand::Delete { key: "registry" }), + ..Default::default() + }); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["config", "unset", "registry"]); + } + + #[test] + fn test_yarn2_config_list_no_subcommand() { + 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::Yarn, + package_name: "yarn".into(), + version: Str::from("4.0.0"), + hash: None, + bin_name: "yarn".into(), + workspace_root: temp_dir_path.clone(), + install_dir, + }; + + let result = pm.resolve_config_command(&ConfigCommandOptions { + subcommand: Some(ConfigSubcommand::List), + ..Default::default() + }); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["config"]); + } +} diff --git a/crates/vite_package_manager/src/commands/list.rs b/crates/vite_package_manager/src/commands/list.rs new file mode 100644 index 0000000000..f54c2064ef --- /dev/null +++ b/crates/vite_package_manager/src/commands/list.rs @@ -0,0 +1,363 @@ +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, +}; + +#[derive(Debug, Default)] +pub struct ListCommandOptions<'a> { + pub pattern: Option<&'a str>, + pub all: bool, + pub depth: Option, + pub json: bool, + pub long: bool, + pub parseable: bool, + pub prod: bool, + pub dev: bool, + pub recursive: bool, + pub filters: Option<&'a [String]>, + pub workspaces: Option<&'a [String]>, + pub global: bool, + pub pass_through_args: Option<&'a [String]>, +} + +impl PackageManager { + /// Run the list command with the package manager. + #[must_use] + pub async fn run_list_command( + &self, + options: &ListCommandOptions<'_>, + cwd: impl AsRef, + ) -> Result { + let resolve_command = self.resolve_list_command(options); + run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) + .await + } + + /// Resolve the list command. + #[must_use] + pub fn resolve_list_command(&self, options: &ListCommandOptions) -> ResolveCommandResult { + 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(); + + // 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.into()); + } + if options.all { + eprintln!("Warning: --all not supported by pnpm, showing all by default"); + } + 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.recursive { + args.push("--recursive".into()); + } + if options.global { + args.push("--global".into()); + } + + // Warn about npm-specific flags + if let Some(workspaces) = options.workspaces { + if !workspaces.is_empty() { + eprintln!( + "Warning: --workspace not supported by pnpm, use --filter instead" + ); + } + } + } + PackageManagerType::Npm => { + bin_name = "npm".into(); + + // npm: --workspace before command + if let Some(workspaces) = options.workspaces { + for workspace in workspaces { + args.push("--workspace".into()); + args.push(workspace.clone()); + } + } + + args.push("list".into()); + + if let Some(pattern) = options.pattern { + args.push(pattern.into()); + } + if options.all { + args.push("--all".into()); + } + 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("--production".into()); + } + if options.dev { + args.push("--development".into()); + } + if options.global { + args.push("--global".into()); + } + + // Warn about pnpm-specific flags + if options.recursive { + eprintln!("Warning: --recursive not supported by npm, use --workspace instead"); + } + if let Some(filters) = options.filters { + if !filters.is_empty() { + eprintln!( + "Warning: --filter not supported by npm, use --workspace instead" + ); + } + } + } + PackageManagerType::Yarn => { + bin_name = "yarn".into(); + args.push("list".into()); + + if let Some(pattern) = options.pattern { + args.push("--pattern".into()); + args.push(pattern.into()); + } + if options.all { + if !self.version.starts_with("1.") { + args.push("--all".into()); + } + } + 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 { + args.push("--production".into()); + } + if options.recursive && !self.version.starts_with("1.") { + args.push("--recursive".into()); + } + + // Warn about unsupported flags + if options.long { + eprintln!("Warning: --long not supported by yarn"); + } + if options.parseable { + eprintln!("Warning: --parseable not supported by yarn"); + } + if options.dev { + eprintln!("Warning: --dev not supported by yarn"); + } + if let Some(filters) = options.filters { + if !filters.is_empty() { + eprintln!("Warning: --filter not supported by yarn"); + } + } + if let Some(workspaces) = options.workspaces { + if !workspaces.is_empty() { + if self.version.starts_with("1.") { + eprintln!("Warning: --workspace not supported by yarn@1"); + } else { + for workspace in workspaces { + args.push("--workspace".into()); + args.push(workspace.clone()); + } + } + } + } + } + } + + // 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) -> 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("1.0.0"), + 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); + let result = pm.resolve_list_command(&ListCommandOptions::default()); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["list"]); + } + + #[test] + fn test_pnpm_list_with_pattern() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_list_command(&ListCommandOptions { + pattern: Some("react"), + ..Default::default() + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["list", "react"]); + } + + #[test] + fn test_pnpm_list_with_depth() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = + pm.resolve_list_command(&ListCommandOptions { depth: Some(2), ..Default::default() }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["list", "--depth", "2"]); + } + + #[test] + fn test_pnpm_list_with_json() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = + pm.resolve_list_command(&ListCommandOptions { json: true, ..Default::default() }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["list", "--json"]); + } + + #[test] + fn test_pnpm_list_with_filter() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_list_command(&ListCommandOptions { + filters: Some(&["app".to_string()]), + recursive: true, + ..Default::default() + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["--filter", "app", "list", "--recursive"]); + } + + #[test] + fn test_pnpm_list_with_prod() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = + pm.resolve_list_command(&ListCommandOptions { prod: true, ..Default::default() }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["list", "--prod"]); + } + + #[test] + fn test_npm_list_basic() { + let pm = create_mock_package_manager(PackageManagerType::Npm); + let result = pm.resolve_list_command(&ListCommandOptions::default()); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["list"]); + } + + #[test] + fn test_npm_list_with_all() { + let pm = create_mock_package_manager(PackageManagerType::Npm); + let result = + pm.resolve_list_command(&ListCommandOptions { all: true, ..Default::default() }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["list", "--all"]); + } + + #[test] + fn test_npm_list_with_workspace() { + let pm = create_mock_package_manager(PackageManagerType::Npm); + let result = pm.resolve_list_command(&ListCommandOptions { + workspaces: Some(&["app".to_string()]), + ..Default::default() + }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["--workspace", "app", "list"]); + } + + #[test] + fn test_npm_list_with_production() { + let pm = create_mock_package_manager(PackageManagerType::Npm); + let result = + pm.resolve_list_command(&ListCommandOptions { prod: true, ..Default::default() }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["list", "--production"]); + } + + #[test] + fn test_yarn_list_basic() { + let pm = create_mock_package_manager(PackageManagerType::Yarn); + let result = pm.resolve_list_command(&ListCommandOptions::default()); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["list"]); + } + + #[test] + fn test_yarn_list_with_pattern() { + let pm = create_mock_package_manager(PackageManagerType::Yarn); + let result = pm.resolve_list_command(&ListCommandOptions { + pattern: Some("react"), + ..Default::default() + }); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["list", "--pattern", "react"]); + } +} diff --git a/crates/vite_package_manager/src/commands/mod.rs b/crates/vite_package_manager/src/commands/mod.rs index ea44602f73..5637a965e5 100644 --- a/crates/vite_package_manager/src/commands/mod.rs +++ b/crates/vite_package_manager/src/commands/mod.rs @@ -1,4 +1,12 @@ pub mod add; +pub mod cache; +pub mod config; mod install; +pub mod list; +pub mod owner; +pub mod pack; +pub mod prune; +pub mod publish; pub mod remove; pub mod update; +pub mod view; diff --git a/crates/vite_package_manager/src/commands/owner.rs b/crates/vite_package_manager/src/commands/owner.rs new file mode 100644 index 0000000000..7c0c8f0e00 --- /dev/null +++ b/crates/vite_package_manager/src/commands/owner.rs @@ -0,0 +1,203 @@ +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, +}; + +pub enum OwnerSubcommand<'a> { + List { package: &'a str }, + Add { user: &'a str, package: &'a str }, + Rm { user: &'a str, package: &'a str }, +} + +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. + #[must_use] + pub fn resolve_owner_command(&self, subcommand: OwnerSubcommand) -> ResolveCommandResult { + 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("owner".into()); + + match subcommand { + OwnerSubcommand::List { package } => { + args.push("list".into()); + args.push(package.into()); + } + OwnerSubcommand::Add { user, package } => { + args.push("add".into()); + args.push(user.into()); + args.push(package.into()); + } + OwnerSubcommand::Rm { user, package } => { + args.push("rm".into()); + args.push(user.into()); + args.push(package.into()); + } + } + } + PackageManagerType::Npm => { + bin_name = "npm".into(); + args.push("owner".into()); + + match subcommand { + OwnerSubcommand::List { package } => { + args.push("list".into()); + args.push(package.into()); + } + OwnerSubcommand::Add { user, package } => { + args.push("add".into()); + args.push(user.into()); + args.push(package.into()); + } + OwnerSubcommand::Rm { user, package } => { + args.push("rm".into()); + args.push(user.into()); + args.push(package.into()); + } + } + } + PackageManagerType::Yarn => { + bin_name = "yarn".into(); + + if !self.version.starts_with("1.") { + args.push("npm".into()); + } + + args.push("owner".into()); + + match subcommand { + OwnerSubcommand::List { package } => { + args.push("list".into()); + args.push(package.into()); + } + OwnerSubcommand::Add { user, package } => { + args.push("add".into()); + args.push(user.into()); + args.push(package.into()); + } + OwnerSubcommand::Rm { user, package } => { + args.push("rm".into()); + args.push(user.into()); + args.push(package.into()); + } + } + } + } + + 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) -> 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("1.0.0"), + hash: None, + bin_name: pm_type.to_string().into(), + workspace_root: temp_dir_path.clone(), + install_dir, + } + } + + #[test] + fn test_pnpm_owner_list() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_owner_command(OwnerSubcommand::List { package: "my-package" }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["owner", "list", "my-package"]); + } + + #[test] + fn test_pnpm_owner_add() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_owner_command(OwnerSubcommand::Add { + user: "username", + package: "my-package", + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["owner", "add", "username", "my-package"]); + } + + #[test] + fn test_pnpm_owner_rm() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm + .resolve_owner_command(OwnerSubcommand::Rm { user: "username", package: "my-package" }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["owner", "rm", "username", "my-package"]); + } + + #[test] + fn test_npm_owner_list() { + let pm = create_mock_package_manager(PackageManagerType::Npm); + let result = pm.resolve_owner_command(OwnerSubcommand::List { package: "my-package" }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["owner", "list", "my-package"]); + } + + #[test] + fn test_yarn1_owner_list() { + let pm = create_mock_package_manager(PackageManagerType::Yarn); + let result = pm.resolve_owner_command(OwnerSubcommand::List { package: "my-package" }); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["owner", "list", "my-package"]); + } + + #[test] + fn test_yarn2_owner_uses_npm_plugin() { + 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::Yarn, + package_name: "yarn".into(), + version: Str::from("4.0.0"), + hash: None, + bin_name: "yarn".into(), + workspace_root: temp_dir_path.clone(), + install_dir, + }; + + let result = pm.resolve_owner_command(OwnerSubcommand::List { package: "my-package" }); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["npm", "owner", "list", "my-package"]); + } +} diff --git a/crates/vite_package_manager/src/commands/pack.rs b/crates/vite_package_manager/src/commands/pack.rs new file mode 100644 index 0000000000..4289f9235e --- /dev/null +++ b/crates/vite_package_manager/src/commands/pack.rs @@ -0,0 +1,229 @@ +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, +}; + +#[derive(Debug, Default)] +pub struct PackCommandOptions<'a> { + pub dry_run: bool, + 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 { + 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; + 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("pack".into()); + + if options.dry_run { + args.push("--dry-run".into()); + } + if let Some(dest) = options.pack_destination { + args.push("--pack-destination".into()); + args.push(dest.into()); + } + if let Some(level) = options.pack_gzip_level { + args.push("--pack-gzip-level".into()); + args.push(level.to_string()); + } + if options.json { + eprintln!("Warning: --json not supported by pnpm pack"); + } + } + PackageManagerType::Npm => { + bin_name = "npm".into(); + args.push("pack".into()); + + if options.dry_run { + args.push("--dry-run".into()); + } + if options.json { + args.push("--json".into()); + } + if options.pack_destination.is_some() { + eprintln!( + "Warning: --pack-destination not supported by npm, use --pack-destination option after -- separator" + ); + } + if options.pack_gzip_level.is_some() { + eprintln!("Warning: --pack-gzip-level not supported by npm"); + } + } + PackageManagerType::Yarn => { + bin_name = "yarn".into(); + args.push("pack".into()); + + if options.dry_run { + args.push("--dry-run".into()); + } + if let Some(dest) = options.pack_destination { + if self.version.starts_with("1.") { + args.push("--filename".into()); + } else { + args.push("--out".into()); + } + args.push(dest.into()); + } + if options.json { + eprintln!("Warning: --json not supported by yarn pack"); + } + if options.pack_gzip_level.is_some() { + eprintln!("Warning: --pack-gzip-level not supported by yarn"); + } + } + } + + // 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) -> 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("1.0.0"), + 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); + 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_with_dry_run() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = + pm.resolve_pack_command(&PackCommandOptions { dry_run: true, ..Default::default() }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["pack", "--dry-run"]); + } + + #[test] + fn test_pnpm_pack_with_destination() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + 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); + 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_npm_pack_basic() { + let pm = create_mock_package_manager(PackageManagerType::Npm); + 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_with_json() { + let pm = create_mock_package_manager(PackageManagerType::Npm); + 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_yarn1_pack_with_destination() { + let pm = create_mock_package_manager(PackageManagerType::Yarn); + let result = pm.resolve_pack_command(&PackCommandOptions { + pack_destination: Some("output.tgz"), + ..Default::default() + }); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["pack", "--filename", "output.tgz"]); + } + + #[test] + fn test_yarn2_pack_with_destination() { + 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::Yarn, + package_name: "yarn".into(), + version: Str::from("4.0.0"), + hash: None, + bin_name: "yarn".into(), + workspace_root: temp_dir_path.clone(), + install_dir, + }; + + let result = pm.resolve_pack_command(&PackCommandOptions { + pack_destination: Some("output.tgz"), + ..Default::default() + }); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["pack", "--out", "output.tgz"]); + } +} diff --git a/crates/vite_package_manager/src/commands/prune.rs b/crates/vite_package_manager/src/commands/prune.rs new file mode 100644 index 0000000000..2557207342 --- /dev/null +++ b/crates/vite_package_manager/src/commands/prune.rs @@ -0,0 +1,207 @@ +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, +}; + +#[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. + #[must_use] + pub async fn run_prune_command( + &self, + options: &PruneCommandOptions<'_>, + cwd: impl AsRef, + ) -> Result { + let resolve_command = self.resolve_prune_command(options); + run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) + .await + } + + /// Resolve the prune command. + #[must_use] + pub fn resolve_prune_command(&self, options: &PruneCommandOptions) -> ResolveCommandResult { + 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 => { + 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, + }; + } + PackageManagerType::Yarn => { + if self.version.starts_with("1.") { + bin_name = "yarn".into(); + 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, + }; + } + } + } + + // 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) -> 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("1.0.0"), + hash: None, + bin_name: pm_type.to_string().into(), + workspace_root: temp_dir_path.clone(), + install_dir, + } + } + + #[test] + fn test_pnpm_prune_basic() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_prune_command(&PruneCommandOptions { + prod: false, + no_optional: false, + pass_through_args: None, + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["prune"]); + } + + #[test] + fn test_pnpm_prune_with_prod() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_prune_command(&PruneCommandOptions { + prod: true, + no_optional: false, + pass_through_args: None, + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["prune", "--prod"]); + } + + #[test] + fn test_pnpm_prune_with_no_optional() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_prune_command(&PruneCommandOptions { + prod: false, + no_optional: true, + pass_through_args: None, + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["prune", "--no-optional"]); + } + + #[test] + fn test_pnpm_prune_with_all_flags() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_prune_command(&PruneCommandOptions { + prod: true, + no_optional: true, + pass_through_args: None, + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["prune", "--prod", "--no-optional"]); + } + + #[test] + fn test_npm_prune_deprecated() { + let pm = create_mock_package_manager(PackageManagerType::Npm); + let result = pm.resolve_prune_command(&PruneCommandOptions { + prod: false, + no_optional: false, + pass_through_args: None, + }); + assert_eq!(result.bin_path, "echo"); + assert_eq!(result.args, vec!["npm prune is deprecated"]); + } + + #[test] + fn test_yarn1_prune_basic() { + let pm = create_mock_package_manager(PackageManagerType::Yarn); + let result = pm.resolve_prune_command(&PruneCommandOptions { + prod: false, + no_optional: false, + pass_through_args: None, + }); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["prune"]); + } + + #[test] + fn test_yarn2_prune_not_supported() { + 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::Yarn, + package_name: "yarn".into(), + version: Str::from("2.0.0"), + hash: None, + bin_name: "yarn".into(), + workspace_root: temp_dir_path.clone(), + install_dir, + }; + + let result = pm.resolve_prune_command(&PruneCommandOptions { + prod: false, + no_optional: false, + pass_through_args: None, + }); + assert_eq!(result.bin_path, "echo"); + assert_eq!(result.args, vec!["yarn@2+ does not support prune"]); + } +} diff --git a/crates/vite_package_manager/src/commands/publish.rs b/crates/vite_package_manager/src/commands/publish.rs new file mode 100644 index 0000000000..8e66f42a0f --- /dev/null +++ b/crates/vite_package_manager/src/commands/publish.rs @@ -0,0 +1,321 @@ +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, +}; + +#[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 no_git_checks: bool, + pub force: bool, + pub recursive: bool, + pub filters: Option<&'a [String]>, + pub workspaces: 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. + #[must_use] + pub fn resolve_publish_command(&self, options: &PublishCommandOptions) -> ResolveCommandResult { + 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(); + + // 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.into()); + } + if options.dry_run { + args.push("--dry-run".into()); + } + if let Some(tag) = options.tag { + args.push("--tag".into()); + args.push(tag.into()); + } + if let Some(access) = options.access { + args.push("--access".into()); + args.push(access.into()); + } + if options.no_git_checks { + args.push("--no-git-checks".into()); + } + if options.force { + args.push("--force".into()); + } + if options.recursive { + args.push("--recursive".into()); + } + + // Warn about npm-specific flags + if let Some(workspaces) = options.workspaces { + if !workspaces.is_empty() { + eprintln!( + "Warning: --workspace not supported by pnpm, use --filter instead" + ); + } + } + } + PackageManagerType::Npm => { + bin_name = "npm".into(); + + // npm: --workspace before command + if let Some(workspaces) = options.workspaces { + for workspace in workspaces { + args.push("--workspace".into()); + args.push(workspace.clone()); + } + } + + args.push("publish".into()); + + if let Some(target) = options.target { + args.push(target.into()); + } + if options.dry_run { + args.push("--dry-run".into()); + } + if let Some(tag) = options.tag { + args.push("--tag".into()); + args.push(tag.into()); + } + if let Some(access) = options.access { + args.push("--access".into()); + args.push(access.into()); + } + if options.force { + args.push("--force".into()); + } + + // Warn about pnpm-specific flags + if options.no_git_checks { + eprintln!("Warning: --no-git-checks not supported by npm"); + } + if options.recursive { + eprintln!("Warning: --recursive not supported by npm, use --workspace instead"); + } + if let Some(filters) = options.filters { + if !filters.is_empty() { + eprintln!( + "Warning: --filter not supported by npm, use --workspace instead" + ); + } + } + } + PackageManagerType::Yarn => { + bin_name = "yarn".into(); + + if !self.version.starts_with("1.") { + args.push("npm".into()); + } + + args.push("publish".into()); + + if let Some(target) = options.target { + args.push(target.into()); + } + if let Some(tag) = options.tag { + args.push("--tag".into()); + args.push(tag.into()); + } + if let Some(access) = options.access { + args.push("--access".into()); + args.push(access.into()); + } + + // Warn about unsupported flags + if options.dry_run { + if self.version.starts_with("1.") { + eprintln!("Warning: --dry-run not supported by yarn@1"); + } else { + args.push("--dry-run".into()); + } + } + if options.no_git_checks { + eprintln!("Warning: --no-git-checks not supported by yarn"); + } + if options.force { + eprintln!("Warning: --force not supported by yarn"); + } + if options.recursive { + eprintln!("Warning: --recursive not supported by yarn"); + } + if let Some(filters) = options.filters { + if !filters.is_empty() { + eprintln!("Warning: --filter not supported by yarn"); + } + } + if let Some(workspaces) = options.workspaces { + if !workspaces.is_empty() { + if self.version.starts_with("1.") { + eprintln!("Warning: --workspace not supported by yarn@1"); + } else { + for workspace in workspaces { + args.push("--workspace".into()); + args.push(workspace.clone()); + } + } + } + } + } + } + + // 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) -> 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("1.0.0"), + hash: None, + bin_name: pm_type.to_string().into(), + workspace_root: temp_dir_path.clone(), + install_dir, + } + } + + #[test] + fn test_pnpm_publish_basic() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_publish_command(&PublishCommandOptions::default()); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["publish"]); + } + + #[test] + fn test_pnpm_publish_with_dry_run() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_publish_command(&PublishCommandOptions { + dry_run: true, + ..Default::default() + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["publish", "--dry-run"]); + } + + #[test] + fn test_pnpm_publish_with_tag() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_publish_command(&PublishCommandOptions { + tag: Some("beta"), + ..Default::default() + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["publish", "--tag", "beta"]); + } + + #[test] + fn test_pnpm_publish_with_filter() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_publish_command(&PublishCommandOptions { + filters: Some(&["app".to_string()]), + recursive: true, + ..Default::default() + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["--filter", "app", "publish", "--recursive"]); + } + + #[test] + fn test_npm_publish_basic() { + let pm = create_mock_package_manager(PackageManagerType::Npm); + let result = pm.resolve_publish_command(&PublishCommandOptions::default()); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["publish"]); + } + + #[test] + fn test_npm_publish_with_access() { + let pm = create_mock_package_manager(PackageManagerType::Npm); + let result = pm.resolve_publish_command(&PublishCommandOptions { + access: Some("public"), + ..Default::default() + }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["publish", "--access", "public"]); + } + + #[test] + fn test_yarn1_publish_basic() { + let pm = create_mock_package_manager(PackageManagerType::Yarn); + let result = pm.resolve_publish_command(&PublishCommandOptions::default()); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["publish"]); + } + + #[test] + fn test_yarn2_publish_uses_npm_plugin() { + 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::Yarn, + package_name: "yarn".into(), + version: Str::from("4.0.0"), + hash: None, + bin_name: "yarn".into(), + workspace_root: temp_dir_path.clone(), + install_dir, + }; + + let result = pm.resolve_publish_command(&PublishCommandOptions::default()); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["npm", "publish"]); + } +} diff --git a/crates/vite_package_manager/src/commands/view.rs b/crates/vite_package_manager/src/commands/view.rs new file mode 100644 index 0000000000..f9ec4c0391 --- /dev/null +++ b/crates/vite_package_manager/src/commands/view.rs @@ -0,0 +1,189 @@ +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, +}; + +#[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. + #[must_use] + pub fn resolve_view_command(&self, options: &ViewCommandOptions) -> ResolveCommandResult { + 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("view".into()); + args.push(options.package.into()); + if let Some(field) = options.field { + args.push(field.into()); + } + if options.json { + args.push("--json".into()); + } + } + PackageManagerType::Npm => { + bin_name = "npm".into(); + args.push("view".into()); + args.push(options.package.into()); + if let Some(field) = options.field { + args.push(field.into()); + } + if options.json { + args.push("--json".into()); + } + } + PackageManagerType::Yarn => { + bin_name = "yarn".into(); + // yarn uses 'info' instead of 'view' + args.push("info".into()); + args.push(options.package.into()); + if let Some(field) = options.field { + args.push(field.into()); + } + if options.json { + args.push("--json".into()); + } + } + } + + // 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) -> 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("1.0.0"), + hash: None, + bin_name: pm_type.to_string().into(), + workspace_root: temp_dir_path.clone(), + install_dir, + } + } + + #[test] + fn test_pnpm_view_basic() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_view_command(&ViewCommandOptions { + package: "react", + field: None, + json: false, + pass_through_args: None, + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["view", "react"]); + } + + #[test] + fn test_pnpm_view_with_field() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_view_command(&ViewCommandOptions { + package: "react", + field: Some("version"), + json: false, + pass_through_args: None, + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["view", "react", "version"]); + } + + #[test] + fn test_pnpm_view_with_json() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_view_command(&ViewCommandOptions { + package: "react", + field: None, + json: true, + pass_through_args: None, + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["view", "react", "--json"]); + } + + #[test] + fn test_npm_view_basic() { + let pm = create_mock_package_manager(PackageManagerType::Npm); + 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_yarn_view_maps_to_info() { + let pm = create_mock_package_manager(PackageManagerType::Yarn); + let result = pm.resolve_view_command(&ViewCommandOptions { + package: "react", + field: None, + json: false, + pass_through_args: None, + }); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["info", "react"]); + } + + #[test] + fn test_yarn_view_with_field() { + let pm = create_mock_package_manager(PackageManagerType::Yarn); + let result = pm.resolve_view_command(&ViewCommandOptions { + package: "react", + field: Some("version"), + json: false, + pass_through_args: None, + }); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["info", "react", "version"]); + } +} diff --git a/packages/cli/binding/src/cli.rs b/packages/cli/binding/src/cli.rs index f2dbb750d2..f827ca50b7 100644 --- a/packages/cli/binding/src/cli.rs +++ b/packages/cli/binding/src/cli.rs @@ -276,6 +276,9 @@ pub enum Commands { #[arg(last = true, allow_hyphen_values = true)] pass_through_args: Option>, }, + /// Package manager utilities + #[command(disable_help_flag = true, subcommand)] + Pm(PmCommands), } impl Commands { @@ -293,6 +296,231 @@ pub enum CacheSubcommand { View, } +#[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, + + /// Include all transitive dependencies + #[arg(long)] + all: bool, + + /// 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, + }, +} + /// Resolve boolean flag value considering both positive and negative forms. /// If the negative form (--no-*) is present, it takes precedence and returns false. /// Otherwise, returns the value of the positive form. @@ -703,6 +931,10 @@ pub async fn main< .await?; return Ok(exit_status); } + Commands::Pm(pm_command) => { + let exit_status = execute_pm_command(cwd, pm_command).await?; + return Ok(exit_status); + } }; let execution_summary_dir = EXECUTION_SUMMARY_DIR.as_path(); @@ -880,6 +1112,165 @@ async fn execute_add_command( .await } +/// Execute pm command with the given subcommand +async fn execute_pm_command( + cwd: AbsolutePathBuf, + pm_command: &PmCommands, +) -> Result { + use vite_package_manager::{ + commands::{ + cache::{CacheCommandOptions, CacheSubcommand}, + config::{ConfigCommandOptions, ConfigSubcommand}, + list::ListCommandOptions, + owner::OwnerSubcommand, + pack::PackCommandOptions, + prune::PruneCommandOptions, + publish::PublishCommandOptions, + view::ViewCommandOptions, + }, + package_manager::PackageManager, + }; + + let package_manager = PackageManager::builder(&cwd).build().await?; + + match pm_command { + PmCommands::Prune { prod, no_optional, args } => { + let options = PruneCommandOptions { + prod: *prod, + no_optional: *no_optional, + pass_through_args: if args.is_empty() { None } else { Some(args) }, + }; + package_manager.run_prune_command(&options, &cwd).await + } + PmCommands::Pack { dry_run, pack_destination, pack_gzip_level, json, args } => { + let options = PackCommandOptions { + dry_run: *dry_run, + pack_destination: pack_destination.as_deref(), + pack_gzip_level: *pack_gzip_level, + json: *json, + pass_through_args: if args.is_empty() { None } else { Some(args) }, + }; + package_manager.run_pack_command(&options, &cwd).await + } + PmCommands::List { + pattern, + all, + depth, + json, + long, + parseable, + prod, + dev, + recursive, + filter, + workspace, + global, + args, + } => { + let options = ListCommandOptions { + pattern: pattern.as_deref(), + all: *all, + depth: *depth, + json: *json, + long: *long, + parseable: *parseable, + prod: *prod, + dev: *dev, + recursive: *recursive, + filters: if filter.is_empty() { None } else { Some(filter) }, + workspaces: if workspace.is_empty() { None } else { Some(workspace) }, + global: *global, + pass_through_args: if args.is_empty() { None } else { Some(args) }, + }; + package_manager.run_list_command(&options, &cwd).await + } + PmCommands::View { package, field, json, args } => { + let options = ViewCommandOptions { + package, + field: field.as_deref(), + json: *json, + pass_through_args: if args.is_empty() { None } else { Some(args) }, + }; + package_manager.run_view_command(&options, &cwd).await + } + PmCommands::Publish { + target, + dry_run, + tag, + access, + no_git_checks, + force, + recursive, + filter, + workspace, + args, + } => { + let options = PublishCommandOptions { + target: target.as_deref(), + dry_run: *dry_run, + tag: tag.as_deref(), + access: access.as_deref(), + no_git_checks: *no_git_checks, + force: *force, + recursive: *recursive, + filters: if filter.is_empty() { None } else { Some(filter) }, + workspaces: if workspace.is_empty() { None } else { Some(workspace) }, + pass_through_args: if args.is_empty() { None } else { Some(args) }, + }; + package_manager.run_publish_command(&options, &cwd).await + } + PmCommands::Owner { command } => match command { + OwnerCommands::List { package } => { + let subcommand = OwnerSubcommand::List { package }; + package_manager.run_owner_command(subcommand, &cwd).await + } + OwnerCommands::Add { user, package } => { + let subcommand = OwnerSubcommand::Add { user, package }; + package_manager.run_owner_command(subcommand, &cwd).await + } + OwnerCommands::Rm { user, package } => { + let subcommand = OwnerSubcommand::Rm { user, package }; + package_manager.run_owner_command(subcommand, &cwd).await + } + }, + PmCommands::Cache { subcommand, force, args } => { + let cache_subcmd = subcommand.as_ref().and_then(|s| match s.as_str() { + "dir" => Some(CacheSubcommand::Dir), + "path" => Some(CacheSubcommand::Path), + "clean" => Some(CacheSubcommand::Clean), + "clear" => Some(CacheSubcommand::Clear), + "verify" => Some(CacheSubcommand::Verify), + "list" => Some(CacheSubcommand::List), + _ => None, + }); + + let options = CacheCommandOptions { + subcommand: cache_subcmd, + force: *force, + pass_through_args: if args.is_empty() { None } else { Some(args) }, + }; + package_manager.run_cache_command(&options, &cwd).await + } + PmCommands::Config { subcommand, key, value, json, global, args } => { + let config_subcmd = match (subcommand.as_deref(), key.as_deref(), value.as_deref()) { + (Some("list"), _, _) | (None, None, None) => Some(ConfigSubcommand::List), + (Some("get"), Some(k), _) => Some(ConfigSubcommand::Get { key: k }), + (Some("set"), Some(k), Some(v)) => Some(ConfigSubcommand::Set { key: k, value: v }), + (Some("delete"), Some(k), _) => Some(ConfigSubcommand::Delete { key: k }), + _ => None, + }; + + let options = ConfigCommandOptions { + subcommand: config_subcmd, + json: *json, + global: *global, + pass_through_args: if args.is_empty() { None } else { Some(args) }, + }; + package_manager.run_config_command(&options, &cwd).await + } + } +} + #[cfg(test)] mod tests { use clap::Parser; 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 fcc02d71eb..7bf284bd8d 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, help] + [subcommands: run, lint, fmt, build, test, lib, dev, doc, cache, install, i, add, remove, rm, un, uninstall, update, up, 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 9d92301178..ec1cc4dbeb 100644 --- a/packages/global/snap-tests/cli-helper-message/snap.txt +++ b/packages/global/snap-tests/cli-helper-message/snap.txt @@ -15,6 +15,7 @@ Commands: add Add packages to dependencies remove Remove packages from dependencies update Update packages to their latest versions + pm Package manager utilities help Print this message or the help of the given subcommand(s) Arguments: diff --git a/rfcs/pm-command-group.md b/rfcs/pm-command-group.md new file mode 100644 index 0000000000..b5afe7a205 --- /dev/null +++ b/rfcs/pm-command-group.md @@ -0,0 +1,1448 @@ +# 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 + +# Dry run to see what would be included +vite pm pack --dry-run + +# Specify output directory +vite pm pack --pack-destination ./dist + +# Custom gzip compression level +vite pm pack --pack-gzip-level 9 +``` + +**Options:** + +- `--dry-run`: Preview what would be packed +- `--pack-destination `: Output directory for tarball +- `--json`: Output in JSON format (npm) +- `--pack-gzip-level `: Compression level 0-9 + +#### 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 all dependencies including transitive +vite pm list --all + +# 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 globally installed packages +vite pm list -g +``` + +**Options:** + +- `--all`: Include all transitive dependencies +- `--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 (pnpm) +- `--workspace `: Specific workspace (npm) +- `-g, --global`: List global packages + +#### 4. vite pm view + +View package information from the registry. + +```bash +vite pm view [@version] [FIELD] [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 dependencies + +# JSON output +vite pm view react --json +``` + +**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 + +Manage package manager configuration. + +```bash +vite pm config [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 + +# Delete config key +vite pm config delete registry + +# JSON output +vite pm config list --json +``` + +**Options:** + +- `--json`: JSON output format +- `--global`: Use global config + +### Command Mapping + +#### Prune Command + +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | +| --------------- | --------------- | ----------------- | ------------ | ------- | ----------------------------- | +| `vite pm prune` | `pnpm prune` | N/A (use install) | `yarn prune` | N/A | Remove unnecessary packages | +| `--prod` | `--prod` | N/A | N/A | N/A | Remove devDependencies (pnpm) | +| `--no-optional` | `--no-optional` | N/A | N/A | N/A | Remove optional deps (pnpm) | + +**Note:** + +- npm doesn't have a prune command (deprecated in v6) +- yarn@1 has prune but it's automatic during install +- yarn@2+ doesn't have separate prune command + +#### Pack Command + +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | +| -------------------- | -------------------- | ----------- | ------------ | ----------- | ------------------------ | +| `vite pm pack` | `pnpm pack` | `npm pack` | `yarn pack` | `yarn pack` | Create package tarball | +| `--dry-run` | `--dry-run` | `--dry-run` | `--dry-run` | `--dry-run` | Preview without creating | +| `--pack-destination` | `--pack-destination` | N/A | `--filename` | `--out` | Output location | +| `--pack-gzip-level` | N/A | N/A | N/A | N/A | Compression level | +| `--json` | N/A | `--json` | N/A | N/A | JSON output (npm) | + +#### List Command + +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | +| -------------------- | ------------------ | --------------- | -------------- | -------------- | ----------------------- | +| `vite pm list` | `pnpm list` | `npm list` | `yarn list` | `yarn list` | List installed packages | +| `vite pm ls` | `pnpm ls` | `npm ls` | N/A | N/A | Alias for list | +| `--all` | N/A | `--all` | N/A | `--all` | Include transitive deps | +| `--depth ` | `--depth ` | `--depth ` | `--depth ` | `--depth ` | Limit tree depth | +| `--json` | `--json` | `--json` | `--json` | `--json` | JSON output | +| `--long` | `--long` | `--long` | N/A | N/A | Extended info | +| `--parseable` | `--parseable` | `--parseable` | N/A | N/A | Parseable format | +| `--prod` | `--prod` | `--production` | `--production` | `--production` | Production deps only | +| `--dev` | `--dev` | `--development` | N/A | N/A | Dev deps only | +| `-r, --recursive` | `-r` | N/A | N/A | `-R` | List across workspaces | +| `--filter ` | `--filter` | N/A | N/A | N/A | Filter workspace (pnpm) | +| `--workspace ` | Maps to `--filter` | `--workspace` | N/A | `--workspace` | Specific workspace | +| `-g, --global` | `-g` | `-g` | N/A | N/A | List global packages | + +#### View Command + +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | +| -------------- | ----------- | ---------- | ----------- | ----------- | ----------------- | +| `vite pm view` | `pnpm view` | `npm view` | `yarn info` | `yarn info` | View package info | +| `--json` | `--json` | `--json` | `--json` | `--json` | JSON output | + +#### Publish Command + +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | +| ------------------ | ------------------ | ------------------ | ------------------ | ------------------ | -------------------------- | +| `vite pm publish` | `pnpm publish` | `npm publish` | `yarn publish` | `yarn npm publish` | Publish package | +| `--dry-run` | `--dry-run` | `--dry-run` | N/A | `--dry-run` | Preview without publishing | +| `--tag ` | `--tag ` | `--tag ` | `--tag ` | `--tag ` | Publish tag | +| `--access ` | `--access ` | `--access ` | `--access ` | `--access ` | Public/restricted | +| `--no-git-checks` | `--no-git-checks` | N/A | N/A | N/A | Skip git checks (pnpm) | +| `--force` | `--force` | `--force` | N/A | N/A | Force publish | +| `-r, --recursive` | `-r` | N/A | N/A | N/A | Publish workspaces (pnpm) | +| `--filter` | `--filter` | N/A | N/A | N/A | Filter workspace (pnpm) | +| `--workspace` | Maps to `--filter` | `--workspace` | N/A | `--workspace` | Specific workspace | + +#### Owner Command + +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | +| --------------------------- | ----------------- | ---------------- | ----------------- | ---------------- | ------------------- | +| `vite pm owner list ` | `pnpm owner list` | `npm owner list` | `yarn owner list` | `yarn npm owner` | List package owners | +| `vite pm owner add

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

` | `pnpm owner rm` | `npm owner rm` | `yarn owner rm` | `yarn npm owner` | Remove owner | + +#### Cache Command + +| Vite+ Flag | pnpm | npm | yarn@1 | yarn@2+ | Description | +| ---------------------- | ------------------ | ------------------ | ------------------ | ------------------ | -------------------- | +| `vite pm cache dir` | `pnpm store path` | `npm cache dir` | `yarn cache dir` | Maps to path | Show cache directory | +| `vite pm cache path` | `pnpm store path` | Maps to `dir` | Maps to `dir` | N/A | Alias for dir | +| `vite pm cache clean` | `pnpm store prune` | `npm cache clean` | `yarn cache clean` | `yarn cache clean` | Clean cache | +| `vite pm cache clear` | Maps to `clean` | Maps to `clean` | Maps to `clean` | Maps to `clean` | Alias for clean | +| `--force` | N/A | `--force` | N/A | N/A | Force clean (npm) | +| `vite pm cache verify` | N/A | `npm cache verify` | N/A | N/A | Verify cache (npm) | +| `vite pm cache list` | `pnpm store list` | N/A | `yarn cache list` | N/A | List cached packages | + +#### Config Command + +| 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` | N/A | `--json` | N/A | N/A | JSON output (npm) | +| `--global` | `--global` | `--global` | `--global` | N/A | Global config | + +### 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, + + /// Include all transitive dependencies + #[arg(long)] + all: bool, + + /// 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: npm@11.0.0 +Warning: npm removed 'prune' command in v6. Use 'vite install --prod' instead. +``` + +### 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 + +### 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 | ❌ Removed | ✅ Full | ❌ N/A | npm deprecated in v6 | +| pack | ✅ Full | ✅ Full | ✅ Full | ✅ Full | All supported | +| list/ls | ✅ Full | ✅ Full | ✅ Full | ✅ Full | All 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 all dependencies +vite pm list --all --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.