Skip to content

Commit b32a3ba

Browse files
committed
feat(pm): add unlink command
1 parent b722c55 commit b32a3ba

5 files changed

Lines changed: 261 additions & 0 deletions

File tree

crates/vite_install/src/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ mod install;
44
pub mod link;
55
pub mod outdated;
66
pub mod remove;
7+
pub mod unlink;
78
pub mod update;
89
pub mod why;
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
use std::{collections::HashMap, process::ExitStatus};
2+
3+
use vite_error::Error;
4+
use vite_path::AbsolutePath;
5+
6+
use crate::package_manager::{
7+
PackageManager, PackageManagerType, ResolveCommandResult, format_path_env, run_command,
8+
};
9+
10+
/// Options for the unlink command.
11+
#[derive(Debug, Default)]
12+
pub struct UnlinkCommandOptions<'a> {
13+
pub package: Option<&'a str>,
14+
pub recursive: bool,
15+
pub pass_through_args: Option<&'a [String]>,
16+
}
17+
18+
impl PackageManager {
19+
/// Run the unlink command with the package manager.
20+
/// Return the exit status of the command.
21+
#[must_use]
22+
pub async fn run_unlink_command(
23+
&self,
24+
options: &UnlinkCommandOptions<'_>,
25+
cwd: impl AsRef<AbsolutePath>,
26+
) -> Result<ExitStatus, Error> {
27+
let resolve_command = self.resolve_unlink_command(options);
28+
run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd)
29+
.await
30+
}
31+
32+
/// Resolve the unlink command.
33+
#[must_use]
34+
pub fn resolve_unlink_command(&self, options: &UnlinkCommandOptions) -> ResolveCommandResult {
35+
let bin_name: String;
36+
let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]);
37+
let mut args: Vec<String> = Vec::new();
38+
39+
match self.client {
40+
PackageManagerType::Pnpm => {
41+
bin_name = "pnpm".into();
42+
args.push("unlink".into());
43+
44+
if options.recursive {
45+
args.push("-r".into());
46+
}
47+
}
48+
PackageManagerType::Yarn => {
49+
bin_name = "yarn".into();
50+
args.push("unlink".into());
51+
52+
if options.recursive {
53+
eprintln!("Warning: yarn doesn't support --recursive for unlink command");
54+
}
55+
}
56+
PackageManagerType::Npm => {
57+
bin_name = "npm".into();
58+
args.push("unlink".into());
59+
60+
if options.recursive {
61+
eprintln!("Warning: npm doesn't support --recursive for unlink command");
62+
}
63+
}
64+
}
65+
66+
// Add package if specified
67+
if let Some(package) = options.package {
68+
args.push(package.to_string());
69+
}
70+
71+
// Add pass-through args
72+
if let Some(pass_through_args) = options.pass_through_args {
73+
args.extend_from_slice(pass_through_args);
74+
}
75+
76+
ResolveCommandResult { bin_path: bin_name, args, envs }
77+
}
78+
}
79+
80+
#[cfg(test)]
81+
mod tests {
82+
use tempfile::{TempDir, tempdir};
83+
use vite_path::AbsolutePathBuf;
84+
use vite_str::Str;
85+
86+
use super::*;
87+
88+
fn create_temp_dir() -> TempDir {
89+
tempdir().expect("Failed to create temp directory")
90+
}
91+
92+
fn create_mock_package_manager(pm_type: PackageManagerType, version: &str) -> PackageManager {
93+
let temp_dir = create_temp_dir();
94+
let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
95+
let install_dir = temp_dir_path.join("install");
96+
97+
PackageManager {
98+
client: pm_type,
99+
package_name: pm_type.to_string().into(),
100+
version: Str::from(version),
101+
hash: None,
102+
bin_name: pm_type.to_string().into(),
103+
workspace_root: temp_dir_path.clone(),
104+
install_dir,
105+
}
106+
}
107+
108+
#[test]
109+
fn test_pnpm_unlink_no_package() {
110+
let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0");
111+
let result = pm.resolve_unlink_command(&UnlinkCommandOptions { ..Default::default() });
112+
assert_eq!(result.bin_path, "pnpm");
113+
assert_eq!(result.args, vec!["unlink"]);
114+
}
115+
116+
#[test]
117+
fn test_pnpm_unlink_package() {
118+
let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0");
119+
let result = pm.resolve_unlink_command(&UnlinkCommandOptions {
120+
package: Some("react"),
121+
..Default::default()
122+
});
123+
assert_eq!(result.bin_path, "pnpm");
124+
assert_eq!(result.args, vec!["unlink", "react"]);
125+
}
126+
127+
#[test]
128+
fn test_pnpm_unlink_recursive() {
129+
let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0");
130+
let result = pm.resolve_unlink_command(&UnlinkCommandOptions {
131+
recursive: true,
132+
..Default::default()
133+
});
134+
assert_eq!(result.bin_path, "pnpm");
135+
assert_eq!(result.args, vec!["unlink", "-r"]);
136+
}
137+
138+
#[test]
139+
fn test_pnpm_unlink_package_recursive() {
140+
let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0");
141+
let result = pm.resolve_unlink_command(&UnlinkCommandOptions {
142+
package: Some("react"),
143+
recursive: true,
144+
..Default::default()
145+
});
146+
assert_eq!(result.bin_path, "pnpm");
147+
assert_eq!(result.args, vec!["unlink", "-r", "react"]);
148+
}
149+
150+
#[test]
151+
fn test_yarn_unlink_basic() {
152+
let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0");
153+
let result = pm.resolve_unlink_command(&UnlinkCommandOptions { ..Default::default() });
154+
assert_eq!(result.bin_path, "yarn");
155+
assert_eq!(result.args, vec!["unlink"]);
156+
}
157+
158+
#[test]
159+
fn test_yarn_unlink_package() {
160+
let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0");
161+
let result = pm.resolve_unlink_command(&UnlinkCommandOptions {
162+
package: Some("react"),
163+
..Default::default()
164+
});
165+
assert_eq!(result.bin_path, "yarn");
166+
assert_eq!(result.args, vec!["unlink", "react"]);
167+
}
168+
169+
#[test]
170+
fn test_npm_unlink_basic() {
171+
let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0");
172+
let result = pm.resolve_unlink_command(&UnlinkCommandOptions { ..Default::default() });
173+
assert_eq!(result.bin_path, "npm");
174+
assert_eq!(result.args, vec!["unlink"]);
175+
}
176+
177+
#[test]
178+
fn test_npm_unlink_package() {
179+
let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0");
180+
let result = pm.resolve_unlink_command(&UnlinkCommandOptions {
181+
package: Some("react"),
182+
..Default::default()
183+
});
184+
assert_eq!(result.bin_path, "npm");
185+
assert_eq!(result.args, vec!["unlink", "react"]);
186+
}
187+
}

packages/cli/binding/src/cli.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ use crate::commands::{
2929
outdated::OutdatedCommand,
3030
remove::RemoveCommand,
3131
test::test,
32+
unlink::UnlinkCommand,
3233
update::UpdateCommand,
3334
vite::vite as vite_cmd,
3435
why::WhyCommand,
@@ -409,6 +410,21 @@ pub enum Commands {
409410
/// If empty, registers current package globally
410411
package: Option<String>,
411412

413+
/// Arguments to pass to package manager
414+
#[arg(allow_hyphen_values = true, trailing_var_arg = true)]
415+
args: Vec<String>,
416+
},
417+
/// Unlink packages
418+
#[command(disable_help_flag = true)]
419+
Unlink {
420+
/// Package name to unlink
421+
/// If empty, unlinks current package globally
422+
package: Option<String>,
423+
424+
/// Unlink in every workspace package (pnpm only)
425+
#[arg(short = 'r', long)]
426+
recursive: bool,
427+
412428
/// Arguments to pass to package manager
413429
#[arg(allow_hyphen_values = true, trailing_var_arg = true)]
414430
args: Vec<String>,
@@ -427,6 +443,7 @@ impl Commands {
427443
| Commands::Outdated { .. }
428444
| Commands::Why { .. }
429445
| Commands::Link { .. }
446+
| Commands::Unlink { .. }
430447
)
431448
}
432449
}
@@ -892,6 +909,11 @@ pub async fn main<
892909
let exit_status = LinkCommand::new(cwd).execute(package.as_deref(), Some(args)).await?;
893910
return Ok(exit_status);
894911
}
912+
Commands::Unlink { package, recursive, args } => {
913+
let exit_status =
914+
UnlinkCommand::new(cwd).execute(package.as_deref(), *recursive, Some(args)).await?;
915+
return Ok(exit_status);
916+
}
895917
Commands::Why {
896918
packages,
897919
json,

packages/cli/binding/src/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub(crate) mod lint;
99
pub(crate) mod outdated;
1010
pub(crate) mod remove;
1111
pub(crate) mod test;
12+
pub(crate) mod unlink;
1213
pub(crate) mod update;
1314
pub(crate) mod vite;
1415
pub(crate) mod why;
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
use std::process::ExitStatus;
2+
3+
use vite_install::{commands::unlink::UnlinkCommandOptions, package_manager::PackageManager};
4+
use vite_path::AbsolutePathBuf;
5+
6+
use crate::Error;
7+
8+
/// Unlink command for removing package links.
9+
///
10+
/// This command automatically detects the package manager and translates
11+
/// the unlink command to the appropriate package manager-specific syntax.
12+
pub struct UnlinkCommand {
13+
cwd: AbsolutePathBuf,
14+
}
15+
16+
impl UnlinkCommand {
17+
pub fn new(cwd: AbsolutePathBuf) -> Self {
18+
Self { cwd }
19+
}
20+
21+
pub async fn execute(
22+
self,
23+
package: Option<&str>,
24+
recursive: bool,
25+
pass_through_args: Option<&[String]>,
26+
) -> Result<ExitStatus, Error> {
27+
// Detect package manager
28+
let package_manager = PackageManager::builder(&self.cwd).build().await?;
29+
30+
let unlink_command_options = UnlinkCommandOptions { package, recursive, pass_through_args };
31+
package_manager.run_unlink_command(&unlink_command_options, &self.cwd).await
32+
}
33+
}
34+
35+
#[cfg(test)]
36+
mod tests {
37+
use super::*;
38+
39+
#[test]
40+
fn test_unlink_command_new() {
41+
let workspace_root = if cfg!(windows) {
42+
AbsolutePathBuf::new("C:\\test".into()).unwrap()
43+
} else {
44+
AbsolutePathBuf::new("/test".into()).unwrap()
45+
};
46+
47+
let cmd = UnlinkCommand::new(workspace_root.clone());
48+
assert_eq!(cmd.cwd, workspace_root);
49+
}
50+
}

0 commit comments

Comments
 (0)