Skip to content

Commit 8be19c4

Browse files
committed
feat(pm): add why commands
1 parent 2f39b24 commit 8be19c4

28 files changed

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

0 commit comments

Comments
 (0)