Skip to content

Commit 300547f

Browse files
committed
feat(pm): support add package
1 parent 154537a commit 300547f

10 files changed

Lines changed: 488 additions & 1 deletion

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 & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,68 @@ impl PackageManager {
153153
envs: HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]),
154154
}
155155
}
156+
157+
/// Build command arguments for adding packages with workspace support
158+
#[must_use]
159+
pub fn build_add_args(
160+
&self,
161+
packages: &[String],
162+
filters: &[String],
163+
workspace_root: bool,
164+
workspace_only: bool,
165+
extra_args: &[String],
166+
) -> Vec<String> {
167+
let mut args = Vec::new();
168+
169+
match self.client {
170+
PackageManagerType::Pnpm => {
171+
// pnpm: --filter must come before command
172+
for filter in filters {
173+
args.push("--filter".to_string());
174+
args.push(filter.clone());
175+
}
176+
args.push("add".to_string());
177+
args.extend_from_slice(packages);
178+
if workspace_root {
179+
args.push("-w".to_string());
180+
}
181+
if workspace_only {
182+
args.push("--workspace".to_string());
183+
}
184+
args.extend_from_slice(extra_args);
185+
}
186+
PackageManagerType::Yarn => {
187+
// yarn: workspace <pkg> add
188+
if !filters.is_empty() {
189+
args.push("workspace".to_string());
190+
args.push(filters[0].clone());
191+
}
192+
args.push("add".to_string());
193+
args.extend_from_slice(packages);
194+
if workspace_root {
195+
args.push("-W".to_string());
196+
}
197+
args.extend_from_slice(extra_args);
198+
}
199+
PackageManagerType::Npm => {
200+
// npm: install --workspace <pkg>
201+
args.push("install".to_string());
202+
args.extend_from_slice(packages);
203+
if !filters.is_empty() {
204+
for filter in filters {
205+
args.push("--workspace".to_string());
206+
args.push(filter.clone());
207+
}
208+
}
209+
if workspace_root {
210+
args.push("-w".to_string());
211+
}
212+
args.extend_from_slice(extra_args);
213+
}
214+
}
215+
216+
args
217+
}
156218
}
157219

158220
/// The package root directory and its package.json file.
@@ -1611,4 +1673,122 @@ mod tests {
16111673
"pnpmfile.cjs should be detected before yarn.config.cjs"
16121674
);
16131675
}
1676+
1677+
// Tests for build_add_args method
1678+
mod build_add_args_tests {
1679+
use super::*;
1680+
1681+
fn create_mock_pm(pm_type: PackageManagerType) -> PackageManager {
1682+
let temp_dir = create_temp_dir();
1683+
let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap();
1684+
let install_dir = temp_dir_path.join("install");
1685+
1686+
PackageManager {
1687+
client: pm_type,
1688+
package_name: pm_type.to_string().into(),
1689+
version: "1.0.0".into(),
1690+
hash: None,
1691+
bin_name: pm_type.to_string().into(),
1692+
workspace_root: temp_dir_path.clone(),
1693+
install_dir,
1694+
}
1695+
}
1696+
1697+
#[test]
1698+
fn test_pnpm_basic_add() {
1699+
let pm = create_mock_pm(PackageManagerType::Pnpm);
1700+
let args = pm.build_add_args(&["react".to_string()], &[], false, false, &[]);
1701+
assert_eq!(args, vec!["add", "react"]);
1702+
}
1703+
1704+
#[test]
1705+
fn test_pnpm_add_with_filter() {
1706+
let pm = create_mock_pm(PackageManagerType::Pnpm);
1707+
let args =
1708+
pm.build_add_args(&["react".to_string()], &["app".to_string()], false, false, &[]);
1709+
assert_eq!(args, vec!["--filter", "app", "add", "react"]);
1710+
}
1711+
1712+
#[test]
1713+
fn test_pnpm_add_workspace_root() {
1714+
let pm = create_mock_pm(PackageManagerType::Pnpm);
1715+
let args = pm.build_add_args(
1716+
&["typescript".to_string()],
1717+
&[],
1718+
true,
1719+
false,
1720+
&["-D".to_string()],
1721+
);
1722+
assert_eq!(args, vec!["add", "typescript", "-w", "-D"]);
1723+
}
1724+
1725+
#[test]
1726+
fn test_pnpm_add_workspace_only() {
1727+
let pm = create_mock_pm(PackageManagerType::Pnpm);
1728+
let args = pm.build_add_args(
1729+
&["@myorg/utils".to_string()],
1730+
&["app".to_string()],
1731+
false,
1732+
true,
1733+
&[],
1734+
);
1735+
assert_eq!(args, vec!["--filter", "app", "add", "@myorg/utils", "--workspace"]);
1736+
}
1737+
1738+
#[test]
1739+
fn test_yarn_basic_add() {
1740+
let pm = create_mock_pm(PackageManagerType::Yarn);
1741+
let args = pm.build_add_args(&["react".to_string()], &[], false, false, &[]);
1742+
assert_eq!(args, vec!["add", "react"]);
1743+
}
1744+
1745+
#[test]
1746+
fn test_yarn_add_with_workspace() {
1747+
let pm = create_mock_pm(PackageManagerType::Yarn);
1748+
let args =
1749+
pm.build_add_args(&["react".to_string()], &["app".to_string()], false, false, &[]);
1750+
assert_eq!(args, vec!["workspace", "app", "add", "react"]);
1751+
}
1752+
1753+
#[test]
1754+
fn test_yarn_add_workspace_root() {
1755+
let pm = create_mock_pm(PackageManagerType::Yarn);
1756+
let args = pm.build_add_args(
1757+
&["typescript".to_string()],
1758+
&[],
1759+
true,
1760+
false,
1761+
&["-D".to_string()],
1762+
);
1763+
assert_eq!(args, vec!["add", "typescript", "-W", "-D"]);
1764+
}
1765+
1766+
#[test]
1767+
fn test_npm_basic_add() {
1768+
let pm = create_mock_pm(PackageManagerType::Npm);
1769+
let args = pm.build_add_args(&["react".to_string()], &[], false, false, &[]);
1770+
assert_eq!(args, vec!["install", "react"]);
1771+
}
1772+
1773+
#[test]
1774+
fn test_npm_add_with_workspace() {
1775+
let pm = create_mock_pm(PackageManagerType::Npm);
1776+
let args =
1777+
pm.build_add_args(&["react".to_string()], &["app".to_string()], false, false, &[]);
1778+
assert_eq!(args, vec!["install", "react", "--workspace", "app"]);
1779+
}
1780+
1781+
#[test]
1782+
fn test_npm_add_multiple_workspaces() {
1783+
let pm = create_mock_pm(PackageManagerType::Npm);
1784+
let args = pm.build_add_args(
1785+
&["lodash".to_string()],
1786+
&["app".to_string(), "web".to_string()],
1787+
false,
1788+
false,
1789+
&[],
1790+
);
1791+
assert_eq!(args, vec!["install", "lodash", "--workspace", "app", "--workspace", "web"]);
1792+
}
1793+
}
16141794
}

crates/vite_task/src/add.rs

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
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+
let resolved_task = ResolvedTask::resolve_from_builtin_with_command_result(
60+
&workspace,
61+
"add",
62+
full_args.iter().map(String::as_str),
63+
ResolveCommandResult { bin_path: resolve_command.bin_path, envs: resolve_command.envs },
64+
false,
65+
)?;
66+
67+
let mut task_graph: StableGraph<ResolvedTask, ()> = Default::default();
68+
task_graph.add_node(resolved_task);
69+
let summary = ExecutionPlan::plan(task_graph, false)?.execute(&workspace).await?;
70+
workspace.unload().await?;
71+
72+
Ok(summary)
73+
}
74+
75+
/// Split args into packages and extra arguments
76+
fn split_packages_and_args(args: &[String]) -> (Vec<String>, Vec<String>) {
77+
// Everything is treated as packages/args - no special separator needed
78+
// Flags like -D, -E etc. are package manager-specific and passed through
79+
(args.to_vec(), vec![])
80+
}
81+
}
82+
83+
#[cfg(test)]
84+
mod tests {
85+
use super::*;
86+
87+
#[test]
88+
fn test_add_command_new() {
89+
let workspace_root = if cfg!(windows) {
90+
AbsolutePathBuf::new("C:\\test".into()).unwrap()
91+
} else {
92+
AbsolutePathBuf::new("/test".into()).unwrap()
93+
};
94+
95+
let cmd = AddCommand::new(workspace_root.clone());
96+
assert_eq!(cmd.workspace_root, workspace_root);
97+
}
98+
99+
#[tokio::test]
100+
async fn test_add_command_no_packages() {
101+
let workspace_root = if cfg!(windows) {
102+
AbsolutePathBuf::new("C:\\test".into()).unwrap()
103+
} else {
104+
AbsolutePathBuf::new("/test".into()).unwrap()
105+
};
106+
107+
let cmd = AddCommand::new(workspace_root);
108+
let result = cmd.execute(vec![], false, false, vec![]).await;
109+
110+
assert!(result.is_err());
111+
assert!(matches!(result.unwrap_err(), Error::NoPackagesSpecified));
112+
}
113+
114+
#[test]
115+
fn test_split_packages_and_args() {
116+
let (packages, extra) = AddCommand::split_packages_and_args(&[
117+
"react".to_string(),
118+
"react-dom".to_string(),
119+
"-D".to_string(),
120+
]);
121+
assert_eq!(packages, vec!["react", "react-dom", "-D"]);
122+
assert!(extra.is_empty());
123+
}
124+
}

0 commit comments

Comments
 (0)