Skip to content

Commit 8bc85e5

Browse files
committed
feat(pm): add why commands
1 parent 08a11f1 commit 8bc85e5

8 files changed

Lines changed: 1839 additions & 1 deletion

File tree

crates/vite_install/src/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ mod install;
44
pub mod outdated;
55
pub mod remove;
66
pub mod update;
7+
pub mod why;
Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
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 why command.
11+
#[derive(Debug, Default)]
12+
pub struct WhyCommandOptions<'a> {
13+
pub packages: &'a [String],
14+
pub json: bool,
15+
pub long: bool,
16+
pub parseable: bool,
17+
pub recursive: bool,
18+
pub filters: Option<&'a [String]>,
19+
pub workspace_root: bool,
20+
pub prod: bool,
21+
pub dev: bool,
22+
pub depth: Option<u32>,
23+
pub no_optional: bool,
24+
pub global: bool,
25+
pub exclude_peers: bool,
26+
pub pass_through_args: Option<&'a [String]>,
27+
}
28+
29+
impl PackageManager {
30+
/// Run the why command with the package manager.
31+
/// Return the exit status of the command.
32+
#[must_use]
33+
pub async fn run_why_command(
34+
&self,
35+
options: &WhyCommandOptions<'_>,
36+
cwd: impl AsRef<AbsolutePath>,
37+
) -> Result<ExitStatus, Error> {
38+
let resolve_command = self.resolve_why_command(options);
39+
run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd)
40+
.await
41+
}
42+
43+
/// Resolve the why command.
44+
#[must_use]
45+
pub fn resolve_why_command(&self, options: &WhyCommandOptions) -> ResolveCommandResult {
46+
let bin_name: String;
47+
let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]);
48+
let mut args: Vec<String> = Vec::new();
49+
50+
if options.packages.is_empty() {
51+
eprintln!("Warning: No packages specified");
52+
return ResolveCommandResult {
53+
bin_path: "echo".into(),
54+
args: vec!["No packages specified".into()],
55+
envs,
56+
};
57+
}
58+
59+
match self.client {
60+
PackageManagerType::Pnpm => {
61+
bin_name = "pnpm".into();
62+
63+
// pnpm: --filter must come before command
64+
if let Some(filters) = options.filters {
65+
for filter in filters {
66+
args.push("--filter".into());
67+
args.push(filter.clone());
68+
}
69+
}
70+
71+
args.push("why".into());
72+
73+
if options.json {
74+
args.push("--json".into());
75+
}
76+
77+
if options.long {
78+
args.push("--long".into());
79+
}
80+
81+
if options.parseable {
82+
args.push("--parseable".into());
83+
}
84+
85+
if options.recursive {
86+
args.push("--recursive".into());
87+
}
88+
89+
if options.workspace_root {
90+
args.push("--workspace-root".into());
91+
}
92+
93+
if options.prod {
94+
args.push("--prod".into());
95+
}
96+
97+
if options.dev {
98+
args.push("--dev".into());
99+
}
100+
101+
if let Some(depth) = options.depth {
102+
args.push("--depth".into());
103+
args.push(depth.to_string());
104+
}
105+
106+
if options.no_optional {
107+
args.push("--no-optional".into());
108+
}
109+
110+
if options.global {
111+
args.push("--global".into());
112+
}
113+
114+
if options.exclude_peers {
115+
args.push("--exclude-peers".into());
116+
}
117+
118+
// Add packages (pnpm supports multiple packages)
119+
args.extend_from_slice(options.packages);
120+
}
121+
PackageManagerType::Yarn => {
122+
bin_name = "yarn".into();
123+
124+
args.push("why".into());
125+
126+
// yarn only supports single package
127+
if options.packages.len() > 1 {
128+
eprintln!(
129+
"Warning: yarn only supports checking one package at a time, using first package"
130+
);
131+
}
132+
args.push(options.packages[0].clone());
133+
134+
// yarn@2+ supports --recursive
135+
if options.recursive && !self.version.starts_with("1.") {
136+
args.push("--recursive".into());
137+
}
138+
139+
// Warn about unsupported flags
140+
if options.json {
141+
eprintln!("Warning: --json not supported by yarn");
142+
}
143+
if options.long {
144+
eprintln!("Warning: --long not supported by yarn");
145+
}
146+
if options.parseable {
147+
eprintln!("Warning: --parseable not supported by yarn");
148+
}
149+
if let Some(filters) = options.filters {
150+
if !filters.is_empty() {
151+
eprintln!("Warning: --filter not supported by yarn");
152+
}
153+
}
154+
if options.prod || options.dev {
155+
eprintln!("Warning: --prod/--dev not supported by yarn");
156+
}
157+
}
158+
PackageManagerType::Npm => {
159+
bin_name = "npm".into();
160+
161+
// npm uses 'explain' as primary command
162+
args.push("explain".into());
163+
164+
if options.json {
165+
args.push("--json".into());
166+
}
167+
168+
// npm only supports single package
169+
if options.packages.len() > 1 {
170+
eprintln!(
171+
"Warning: npm only supports checking one package at a time, using first package"
172+
);
173+
}
174+
args.push(options.packages[0].clone());
175+
176+
// Warn about pnpm-specific flags
177+
if options.long {
178+
eprintln!("Warning: --long not supported by npm");
179+
}
180+
if options.parseable {
181+
eprintln!("Warning: --parseable not supported by npm");
182+
}
183+
if let Some(filters) = options.filters {
184+
if !filters.is_empty() {
185+
eprintln!("Warning: --filter not supported by npm");
186+
}
187+
}
188+
if options.prod || options.dev {
189+
eprintln!("Warning: --prod/--dev not supported by npm");
190+
}
191+
if options.depth.is_some() {
192+
eprintln!("Warning: --depth not supported by npm");
193+
}
194+
}
195+
}
196+
197+
// Add pass-through args
198+
if let Some(pass_through_args) = options.pass_through_args {
199+
args.extend_from_slice(pass_through_args);
200+
}
201+
202+
ResolveCommandResult { bin_path: bin_name, args, envs }
203+
}
204+
}
205+
206+
#[cfg(test)]
207+
mod tests {
208+
use tempfile::{TempDir, tempdir};
209+
use vite_path::AbsolutePathBuf;
210+
use vite_str::Str;
211+
212+
use super::*;
213+
214+
fn create_temp_dir() -> TempDir {
215+
tempdir().expect("Failed to create temp directory")
216+
}
217+
218+
fn create_mock_package_manager(pm_type: PackageManagerType, version: &str) -> PackageManager {
219+
let temp_dir = create_temp_dir();
220+
let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
221+
let install_dir = temp_dir_path.join("install");
222+
223+
PackageManager {
224+
client: pm_type,
225+
package_name: pm_type.to_string().into(),
226+
version: Str::from(version),
227+
hash: None,
228+
bin_name: pm_type.to_string().into(),
229+
workspace_root: temp_dir_path.clone(),
230+
install_dir,
231+
}
232+
}
233+
234+
#[test]
235+
fn test_pnpm_why_basic() {
236+
let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0");
237+
let packages = vec!["react".to_string()];
238+
let result = pm
239+
.resolve_why_command(&WhyCommandOptions { packages: &packages, ..Default::default() });
240+
assert_eq!(result.bin_path, "pnpm");
241+
assert_eq!(result.args, vec!["why", "react"]);
242+
}
243+
244+
#[test]
245+
fn test_pnpm_why_multiple_packages() {
246+
let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0");
247+
let packages = vec!["react".to_string(), "lodash".to_string()];
248+
let result = pm
249+
.resolve_why_command(&WhyCommandOptions { packages: &packages, ..Default::default() });
250+
assert_eq!(result.bin_path, "pnpm");
251+
assert_eq!(result.args, vec!["why", "react", "lodash"]);
252+
}
253+
254+
#[test]
255+
fn test_pnpm_why_json() {
256+
let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0");
257+
let packages = vec!["react".to_string()];
258+
let result = pm.resolve_why_command(&WhyCommandOptions {
259+
packages: &packages,
260+
json: true,
261+
..Default::default()
262+
});
263+
assert_eq!(result.bin_path, "pnpm");
264+
assert_eq!(result.args, vec!["why", "--json", "react"]);
265+
}
266+
267+
#[test]
268+
fn test_npm_explain_basic() {
269+
let pm = create_mock_package_manager(PackageManagerType::Npm, "11.0.0");
270+
let packages = vec!["react".to_string()];
271+
let result = pm
272+
.resolve_why_command(&WhyCommandOptions { packages: &packages, ..Default::default() });
273+
assert_eq!(result.bin_path, "npm");
274+
assert_eq!(result.args, vec!["explain", "react"]);
275+
}
276+
277+
#[test]
278+
fn test_yarn_why_basic() {
279+
let pm = create_mock_package_manager(PackageManagerType::Yarn, "4.0.0");
280+
let packages = vec!["react".to_string()];
281+
let result = pm
282+
.resolve_why_command(&WhyCommandOptions { packages: &packages, ..Default::default() });
283+
assert_eq!(result.bin_path, "yarn");
284+
assert_eq!(result.args, vec!["why", "react"]);
285+
}
286+
287+
#[test]
288+
fn test_pnpm_why_with_filter() {
289+
let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0");
290+
let packages = vec!["react".to_string()];
291+
let filters = vec!["app".to_string()];
292+
let result = pm.resolve_why_command(&WhyCommandOptions {
293+
packages: &packages,
294+
filters: Some(&filters),
295+
..Default::default()
296+
});
297+
assert_eq!(result.bin_path, "pnpm");
298+
assert_eq!(result.args, vec!["--filter", "app", "why", "react"]);
299+
}
300+
301+
#[test]
302+
fn test_pnpm_why_with_depth() {
303+
let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0");
304+
let packages = vec!["react".to_string()];
305+
let result = pm.resolve_why_command(&WhyCommandOptions {
306+
packages: &packages,
307+
depth: Some(3),
308+
..Default::default()
309+
});
310+
assert_eq!(result.bin_path, "pnpm");
311+
assert_eq!(result.args, vec!["why", "--depth", "3", "react"]);
312+
}
313+
}

0 commit comments

Comments
 (0)