diff --git a/crates/vite_install/src/commands/mod.rs b/crates/vite_install/src/commands/mod.rs index e68cb39f02..ca9b7e12ee 100644 --- a/crates/vite_install/src/commands/mod.rs +++ b/crates/vite_install/src/commands/mod.rs @@ -4,5 +4,6 @@ mod install; pub mod link; pub mod outdated; pub mod remove; +pub mod unlink; pub mod update; pub mod why; diff --git a/crates/vite_install/src/commands/unlink.rs b/crates/vite_install/src/commands/unlink.rs new file mode 100644 index 0000000000..7c7ce760f8 --- /dev/null +++ b/crates/vite_install/src/commands/unlink.rs @@ -0,0 +1,187 @@ +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, +}; + +/// Options for the unlink command. +#[derive(Debug, Default)] +pub struct UnlinkCommandOptions<'a> { + pub package: Option<&'a str>, + pub recursive: bool, + pub pass_through_args: Option<&'a [String]>, +} + +impl PackageManager { + /// Run the unlink command with the package manager. + /// Return the exit status of the command. + #[must_use] + pub async fn run_unlink_command( + &self, + options: &UnlinkCommandOptions<'_>, + cwd: impl AsRef, + ) -> Result { + let resolve_command = self.resolve_unlink_command(options); + run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) + .await + } + + /// Resolve the unlink command. + #[must_use] + pub fn resolve_unlink_command(&self, options: &UnlinkCommandOptions) -> 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("unlink".into()); + + if options.recursive { + args.push("-r".into()); + } + } + PackageManagerType::Yarn => { + bin_name = "yarn".into(); + args.push("unlink".into()); + + if options.recursive { + eprintln!("Warning: yarn doesn't support --recursive for unlink command"); + } + } + PackageManagerType::Npm => { + bin_name = "npm".into(); + args.push("unlink".into()); + + if options.recursive { + eprintln!("Warning: npm doesn't support --recursive for unlink command"); + } + } + } + + // Add package if specified + if let Some(package) = options.package { + args.push(package.to_string()); + } + + // Add pass-through args + if let Some(pass_through_args) = options.pass_through_args { + args.extend_from_slice(pass_through_args); + } + + ResolveCommandResult { bin_path: bin_name, args, envs } + } +} + +#[cfg(test)] +mod tests { + use tempfile::{TempDir, tempdir}; + use vite_path::AbsolutePathBuf; + use vite_str::Str; + + use super::*; + + fn create_temp_dir() -> TempDir { + tempdir().expect("Failed to create temp directory") + } + + fn create_mock_package_manager(pm_type: PackageManagerType, version: &str) -> PackageManager { + let temp_dir = create_temp_dir(); + let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + let install_dir = temp_dir_path.join("install"); + + PackageManager { + client: pm_type, + package_name: pm_type.to_string().into(), + version: Str::from(version), + hash: None, + bin_name: pm_type.to_string().into(), + workspace_root: temp_dir_path.clone(), + install_dir, + } + } + + #[test] + fn test_pnpm_unlink_no_package() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = pm.resolve_unlink_command(&UnlinkCommandOptions { ..Default::default() }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["unlink"]); + } + + #[test] + fn test_pnpm_unlink_package() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = pm.resolve_unlink_command(&UnlinkCommandOptions { + package: Some("react"), + ..Default::default() + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["unlink", "react"]); + } + + #[test] + fn test_pnpm_unlink_recursive() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = pm.resolve_unlink_command(&UnlinkCommandOptions { + recursive: true, + ..Default::default() + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["unlink", "-r"]); + } + + #[test] + fn test_pnpm_unlink_package_recursive() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let result = pm.resolve_unlink_command(&UnlinkCommandOptions { + package: Some("react"), + recursive: true, + ..Default::default() + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["unlink", "-r", "react"]); + } + + #[test] + fn test_yarn_unlink_basic() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); + let result = pm.resolve_unlink_command(&UnlinkCommandOptions { ..Default::default() }); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["unlink"]); + } + + #[test] + fn test_yarn_unlink_package() { + let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0"); + let result = pm.resolve_unlink_command(&UnlinkCommandOptions { + package: Some("react"), + ..Default::default() + }); + assert_eq!(result.bin_path, "yarn"); + assert_eq!(result.args, vec!["unlink", "react"]); + } + + #[test] + fn test_npm_unlink_basic() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = pm.resolve_unlink_command(&UnlinkCommandOptions { ..Default::default() }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["unlink"]); + } + + #[test] + fn test_npm_unlink_package() { + let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0"); + let result = pm.resolve_unlink_command(&UnlinkCommandOptions { + package: Some("react"), + ..Default::default() + }); + assert_eq!(result.bin_path, "npm"); + assert_eq!(result.args, vec!["unlink", "react"]); + } +} diff --git a/packages/cli/binding/src/cli.rs b/packages/cli/binding/src/cli.rs index ef8f982ebd..4f4b1fe484 100644 --- a/packages/cli/binding/src/cli.rs +++ b/packages/cli/binding/src/cli.rs @@ -29,6 +29,7 @@ use crate::commands::{ outdated::OutdatedCommand, remove::RemoveCommand, test::test, + unlink::UnlinkCommand, update::UpdateCommand, vite::vite as vite_cmd, why::WhyCommand, @@ -414,6 +415,21 @@ pub enum Commands { /// If empty, registers current package globally package: Option, + /// Arguments to pass to package manager + #[arg(allow_hyphen_values = true, trailing_var_arg = true)] + args: Vec, + }, + /// Unlink packages + #[command(disable_help_flag = true)] + Unlink { + /// Package name to unlink + /// If empty, unlinks current package globally + package: Option, + + /// Unlink in every workspace package (pnpm only) + #[arg(short = 'r', long)] + recursive: bool, + /// Arguments to pass to package manager #[arg(allow_hyphen_values = true, trailing_var_arg = true)] args: Vec, @@ -432,6 +448,7 @@ impl Commands { | Commands::Outdated { .. } | Commands::Why { .. } | Commands::Link { .. } + | Commands::Unlink { .. } ) } } @@ -897,6 +914,11 @@ pub async fn main< let exit_status = LinkCommand::new(cwd).execute(package.as_deref(), Some(args)).await?; return Ok(exit_status); } + Commands::Unlink { package, recursive, args } => { + let exit_status = + UnlinkCommand::new(cwd).execute(package.as_deref(), *recursive, Some(args)).await?; + return Ok(exit_status); + } Commands::Why { packages, json, diff --git a/packages/cli/binding/src/commands/mod.rs b/packages/cli/binding/src/commands/mod.rs index 8736b6efe1..0164630fb9 100644 --- a/packages/cli/binding/src/commands/mod.rs +++ b/packages/cli/binding/src/commands/mod.rs @@ -9,6 +9,7 @@ pub(crate) mod lint; pub(crate) mod outdated; pub(crate) mod remove; pub(crate) mod test; +pub(crate) mod unlink; pub(crate) mod update; pub(crate) mod vite; pub(crate) mod why; diff --git a/packages/cli/binding/src/commands/unlink.rs b/packages/cli/binding/src/commands/unlink.rs new file mode 100644 index 0000000000..a3e81a693a --- /dev/null +++ b/packages/cli/binding/src/commands/unlink.rs @@ -0,0 +1,50 @@ +use std::process::ExitStatus; + +use vite_install::{commands::unlink::UnlinkCommandOptions, package_manager::PackageManager}; +use vite_path::AbsolutePathBuf; + +use crate::Error; + +/// Unlink command for removing package links. +/// +/// This command automatically detects the package manager and translates +/// the unlink command to the appropriate package manager-specific syntax. +pub struct UnlinkCommand { + cwd: AbsolutePathBuf, +} + +impl UnlinkCommand { + pub fn new(cwd: AbsolutePathBuf) -> Self { + Self { cwd } + } + + pub async fn execute( + self, + package: Option<&str>, + recursive: bool, + pass_through_args: Option<&[String]>, + ) -> Result { + // Detect package manager + let package_manager = PackageManager::builder(&self.cwd).build().await?; + + let unlink_command_options = UnlinkCommandOptions { package, recursive, pass_through_args }; + package_manager.run_unlink_command(&unlink_command_options, &self.cwd).await + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_unlink_command_new() { + let workspace_root = if cfg!(windows) { + AbsolutePathBuf::new("C:\\test".into()).unwrap() + } else { + AbsolutePathBuf::new("/test".into()).unwrap() + }; + + let cmd = UnlinkCommand::new(workspace_root.clone()); + assert_eq!(cmd.cwd, workspace_root); + } +}