Skip to content

Commit 2695939

Browse files
committed
feat(pm): support add package
1 parent 1860a27 commit 2695939

10 files changed

Lines changed: 490 additions & 18 deletions

File tree

crates/vite_error/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,12 @@ pub enum Error {
133133
#[error("Unrecognized any package manager, please specify the package manager")]
134134
UnrecognizedPackageManager,
135135

136+
#[error("No package manager detected. Run 'vite install' to set up a package manager.")]
137+
NoPackageManagerDetected,
138+
139+
#[error("No packages specified. Usage: vite add <packages>...")]
140+
NoPackagesSpecified,
141+
136142
#[error(
137143
"Package manager {name}@{version} in {package_json_path:?} is invalid, expected format: 'package-manager-name@major.minor.patch'"
138144
)]

crates/vite_package_manager/src/package_manager.rs

Lines changed: 180 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,68 @@ impl PackageManager {
201201

202202
ignores
203203
}
204+
205+
/// Build command arguments for adding packages with workspace support
206+
#[must_use]
207+
pub fn build_add_args(
208+
&self,
209+
packages: &[String],
210+
filters: &[String],
211+
workspace_root: bool,
212+
workspace_only: bool,
213+
extra_args: &[String],
214+
) -> Vec<String> {
215+
let mut args = Vec::new();
216+
217+
match self.client {
218+
PackageManagerType::Pnpm => {
219+
// pnpm: --filter must come before command
220+
for filter in filters {
221+
args.push("--filter".to_string());
222+
args.push(filter.clone());
223+
}
224+
args.push("add".to_string());
225+
args.extend_from_slice(packages);
226+
if workspace_root {
227+
args.push("-w".to_string());
228+
}
229+
if workspace_only {
230+
args.push("--workspace".to_string());
231+
}
232+
args.extend_from_slice(extra_args);
233+
}
234+
PackageManagerType::Yarn => {
235+
// yarn: workspace <pkg> add
236+
if !filters.is_empty() {
237+
args.push("workspace".to_string());
238+
args.push(filters[0].clone());
239+
}
240+
args.push("add".to_string());
241+
args.extend_from_slice(packages);
242+
if workspace_root {
243+
args.push("-W".to_string());
244+
}
245+
args.extend_from_slice(extra_args);
246+
}
247+
PackageManagerType::Npm => {
248+
// npm: install --workspace <pkg>
249+
args.push("install".to_string());
250+
args.extend_from_slice(packages);
251+
if !filters.is_empty() {
252+
for filter in filters {
253+
args.push("--workspace".to_string());
254+
args.push(filter.clone());
255+
}
256+
}
257+
if workspace_root {
258+
args.push("-w".to_string());
259+
}
260+
args.extend_from_slice(extra_args);
261+
}
262+
}
263+
264+
args
265+
}
204266
}
205267

206268
/// The package root directory and its package.json file.
@@ -628,6 +690,22 @@ mod tests {
628690
.expect("Failed to write pnpm-workspace.yaml");
629691
}
630692

693+
fn create_mock_package_manager(pm_type: PackageManagerType) -> PackageManager {
694+
let temp_dir = create_temp_dir();
695+
let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
696+
let install_dir = temp_dir_path.join("install");
697+
698+
PackageManager {
699+
client: pm_type,
700+
package_name: pm_type.to_string().into(),
701+
version: "1.0.0".into(),
702+
hash: None,
703+
bin_name: pm_type.to_string().into(),
704+
workspace_root: temp_dir_path.clone(),
705+
install_dir,
706+
}
707+
}
708+
631709
#[test]
632710
fn test_find_package_root() {
633711
let temp_dir = create_temp_dir();
@@ -1665,29 +1743,12 @@ mod tests {
16651743
"pnpmfile.cjs should be detected before yarn.config.cjs"
16661744
);
16671745
}
1668-
16691746
// Tests for get_fingerprint_ignores method
16701747
mod get_fingerprint_ignores_tests {
16711748
use vite_glob::GlobPatternSet;
16721749

16731750
use super::*;
16741751

1675-
fn create_mock_package_manager(pm_type: PackageManagerType) -> PackageManager {
1676-
let temp_dir = create_temp_dir();
1677-
let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
1678-
let install_dir = temp_dir_path.join("install");
1679-
1680-
PackageManager {
1681-
client: pm_type,
1682-
package_name: pm_type.to_string().into(),
1683-
version: "1.0.0".into(),
1684-
hash: None,
1685-
bin_name: pm_type.to_string().into(),
1686-
workspace_root: temp_dir_path.clone(),
1687-
install_dir,
1688-
}
1689-
}
1690-
16911752
#[test]
16921753
fn test_pnpm_fingerprint_ignores() {
16931754
let pm = create_mock_package_manager(PackageManagerType::Pnpm);
@@ -1893,4 +1954,106 @@ mod tests {
18931954
assert!(matcher.is_match("src/app.ts"), "Should ignore source files");
18941955
}
18951956
}
1957+
1958+
// Tests for build_add_args method
1959+
mod build_add_args_tests {
1960+
use super::*;
1961+
1962+
#[test]
1963+
fn test_pnpm_basic_add() {
1964+
let pm = create_mock_package_manager(PackageManagerType::Pnpm);
1965+
let args = pm.build_add_args(&["react".to_string()], &[], false, false, &[]);
1966+
assert_eq!(args, vec!["add", "react"]);
1967+
}
1968+
1969+
#[test]
1970+
fn test_pnpm_add_with_filter() {
1971+
let pm = create_mock_package_manager(PackageManagerType::Pnpm);
1972+
let args =
1973+
pm.build_add_args(&["react".to_string()], &["app".to_string()], false, false, &[]);
1974+
assert_eq!(args, vec!["--filter", "app", "add", "react"]);
1975+
}
1976+
1977+
#[test]
1978+
fn test_pnpm_add_workspace_root() {
1979+
let pm = create_mock_package_manager(PackageManagerType::Pnpm);
1980+
let args = pm.build_add_args(
1981+
&["typescript".to_string()],
1982+
&[],
1983+
true,
1984+
false,
1985+
&["-D".to_string()],
1986+
);
1987+
assert_eq!(args, vec!["add", "typescript", "-w", "-D"]);
1988+
}
1989+
1990+
#[test]
1991+
fn test_pnpm_add_workspace_only() {
1992+
let pm = create_mock_package_manager(PackageManagerType::Pnpm);
1993+
let args = pm.build_add_args(
1994+
&["@myorg/utils".to_string()],
1995+
&["app".to_string()],
1996+
false,
1997+
true,
1998+
&[],
1999+
);
2000+
assert_eq!(args, vec!["--filter", "app", "add", "@myorg/utils", "--workspace"]);
2001+
}
2002+
2003+
#[test]
2004+
fn test_yarn_basic_add() {
2005+
let pm = create_mock_package_manager(PackageManagerType::Yarn);
2006+
let args = pm.build_add_args(&["react".to_string()], &[], false, false, &[]);
2007+
assert_eq!(args, vec!["add", "react"]);
2008+
}
2009+
2010+
#[test]
2011+
fn test_yarn_add_with_workspace() {
2012+
let pm = create_mock_package_manager(PackageManagerType::Yarn);
2013+
let args =
2014+
pm.build_add_args(&["react".to_string()], &["app".to_string()], false, false, &[]);
2015+
assert_eq!(args, vec!["workspace", "app", "add", "react"]);
2016+
}
2017+
2018+
#[test]
2019+
fn test_yarn_add_workspace_root() {
2020+
let pm = create_mock_package_manager(PackageManagerType::Yarn);
2021+
let args = pm.build_add_args(
2022+
&["typescript".to_string()],
2023+
&[],
2024+
true,
2025+
false,
2026+
&["-D".to_string()],
2027+
);
2028+
assert_eq!(args, vec!["add", "typescript", "-W", "-D"]);
2029+
}
2030+
2031+
#[test]
2032+
fn test_npm_basic_add() {
2033+
let pm = create_mock_package_manager(PackageManagerType::Npm);
2034+
let args = pm.build_add_args(&["react".to_string()], &[], false, false, &[]);
2035+
assert_eq!(args, vec!["install", "react"]);
2036+
}
2037+
2038+
#[test]
2039+
fn test_npm_add_with_workspace() {
2040+
let pm = create_mock_package_manager(PackageManagerType::Npm);
2041+
let args =
2042+
pm.build_add_args(&["react".to_string()], &["app".to_string()], false, false, &[]);
2043+
assert_eq!(args, vec!["install", "react", "--workspace", "app"]);
2044+
}
2045+
2046+
#[test]
2047+
fn test_npm_add_multiple_workspaces() {
2048+
let pm = create_mock_package_manager(PackageManagerType::Npm);
2049+
let args = pm.build_add_args(
2050+
&["lodash".to_string()],
2051+
&["app".to_string(), "web".to_string()],
2052+
false,
2053+
false,
2054+
&[],
2055+
);
2056+
assert_eq!(args, vec!["install", "lodash", "--workspace", "app", "--workspace", "web"]);
2057+
}
2058+
}
18962059
}

crates/vite_task/src/add.rs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
use petgraph::stable_graph::StableGraph;
2+
use vite_package_manager::package_manager::PackageManager;
3+
use vite_path::AbsolutePathBuf;
4+
5+
use crate::{
6+
Error, ResolveCommandResult, Workspace,
7+
config::ResolvedTask,
8+
schedule::{ExecutionPlan, ExecutionSummary},
9+
};
10+
11+
/// Add command for adding packages to dependencies.
12+
///
13+
/// This command automatically detects the package manager and translates
14+
/// the add command to the appropriate package manager-specific syntax.
15+
pub struct AddCommand {
16+
workspace_root: AbsolutePathBuf,
17+
}
18+
19+
impl AddCommand {
20+
pub fn new(workspace_root: AbsolutePathBuf) -> Self {
21+
Self { workspace_root }
22+
}
23+
24+
pub async fn execute(
25+
self,
26+
filters: Vec<String>,
27+
workspace_root: bool,
28+
workspace_only: bool,
29+
args: Vec<String>,
30+
) -> Result<ExecutionSummary, Error> {
31+
if args.is_empty() {
32+
return Err(Error::NoPackagesSpecified);
33+
}
34+
35+
// Detect package manager
36+
let package_manager = match PackageManager::builder(&self.workspace_root).build().await {
37+
Ok(pm) => pm,
38+
Err(Error::UnrecognizedPackageManager) => {
39+
return Err(Error::NoPackageManagerDetected);
40+
}
41+
Err(e) => return Err(e),
42+
};
43+
44+
let workspace = Workspace::partial_load(self.workspace_root)?;
45+
let resolve_command = package_manager.resolve_command();
46+
47+
// Separate packages from extra args (everything after first -- or flag)
48+
let (packages, extra_args) = Self::split_packages_and_args(&args);
49+
50+
// Build command arguments with workspace support
51+
let full_args = package_manager.build_add_args(
52+
&packages,
53+
&filters,
54+
workspace_root,
55+
workspace_only,
56+
&extra_args,
57+
);
58+
59+
// TODO: set cacheable to false
60+
let resolved_task = ResolvedTask::resolve_from_builtin_with_command_result(
61+
&workspace,
62+
"add",
63+
full_args.iter().map(String::as_str),
64+
ResolveCommandResult { bin_path: resolve_command.bin_path, envs: resolve_command.envs },
65+
false,
66+
None,
67+
)?;
68+
69+
let mut task_graph: StableGraph<ResolvedTask, ()> = Default::default();
70+
task_graph.add_node(resolved_task);
71+
let summary = ExecutionPlan::plan(task_graph, false)?.execute(&workspace).await?;
72+
workspace.unload().await?;
73+
74+
Ok(summary)
75+
}
76+
77+
/// Split args into packages and extra arguments
78+
fn split_packages_and_args(args: &[String]) -> (Vec<String>, Vec<String>) {
79+
// Everything is treated as packages/args - no special separator needed
80+
// Flags like -D, -E etc. are package manager-specific and passed through
81+
(args.to_vec(), vec![])
82+
}
83+
}
84+
85+
#[cfg(test)]
86+
mod tests {
87+
use super::*;
88+
89+
#[test]
90+
fn test_add_command_new() {
91+
let workspace_root = if cfg!(windows) {
92+
AbsolutePathBuf::new("C:\\test".into()).unwrap()
93+
} else {
94+
AbsolutePathBuf::new("/test".into()).unwrap()
95+
};
96+
97+
let cmd = AddCommand::new(workspace_root.clone());
98+
assert_eq!(cmd.workspace_root, workspace_root);
99+
}
100+
101+
#[tokio::test]
102+
async fn test_add_command_no_packages() {
103+
let workspace_root = if cfg!(windows) {
104+
AbsolutePathBuf::new("C:\\test".into()).unwrap()
105+
} else {
106+
AbsolutePathBuf::new("/test".into()).unwrap()
107+
};
108+
109+
let cmd = AddCommand::new(workspace_root);
110+
let result = cmd.execute(vec![], false, false, vec![]).await;
111+
112+
assert!(result.is_err());
113+
assert!(matches!(result.unwrap_err(), Error::NoPackagesSpecified));
114+
}
115+
116+
#[test]
117+
fn test_split_packages_and_args() {
118+
let (packages, extra) = AddCommand::split_packages_and_args(&[
119+
"react".to_string(),
120+
"react-dom".to_string(),
121+
"-D".to_string(),
122+
]);
123+
assert_eq!(packages, vec!["react", "react-dom", "-D"]);
124+
assert!(extra.is_empty());
125+
}
126+
}

0 commit comments

Comments
 (0)