diff --git a/Cargo.lock b/Cargo.lock index b82ed1d9f8..89ab7b4478 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4784,6 +4784,7 @@ dependencies = [ "hex", "httpmock", "indoc", + "nix 0.30.1", "pathdiff", "petgraph 0.8.3", "reqwest", diff --git a/crates/vite_error/src/lib.rs b/crates/vite_error/src/lib.rs index 9f60e73b12..113622acc2 100644 --- a/crates/vite_error/src/lib.rs +++ b/crates/vite_error/src/lib.rs @@ -133,6 +133,9 @@ pub enum Error { #[error("Unrecognized any package manager, please specify the package manager")] UnrecognizedPackageManager, + #[error("No packages specified. Usage: vite add ...")] + NoPackagesSpecified, + #[error( "Package manager {name}@{version} in {package_json_path:?} is invalid, expected format: 'package-manager-name@major.minor.patch'" )] diff --git a/crates/vite_package_manager/Cargo.toml b/crates/vite_package_manager/Cargo.toml index 43bedeeed9..4d1bf13ac4 100644 --- a/crates/vite_package_manager/Cargo.toml +++ b/crates/vite_package_manager/Cargo.toml @@ -41,6 +41,7 @@ reqwest = { workspace = true, features = ["stream", "native-tls-vendored", "json [target.'cfg(not(target_os = "windows"))'.dependencies] reqwest = { workspace = true, features = ["stream", "rustls-tls", "json"] } +nix = { workspace = true } [dev-dependencies] httpmock = { workspace = true } diff --git a/crates/vite_package_manager/src/add.rs b/crates/vite_package_manager/src/add.rs new file mode 100644 index 0000000000..5cbf0016a0 --- /dev/null +++ b/crates/vite_package_manager/src/add.rs @@ -0,0 +1,559 @@ +use std::{collections::HashMap, process::ExitStatus}; + +use vite_error::Error; +use vite_path::AbsolutePath; + +use crate::package_manager::{ + PackageManager, PackageManagerType, ResolveCommandResult, format_path_env, run_command, +}; + +/// The type of dependency to save. +#[derive(Debug, Default, Clone, Copy)] +pub enum SaveDependencyType { + /// Save as dependencies. + #[default] + Production, + /// Save as devDependencies. + Dev, + /// Save as peerDependencies. + Peer, + /// Save as optionalDependencies. + Optional, +} + +#[derive(Debug, Default)] +pub struct AddCommandOptions<'a> { + pub packages: &'a [String], + pub save_dependency_type: Option, + pub save_exact: bool, + pub save_catalog_name: Option<&'a str>, + pub filters: Option<&'a [String]>, + pub workspace_root: bool, + pub workspace_only: bool, + pub global: bool, + pub allow_build: Option<&'a str>, + pub pass_through_args: Option<&'a [String]>, +} + +impl PackageManager { + /// Run the add command with the package manager. + /// Return the exit status of the command. + #[must_use] + pub async fn run_add_command( + &self, + options: &AddCommandOptions<'_>, + cwd: impl AsRef, + ) -> Result { + let resolve_command = self.resolve_add_command(options); + run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) + .await + } + + /// Resolve the add command. + #[must_use] + pub fn resolve_add_command(&self, options: &AddCommandOptions) -> ResolveCommandResult { + let bin_name: String; + let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); + let mut args: Vec = Vec::new(); + + // global packages should use npm cli only + if options.global { + bin_name = "npm".into(); + args.push("install".into()); + args.push("--global".into()); + if let Some(pass_through_args) = options.pass_through_args { + args.extend_from_slice(pass_through_args); + } + args.extend_from_slice(options.packages); + + return ResolveCommandResult { bin_path: bin_name, args, envs }; + } + + match self.client { + PackageManagerType::Pnpm => { + bin_name = "pnpm".into(); + // pnpm: --filter must come before command + if let Some(filters) = options.filters { + for filter in filters { + args.push("--filter".into()); + args.push(filter.clone()); + } + } + args.push("add".into()); + if options.workspace_root { + args.push("--workspace-root".into()); + } + if options.workspace_only { + args.push("--workspace".into()); + } + + // https://pnpm.io/cli/add#options + if let Some(save_dependency_type) = options.save_dependency_type { + match save_dependency_type { + SaveDependencyType::Production => { + args.push("--save-prod".into()); + } + SaveDependencyType::Dev => { + args.push("--save-dev".into()); + } + SaveDependencyType::Peer => { + args.push("--save-peer".into()); + } + SaveDependencyType::Optional => { + args.push("--save-optional".into()); + } + } + } + if options.save_exact { + args.push("--save-exact".into()); + } + + if let Some(save_catalog_name) = options.save_catalog_name { + if save_catalog_name.is_empty() { + args.push("--save-catalog".into()); + } else { + args.push(format!("--save-catalog-name={}", save_catalog_name)); + } + } + + if let Some(allow_build) = options.allow_build { + args.push(format!("--allow-build={}", allow_build)); + } + } + PackageManagerType::Yarn => { + bin_name = "yarn".into(); + // yarn: workspaces foreach --all --include {filter} add + // https://yarnpkg.com/cli/workspaces/foreach + if let Some(filters) = options.filters { + args.push("workspaces".into()); + args.push("foreach".into()); + args.push("--all".into()); + for filter in filters { + args.push("--include".into()); + args.push(filter.clone()); + } + } + args.push("add".into()); + + // https://yarnpkg.com/cli/add#options + if let Some(save_dependency_type) = options.save_dependency_type { + match save_dependency_type { + SaveDependencyType::Production => { + // default + // no need to add anything + } + SaveDependencyType::Dev => { + args.push("--dev".into()); + } + SaveDependencyType::Peer => { + args.push("--peer".into()); + } + SaveDependencyType::Optional => { + args.push("--optional".into()); + } + } + } + if options.save_exact { + args.push("--exact".into()); + } + } + PackageManagerType::Npm => { + bin_name = "npm".into(); + // npm: install --workspace + args.push("install".into()); + if let Some(filters) = options.filters { + for filter in filters { + args.push("--workspace".into()); + args.push(filter.clone()); + } + } + // https://docs.npmjs.com/cli/v11/commands/npm-install#include-workspace-root + if options.workspace_root { + args.push("--include-workspace-root".into()); + } + + // https://docs.npmjs.com/cli/v11/commands/npm-install#configuration + if let Some(save_dependency_type) = options.save_dependency_type { + match save_dependency_type { + SaveDependencyType::Production => { + args.push("--save".into()); + } + SaveDependencyType::Dev => { + args.push("--save-dev".into()); + } + SaveDependencyType::Peer => { + args.push("--save-peer".into()); + } + SaveDependencyType::Optional => { + args.push("--save-optional".into()); + } + } + } + + if options.save_exact { + args.push("--save-exact".into()); + } + } + } + + if let Some(pass_through_args) = options.pass_through_args { + args.extend_from_slice(pass_through_args); + } + args.extend_from_slice(options.packages); + + ResolveCommandResult { bin_path: bin_name, args, envs } + } +} + +#[cfg(test)] +mod tests { + use tempfile::{TempDir, tempdir}; + use vite_path::AbsolutePathBuf; + use vite_str::Str; + + use super::*; + + fn create_temp_dir() -> TempDir { + tempdir().expect("Failed to create temp directory") + } + + fn create_mock_package_manager(pm_type: PackageManagerType) -> PackageManager { + let temp_dir = create_temp_dir(); + let temp_dir_path = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + let install_dir = temp_dir_path.join("install"); + + PackageManager { + client: pm_type, + package_name: pm_type.to_string().into(), + version: Str::from("1.0.0"), + hash: None, + bin_name: pm_type.to_string().into(), + workspace_root: temp_dir_path.clone(), + install_dir, + } + } + + #[test] + fn test_pnpm_basic_add() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_add_command(&AddCommandOptions { + packages: &["react".to_string()], + save_dependency_type: None, + save_exact: false, + filters: None, + workspace_root: false, + workspace_only: false, + global: false, + save_catalog_name: None, + allow_build: None, + pass_through_args: None, + }); + assert_eq!(result.bin_path, "pnpm"); + assert_eq!(result.args, vec!["add", "react"]); + } + + #[test] + fn test_pnpm_add_with_filter() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_add_command(&AddCommandOptions { + packages: &["react".to_string()], + save_dependency_type: None, + save_exact: false, + filters: Some(&["app".to_string()]), + workspace_root: false, + workspace_only: false, + global: false, + save_catalog_name: None, + allow_build: None, + pass_through_args: None, + }); + assert_eq!(result.args, vec!["--filter", "app", "add", "react"]); + assert_eq!(result.bin_path, "pnpm"); + } + + #[test] + fn test_pnpm_add_with_save_catalog_name() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_add_command(&AddCommandOptions { + packages: &["react".to_string()], + save_dependency_type: None, + save_exact: false, + filters: Some(&["app".to_string()]), + workspace_root: false, + workspace_only: false, + global: false, + save_catalog_name: Some("react18"), + allow_build: None, + pass_through_args: None, + }); + assert_eq!( + result.args, + vec!["--filter", "app", "add", "--save-catalog-name=react18", "react"] + ); + assert_eq!(result.bin_path, "pnpm"); + } + + #[test] + fn test_pnpm_add_with_save_catalog_name_and_empty_name() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_add_command(&AddCommandOptions { + packages: &["react".to_string()], + save_dependency_type: None, + save_exact: false, + filters: Some(&["app".to_string()]), + workspace_root: false, + workspace_only: false, + global: false, + save_catalog_name: Some(""), + allow_build: None, + pass_through_args: None, + }); + assert_eq!(result.args, vec!["--filter", "app", "add", "--save-catalog", "react"]); + assert_eq!(result.bin_path, "pnpm"); + } + + #[test] + fn test_pnpm_add_with_filter_and_workspace_root() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_add_command(&AddCommandOptions { + packages: &["react".to_string()], + save_dependency_type: None, + save_exact: false, + filters: Some(&["app".to_string()]), + workspace_root: true, + workspace_only: false, + global: false, + save_catalog_name: None, + allow_build: None, + pass_through_args: None, + }); + assert_eq!(result.args, vec!["--filter", "app", "add", "--workspace-root", "react"]); + assert_eq!(result.bin_path, "pnpm"); + } + + #[test] + fn test_pnpm_add_workspace_root() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_add_command(&AddCommandOptions { + packages: &["typescript".to_string()], + save_dependency_type: Some(SaveDependencyType::Dev), + save_exact: false, + filters: None, + workspace_root: true, + workspace_only: false, + global: false, + save_catalog_name: None, + allow_build: None, + pass_through_args: None, + }); + assert_eq!(result.args, vec!["add", "--workspace-root", "--save-dev", "typescript"]); + assert_eq!(result.bin_path, "pnpm"); + } + + #[test] + fn test_pnpm_add_workspace_only() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_add_command(&AddCommandOptions { + packages: &["@myorg/utils".to_string()], + save_dependency_type: None, + save_exact: false, + filters: Some(&["app".to_string()]), + workspace_root: false, + workspace_only: true, + global: false, + save_catalog_name: None, + allow_build: None, + pass_through_args: None, + }); + assert_eq!(result.args, vec!["--filter", "app", "add", "--workspace", "@myorg/utils"]); + assert_eq!(result.bin_path, "pnpm"); + } + + #[test] + fn test_yarn_basic_add() { + let pm = create_mock_package_manager(PackageManagerType::Yarn); + let result = pm.resolve_add_command(&AddCommandOptions { + packages: &["react".to_string()], + save_dependency_type: None, + save_exact: false, + filters: None, + workspace_root: false, + workspace_only: false, + global: false, + save_catalog_name: None, + allow_build: None, + pass_through_args: None, + }); + assert_eq!(result.args, vec!["add", "react"]); + assert_eq!(result.bin_path, "yarn"); + } + + #[test] + fn test_yarn_add_with_workspace() { + let pm = create_mock_package_manager(PackageManagerType::Yarn); + let result = pm.resolve_add_command(&AddCommandOptions { + packages: &["react".to_string()], + save_dependency_type: None, + save_exact: false, + filters: Some(&["app".to_string()]), + workspace_root: false, + workspace_only: false, + global: false, + save_catalog_name: None, + allow_build: None, + pass_through_args: None, + }); + assert_eq!( + result.args, + vec!["workspaces", "foreach", "--all", "--include", "app", "add", "react"] + ); + assert_eq!(result.bin_path, "yarn"); + } + + #[test] + fn test_yarn_add_workspace_root() { + let pm = create_mock_package_manager(PackageManagerType::Yarn); + let result = pm.resolve_add_command(&AddCommandOptions { + packages: &["typescript".to_string()], + save_dependency_type: Some(SaveDependencyType::Dev), + save_exact: false, + filters: None, + workspace_root: true, + workspace_only: false, + global: false, + save_catalog_name: None, + allow_build: None, + pass_through_args: None, + }); + assert_eq!(result.args, vec!["add", "--dev", "typescript"]); + assert_eq!(result.bin_path, "yarn"); + } + + #[test] + fn test_npm_basic_add() { + let pm = create_mock_package_manager(PackageManagerType::Npm); + let result = pm.resolve_add_command(&AddCommandOptions { + packages: &["react".to_string()], + save_dependency_type: None, + save_exact: false, + filters: None, + workspace_root: false, + workspace_only: false, + global: false, + save_catalog_name: None, + allow_build: None, + pass_through_args: None, + }); + assert_eq!(result.args, vec!["install", "react"]); + assert_eq!(result.bin_path, "npm"); + } + + #[test] + fn test_npm_add_with_workspace() { + let pm = create_mock_package_manager(PackageManagerType::Npm); + let result = pm.resolve_add_command(&AddCommandOptions { + packages: &["react".to_string()], + save_dependency_type: None, + save_exact: false, + filters: Some(&["app".to_string()]), + workspace_root: false, + workspace_only: false, + global: false, + save_catalog_name: None, + allow_build: None, + pass_through_args: None, + }); + assert_eq!(result.args, vec!["install", "--workspace", "app", "react"]); + assert_eq!(result.bin_path, "npm"); + } + + #[test] + fn test_npm_add_workspace_root() { + let pm = create_mock_package_manager(PackageManagerType::Npm); + let result = pm.resolve_add_command(&AddCommandOptions { + packages: &["typescript".to_string()], + save_dependency_type: None, + save_exact: false, + filters: None, + workspace_root: true, + workspace_only: false, + global: false, + save_catalog_name: None, + allow_build: None, + pass_through_args: None, + }); + assert_eq!(result.args, vec!["install", "--include-workspace-root", "typescript"]); + assert_eq!(result.bin_path, "npm"); + } + + #[test] + fn test_npm_add_multiple_workspaces() { + let pm = create_mock_package_manager(PackageManagerType::Npm); + let result = pm.resolve_add_command(&AddCommandOptions { + packages: &["lodash".to_string()], + save_dependency_type: None, + save_exact: false, + filters: Some(&["app".to_string(), "web".to_string()]), + workspace_root: false, + workspace_only: false, + global: false, + save_catalog_name: None, + allow_build: None, + pass_through_args: None, + }); + assert_eq!( + result.args, + vec!["install", "--workspace", "app", "--workspace", "web", "lodash"] + ); + assert_eq!(result.bin_path, "npm"); + } + + #[test] + fn test_npm_add_multiple_workspaces_and_workspace_root() { + let pm = create_mock_package_manager(PackageManagerType::Npm); + let result = pm.resolve_add_command(&AddCommandOptions { + packages: &["lodash".to_string()], + save_dependency_type: None, + save_exact: false, + filters: Some(&["app".to_string(), "web".to_string()]), + workspace_root: true, + workspace_only: false, + global: false, + save_catalog_name: None, + allow_build: None, + pass_through_args: None, + }); + assert_eq!( + result.args, + vec![ + "install", + "--workspace", + "app", + "--workspace", + "web", + "--include-workspace-root", + "lodash" + ] + ); + assert_eq!(result.bin_path, "npm"); + } + + #[test] + fn test_pnpm_add_with_allow_build() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let result = pm.resolve_add_command(&AddCommandOptions { + packages: &["react".to_string()], + save_dependency_type: None, + save_exact: false, + filters: None, + workspace_root: false, + workspace_only: false, + global: false, + save_catalog_name: None, + allow_build: Some("react,napi"), + pass_through_args: None, + }); + assert_eq!(result.args, vec!["add", "--allow-build=react,napi", "react"]); + assert_eq!(result.bin_path, "pnpm"); + } +} diff --git a/crates/vite_package_manager/src/lib.rs b/crates/vite_package_manager/src/lib.rs index 461b004bd1..30a1feca27 100644 --- a/crates/vite_package_manager/src/lib.rs +++ b/crates/vite_package_manager/src/lib.rs @@ -1,3 +1,4 @@ +pub mod add; mod config; mod install; pub mod package; @@ -17,6 +18,7 @@ use vite_str::Str; use wax::Glob; pub use crate::{ + add::{AddCommandOptions, SaveDependencyType}, package::{DependencyType, PackageJson}, package_manager::{WorkspaceFile, WorkspaceRoot, find_package_root, find_workspace_root}, }; diff --git a/crates/vite_package_manager/src/package_manager.rs b/crates/vite_package_manager/src/package_manager.rs index 5d5c133b3f..3d8120bfcf 100644 --- a/crates/vite_package_manager/src/package_manager.rs +++ b/crates/vite_package_manager/src/package_manager.rs @@ -4,11 +4,12 @@ use std::{ fs::{self, File}, io::{BufReader, Seek, SeekFrom}, path::Path, + process::{ExitStatus, Stdio}, }; use semver::{Version, VersionReq}; use serde::{Deserialize, Serialize}; -use tokio::fs::remove_dir_all; +use tokio::{fs::remove_dir_all, process::Command}; use vite_error::Error; use vite_path::{AbsolutePath, AbsolutePathBuf, RelativePathBuf}; use vite_str::Str; @@ -138,8 +139,8 @@ impl PackageManagerBuilder { } impl PackageManager { - pub fn builder(workspace_root: impl AsRef) -> PackageManagerBuilder { - PackageManagerBuilder::new(workspace_root) + pub fn builder(cwd: impl AsRef) -> PackageManagerBuilder { + PackageManagerBuilder::new(cwd) } #[must_use] @@ -600,6 +601,82 @@ pub(crate) fn format_path_env(bin_prefix: impl AsRef) -> String { env::join_paths(paths).unwrap().to_string_lossy().to_string() } +#[cfg(unix)] +fn fix_stdio_streams() { + // libuv may mark stdin/stdout/stderr as close-on-exec, which interferes with Rust's subprocess spawning. + // As a workaround, we clear the FD_CLOEXEC flag on these file descriptors to prevent them from being closed when spawning child processes. + // + // For details see https://github.com/libuv/libuv/issues/2062 + // Fixed by reference from https://github.com/electron/electron/pull/15555 + + use std::os::fd::BorrowedFd; + + use nix::{ + fcntl::{FcntlArg, FdFlag, fcntl}, + libc::{STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO}, + }; + + // Safe function to clear FD_CLOEXEC flag + fn clear_cloexec(fd: BorrowedFd<'_>) { + // Borrow RawFd as BorrowedFd to satisfy AsFd constraint + if let Ok(flags) = fcntl(fd, FcntlArg::F_GETFD) { + let mut fd_flags = FdFlag::from_bits_retain(flags); + if fd_flags.contains(FdFlag::FD_CLOEXEC) { + fd_flags.remove(FdFlag::FD_CLOEXEC); + // Ignore errors: some fd may be closed + let _ = fcntl(fd, FcntlArg::F_SETFD(fd_flags)); + } + } + } + + // Clear FD_CLOEXEC on stdin, stdout, stderr + clear_cloexec(unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) }); + clear_cloexec(unsafe { BorrowedFd::borrow_raw(STDOUT_FILENO) }); + clear_cloexec(unsafe { BorrowedFd::borrow_raw(STDERR_FILENO) }); +} + +// TODO: should move to vite-command crate later +/// Run a command with the given bin name, arguments, environment variables, and current working directory. +/// +/// # Arguments +/// +/// * `bin_name`: The name of the binary to run. +/// * `args`: The arguments to pass to the binary. +/// * `envs`: The custom environment variables to set for the command, will be merged with the system environment variables. +/// * `cwd`: The current working directory for the command. +/// +/// # Returns +/// +/// Returns the exit status of the command. +pub(crate) async fn run_command( + bin_name: &str, + args: &[String], + envs: &HashMap, + cwd: impl AsRef, +) -> Result { + println!("Running: {} {}", bin_name, args.join(" ")); + + let mut cmd = Command::new(bin_name); + cmd.args(args) + .envs(envs) + .current_dir(cwd.as_ref()) + .stdin(Stdio::inherit()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()); + + // fix stdio streams on unix + #[cfg(unix)] + unsafe { + cmd.pre_exec(|| { + fix_stdio_streams(); + Ok(()) + }); + } + + let status = cmd.status().await?; + Ok(status) +} + #[cfg(test)] mod tests { use std::fs; @@ -1658,7 +1735,6 @@ mod tests { "pnpmfile.cjs should be detected before yarn.config.cjs" ); } - // Tests for get_fingerprint_ignores method mod get_fingerprint_ignores_tests { use vite_glob::GlobPatternSet; diff --git a/package.json b/package.json index ffb720aca8..ff52906fe0 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "install-global-cli": "npm install -g ./packages/global", "typecheck": "tsc -b tsconfig.json", "lint": "vite lint && vite run typecheck", - "test": "vite test && pnpm -r snap-test", + "test": "vite test run && pnpm -r snap-test", "prepare": "husky" }, "devDependencies": { diff --git a/packages/cli/binding/src/cli.rs b/packages/cli/binding/src/cli.rs index fdfbab3e02..fc73cafd5c 100644 --- a/packages/cli/binding/src/cli.rs +++ b/packages/cli/binding/src/cli.rs @@ -9,6 +9,7 @@ use clap::{Parser, Subcommand}; use serde::{Deserialize, Serialize}; use tokio::fs::write; use vite_error::Error; +use vite_package_manager::SaveDependencyType; use vite_path::AbsolutePathBuf; use vite_str::Str; use vite_task::{ @@ -17,6 +18,7 @@ use vite_task::{ }; use crate::commands::{ + add::AddCommand, doc::doc as doc_cmd, fmt::{FmtConfig, fmt}, install::InstallCommand, @@ -102,14 +104,6 @@ pub enum Commands { #[arg(allow_hyphen_values = true, trailing_var_arg = true)] args: Vec, }, - /// Install command. - /// It will be passed to the package manager's install command currently. - #[command(disable_help_flag = true, alias = "i")] - Install { - /// Arguments to pass to vite install - #[arg(allow_hyphen_values = true, trailing_var_arg = true)] - args: Vec, - }, Dev { #[arg(allow_hyphen_values = true, trailing_var_arg = true)] /// Arguments to pass to vite dev @@ -126,6 +120,68 @@ pub enum Commands { #[clap(subcommand)] subcmd: CacheSubcommand, }, + // package manager commands + /// Install command. + /// It will be passed to the package manager's install command currently. + #[command(disable_help_flag = true, alias = "i")] + Install { + /// Arguments to pass to vite install + #[arg(allow_hyphen_values = true, trailing_var_arg = true)] + args: Vec, + }, + /// Add packages to dependencies + Add { + /// Save to `dependencies` (default) + #[arg(short = 'P', long)] + save_prod: bool, + /// Save to `devDependencies` + #[arg(short = 'D', long)] + save_dev: bool, + /// Save to `peerDependencies` and `devDependencies` + #[arg(long)] + save_peer: bool, + /// Save to `optionalDependencies` + #[arg(short = 'O', long)] + save_optional: bool, + /// Save exact version rather than semver range (e.g., `^1.0.0` -> `1.0.0`) + #[arg(short = 'E', long)] + save_exact: bool, + + /// Save the new dependency to the specified catalog name. + /// Example: `vite add vue --save-catalog-name vue3` + #[arg(long, value_name = "CATALOG_NAME")] + save_catalog_name: Option, + /// Save the new dependency to the default catalog + #[arg(long)] + save_catalog: bool, + + /// A list of package names allowed to run postinstall + #[arg(long, value_name = "NAMES")] + allow_build: Option, + + /// Filter packages in monorepo (can be used multiple times) + #[arg(long, value_name = "PATTERN")] + filter: Option>, + + /// Add to workspace root (ignore-workspace-root-check) + #[arg(short = 'w', long)] + workspace_root: bool, + + /// Only add if package exists in workspace (pnpm-specific) + #[arg(long)] + workspace: bool, + + /// Install globally + #[arg(short = 'g', long)] + global: bool, + + /// Packages to add + packages: Vec, + + /// Additional arguments to pass through to the package manager + #[arg(last = true, allow_hyphen_values = true)] + pass_through_args: Option>, + }, } #[derive(Subcommand, Debug)] @@ -270,7 +326,7 @@ pub async fn main< >, ) -> Result { // Auto-install dependencies if needed, but skip for install command itself, or if `VITE_DISABLE_AUTO_INSTALL=1` is set. - if !matches!(args.commands, Commands::Install { .. }) + if !matches!(args.commands, Commands::Install { .. } | Commands::Add { .. }) && std::env::var_os("VITE_DISABLE_AUTO_INSTALL") != Some("1".into()) { auto_install(&cwd).await?; @@ -378,7 +434,6 @@ pub async fn main< workspace.unload().await?; summary } - Commands::Install { args } => InstallCommand::builder(cwd).build().execute(args).await?, Commands::Dev { args } => { let workspace = Workspace::partial_load(cwd)?; let vite_fn = options.map(|o| o.vite).expect("dev command requires CliOptions"); @@ -406,6 +461,87 @@ pub async fn main< } return Ok(ExitStatus::default()); } + + // package manager commands + Commands::Install { args } => { + // Check if args contain packages - if yes, redirect to Add command + // This allows `vite install ` to work as an alias for `vite add ` + if let Some(Commands::Add { + filter, + workspace_root, + workspace, + packages, + save_prod, + save_dev, + save_peer, + save_optional, + save_exact, + save_catalog, + save_catalog_name, + global, + allow_build, + pass_through_args, + }) = parse_install_as_add(args) + { + let exit_status = execute_add_command( + cwd, + &packages, + save_prod, + save_dev, + save_peer, + save_optional, + save_exact, + save_catalog, + save_catalog_name.as_deref(), + filter.as_deref(), + workspace_root, + workspace, + global, + allow_build.as_deref(), + pass_through_args.as_deref(), + ) + .await?; + return Ok(exit_status); + } else { + InstallCommand::builder(cwd).build().execute(args).await? + } + } + Commands::Add { + filter, + workspace_root, + workspace, + packages, + save_prod, + save_dev, + save_peer, + save_optional, + save_exact, + save_catalog, + save_catalog_name, + global, + allow_build, + pass_through_args, + } => { + let exit_status = execute_add_command( + cwd, + packages, + *save_prod, + *save_dev, + *save_peer, + *save_optional, + *save_exact, + *save_catalog, + save_catalog_name.as_deref(), + filter.as_deref(), + *workspace_root, + *workspace, + *global, + allow_build.as_deref(), + pass_through_args.as_deref(), + ) + .await?; + return Ok(exit_status); + } }; let execution_summary_dir = EXECUTION_SUMMARY_DIR.as_path(); @@ -513,6 +649,76 @@ async fn read_vite_config_from_workspace_root< Ok(None) } +/// Check if install args contain packages (non-flag arguments). +/// If packages are detected, reparse as Add command. +fn parse_install_as_add(args: &[String]) -> Option { + // Check if there are any non-flag arguments (potential package names) + let has_packages = args.iter().any(|arg| !arg.starts_with('-')); + + if !has_packages { + return None; + } + + // Reconstruct command line with "add" subcommand + let mut cmd_args = vec!["vite".to_string(), "add".to_string()]; + cmd_args.extend_from_slice(args); + + // Try to parse as Add command + match Args::try_parse_from(&cmd_args) { + Ok(parsed_args) => Some(parsed_args.commands), + Err(_) => None, // If parsing fails, fall back to regular install + } +} + +/// Execute add command with the given parameters +async fn execute_add_command( + cwd: AbsolutePathBuf, + packages: &[String], + save_prod: bool, + save_dev: bool, + save_peer: bool, + save_optional: bool, + save_exact: bool, + save_catalog: bool, + save_catalog_name: Option<&str>, + filter: Option<&[String]>, + workspace_root: bool, + workspace: bool, + global: bool, + allow_build: Option<&str>, + pass_through_args: Option<&[String]>, +) -> Result { + let save_dependency_type = if save_dev { + Some(SaveDependencyType::Dev) + } else if save_peer { + Some(SaveDependencyType::Peer) + } else if save_optional { + Some(SaveDependencyType::Optional) + } else if save_prod { + Some(SaveDependencyType::Production) + } else { + None + }; + + // empty string means save as `catalog:` + let save_catalog_name = if save_catalog { Some("") } else { save_catalog_name }; + + AddCommand::new(cwd) + .execute( + packages, + save_dependency_type, + save_exact, + save_catalog_name, + filter, + workspace_root, + workspace, + global, + allow_build, + pass_through_args, + ) + .await +} + #[cfg(test)] mod tests { use clap::Parser; @@ -991,4 +1197,285 @@ mod tests { std::env::remove_var("VITE_TASK_EXECUTION_ENV"); } } + + mod install_as_add_tests { + use super::*; + + #[test] + fn test_parse_install_as_add_with_packages() { + let args = vec!["react".to_string(), "react-dom".to_string()]; + let result = parse_install_as_add(&args); + assert!(result.is_some()); + if let Some(Commands::Add { packages, save_dev, save_exact, .. }) = result { + assert_eq!(packages, vec!["react", "react-dom"]); + assert!(!save_dev); + assert!(!save_exact); + } else { + panic!("Expected Add command"); + } + } + + #[test] + fn test_parse_install_as_add_with_dev_flag() { + let args = vec!["-D".to_string(), "typescript".to_string()]; + let result = parse_install_as_add(&args); + assert!(result.is_some()); + if let Some(Commands::Add { packages, save_dev, .. }) = result { + assert_eq!(packages, vec!["typescript"]); + assert!(save_dev); + } else { + panic!("Expected Add command"); + } + } + + #[test] + fn test_parse_install_as_add_without_packages() { + let args = vec![]; + let result = parse_install_as_add(&args); + assert!(result.is_none()); + } + + #[test] + fn test_parse_install_as_add_with_only_flags() { + let args = vec!["--some-install-flag".to_string()]; + let result = parse_install_as_add(&args); + assert!(result.is_none()); + } + + #[test] + fn test_parse_install_as_add_complex() { + let args = vec![ + "-D".to_string(), + "-E".to_string(), + "--filter".to_string(), + "app".to_string(), + "typescript".to_string(), + "eslint".to_string(), + ]; + let result = parse_install_as_add(&args); + assert!(result.is_some()); + if let Some(Commands::Add { packages, save_dev, save_exact, filter, .. }) = result { + assert_eq!(packages, vec!["typescript", "eslint"]); + assert!(save_dev); + assert!(save_exact); + assert_eq!(filter.unwrap(), vec!["app"]); + } else { + panic!("Expected Add command"); + } + } + } + + mod add_command_tests { + use super::*; + + #[test] + fn test_args_add_command() { + let args = Args::try_parse_from(&["vite-plus", "add", "react"]).unwrap(); + if let Commands::Add { filter, workspace_root, workspace, packages, .. } = + &args.commands + { + assert_eq!(packages, &vec!["react".to_string()]); + assert!(filter.is_none()); + assert!(!workspace_root); + assert!(!workspace); + } else { + panic!("Expected Add command"); + } + + let args = Args::try_parse_from(&["vite-plus", "add", "--save-peer", "react"]).unwrap(); + if let Commands::Add { + filter, workspace_root, workspace, packages, save_peer, .. + } = &args.commands + { + assert_eq!(packages, &vec!["react".to_string()]); + assert!(filter.is_none()); + assert!(!workspace_root); + assert!(!workspace); + assert!(save_peer); + } else { + panic!("Expected Add command"); + } + } + + #[test] + fn test_args_add_command_with_workspace_root() { + let args = Args::try_parse_from(&["vite-plus", "add", "-w", "react"]).unwrap(); + if let Commands::Add { filter, workspace_root, workspace, packages, .. } = + &args.commands + { + assert_eq!(packages, &vec!["react".to_string()]); + assert!(filter.is_none()); + assert!(workspace_root); + assert!(!workspace); + } else { + panic!("Expected Add command"); + } + let args = Args::try_parse_from(&["vite-plus", "add", "react", "-w"]).unwrap(); + if let Commands::Add { filter, workspace_root, workspace, packages, .. } = + &args.commands + { + assert_eq!(packages, &vec!["react".to_string()]); + assert!(filter.is_none()); + assert!(workspace_root); + assert!(!workspace); + } else { + panic!("Expected Add command"); + } + + let args = + Args::try_parse_from(&["vite-plus", "add", "react", "--workspace-root"]).unwrap(); + if let Commands::Add { filter, workspace_root, workspace, packages, .. } = + &args.commands + { + assert_eq!(packages, &vec!["react".to_string()]); + assert!(filter.is_none()); + assert!(workspace_root); + assert!(!workspace); + } else { + panic!("Expected Add command"); + } + } + + #[test] + fn test_args_add_command_multiple_packages() { + let args = + Args::try_parse_from(&["vite-plus", "add", "react", "react-dom", "@types/react"]) + .unwrap(); + if let Commands::Add { packages, .. } = &args.commands { + assert_eq!(packages, &vec!["react", "react-dom", "@types/react"]); + } else { + panic!("Expected Add command"); + } + } + + #[test] + fn test_args_add_command_with_flags() { + let args = Args::try_parse_from(&[ + "vite-plus", + "add", + "--filter", + "app", + "-w", + "--workspace", + "typescript", + "-D", + ]) + .unwrap(); + if let Commands::Add { filter, workspace_root, workspace, packages, save_dev, .. } = + &args.commands + { + assert_eq!(filter, &Some(vec!["app".to_string()])); + assert!(workspace_root); + assert!(workspace); + assert_eq!(packages, &vec!["typescript"]); + assert!(save_dev); + } else { + panic!("Expected Add command"); + } + } + + #[test] + fn test_args_add_command_with_allow_build() { + let args = Args::try_parse_from(&[ + "vite-plus", + "add", + "--filter", + "app", + "-w", + "--workspace", + "typescript", + "-D", + "--allow-build=react,napi", + ]) + .unwrap(); + if let Commands::Add { + filter, + workspace_root, + workspace, + packages, + save_dev, + allow_build, + .. + } = &args.commands + { + assert_eq!(filter, &Some(vec!["app".to_string()])); + assert!(workspace_root); + assert!(workspace); + assert_eq!(packages, &vec!["typescript"]); + assert!(save_dev); + assert_eq!(allow_build, &Some("react,napi".to_string())); + } else { + panic!("Expected Add command"); + } + } + + #[test] + fn test_args_add_command_multiple_filters() { + let args = Args::try_parse_from(&[ + "vite-plus", + "add", + "--filter", + "app", + "--filter", + "web", + "react", + ]) + .unwrap(); + if let Commands::Add { filter, packages, .. } = &args.commands { + assert_eq!(filter, &Some(vec!["app".to_string(), "web".to_string()])); + assert_eq!(packages, &vec!["react"]); + } else { + panic!("Expected Add command"); + } + } + + #[test] + fn test_args_add_command_invalid_filter() { + let args = Args::try_parse_from(&["vite-plus", "add", "react", "--filter"]); + assert!(args.is_err()); + } + + #[test] + fn test_args_add_command_with_pass_through_args() { + let args = Args::try_parse_from(&[ + "vite-plus", + "add", + "react", + "--", + "--watch", + "--mode=production", + "--use-stderr", + ]) + .unwrap(); + if let Commands::Add { packages, pass_through_args, .. } = &args.commands { + assert_eq!(packages, &vec!["react"]); + assert_eq!( + pass_through_args, + &Some(vec![ + "--watch".to_string(), + "--mode=production".to_string(), + "--use-stderr".to_string() + ]) + ); + } else { + panic!("Expected Add command"); + } + + let args = Args::try_parse_from(&[ + "vite-plus", + "add", + "react", + "napi", + "--", + "--allow-build=react,napi", + ]) + .unwrap(); + if let Commands::Add { packages, pass_through_args, .. } = &args.commands { + assert_eq!(packages, &vec!["react", "napi"]); + assert_eq!(pass_through_args, &Some(vec!["--allow-build=react,napi".to_string()])); + } else { + panic!("Expected Add command"); + } + } + } } diff --git a/packages/cli/binding/src/commands/add.rs b/packages/cli/binding/src/commands/add.rs new file mode 100644 index 0000000000..3b50f1038e --- /dev/null +++ b/packages/cli/binding/src/commands/add.rs @@ -0,0 +1,92 @@ +use std::process::ExitStatus; + +use vite_package_manager::{ + add::{AddCommandOptions, SaveDependencyType}, + package_manager::PackageManager, +}; +use vite_path::AbsolutePathBuf; + +use crate::Error; + +/// Add command for adding packages to dependencies. +/// +/// This command automatically detects the package manager and translates +/// the add command to the appropriate package manager-specific syntax. +pub struct AddCommand { + cwd: AbsolutePathBuf, +} + +impl AddCommand { + pub fn new(cwd: AbsolutePathBuf) -> Self { + Self { cwd } + } + + pub async fn execute( + self, + packages: &[String], + save_dependency_type: Option, + save_exact: bool, + save_catalog_name: Option<&str>, + filters: Option<&[String]>, + workspace_root: bool, + workspace_only: bool, + global: bool, + allow_build: Option<&str>, + pass_through_args: Option<&[String]>, + ) -> Result { + if packages.is_empty() { + return Err(Error::NoPackagesSpecified); + } + + let add_command_options = AddCommandOptions { + packages, + save_dependency_type, + save_exact, + filters, + workspace_root, + workspace_only, + global, + save_catalog_name, + allow_build, + pass_through_args, + }; + + // Detect package manager + let package_manager = PackageManager::builder(&self.cwd).build().await?; + + package_manager.run_add_command(&add_command_options, &self.cwd).await + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_add_command_new() { + let workspace_root = if cfg!(windows) { + AbsolutePathBuf::new("C:\\test".into()).unwrap() + } else { + AbsolutePathBuf::new("/test".into()).unwrap() + }; + + let cmd = AddCommand::new(workspace_root.clone()); + assert_eq!(cmd.cwd, workspace_root); + } + + #[tokio::test] + async fn test_add_command_no_packages() { + let workspace_root = if cfg!(windows) { + AbsolutePathBuf::new("C:\\test".into()).unwrap() + } else { + AbsolutePathBuf::new("/test".into()).unwrap() + }; + + let cmd = AddCommand::new(workspace_root); + let result = + cmd.execute(&vec![], None, false, None, None, false, false, false, None, None).await; + + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), Error::NoPackagesSpecified)); + } +} diff --git a/packages/cli/binding/src/commands/mod.rs b/packages/cli/binding/src/commands/mod.rs index e523b8a314..fda8da6f0d 100644 --- a/packages/cli/binding/src/commands/mod.rs +++ b/packages/cli/binding/src/commands/mod.rs @@ -1,3 +1,4 @@ +pub(crate) mod add; pub(crate) mod doc; pub(crate) mod fmt; pub(crate) mod install; diff --git a/packages/cli/binding/src/lib.rs b/packages/cli/binding/src/lib.rs index 3a23aa3e32..166b2c8274 100644 --- a/packages/cli/binding/src/lib.rs +++ b/packages/cli/binding/src/lib.rs @@ -202,6 +202,8 @@ pub async fn run(options: CliOptions) -> Result { ) .await; + tracing::debug!("Result: {result:?}"); + match result { Ok(exit_status) => Ok(exit_status.code().unwrap_or(1)), Err(e) => { diff --git a/packages/cli/snap-tests/exit-non-zero-on-cmd-not-exists/snap.txt b/packages/cli/snap-tests/exit-non-zero-on-cmd-not-exists/snap.txt index 74818565b5..8a5cc1a1b4 100644 --- a/packages/cli/snap-tests/exit-non-zero-on-cmd-not-exists/snap.txt +++ b/packages/cli/snap-tests/exit-non-zero-on-cmd-not-exists/snap.txt @@ -1,6 +1,6 @@ [2]> vite command-not-exists # should exit with non-zero code error: 'vite' requires a subcommand but one was not provided - [subcommands: run, lint, fmt, build, test, lib, install, i, dev, doc, cache, help] + [subcommands: run, lint, fmt, build, test, lib, dev, doc, cache, install, i, add, help] Usage: vite [OPTIONS] [TASK] [-- ...] diff --git a/packages/global/snap-tests/cli-helper-message/snap.txt b/packages/global/snap-tests/cli-helper-message/snap.txt index 50ab0019e6..e6ec6a72f4 100644 --- a/packages/global/snap-tests/cli-helper-message/snap.txt +++ b/packages/global/snap-tests/cli-helper-message/snap.txt @@ -8,10 +8,11 @@ Commands: build test lib Lib command, build a library - install Install command. It will be passed to the package manager's install command currently dev doc Doc command, build documentation cache Manage the task cache + install Install command. It will be passed to the package manager's install command currently + add Add packages to dependencies help Print this message or the help of the given subcommand(s) Arguments: diff --git a/packages/global/snap-tests/command-add-npm10-with-workspace/package.json b/packages/global/snap-tests/command-add-npm10-with-workspace/package.json new file mode 100644 index 0000000000..2f22a116eb --- /dev/null +++ b/packages/global/snap-tests/command-add-npm10-with-workspace/package.json @@ -0,0 +1,8 @@ +{ + "name": "command-add-npm10-with-workspace", + "version": "1.0.0", + "packageManager": "npm@10.9.4", + "workspaces": [ + "packages/*" + ] +} diff --git a/packages/global/snap-tests/command-add-npm10-with-workspace/packages/app/package.json b/packages/global/snap-tests/command-add-npm10-with-workspace/packages/app/package.json new file mode 100644 index 0000000000..fd1f8f6386 --- /dev/null +++ b/packages/global/snap-tests/command-add-npm10-with-workspace/packages/app/package.json @@ -0,0 +1,3 @@ +{ + "name": "app" +} diff --git a/packages/global/snap-tests/command-add-npm10-with-workspace/packages/utils/package.json b/packages/global/snap-tests/command-add-npm10-with-workspace/packages/utils/package.json new file mode 100644 index 0000000000..d063b255a7 --- /dev/null +++ b/packages/global/snap-tests/command-add-npm10-with-workspace/packages/utils/package.json @@ -0,0 +1,5 @@ +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true +} diff --git a/packages/global/snap-tests/command-add-npm10-with-workspace/snap.txt b/packages/global/snap-tests/command-add-npm10-with-workspace/snap.txt new file mode 100644 index 0000000000..287fa04ff4 --- /dev/null +++ b/packages/global/snap-tests/command-add-npm10-with-workspace/snap.txt @@ -0,0 +1,226 @@ +> vp add testnpm2 -D -w -- --no-audit && cat package.json packages/app/package.json packages/utils/package.json # should add package to workspace root +Running: npm install --include-workspace-root --save-dev --no-audit testnpm2 + +added 3 packages in ms +{ + "name": "command-add-npm10-with-workspace", + "version": "1.0.0", + "packageManager": "npm@", + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "testnpm2": "^1.0.1" + } +} +{ + "name": "app" +} +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true +} + +> vp add @vite-plus-test/utils --workspace -- --no-audit && cat package.json packages/app/package.json packages/utils/package.json # should add @vite-plus-test/utils to workspace root +Running: npm install --no-audit @vite-plus-test/utils + +up to date in ms +{ + "name": "command-add-npm10-with-workspace", + "version": "1.0.0", + "packageManager": "npm@", + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "@vite-plus-test/utils": "^1.0.0" + } +} +{ + "name": "app" +} +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true +} + +> vp add testnpm2 test-vite-plus-install@1.0.0 --filter app -- --no-audit && cat package.json packages/app/package.json packages/utils/package.json # should add packages to packages/app +Running: npm install --workspace app --no-audit testnpm2 test-vite-plus-install@ + +added 1 package in ms +{ + "name": "command-add-npm10-with-workspace", + "version": "1.0.0", + "packageManager": "npm@", + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "@vite-plus-test/utils": "^1.0.0" + } +} +{ + "name": "app", + "dependencies": { + "test-vite-plus-install": "^1.0.0", + "testnpm2": "^1.0.1" + } +} +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true +} + +> vp add @vite-plus-test/utils --workspace --filter app -- --no-audit && cat package.json packages/app/package.json packages/utils/package.json # should add @vite-plus-test/utils to packages/app +Running: npm install --workspace app --no-audit @vite-plus-test/utils + +up to date in ms +{ + "name": "command-add-npm10-with-workspace", + "version": "1.0.0", + "packageManager": "npm@", + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "@vite-plus-test/utils": "^1.0.0" + } +} +{ + "name": "app", + "dependencies": { + "@vite-plus-test/utils": "^1.0.0", + "test-vite-plus-install": "^1.0.0", + "testnpm2": "^1.0.1" + } +} +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true +} + +> vp add testnpm2 test-vite-plus-install@1.0.0 --filter "*" -- --no-audit && cat package.json packages/app/package.json packages/utils/package.json # should add testnpm2 test-vite-plus-install to all packages except workspace root +Running: npm install --workspace * --no-audit testnpm2 test-vite-plus-install@ + +up to date in ms +{ + "name": "command-add-npm10-with-workspace", + "version": "1.0.0", + "packageManager": "npm@", + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "@vite-plus-test/utils": "^1.0.0" + } +} +{ + "name": "app", + "dependencies": { + "@vite-plus-test/utils": "^1.0.0", + "test-vite-plus-install": "^1.0.0", + "testnpm2": "^1.0.1" + } +} +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true, + "dependencies": { + "test-vite-plus-install": "^1.0.0", + "testnpm2": "^1.0.1" + } +} + +> vp add -E testnpm2 test-vite-plus-install@1.0.0 --filter "*" --workspace-root -- --no-audit && cat package.json packages/app/package.json packages/utils/package.json # should add testnpm2 test-vite-plus-install to all packages include workspace root +Running: npm install --workspace * --include-workspace-root --save-exact --no-audit testnpm2 test-vite-plus-install@ + +up to date in ms +{ + "name": "command-add-npm10-with-workspace", + "version": "1.0.0", + "packageManager": "npm@", + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "testnpm2": "1.0.1" + }, + "dependencies": { + "@vite-plus-test/utils": "^1.0.0", + "test-vite-plus-install": "1.0.0" + } +} +{ + "name": "app", + "dependencies": { + "@vite-plus-test/utils": "^1.0.0", + "test-vite-plus-install": "1.0.0", + "testnpm2": "1.0.1" + } +} +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true, + "dependencies": { + "test-vite-plus-install": "1.0.0", + "testnpm2": "1.0.1" + } +} + +> vp install test-vite-plus-package@1.0.0 --filter "*" --workspace-root -- --no-audit && cat package.json packages/app/package.json packages/utils/package.json # should install packages alias for add command +Running: npm install --workspace * --include-workspace-root --no-audit test-vite-plus-package@ + +added 1 package in ms +{ + "name": "command-add-npm10-with-workspace", + "version": "1.0.0", + "packageManager": "npm@", + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "testnpm2": "1.0.1" + }, + "dependencies": { + "@vite-plus-test/utils": "^1.0.0", + "test-vite-plus-install": "1.0.0", + "test-vite-plus-package": "^1.0.0" + } +} +{ + "name": "app", + "dependencies": { + "@vite-plus-test/utils": "^1.0.0", + "test-vite-plus-install": "1.0.0", + "test-vite-plus-package": "^1.0.0", + "testnpm2": "1.0.1" + } +} +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true, + "dependencies": { + "test-vite-plus-install": "1.0.0", + "test-vite-plus-package": "^1.0.0", + "testnpm2": "1.0.1" + } +} diff --git a/packages/global/snap-tests/command-add-npm10-with-workspace/steps.json b/packages/global/snap-tests/command-add-npm10-with-workspace/steps.json new file mode 100644 index 0000000000..66416c00f3 --- /dev/null +++ b/packages/global/snap-tests/command-add-npm10-with-workspace/steps.json @@ -0,0 +1,14 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp add testnpm2 -D -w -- --no-audit && cat package.json packages/app/package.json packages/utils/package.json # should add package to workspace root", + "vp add @vite-plus-test/utils --workspace -- --no-audit && cat package.json packages/app/package.json packages/utils/package.json # should add @vite-plus-test/utils to workspace root", + "vp add testnpm2 test-vite-plus-install@1.0.0 --filter app -- --no-audit && cat package.json packages/app/package.json packages/utils/package.json # should add packages to packages/app", + "vp add @vite-plus-test/utils --workspace --filter app -- --no-audit && cat package.json packages/app/package.json packages/utils/package.json # should add @vite-plus-test/utils to packages/app", + "vp add testnpm2 test-vite-plus-install@1.0.0 --filter \"*\" -- --no-audit && cat package.json packages/app/package.json packages/utils/package.json # should add testnpm2 test-vite-plus-install to all packages except workspace root", + "vp add -E testnpm2 test-vite-plus-install@1.0.0 --filter \"*\" --workspace-root -- --no-audit && cat package.json packages/app/package.json packages/utils/package.json # should add testnpm2 test-vite-plus-install to all packages include workspace root", + "vp install test-vite-plus-package@1.0.0 --filter \"*\" --workspace-root -- --no-audit && cat package.json packages/app/package.json packages/utils/package.json # should install packages alias for add command" + ] +} diff --git a/packages/global/snap-tests/command-add-npm10/package.json b/packages/global/snap-tests/command-add-npm10/package.json new file mode 100644 index 0000000000..685d3aeb89 --- /dev/null +++ b/packages/global/snap-tests/command-add-npm10/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-add-npm10", + "version": "1.0.0", + "packageManager": "npm@10.9.4" +} diff --git a/packages/global/snap-tests/command-add-npm10/snap.txt b/packages/global/snap-tests/command-add-npm10/snap.txt new file mode 100644 index 0000000000..07ecc12041 --- /dev/null +++ b/packages/global/snap-tests/command-add-npm10/snap.txt @@ -0,0 +1,128 @@ +> vp add --help # should show help +Add packages to dependencies + +Usage: vp add [OPTIONS] [PACKAGES]... [-- ...] + +Arguments: + [PACKAGES]... Packages to add + [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager + +Options: + -P, --save-prod + Save to `dependencies` (default) + -D, --save-dev + Save to `devDependencies` + --save-peer + Save to `peerDependencies` and `devDependencies` + -O, --save-optional + Save to `optionalDependencies` + -E, --save-exact + Save exact version rather than semver range (e.g., `^1.0.0` -> `1.0.0`) + --save-catalog-name + Save the new dependency to the specified catalog name. Example: `vite add vue --save-catalog-name vue3` + --save-catalog + Save the new dependency to the default catalog + --allow-build + A list of package names allowed to run postinstall + --filter + Filter packages in monorepo (can be used multiple times) + -w, --workspace-root + Add to workspace root (ignore-workspace-root-check) + --workspace + Only add if package exists in workspace (pnpm-specific) + -g, --global + Install globally + -h, --help + Print help + +> vp add testnpm2 -D -- --no-audit && cat package.json # should add package as dev dependencies +Running: npm install --save-dev --no-audit testnpm2 + +added 1 package in ms +{ + "name": "command-add-npm10", + "version": "1.0.0", + "packageManager": "npm@", + "devDependencies": { + "testnpm2": "^1.0.1" + } +} + +> vp add testnpm2 test-vite-plus-install --allow-build=test-vite-plus-install -- --no-audit && cat package.json # should add packages to dependencies +Running: npm install --no-audit testnpm2 test-vite-plus-install + +added 1 package in ms +{ + "name": "command-add-npm10", + "version": "1.0.0", + "packageManager": "npm@", + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "test-vite-plus-install": "^1.0.0" + } +} + +> vp install test-vite-plus-package@1.0.0 --save-peer -- --no-audit && cat package.json # should install package alias for add +Running: npm install --save-peer --no-audit test-vite-plus-package@ + +added 1 package in ms +{ + "name": "command-add-npm10", + "version": "1.0.0", + "packageManager": "npm@", + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "test-vite-plus-install": "^1.0.0" + }, + "peerDependencies": { + "test-vite-plus-package": "^1.0.0" + } +} + +> vp add test-vite-plus-package-optional -O -- --no-audit && cat package.json # should add package as optional dependencies +Running: npm install --save-optional --no-audit test-vite-plus-package-optional + +added 1 package in ms +{ + "name": "command-add-npm10", + "version": "1.0.0", + "packageManager": "npm@", + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "test-vite-plus-install": "^1.0.0" + }, + "peerDependencies": { + "test-vite-plus-package": "^1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "^1.0.0" + } +} + +> vp add test-vite-plus-package-optional -- --loglevel=warn --no-audit && cat package.json # support pass through arguments +Running: npm install --loglevel=warn --no-audit test-vite-plus-package-optional + +up to date in ms +{ + "name": "command-add-npm10", + "version": "1.0.0", + "packageManager": "npm@", + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "test-vite-plus-install": "^1.0.0" + }, + "peerDependencies": { + "test-vite-plus-package": "^1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "^1.0.0" + } +} diff --git a/packages/global/snap-tests/command-add-npm10/steps.json b/packages/global/snap-tests/command-add-npm10/steps.json new file mode 100644 index 0000000000..4e1d073fd6 --- /dev/null +++ b/packages/global/snap-tests/command-add-npm10/steps.json @@ -0,0 +1,13 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp add --help # should show help", + "vp add testnpm2 -D -- --no-audit && cat package.json # should add package as dev dependencies", + "vp add testnpm2 test-vite-plus-install --allow-build=test-vite-plus-install -- --no-audit && cat package.json # should add packages to dependencies", + "vp install test-vite-plus-package@1.0.0 --save-peer -- --no-audit && cat package.json # should install package alias for add", + "vp add test-vite-plus-package-optional -O -- --no-audit && cat package.json # should add package as optional dependencies", + "vp add test-vite-plus-package-optional -- --loglevel=warn --no-audit && cat package.json # support pass through arguments" + ] +} diff --git a/packages/global/snap-tests/command-add-npm11-with-workspace/package.json b/packages/global/snap-tests/command-add-npm11-with-workspace/package.json new file mode 100644 index 0000000000..bfef069a65 --- /dev/null +++ b/packages/global/snap-tests/command-add-npm11-with-workspace/package.json @@ -0,0 +1,8 @@ +{ + "name": "command-add-npm11-with-workspace", + "version": "1.0.0", + "packageManager": "npm@11.6.2", + "workspaces": [ + "packages/*" + ] +} diff --git a/packages/global/snap-tests/command-add-npm11-with-workspace/packages/app/package.json b/packages/global/snap-tests/command-add-npm11-with-workspace/packages/app/package.json new file mode 100644 index 0000000000..fd1f8f6386 --- /dev/null +++ b/packages/global/snap-tests/command-add-npm11-with-workspace/packages/app/package.json @@ -0,0 +1,3 @@ +{ + "name": "app" +} diff --git a/packages/global/snap-tests/command-add-npm11-with-workspace/packages/utils/package.json b/packages/global/snap-tests/command-add-npm11-with-workspace/packages/utils/package.json new file mode 100644 index 0000000000..d063b255a7 --- /dev/null +++ b/packages/global/snap-tests/command-add-npm11-with-workspace/packages/utils/package.json @@ -0,0 +1,5 @@ +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true +} diff --git a/packages/global/snap-tests/command-add-npm11-with-workspace/snap.txt b/packages/global/snap-tests/command-add-npm11-with-workspace/snap.txt new file mode 100644 index 0000000000..26297a7286 --- /dev/null +++ b/packages/global/snap-tests/command-add-npm11-with-workspace/snap.txt @@ -0,0 +1,226 @@ +> vp add testnpm2 -D -w -- --no-audit && cat package.json packages/app/package.json packages/utils/package.json # should add package to workspace root +Running: npm install --include-workspace-root --save-dev --no-audit testnpm2 + +added 3 packages in ms +{ + "name": "command-add-npm11-with-workspace", + "version": "1.0.0", + "packageManager": "npm@", + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "testnpm2": "^1.0.1" + } +} +{ + "name": "app" +} +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true +} + +> vp add @vite-plus-test/utils --workspace -- --no-audit && cat package.json packages/app/package.json packages/utils/package.json # should add @vite-plus-test/utils to workspace root +Running: npm install --no-audit @vite-plus-test/utils + +up to date in ms +{ + "name": "command-add-npm11-with-workspace", + "version": "1.0.0", + "packageManager": "npm@", + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "@vite-plus-test/utils": "^1.0.0" + } +} +{ + "name": "app" +} +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true +} + +> vp add testnpm2 test-vite-plus-install@1.0.0 --filter app -- --no-audit && cat package.json packages/app/package.json packages/utils/package.json # should add packages to packages/app +Running: npm install --workspace app --no-audit testnpm2 test-vite-plus-install@ + +added 1 package in ms +{ + "name": "command-add-npm11-with-workspace", + "version": "1.0.0", + "packageManager": "npm@", + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "@vite-plus-test/utils": "^1.0.0" + } +} +{ + "name": "app", + "dependencies": { + "test-vite-plus-install": "^1.0.0", + "testnpm2": "^1.0.1" + } +} +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true +} + +> vp add @vite-plus-test/utils --workspace --filter app -- --no-audit && cat package.json packages/app/package.json packages/utils/package.json # should add @vite-plus-test/utils to packages/app +Running: npm install --workspace app --no-audit @vite-plus-test/utils + +up to date in ms +{ + "name": "command-add-npm11-with-workspace", + "version": "1.0.0", + "packageManager": "npm@", + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "@vite-plus-test/utils": "^1.0.0" + } +} +{ + "name": "app", + "dependencies": { + "@vite-plus-test/utils": "^1.0.0", + "test-vite-plus-install": "^1.0.0", + "testnpm2": "^1.0.1" + } +} +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true +} + +> vp add testnpm2 test-vite-plus-install@1.0.0 --filter "*" -- --no-audit && cat package.json packages/app/package.json packages/utils/package.json # should add testnpm2 test-vite-plus-install to all packages except workspace root +Running: npm install --workspace * --no-audit testnpm2 test-vite-plus-install@ + +up to date in ms +{ + "name": "command-add-npm11-with-workspace", + "version": "1.0.0", + "packageManager": "npm@", + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "@vite-plus-test/utils": "^1.0.0" + } +} +{ + "name": "app", + "dependencies": { + "@vite-plus-test/utils": "^1.0.0", + "test-vite-plus-install": "^1.0.0", + "testnpm2": "^1.0.1" + } +} +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true, + "dependencies": { + "test-vite-plus-install": "^1.0.0", + "testnpm2": "^1.0.1" + } +} + +> vp add -E testnpm2 test-vite-plus-install@1.0.0 --filter "*" --workspace-root -- --no-audit && cat package.json packages/app/package.json packages/utils/package.json # should add testnpm2 test-vite-plus-install to all packages include workspace root +Running: npm install --workspace * --include-workspace-root --save-exact --no-audit testnpm2 test-vite-plus-install@ + +up to date in ms +{ + "name": "command-add-npm11-with-workspace", + "version": "1.0.0", + "packageManager": "npm@", + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "testnpm2": "1.0.1" + }, + "dependencies": { + "@vite-plus-test/utils": "^1.0.0", + "test-vite-plus-install": "1.0.0" + } +} +{ + "name": "app", + "dependencies": { + "@vite-plus-test/utils": "^1.0.0", + "test-vite-plus-install": "1.0.0", + "testnpm2": "1.0.1" + } +} +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true, + "dependencies": { + "test-vite-plus-install": "1.0.0", + "testnpm2": "1.0.1" + } +} + +> vp install test-vite-plus-package@1.0.0 --filter "*" --workspace-root -- --no-audit && cat package.json packages/app/package.json packages/utils/package.json # should install packages alias for add command +Running: npm install --workspace * --include-workspace-root --no-audit test-vite-plus-package@ + +added 1 package in ms +{ + "name": "command-add-npm11-with-workspace", + "version": "1.0.0", + "packageManager": "npm@", + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "testnpm2": "1.0.1" + }, + "dependencies": { + "@vite-plus-test/utils": "^1.0.0", + "test-vite-plus-install": "1.0.0", + "test-vite-plus-package": "^1.0.0" + } +} +{ + "name": "app", + "dependencies": { + "@vite-plus-test/utils": "^1.0.0", + "test-vite-plus-install": "1.0.0", + "test-vite-plus-package": "^1.0.0", + "testnpm2": "1.0.1" + } +} +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true, + "dependencies": { + "test-vite-plus-install": "1.0.0", + "test-vite-plus-package": "^1.0.0", + "testnpm2": "1.0.1" + } +} diff --git a/packages/global/snap-tests/command-add-npm11-with-workspace/steps.json b/packages/global/snap-tests/command-add-npm11-with-workspace/steps.json new file mode 100644 index 0000000000..66416c00f3 --- /dev/null +++ b/packages/global/snap-tests/command-add-npm11-with-workspace/steps.json @@ -0,0 +1,14 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp add testnpm2 -D -w -- --no-audit && cat package.json packages/app/package.json packages/utils/package.json # should add package to workspace root", + "vp add @vite-plus-test/utils --workspace -- --no-audit && cat package.json packages/app/package.json packages/utils/package.json # should add @vite-plus-test/utils to workspace root", + "vp add testnpm2 test-vite-plus-install@1.0.0 --filter app -- --no-audit && cat package.json packages/app/package.json packages/utils/package.json # should add packages to packages/app", + "vp add @vite-plus-test/utils --workspace --filter app -- --no-audit && cat package.json packages/app/package.json packages/utils/package.json # should add @vite-plus-test/utils to packages/app", + "vp add testnpm2 test-vite-plus-install@1.0.0 --filter \"*\" -- --no-audit && cat package.json packages/app/package.json packages/utils/package.json # should add testnpm2 test-vite-plus-install to all packages except workspace root", + "vp add -E testnpm2 test-vite-plus-install@1.0.0 --filter \"*\" --workspace-root -- --no-audit && cat package.json packages/app/package.json packages/utils/package.json # should add testnpm2 test-vite-plus-install to all packages include workspace root", + "vp install test-vite-plus-package@1.0.0 --filter \"*\" --workspace-root -- --no-audit && cat package.json packages/app/package.json packages/utils/package.json # should install packages alias for add command" + ] +} diff --git a/packages/global/snap-tests/command-add-npm11/package.json b/packages/global/snap-tests/command-add-npm11/package.json new file mode 100644 index 0000000000..8b7e595379 --- /dev/null +++ b/packages/global/snap-tests/command-add-npm11/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-add-npm11", + "version": "1.0.0", + "packageManager": "npm@11.6.2" +} diff --git a/packages/global/snap-tests/command-add-npm11/snap.txt b/packages/global/snap-tests/command-add-npm11/snap.txt new file mode 100644 index 0000000000..25091fd899 --- /dev/null +++ b/packages/global/snap-tests/command-add-npm11/snap.txt @@ -0,0 +1,128 @@ +> vp add --help # should show help +Add packages to dependencies + +Usage: vp add [OPTIONS] [PACKAGES]... [-- ...] + +Arguments: + [PACKAGES]... Packages to add + [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager + +Options: + -P, --save-prod + Save to `dependencies` (default) + -D, --save-dev + Save to `devDependencies` + --save-peer + Save to `peerDependencies` and `devDependencies` + -O, --save-optional + Save to `optionalDependencies` + -E, --save-exact + Save exact version rather than semver range (e.g., `^1.0.0` -> `1.0.0`) + --save-catalog-name + Save the new dependency to the specified catalog name. Example: `vite add vue --save-catalog-name vue3` + --save-catalog + Save the new dependency to the default catalog + --allow-build + A list of package names allowed to run postinstall + --filter + Filter packages in monorepo (can be used multiple times) + -w, --workspace-root + Add to workspace root (ignore-workspace-root-check) + --workspace + Only add if package exists in workspace (pnpm-specific) + -g, --global + Install globally + -h, --help + Print help + +> vp add testnpm2 -D -- --no-audit && cat package.json # should add package as dev dependencies +Running: npm install --save-dev --no-audit testnpm2 + +added 1 package in ms +{ + "name": "command-add-npm11", + "version": "1.0.0", + "packageManager": "npm@", + "devDependencies": { + "testnpm2": "^1.0.1" + } +} + +> vp add testnpm2 test-vite-plus-install --allow-build=test-vite-plus-install -- --no-audit && cat package.json # should add packages to dependencies +Running: npm install --no-audit testnpm2 test-vite-plus-install + +added 1 package in ms +{ + "name": "command-add-npm11", + "version": "1.0.0", + "packageManager": "npm@", + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "test-vite-plus-install": "^1.0.0" + } +} + +> vp install test-vite-plus-package@1.0.0 --save-peer -- --no-audit && cat package.json # should install package alias for add +Running: npm install --save-peer --no-audit test-vite-plus-package@ + +added 1 package in ms +{ + "name": "command-add-npm11", + "version": "1.0.0", + "packageManager": "npm@", + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "test-vite-plus-install": "^1.0.0" + }, + "peerDependencies": { + "test-vite-plus-package": "^1.0.0" + } +} + +> vp add test-vite-plus-package-optional -O -- --no-audit && cat package.json # should add package as optional dependencies +Running: npm install --save-optional --no-audit test-vite-plus-package-optional + +added 1 package in ms +{ + "name": "command-add-npm11", + "version": "1.0.0", + "packageManager": "npm@", + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "test-vite-plus-install": "^1.0.0" + }, + "peerDependencies": { + "test-vite-plus-package": "^1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "^1.0.0" + } +} + +> vp add test-vite-plus-package-optional -- --loglevel=warn --no-audit && cat package.json # support pass through arguments +Running: npm install --loglevel=warn --no-audit test-vite-plus-package-optional + +up to date in ms +{ + "name": "command-add-npm11", + "version": "1.0.0", + "packageManager": "npm@", + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "test-vite-plus-install": "^1.0.0" + }, + "peerDependencies": { + "test-vite-plus-package": "^1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "^1.0.0" + } +} diff --git a/packages/global/snap-tests/command-add-npm11/steps.json b/packages/global/snap-tests/command-add-npm11/steps.json new file mode 100644 index 0000000000..4e1d073fd6 --- /dev/null +++ b/packages/global/snap-tests/command-add-npm11/steps.json @@ -0,0 +1,13 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp add --help # should show help", + "vp add testnpm2 -D -- --no-audit && cat package.json # should add package as dev dependencies", + "vp add testnpm2 test-vite-plus-install --allow-build=test-vite-plus-install -- --no-audit && cat package.json # should add packages to dependencies", + "vp install test-vite-plus-package@1.0.0 --save-peer -- --no-audit && cat package.json # should install package alias for add", + "vp add test-vite-plus-package-optional -O -- --no-audit && cat package.json # should add package as optional dependencies", + "vp add test-vite-plus-package-optional -- --loglevel=warn --no-audit && cat package.json # support pass through arguments" + ] +} diff --git a/packages/global/snap-tests/command-add-pnpm10-with-workspace/package.json b/packages/global/snap-tests/command-add-pnpm10-with-workspace/package.json new file mode 100644 index 0000000000..85219debb0 --- /dev/null +++ b/packages/global/snap-tests/command-add-pnpm10-with-workspace/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-add-pnpm10-with-workspace", + "version": "1.0.0", + "packageManager": "pnpm@10.19.0" +} diff --git a/packages/global/snap-tests/command-add-pnpm10-with-workspace/packages/app/package.json b/packages/global/snap-tests/command-add-pnpm10-with-workspace/packages/app/package.json new file mode 100644 index 0000000000..fd1f8f6386 --- /dev/null +++ b/packages/global/snap-tests/command-add-pnpm10-with-workspace/packages/app/package.json @@ -0,0 +1,3 @@ +{ + "name": "app" +} diff --git a/packages/global/snap-tests/command-add-pnpm10-with-workspace/packages/utils/package.json b/packages/global/snap-tests/command-add-pnpm10-with-workspace/packages/utils/package.json new file mode 100644 index 0000000000..d063b255a7 --- /dev/null +++ b/packages/global/snap-tests/command-add-pnpm10-with-workspace/packages/utils/package.json @@ -0,0 +1,5 @@ +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true +} diff --git a/packages/global/snap-tests/command-add-pnpm10-with-workspace/pnpm-workspace.yaml b/packages/global/snap-tests/command-add-pnpm10-with-workspace/pnpm-workspace.yaml new file mode 100644 index 0000000000..924b55f42e --- /dev/null +++ b/packages/global/snap-tests/command-add-pnpm10-with-workspace/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - packages/* diff --git a/packages/global/snap-tests/command-add-pnpm10-with-workspace/snap.txt b/packages/global/snap-tests/command-add-pnpm10-with-workspace/snap.txt new file mode 100644 index 0000000000..621deb5f8f --- /dev/null +++ b/packages/global/snap-tests/command-add-pnpm10-with-workspace/snap.txt @@ -0,0 +1,226 @@ +> vp add testnpm2 -D -w && cat package.json # should add package to workspace root +Running: pnpm add --workspace-root --save-dev testnpm2 +Progress: resolved , reused , downloaded , added , done + +devDependencies: ++ testnpm2 ^1.0.1 + +Packages: + ++ +Done in ms using pnpm v +{ + "name": "command-add-pnpm10-with-workspace", + "version": "1.0.0", + "packageManager": "pnpm@", + "devDependencies": { + "testnpm2": "^1.0.1" + } +} + +> vp add @vite-plus-test/utils --workspace && cat package.json # should add @vite-plus-test/utils to workspace root +Running: pnpm add --workspace @vite-plus-test/utils +Progress: resolved , reused , downloaded , added , done + +dependencies: ++ @vite-plus-test/utils workspace:* + +Already up to date +Done in ms using pnpm v +{ + "name": "command-add-pnpm10-with-workspace", + "version": "1.0.0", + "packageManager": "pnpm@", + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "@vite-plus-test/utils": "workspace:*" + } +} + +> vp add testnpm2 test-vite-plus-install@1.0.0 --filter app && cat package.json packages/app/package.json packages/utils/package.json # should add packages to packages/app +Running: pnpm --filter app add testnpm2 test-vite-plus-install@ +Progress: resolved , reused , downloaded , added , done +. | +1 + +Done in ms using pnpm v +{ + "name": "command-add-pnpm10-with-workspace", + "version": "1.0.0", + "packageManager": "pnpm@", + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "@vite-plus-test/utils": "workspace:*" + } +} +{ + "name": "app", + "dependencies": { + "test-vite-plus-install": "1.0.0", + "testnpm2": "^1.0.1" + } +} +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true +} + +> vp add @vite-plus-test/utils --workspace --filter app && cat package.json packages/app/package.json packages/utils/package.json # should add @vite-plus-test/utils to packages/app +Running: pnpm --filter app add --workspace @vite-plus-test/utils +Progress: resolved , reused , downloaded , added , done +Done in ms using pnpm v +{ + "name": "command-add-pnpm10-with-workspace", + "version": "1.0.0", + "packageManager": "pnpm@", + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "@vite-plus-test/utils": "workspace:*" + } +} +{ + "name": "app", + "dependencies": { + "@vite-plus-test/utils": "workspace:*", + "test-vite-plus-install": "1.0.0", + "testnpm2": "^1.0.1" + } +} +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true +} + +> vp add -E testnpm2 test-vite-plus-install --filter "*" && cat package.json packages/app/package.json packages/utils/package.json # should add testnpm2 test-vite-plus-install to all packages except workspace root +Running: pnpm --filter * add --save-exact testnpm2 test-vite-plus-install +Progress: resolved , reused , downloaded , added , done +Done in ms using pnpm v +{ + "name": "command-add-pnpm10-with-workspace", + "version": "1.0.0", + "packageManager": "pnpm@", + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "@vite-plus-test/utils": "workspace:*" + } +} +{ + "name": "app", + "dependencies": { + "@vite-plus-test/utils": "workspace:*", + "test-vite-plus-install": "1.0.0", + "testnpm2": "^1.0.1" + } +} +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true, + "dependencies": { + "test-vite-plus-install": "1.0.0", + "testnpm2": "^1.0.1" + } +} + +> vp install test-vite-plus-package@1.0.0 --filter "*" --workspace-root --save-catalog && cat package.json packages/app/package.json packages/utils/package.json pnpm-workspace.yaml # should install packages alias for add command +Running: pnpm --filter * add --workspace-root --save-catalog test-vite-plus-package@ +. | +1 + +Progress: resolved , reused , downloaded , added , done +Done in ms using pnpm v +{ + "name": "command-add-pnpm10-with-workspace", + "version": "1.0.0", + "packageManager": "pnpm@", + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "@vite-plus-test/utils": "workspace:*", + "test-vite-plus-package": "catalog:" + } +} +{ + "name": "app", + "dependencies": { + "@vite-plus-test/utils": "workspace:*", + "test-vite-plus-install": "1.0.0", + "test-vite-plus-package": "catalog:", + "testnpm2": "^1.0.1" + } +} +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true, + "dependencies": { + "test-vite-plus-install": "1.0.0", + "test-vite-plus-package": "catalog:", + "testnpm2": "^1.0.1" + } +} +packages: + - packages/* + +catalog: + test-vite-plus-package: + +> vp add --filter app test-vite-plus-package-optional --save-catalog-name v1 && cat packages/app/package.json pnpm-workspace.yaml # should add with save-catalog-name +Running: pnpm --filter app add --save-catalog-name=v1 test-vite-plus-package-optional +Progress: resolved , reused , downloaded , added , done +. | +1 + +Done in ms using pnpm v +{ + "name": "app", + "dependencies": { + "@vite-plus-test/utils": "workspace:*", + "test-vite-plus-install": "1.0.0", + "test-vite-plus-package": "catalog:", + "test-vite-plus-package-optional": "catalog:v1", + "testnpm2": "^1.0.1" + } +} +packages: + - packages/* + +catalog: + test-vite-plus-package: + +catalogs: + v1: + test-vite-plus-package-optional: ^1.0.0 + +> vp add --filter=./packages/utils test-vite-plus-package-optional -O --save-catalog-name v2 && cat packages/utils/package.json pnpm-workspace.yaml # should add other with save-catalog-name +Running: pnpm --filter ./packages/utils add --save-optional --save-catalog-name=v2 test-vite-plus-package-optional +Progress: resolved , reused , downloaded , added , done +Done in ms using pnpm v +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true, + "dependencies": { + "test-vite-plus-install": "1.0.0", + "test-vite-plus-package": "catalog:", + "testnpm2": "^1.0.1" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "catalog:v2" + } +} +packages: + - packages/* + +catalog: + test-vite-plus-package: + +catalogs: + v1: + test-vite-plus-package-optional: ^1.0.0 + v2: + test-vite-plus-package-optional: ^1.0.0 diff --git a/packages/global/snap-tests/command-add-pnpm10-with-workspace/steps.json b/packages/global/snap-tests/command-add-pnpm10-with-workspace/steps.json new file mode 100644 index 0000000000..55b864d55d --- /dev/null +++ b/packages/global/snap-tests/command-add-pnpm10-with-workspace/steps.json @@ -0,0 +1,15 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp add testnpm2 -D -w && cat package.json # should add package to workspace root", + "vp add @vite-plus-test/utils --workspace && cat package.json # should add @vite-plus-test/utils to workspace root", + "vp add testnpm2 test-vite-plus-install@1.0.0 --filter app && cat package.json packages/app/package.json packages/utils/package.json # should add packages to packages/app", + "vp add @vite-plus-test/utils --workspace --filter app && cat package.json packages/app/package.json packages/utils/package.json # should add @vite-plus-test/utils to packages/app", + "vp add -E testnpm2 test-vite-plus-install --filter \"*\" && cat package.json packages/app/package.json packages/utils/package.json # should add testnpm2 test-vite-plus-install to all packages except workspace root", + "vp install test-vite-plus-package@1.0.0 --filter \"*\" --workspace-root --save-catalog && cat package.json packages/app/package.json packages/utils/package.json pnpm-workspace.yaml # should install packages alias for add command", + "vp add --filter app test-vite-plus-package-optional --save-catalog-name v1 && cat packages/app/package.json pnpm-workspace.yaml # should add with save-catalog-name", + "vp add --filter=./packages/utils test-vite-plus-package-optional -O --save-catalog-name v2 && cat packages/utils/package.json pnpm-workspace.yaml # should add other with save-catalog-name" + ] +} diff --git a/packages/global/snap-tests/command-add-pnpm10/package.json b/packages/global/snap-tests/command-add-pnpm10/package.json new file mode 100644 index 0000000000..4981001600 --- /dev/null +++ b/packages/global/snap-tests/command-add-pnpm10/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-add-pnpm10", + "version": "1.0.0", + "packageManager": "pnpm@10.19.0" +} diff --git a/packages/global/snap-tests/command-add-pnpm10/snap.txt b/packages/global/snap-tests/command-add-pnpm10/snap.txt new file mode 100644 index 0000000000..0a65f5b36e --- /dev/null +++ b/packages/global/snap-tests/command-add-pnpm10/snap.txt @@ -0,0 +1,63 @@ +> vp add --help # should show help +Add packages to dependencies + +Usage: vp add [OPTIONS] [PACKAGES]... [-- ...] + +Arguments: + [PACKAGES]... Packages to add + [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager + +Options: + -P, --save-prod + Save to `dependencies` (default) + -D, --save-dev + Save to `devDependencies` + --save-peer + Save to `peerDependencies` and `devDependencies` + -O, --save-optional + Save to `optionalDependencies` + -E, --save-exact + Save exact version rather than semver range (e.g., `^1.0.0` -> `1.0.0`) + --save-catalog-name + Save the new dependency to the specified catalog name. Example: `vite add vue --save-catalog-name vue3` + --save-catalog + Save the new dependency to the default catalog + --allow-build + A list of package names allowed to run postinstall + --filter + Filter packages in monorepo (can be used multiple times) + -w, --workspace-root + Add to workspace root (ignore-workspace-root-check) + --workspace + Only add if package exists in workspace (pnpm-specific) + -g, --global + Install globally + -h, --help + Print help + +> vp add testnpm2 urllib -D -- --loglevel=verbose --verbose && cat package.json # should add package as dev dependencies +Running: pnpm add --save-dev --loglevel=verbose --verbose testnpm2 urllib +Packages: + ++ +Progress: resolved , reused , downloaded , added , done + +devDependencies: ++ testnpm2 ++ urllib + +Done in ms using pnpm v +{ + "name": "command-add-pnpm10", + "version": "1.0.0", + "packageManager": "pnpm@", + "devDependencies": { + "testnpm2": "^1.0.1", + "urllib": "^4.8.2" + } +} + +> #vp add testnpm2 test-vite-plus-install --allow-build=test-vite-plus-install && cat package.json # should add packages to dependencies +> #vp install test-vite-plus-package@1.0.0 --save-peer && cat package.json # should install package alias for add +> #vp add test-vite-plus-package-optional -O && cat package.json # should add package as optional dependencies +> #vp add test-vite-plus-package-optional -- --loglevel=warn && cat package.json # support pass through arguments +> #vp add -g testnpm2 -- --dry-run && cat package.json # support add global package with dry-run \ No newline at end of file diff --git a/packages/global/snap-tests/command-add-pnpm10/steps.json b/packages/global/snap-tests/command-add-pnpm10/steps.json new file mode 100644 index 0000000000..44bb6ebd68 --- /dev/null +++ b/packages/global/snap-tests/command-add-pnpm10/steps.json @@ -0,0 +1,14 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp add --help # should show help", + "vp add testnpm2 urllib -D -- --loglevel=verbose --verbose && cat package.json # should add package as dev dependencies", + "#vp add testnpm2 test-vite-plus-install --allow-build=test-vite-plus-install && cat package.json # should add packages to dependencies", + "#vp install test-vite-plus-package@1.0.0 --save-peer && cat package.json # should install package alias for add", + "#vp add test-vite-plus-package-optional -O && cat package.json # should add package as optional dependencies", + "#vp add test-vite-plus-package-optional -- --loglevel=warn && cat package.json # support pass through arguments", + "#vp add -g testnpm2 -- --dry-run && cat package.json # support add global package with dry-run" + ] +} diff --git a/packages/global/snap-tests/command-add-pnpm9-with-workspace/package.json b/packages/global/snap-tests/command-add-pnpm9-with-workspace/package.json new file mode 100644 index 0000000000..2a0b3bf02f --- /dev/null +++ b/packages/global/snap-tests/command-add-pnpm9-with-workspace/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-add-pnpm9-with-workspace", + "version": "1.0.0", + "packageManager": "pnpm@9.15.9" +} diff --git a/packages/global/snap-tests/command-add-pnpm9-with-workspace/packages/app/package.json b/packages/global/snap-tests/command-add-pnpm9-with-workspace/packages/app/package.json new file mode 100644 index 0000000000..fd1f8f6386 --- /dev/null +++ b/packages/global/snap-tests/command-add-pnpm9-with-workspace/packages/app/package.json @@ -0,0 +1,3 @@ +{ + "name": "app" +} diff --git a/packages/global/snap-tests/command-add-pnpm9-with-workspace/packages/utils/package.json b/packages/global/snap-tests/command-add-pnpm9-with-workspace/packages/utils/package.json new file mode 100644 index 0000000000..d063b255a7 --- /dev/null +++ b/packages/global/snap-tests/command-add-pnpm9-with-workspace/packages/utils/package.json @@ -0,0 +1,5 @@ +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true +} diff --git a/packages/global/snap-tests/command-add-pnpm9-with-workspace/pnpm-workspace.yaml b/packages/global/snap-tests/command-add-pnpm9-with-workspace/pnpm-workspace.yaml new file mode 100644 index 0000000000..924b55f42e --- /dev/null +++ b/packages/global/snap-tests/command-add-pnpm9-with-workspace/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - packages/* diff --git a/packages/global/snap-tests/command-add-pnpm9-with-workspace/snap.txt b/packages/global/snap-tests/command-add-pnpm9-with-workspace/snap.txt new file mode 100644 index 0000000000..78e39bd980 --- /dev/null +++ b/packages/global/snap-tests/command-add-pnpm9-with-workspace/snap.txt @@ -0,0 +1,158 @@ +> vp add testnpm2 -D -w && cat package.json # should add package to workspace root +Running: pnpm add --workspace-root --save-dev testnpm2 +Progress: resolved , reused , downloaded , added , done + +devDependencies: ++ testnpm2 ^1.0.1 + +Packages: + ++ +Done in ms using pnpm v +{ + "name": "command-add-pnpm9-with-workspace", + "version": "1.0.0", + "packageManager": "pnpm@", + "devDependencies": { + "testnpm2": "^1.0.1" + } +} + +[1]> vp add @vite-plus-test/utils --workspace && cat package.json # should add @vite-plus-test/utils to workspace root +Running: pnpm add --workspace @vite-plus-test/utils + ERR_PNPM_ADDING_TO_ROOT  Running this command will add the dependency to the workspace root, which might not be what you want - if you really meant it, make it explicit by running this command again with the -w flag (or --workspace-root). If you don't want to see this warning anymore, you may set the ignore-workspace-root-check setting to true. + +> vp add testnpm2 test-vite-plus-install@1.0.0 --filter app && cat package.json packages/app/package.json packages/utils/package.json # should add packages to packages/app +Running: pnpm --filter app add testnpm2 test-vite-plus-install@ +. |  WARN  `node_modules` is present. Lockfile only installation will make it out-of-date +Progress: resolved , reused , downloaded , added , done +. | +1 + +Done in ms using pnpm v +{ + "name": "command-add-pnpm9-with-workspace", + "version": "1.0.0", + "packageManager": "pnpm@", + "devDependencies": { + "testnpm2": "^1.0.1" + } +} +{ + "name": "app", + "dependencies": { + "test-vite-plus-install": "1.0.0", + "testnpm2": "^1.0.1" + } +} +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true +} + +> vp add @vite-plus-test/utils --workspace --filter app && cat package.json packages/app/package.json packages/utils/package.json # should add @vite-plus-test/utils to packages/app +Running: pnpm --filter app add --workspace @vite-plus-test/utils +. |  WARN  `node_modules` is present. Lockfile only installation will make it out-of-date +Progress: resolved , reused , downloaded , added , done +Done in ms using pnpm v +{ + "name": "command-add-pnpm9-with-workspace", + "version": "1.0.0", + "packageManager": "pnpm@", + "devDependencies": { + "testnpm2": "^1.0.1" + } +} +{ + "name": "app", + "dependencies": { + "@vite-plus-test/utils": "workspace:^", + "test-vite-plus-install": "1.0.0", + "testnpm2": "^1.0.1" + } +} +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true +} + +> vp add -E testnpm2 test-vite-plus-install --filter "*" && cat package.json packages/app/package.json packages/utils/package.json # should add testnpm2 test-vite-plus-install to all packages except workspace root +Running: pnpm --filter * add --save-exact testnpm2 test-vite-plus-install +Progress: resolved , reused , downloaded , added , done +Done in ms using pnpm v +{ + "name": "command-add-pnpm9-with-workspace", + "version": "1.0.0", + "packageManager": "pnpm@", + "devDependencies": { + "testnpm2": "^1.0.1" + } +} +{ + "name": "app", + "dependencies": { + "@vite-plus-test/utils": "workspace:^", + "test-vite-plus-install": "1.0.0", + "testnpm2": "^1.0.1" + } +} +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true, + "dependencies": { + "test-vite-plus-install": "1.0.0", + "testnpm2": "^1.0.1" + } +} + +> vp install test-vite-plus-package@1.0.0 --filter "*" --workspace-root && cat package.json packages/app/package.json packages/utils/package.json pnpm-workspace.yaml # should install packages alias for add command +Running: pnpm --filter * add --workspace-root test-vite-plus-package@ +. | +1 + +Progress: resolved , reused , downloaded , added , done +Done in ms using pnpm v +{ + "name": "command-add-pnpm9-with-workspace", + "version": "1.0.0", + "packageManager": "pnpm@", + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "test-vite-plus-package": "1.0.0" + } +} +{ + "name": "app", + "dependencies": { + "@vite-plus-test/utils": "workspace:^", + "test-vite-plus-install": "1.0.0", + "test-vite-plus-package": "1.0.0", + "testnpm2": "^1.0.1" + } +} +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true, + "dependencies": { + "test-vite-plus-install": "1.0.0", + "test-vite-plus-package": "1.0.0", + "testnpm2": "^1.0.1" + } +} +packages: + - packages/* + +[1]> vp add --filter app test-vite-plus-package-optional --save-catalog-name v1 # should error because save-catalog-name is not supported at pnpm@9 +Running: pnpm --filter app add --save-catalog-name=v1 test-vite-plus-package-optional + + ERROR  Unknown option: 'save-catalog-name' +Did you mean 'save-optional'? Use "--config.unknown=value" to force an unknown option. +For help, run: pnpm help add + +[1]> vp add --filter=./packages/utils test-vite-plus-package-optional -O --save-catalog v2 # should error because save-catalog is not supported at pnpm@9 +Running: pnpm --filter ./packages/utils add --save-optional --save-catalog test-vite-plus-package-optional v2 + + ERROR  Unknown option: 'save-catalog' +Did you mean 'save-exact', or 'save-prod'? Use "--config.unknown=value" to force an unknown option. +For help, run: pnpm help add diff --git a/packages/global/snap-tests/command-add-pnpm9-with-workspace/steps.json b/packages/global/snap-tests/command-add-pnpm9-with-workspace/steps.json new file mode 100644 index 0000000000..6214ba74b3 --- /dev/null +++ b/packages/global/snap-tests/command-add-pnpm9-with-workspace/steps.json @@ -0,0 +1,15 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp add testnpm2 -D -w && cat package.json # should add package to workspace root", + "vp add @vite-plus-test/utils --workspace && cat package.json # should add @vite-plus-test/utils to workspace root", + "vp add testnpm2 test-vite-plus-install@1.0.0 --filter app && cat package.json packages/app/package.json packages/utils/package.json # should add packages to packages/app", + "vp add @vite-plus-test/utils --workspace --filter app && cat package.json packages/app/package.json packages/utils/package.json # should add @vite-plus-test/utils to packages/app", + "vp add -E testnpm2 test-vite-plus-install --filter \"*\" && cat package.json packages/app/package.json packages/utils/package.json # should add testnpm2 test-vite-plus-install to all packages except workspace root", + "vp install test-vite-plus-package@1.0.0 --filter \"*\" --workspace-root && cat package.json packages/app/package.json packages/utils/package.json pnpm-workspace.yaml # should install packages alias for add command", + "vp add --filter app test-vite-plus-package-optional --save-catalog-name v1 # should error because save-catalog-name is not supported at pnpm@9", + "vp add --filter=./packages/utils test-vite-plus-package-optional -O --save-catalog v2 # should error because save-catalog is not supported at pnpm@9" + ] +} diff --git a/packages/global/snap-tests/command-add-pnpm9/package.json b/packages/global/snap-tests/command-add-pnpm9/package.json new file mode 100644 index 0000000000..5430fe6cf1 --- /dev/null +++ b/packages/global/snap-tests/command-add-pnpm9/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-add-pnpm9", + "version": "1.0.0", + "packageManager": "pnpm@9.15.9" +} diff --git a/packages/global/snap-tests/command-add-pnpm9/snap.txt b/packages/global/snap-tests/command-add-pnpm9/snap.txt new file mode 100644 index 0000000000..eb5c087e49 --- /dev/null +++ b/packages/global/snap-tests/command-add-pnpm9/snap.txt @@ -0,0 +1,162 @@ +> vp add --help # should show help +Add packages to dependencies + +Usage: vp add [OPTIONS] [PACKAGES]... [-- ...] + +Arguments: + [PACKAGES]... Packages to add + [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager + +Options: + -P, --save-prod + Save to `dependencies` (default) + -D, --save-dev + Save to `devDependencies` + --save-peer + Save to `peerDependencies` and `devDependencies` + -O, --save-optional + Save to `optionalDependencies` + -E, --save-exact + Save exact version rather than semver range (e.g., `^1.0.0` -> `1.0.0`) + --save-catalog-name + Save the new dependency to the specified catalog name. Example: `vite add vue --save-catalog-name vue3` + --save-catalog + Save the new dependency to the default catalog + --allow-build + A list of package names allowed to run postinstall + --filter + Filter packages in monorepo (can be used multiple times) + -w, --workspace-root + Add to workspace root (ignore-workspace-root-check) + --workspace + Only add if package exists in workspace (pnpm-specific) + -g, --global + Install globally + -h, --help + Print help + +> vp add testnpm2 -D && cat package.json # should add package as dev dependencies +Running: pnpm add --save-dev testnpm2 +Packages: + ++ +Progress: resolved , reused , downloaded , added , done + +devDependencies: ++ testnpm2 + +Done in ms using pnpm v +{ + "name": "command-add-pnpm9", + "version": "1.0.0", + "packageManager": "pnpm@", + "devDependencies": { + "testnpm2": "^1.0.1" + } +} + +> vp add testnpm2 test-vite-plus-install && cat package.json # should add packages to dependencies +Running: pnpm add testnpm2 test-vite-plus-install +Packages: + ++ +Progress: resolved , reused , downloaded , added , done + +dependencies: ++ test-vite-plus-install + +Done in ms using pnpm v +{ + "name": "command-add-pnpm9", + "version": "1.0.0", + "packageManager": "pnpm@", + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "test-vite-plus-install": "^1.0.0" + } +} + +[1]> vp add testnpm2 test-vite-plus-install --allow-build=test-vite-plus-install # should error because allow-build is not supported at pnpm@9 +Running: pnpm add --allow-build=test-vite-plus-install testnpm2 test-vite-plus-install + + ERROR  Unknown option: 'allow-build' +For help, run: pnpm help add + +> vp install test-vite-plus-package@1.0.0 --save-peer && cat package.json # should install package alias for add +Running: pnpm add --save-peer test-vite-plus-package@ +Packages: + ++ +Progress: resolved , reused , downloaded , added , done + +peerDependencies: ++ test-vite-plus-package + +devDependencies: ++ test-vite-plus-package already in devDependencies, was not moved to dependencies. + +Done in ms using pnpm v +{ + "name": "command-add-pnpm9", + "version": "1.0.0", + "packageManager": "pnpm@", + "devDependencies": { + "test-vite-plus-package": "1.0.0", + "testnpm2": "^1.0.1" + }, + "dependencies": { + "test-vite-plus-install": "^1.0.0" + }, + "peerDependencies": { + "test-vite-plus-package": "1.0.0" + } +} + +> vp add test-vite-plus-package-optional -O && cat package.json # should add package as optional dependencies +Running: pnpm add --save-optional test-vite-plus-package-optional +Packages: + ++ +Progress: resolved , reused , downloaded , added , done + +optionalDependencies: ++ test-vite-plus-package-optional + +Done in ms using pnpm v +{ + "name": "command-add-pnpm9", + "version": "1.0.0", + "packageManager": "pnpm@", + "devDependencies": { + "test-vite-plus-package": "1.0.0", + "testnpm2": "^1.0.1" + }, + "dependencies": { + "test-vite-plus-install": "^1.0.0" + }, + "peerDependencies": { + "test-vite-plus-package": "1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "^1.0.0" + } +} + +> vp add test-vite-plus-package-optional -- --loglevel=warn && cat package.json # support pass through arguments +Running: pnpm add --loglevel=warn test-vite-plus-package-optional +{ + "name": "command-add-pnpm9", + "version": "1.0.0", + "packageManager": "pnpm@", + "devDependencies": { + "test-vite-plus-package": "1.0.0", + "testnpm2": "^1.0.1" + }, + "dependencies": { + "test-vite-plus-install": "^1.0.0" + }, + "peerDependencies": { + "test-vite-plus-package": "1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "^1.0.0" + } +} diff --git a/packages/global/snap-tests/command-add-pnpm9/steps.json b/packages/global/snap-tests/command-add-pnpm9/steps.json new file mode 100644 index 0000000000..5fc6cb54a4 --- /dev/null +++ b/packages/global/snap-tests/command-add-pnpm9/steps.json @@ -0,0 +1,14 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp add --help # should show help", + "vp add testnpm2 -D && cat package.json # should add package as dev dependencies", + "vp add testnpm2 test-vite-plus-install && cat package.json # should add packages to dependencies", + "vp add testnpm2 test-vite-plus-install --allow-build=test-vite-plus-install # should error because allow-build is not supported at pnpm@9", + "vp install test-vite-plus-package@1.0.0 --save-peer && cat package.json # should install package alias for add", + "vp add test-vite-plus-package-optional -O && cat package.json # should add package as optional dependencies", + "vp add test-vite-plus-package-optional -- --loglevel=warn && cat package.json # support pass through arguments" + ] +} diff --git a/packages/global/snap-tests/command-add-yarn4-with-workspace/package.json b/packages/global/snap-tests/command-add-yarn4-with-workspace/package.json new file mode 100644 index 0000000000..2c8cf3eb9c --- /dev/null +++ b/packages/global/snap-tests/command-add-yarn4-with-workspace/package.json @@ -0,0 +1,8 @@ +{ + "name": "command-add-yarn4-with-workspace", + "version": "1.0.0", + "packageManager": "yarn@4.10.3", + "workspaces": [ + "packages/*" + ] +} diff --git a/packages/global/snap-tests/command-add-yarn4-with-workspace/packages/admin/package.json b/packages/global/snap-tests/command-add-yarn4-with-workspace/packages/admin/package.json new file mode 100644 index 0000000000..789682bf09 --- /dev/null +++ b/packages/global/snap-tests/command-add-yarn4-with-workspace/packages/admin/package.json @@ -0,0 +1,3 @@ +{ + "name": "admin" +} diff --git a/packages/global/snap-tests/command-add-yarn4-with-workspace/packages/app/package.json b/packages/global/snap-tests/command-add-yarn4-with-workspace/packages/app/package.json new file mode 100644 index 0000000000..fd1f8f6386 --- /dev/null +++ b/packages/global/snap-tests/command-add-yarn4-with-workspace/packages/app/package.json @@ -0,0 +1,3 @@ +{ + "name": "app" +} diff --git a/packages/global/snap-tests/command-add-yarn4-with-workspace/packages/utils/package.json b/packages/global/snap-tests/command-add-yarn4-with-workspace/packages/utils/package.json new file mode 100644 index 0000000000..d063b255a7 --- /dev/null +++ b/packages/global/snap-tests/command-add-yarn4-with-workspace/packages/utils/package.json @@ -0,0 +1,5 @@ +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true +} diff --git a/packages/global/snap-tests/command-add-yarn4-with-workspace/snap.txt b/packages/global/snap-tests/command-add-yarn4-with-workspace/snap.txt new file mode 100644 index 0000000000..e8c12b28fc --- /dev/null +++ b/packages/global/snap-tests/command-add-yarn4-with-workspace/snap.txt @@ -0,0 +1,293 @@ +> vp add testnpm2 -D -w && cat package.json packages/app/package.json packages/utils/package.json # should add package to workspace root +Running: yarn add --dev testnpm2 +➤ YN0000: · Yarn +➤ YN0000: ┌ Resolution step +➤ YN0085: │ + testnpm2@npm:1.0.1 +➤ YN0000: └ Completed +➤ YN0000: ┌ Fetch step +➤ YN0000: └ Completed +➤ YN0000: ┌ Link step +➤ YN0000: └ Completed +➤ YN0000: · Done in ms ms +{ + "name": "command-add-yarn4-with-workspace", + "version": "1.0.0", + "packageManager": "yarn@", + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "testnpm2": "^1.0.1" + } +} +{ + "name": "app" +} +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true +} + +> vp add @vite-plus-test/utils --workspace -w && cat package.json packages/app/package.json packages/utils/package.json # should add @vite-plus-test/utils to workspace root +Running: yarn add @vite-plus-test/utils +➤ YN0000: · Yarn +➤ YN0000: ┌ Resolution step +➤ YN0000: └ Completed +➤ YN0000: ┌ Fetch step +➤ YN0000: └ Completed +➤ YN0000: ┌ Link step +➤ YN0000: └ Completed +➤ YN0000: · Done in ms ms +{ + "name": "command-add-yarn4-with-workspace", + "version": "1.0.0", + "packageManager": "yarn@", + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "@vite-plus-test/utils": "workspace:^" + } +} +{ + "name": "app" +} +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true +} + +> vp add testnpm2 test-vite-plus-install@1.0.0 --filter app && cat package.json packages/app/package.json packages/utils/package.json # should add packages to packages/app +Running: yarn workspaces foreach --all --include app add testnpm2 test-vite-plus-install@ +➤ YN0000: · Yarn +➤ YN0000: ┌ Resolution step +➤ YN0085: │ + test-vite-plus-install@npm:1.0.0 +➤ YN0000: └ Completed +➤ YN0000: ┌ Fetch step +➤ YN0000: └ Completed +➤ YN0000: ┌ Link step +➤ YN0000: └ Completed +➤ YN0000: · Done in ms ms +Done in ms ms +{ + "name": "command-add-yarn4-with-workspace", + "version": "1.0.0", + "packageManager": "yarn@", + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "@vite-plus-test/utils": "workspace:^" + } +} +{ + "name": "app", + "dependencies": { + "test-vite-plus-install": "1.0.0", + "testnpm2": "^1.0.1" + } +} +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true +} + +> vp add @vite-plus-test/utils --workspace --filter app && cat package.json packages/app/package.json packages/utils/package.json # should add @vite-plus-test/utils to packages/app +Running: yarn workspaces foreach --all --include app add @vite-plus-test/utils +➤ YN0000: · Yarn +➤ YN0000: ┌ Resolution step +➤ YN0000: └ Completed +➤ YN0000: ┌ Fetch step +➤ YN0000: └ Completed +➤ YN0000: ┌ Link step +➤ YN0000: └ Completed +➤ YN0000: · Done in ms ms +Done in ms ms +{ + "name": "command-add-yarn4-with-workspace", + "version": "1.0.0", + "packageManager": "yarn@", + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "@vite-plus-test/utils": "workspace:^" + } +} +{ + "name": "app", + "dependencies": { + "@vite-plus-test/utils": "workspace:^", + "test-vite-plus-install": "1.0.0", + "testnpm2": "^1.0.1" + } +} +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true +} + +> vp add testnpm2 test-vite-plus-install@1.0.0 --filter "*" --filter @vite-plus-test/utils && cat package.json packages/app/package.json packages/admin/package.json packages/utils/package.json # should add testnpm2 test-vite-plus-install to all packages and workspace root +Running: yarn workspaces foreach --all --include * --include @vite-plus-test/utils add testnpm2 test-vite-plus-install@ +➤ YN0000: · Yarn +➤ YN0000: ┌ Resolution step +➤ YN0000: └ Completed +➤ YN0000: ┌ Fetch step +➤ YN0000: └ Completed +➤ YN0000: ┌ Link step +➤ YN0000: └ Completed +➤ YN0000: · Done in ms ms +➤ YN0000: · Yarn +➤ YN0000: ┌ Resolution step +➤ YN0000: └ Completed +➤ YN0000: ┌ Fetch step +➤ YN0000: └ Completed +➤ YN0000: ┌ Link step +➤ YN0000: └ Completed +➤ YN0000: · Done in ms ms +➤ YN0000: · Yarn +➤ YN0000: ┌ Resolution step +➤ YN0000: └ Completed +➤ YN0000: ┌ Fetch step +➤ YN0000: └ Completed +➤ YN0000: ┌ Link step +➤ YN0000: └ Completed +➤ YN0000: · Done in ms ms +➤ YN0000: · Yarn +➤ YN0000: ┌ Resolution step +➤ YN0000: └ Completed +➤ YN0000: ┌ Fetch step +➤ YN0000: └ Completed +➤ YN0000: ┌ Link step +➤ YN0000: └ Completed +➤ YN0000: · Done in ms ms +Done in ms ms +{ + "name": "command-add-yarn4-with-workspace", + "version": "1.0.0", + "packageManager": "yarn@", + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "@vite-plus-test/utils": "workspace:^", + "test-vite-plus-install": "1.0.0" + } +} +{ + "name": "app", + "dependencies": { + "@vite-plus-test/utils": "workspace:^", + "test-vite-plus-install": "1.0.0", + "testnpm2": "^1.0.1" + } +} +{ + "name": "admin", + "dependencies": { + "test-vite-plus-install": "1.0.0", + "testnpm2": "^1.0.1" + } +} +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true, + "dependencies": { + "test-vite-plus-install": "1.0.0", + "testnpm2": "^1.0.1" + } +} + +> vp install -O test-vite-plus-package-optional --filter "*" && cat package.json packages/app/package.json packages/admin/package.json packages/utils/package.json # should install packages alias for add command +Running: yarn workspaces foreach --all --include * add --optional test-vite-plus-package-optional +➤ YN0000: · Yarn +➤ YN0000: ┌ Resolution step +➤ YN0085: │ + test-vite-plus-package-optional@npm:1.0.0 +➤ YN0000: └ Completed +➤ YN0000: ┌ Fetch step +➤ YN0000: └ Completed +➤ YN0000: ┌ Link step +➤ YN0000: └ Completed +➤ YN0000: · Done in ms ms +➤ YN0000: · Yarn +➤ YN0000: ┌ Resolution step +➤ YN0000: └ Completed +➤ YN0000: ┌ Fetch step +➤ YN0000: └ Completed +➤ YN0000: ┌ Link step +➤ YN0000: └ Completed +➤ YN0000: · Done in ms ms +➤ YN0000: · Yarn +➤ YN0000: ┌ Resolution step +➤ YN0000: └ Completed +➤ YN0000: ┌ Fetch step +➤ YN0000: └ Completed +➤ YN0000: ┌ Link step +➤ YN0000: └ Completed +➤ YN0000: · Done in ms ms +Done in ms ms +{ + "name": "command-add-yarn4-with-workspace", + "version": "1.0.0", + "packageManager": "yarn@", + "workspaces": [ + "packages/*" + ], + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "@vite-plus-test/utils": "workspace:^", + "test-vite-plus-install": "1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "^1.0.0" + } +} +{ + "name": "app", + "dependencies": { + "@vite-plus-test/utils": "workspace:^", + "test-vite-plus-install": "1.0.0", + "testnpm2": "^1.0.1" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "^1.0.0" + } +} +{ + "name": "admin", + "dependencies": { + "test-vite-plus-install": "1.0.0", + "testnpm2": "^1.0.1" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "^1.0.0" + } +} +{ + "name": "@vite-plus-test/utils", + "version": "1.0.0", + "private": true, + "dependencies": { + "test-vite-plus-install": "1.0.0", + "testnpm2": "^1.0.1" + } +} diff --git a/packages/global/snap-tests/command-add-yarn4-with-workspace/steps.json b/packages/global/snap-tests/command-add-yarn4-with-workspace/steps.json new file mode 100644 index 0000000000..2251e6009b --- /dev/null +++ b/packages/global/snap-tests/command-add-yarn4-with-workspace/steps.json @@ -0,0 +1,13 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp add testnpm2 -D -w && cat package.json packages/app/package.json packages/utils/package.json # should add package to workspace root", + "vp add @vite-plus-test/utils --workspace -w && cat package.json packages/app/package.json packages/utils/package.json # should add @vite-plus-test/utils to workspace root", + "vp add testnpm2 test-vite-plus-install@1.0.0 --filter app && cat package.json packages/app/package.json packages/utils/package.json # should add packages to packages/app", + "vp add @vite-plus-test/utils --workspace --filter app && cat package.json packages/app/package.json packages/utils/package.json # should add @vite-plus-test/utils to packages/app", + "vp add testnpm2 test-vite-plus-install@1.0.0 --filter \"*\" --filter @vite-plus-test/utils && cat package.json packages/app/package.json packages/admin/package.json packages/utils/package.json # should add testnpm2 test-vite-plus-install to all packages and workspace root", + "vp install -O test-vite-plus-package-optional --filter \"*\" && cat package.json packages/app/package.json packages/admin/package.json packages/utils/package.json # should install packages alias for add command" + ] +} diff --git a/packages/global/snap-tests/command-add-yarn4/package.json b/packages/global/snap-tests/command-add-yarn4/package.json new file mode 100644 index 0000000000..d3b786c617 --- /dev/null +++ b/packages/global/snap-tests/command-add-yarn4/package.json @@ -0,0 +1,5 @@ +{ + "name": "command-add-yarn4", + "version": "1.0.0", + "packageManager": "yarn@4.10.3" +} diff --git a/packages/global/snap-tests/command-add-yarn4/snap.txt b/packages/global/snap-tests/command-add-yarn4/snap.txt new file mode 100644 index 0000000000..64ab93e8c7 --- /dev/null +++ b/packages/global/snap-tests/command-add-yarn4/snap.txt @@ -0,0 +1,161 @@ +> vp add --help # should show help +Add packages to dependencies + +Usage: vp add [OPTIONS] [PACKAGES]... [-- ...] + +Arguments: + [PACKAGES]... Packages to add + [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager + +Options: + -P, --save-prod + Save to `dependencies` (default) + -D, --save-dev + Save to `devDependencies` + --save-peer + Save to `peerDependencies` and `devDependencies` + -O, --save-optional + Save to `optionalDependencies` + -E, --save-exact + Save exact version rather than semver range (e.g., `^1.0.0` -> `1.0.0`) + --save-catalog-name + Save the new dependency to the specified catalog name. Example: `vite add vue --save-catalog-name vue3` + --save-catalog + Save the new dependency to the default catalog + --allow-build + A list of package names allowed to run postinstall + --filter + Filter packages in monorepo (can be used multiple times) + -w, --workspace-root + Add to workspace root (ignore-workspace-root-check) + --workspace + Only add if package exists in workspace (pnpm-specific) + -g, --global + Install globally + -h, --help + Print help + +> vp add testnpm2 -D && cat package.json # should add package as dev dependencies +Running: yarn add --dev testnpm2 +➤ YN0000: · Yarn +➤ YN0000: ┌ Resolution step +➤ YN0085: │ + testnpm2@npm:1.0.1 +➤ YN0000: └ Completed +➤ YN0000: ┌ Fetch step +➤ YN0000: └ Completed +➤ YN0000: ┌ Link step +➤ YN0000: └ Completed +➤ YN0000: · Done in ms ms +{ + "name": "command-add-yarn4", + "version": "1.0.0", + "packageManager": "yarn@", + "devDependencies": { + "testnpm2": "^1.0.1" + } +} + +> vp add testnpm2 test-vite-plus-install --allow-build=test-vite-plus-install && cat package.json # should add packages to dependencies +Running: yarn add testnpm2 test-vite-plus-install +➤ YN0000: · Yarn +➤ YN0000: ┌ Resolution step +➤ YN0085: │ + test-vite-plus-install@npm:1.0.0 +➤ YN0000: └ Completed +➤ YN0000: ┌ Fetch step +➤ YN0000: └ Completed +➤ YN0000: ┌ Link step +➤ YN0000: └ Completed +➤ YN0000: · Done in ms ms +{ + "name": "command-add-yarn4", + "version": "1.0.0", + "packageManager": "yarn@", + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "test-vite-plus-install": "^1.0.0" + } +} + +> vp install test-vite-plus-package@1.0.0 --save-peer && cat package.json # should install package alias for add +Running: yarn add --peer test-vite-plus-package@ +➤ YN0000: · Yarn +➤ YN0000: ┌ Resolution step +➤ YN0000: └ Completed +➤ YN0000: ┌ Fetch step +➤ YN0000: └ Completed +➤ YN0000: ┌ Link step +➤ YN0000: └ Completed +➤ YN0000: · Done in ms ms +{ + "name": "command-add-yarn4", + "version": "1.0.0", + "packageManager": "yarn@", + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "test-vite-plus-install": "^1.0.0" + }, + "peerDependencies": { + "test-vite-plus-package": "1.0.0" + } +} + +> vp add test-vite-plus-package-optional -O && cat package.json # should add package as optional dependencies +Running: yarn add --optional test-vite-plus-package-optional +➤ YN0000: · Yarn +➤ YN0000: ┌ Resolution step +➤ YN0085: │ + test-vite-plus-package-optional@npm:1.0.0 +➤ YN0000: └ Completed +➤ YN0000: ┌ Fetch step +➤ YN0000: └ Completed +➤ YN0000: ┌ Link step +➤ YN0000: └ Completed +➤ YN0000: · Done in ms ms +{ + "name": "command-add-yarn4", + "version": "1.0.0", + "packageManager": "yarn@", + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "test-vite-plus-install": "^1.0.0" + }, + "peerDependencies": { + "test-vite-plus-package": "1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "^1.0.0" + } +} + +> vp add test-vite-plus-package-optional -- --tilde && cat package.json # support pass through arguments +Running: yarn add --tilde test-vite-plus-package-optional +➤ YN0000: · Yarn +➤ YN0000: ┌ Resolution step +➤ YN0000: └ Completed +➤ YN0000: ┌ Fetch step +➤ YN0000: └ Completed +➤ YN0000: ┌ Link step +➤ YN0000: └ Completed +➤ YN0000: · Done in ms ms +{ + "name": "command-add-yarn4", + "version": "1.0.0", + "packageManager": "yarn@", + "devDependencies": { + "testnpm2": "^1.0.1" + }, + "dependencies": { + "test-vite-plus-install": "^1.0.0" + }, + "peerDependencies": { + "test-vite-plus-package": "1.0.0" + }, + "optionalDependencies": { + "test-vite-plus-package-optional": "~1.0.0" + } +} diff --git a/packages/global/snap-tests/command-add-yarn4/steps.json b/packages/global/snap-tests/command-add-yarn4/steps.json new file mode 100644 index 0000000000..76a61e22ed --- /dev/null +++ b/packages/global/snap-tests/command-add-yarn4/steps.json @@ -0,0 +1,13 @@ +{ + "env": { + "VITE_DISABLE_AUTO_INSTALL": "1" + }, + "commands": [ + "vp add --help # should show help", + "vp add testnpm2 -D && cat package.json # should add package as dev dependencies", + "vp add testnpm2 test-vite-plus-install --allow-build=test-vite-plus-install && cat package.json # should add packages to dependencies", + "vp install test-vite-plus-package@1.0.0 --save-peer && cat package.json # should install package alias for add", + "vp add test-vite-plus-package-optional -O && cat package.json # should add package as optional dependencies", + "vp add test-vite-plus-package-optional -- --tilde && cat package.json # support pass through arguments" + ] +} diff --git a/packages/tools/package.json b/packages/tools/package.json index 67246f9d2c..0a90910d90 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -5,5 +5,8 @@ "bin": { "json-edit": "./src/json-edit.ts", "snap-test": "./src/snap-test.ts" + }, + "devDependencies": { + "minimatch": "catalog:" } } diff --git a/packages/tools/src/__tests__/__snapshots__/utils.spec.ts.snap b/packages/tools/src/__tests__/__snapshots__/utils.spec.ts.snap index 7856a6c0f2..aa495e52eb 100644 --- a/packages/tools/src/__tests__/__snapshots__/utils.spec.ts.snap +++ b/packages/tools/src/__tests__/__snapshots__/utils.spec.ts.snap @@ -1,11 +1,11 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`replaceUnstableOutput > replace date 1`] = ` +exports[`replaceUnstableOutput() > replace date 1`] = ` "Start at " `; -exports[`replaceUnstableOutput > replace tsdown output 1`] = ` +exports[`replaceUnstableOutput() > replace tsdown output 1`] = ` "ℹ tsdown v powered by rolldown v ℹ entry: src/index.ts ℹ Build start @@ -14,9 +14,9 @@ exports[`replaceUnstableOutput > replace tsdown output 1`] = ` ✔ Build complete in ms" `; -exports[`replaceUnstableOutput > replace unstable cwd 1`] = `"/foo.txt"`; +exports[`replaceUnstableOutput() > replace unstable cwd 1`] = `"/foo.txt"`; -exports[`replaceUnstableOutput > replace unstable pnpm install output 1`] = ` +exports[`replaceUnstableOutput() > replace unstable pnpm install output 1`] = ` "Scope: all workspace projects Packages: + + @@ -27,7 +27,7 @@ devDependencies: + vitest " `; -exports[`replaceUnstableOutput > replace unstable pnpm install output 2`] = ` +exports[`replaceUnstableOutput() > replace unstable pnpm install output 2`] = ` "Scope: all workspace projects Lockfile is up to date, resolution step is skipped Already up to date @@ -42,7 +42,7 @@ Already up to date Done in ms using pnpm v" `; -exports[`replaceUnstableOutput > replace unstable semver version 1`] = ` +exports[`replaceUnstableOutput() > replace unstable semver version 1`] = ` "foo v v v @@ -55,3 +55,8 @@ foo/v foo@ bar@v" `; + +exports[`replaceUnstableOutput() > replace yarn YN0013 1`] = ` +"➤ YN0000: ┌ Fetch step +➤ YN0000: └ Completed" +`; diff --git a/packages/tools/src/__tests__/utils.spec.ts b/packages/tools/src/__tests__/utils.spec.ts index 25bb181d37..f4a0f17a88 100644 --- a/packages/tools/src/__tests__/utils.spec.ts +++ b/packages/tools/src/__tests__/utils.spec.ts @@ -2,9 +2,9 @@ import { tmpdir } from 'node:os'; import { describe, expect, test } from '@voidzero-dev/vite-plus/test'; -import { replaceUnstableOutput } from '../utils.ts'; +import { isPassThroughEnv, replaceUnstableOutput } from '../utils.ts'; -describe('replaceUnstableOutput', () => { +describe('replaceUnstableOutput()', () => { test('replace unstable semver version', () => { const output = ` foo v1.0.0 @@ -83,4 +83,25 @@ Done in 171ms using pnpm v10.16.1 `; expect(replaceUnstableOutput(output.trim())).toMatchSnapshot(); }); + + test('replace yarn YN0013', () => { + const output = ` +➤ YN0000: ┌ Fetch step +➤ YN0013: │ A package was added to the project (+ 0.7 KiB). +➤ YN0000: └ Completed + `; + expect(replaceUnstableOutput(output.trim())).toMatchSnapshot(); + }); +}); + +describe('isPassThroughEnv()', () => { + test('should return true if env is pass-through', () => { + expect(isPassThroughEnv('NPM_AUTH_TOKEN')).toBe(true); + expect(isPassThroughEnv('PATH')).toBe(true); + }); + + test('should return false if env is not pass-through', () => { + expect(isPassThroughEnv('NODE_ENV')).toBe(false); + expect(isPassThroughEnv('API_URL')).toBe(false); + }); }); diff --git a/packages/tools/src/snap-test.ts b/packages/tools/src/snap-test.ts index 73e88030c0..fafb889617 100755 --- a/packages/tools/src/snap-test.ts +++ b/packages/tools/src/snap-test.ts @@ -6,8 +6,9 @@ import fs from 'node:fs'; import fsPromises from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; -import { promisify } from 'node:util'; +import { debuglog, promisify } from 'node:util'; +const debug = debuglog('vite-plus/snap-test'); const cpExec = promisify(cp.exec); const exec = async (command: string, options: cp.ExecOptionsWithStringEncoding) => cpExec( @@ -15,7 +16,7 @@ const exec = async (command: string, options: cp.ExecOptionsWithStringEncoding) process.platform === 'win32' ? { ...options, shell: 'pwsh.exe' } : options, ); -import { replaceUnstableOutput } from './utils.ts'; +import { isPassThroughEnv, replaceUnstableOutput } from './utils.ts'; // Create a unique temporary directory for testing // On macOS, `tmpdir()` is a symlink. Resolve it so that we can replace the resolved cwd in outputs. @@ -26,7 +27,7 @@ fs.mkdirSync(tempTmpDir, { recursive: true }); fs.symlinkSync( path.resolve('node_modules'), path.join(tempTmpDir, 'node_modules'), - process.platform === 'win32' ? 'junction' : 'dir' + process.platform === 'win32' ? 'junction' : 'dir', ); // Clean up the temporary directory on exit @@ -65,8 +66,9 @@ async function runTestCase(name: string) { const caseTmpDir = `${tempTmpDir}/${name}`; await fsPromises.cp(`${casesDir}/${name}`, caseTmpDir, { recursive: true, errorOnExist: true }); + const passThroughEnvs = Object.fromEntries(Object.entries(process.env).filter(([key]) => isPassThroughEnv(key))); const env = { - ...process.env, + ...passThroughEnvs, // Indicate CLI is running in test mode, so that it prints more detailed outputs. VITE_PLUS_CLI_TEST: '1', NO_COLOR: 'true', @@ -92,6 +94,7 @@ async function runTestCase(name: string) { const newSnap: string[] = []; for (const command of steps.commands) { + debug('running command: %s, cwd: %s, env: %o', command, caseTmpDir, env); try { const { stdout, stderr } = await exec(command, { env, cwd: caseTmpDir, encoding: 'utf-8' }); newSnap.push(`> ${command}`); diff --git a/packages/tools/src/utils.ts b/packages/tools/src/utils.ts index 8b5708db07..8f1b4f1eb6 100644 --- a/packages/tools/src/utils.ts +++ b/packages/tools/src/utils.ts @@ -1,3 +1,5 @@ +import { Minimatch } from 'minimatch'; + export function replaceUnstableOutput(output: string, cwd?: string) { if (cwd) { output = output.replaceAll(cwd, ''); @@ -27,6 +29,81 @@ export function replaceUnstableOutput(output: string, cwd?: string) { .replaceAll(/WARN\s+Skip\s+adding .+?\n/g, '') .replaceAll(/Scope: all \d+ workspace projects/g, 'Scope: all workspace projects') .replaceAll(/\++\n/g, '+\n') + // ignore yarn YN0013, because it's unstable output, only exists on CI environment + // ➤ YN0013: │ A package was added to the project (+ 0.7 KiB). + .replaceAll(/➤ YN0013:[^\n]+\n/g, '') + // ignore npm warn + // npm warn Unknown env config "recursive". This will stop working in the next major version of npm + .replaceAll(/npm warn Unknown env config .+?\n/g, '') + // WARN  Issue while reading "/path/to/.npmrc". Failed to replace env in config: ${NPM_AUTH_TOKEN} + .replaceAll(/WARN\s+Issue\s+while\s+reading .+?\n/g, '') // replace size for tsdown .replaceAll(/ \d+(\.\d+)? ([km]B)/g, ' $2'); } + +// Exact matches for common environment variables +const DEFAULT_PASSTHROUGH_ENVS = [ + // System and shell + 'HOME', + 'USER', + 'TZ', + 'LANG', + 'SHELL', + 'PWD', + 'PATH', + // CI/CD environments + 'CI', + // Node.js specific + 'NODE_OPTIONS', + 'COREPACK_HOME', + 'NPM_CONFIG_STORE_DIR', + 'PNPM_HOME', + // Library paths + 'LD_LIBRARY_PATH', + 'DYLD_FALLBACK_LIBRARY_PATH', + 'LIBPATH', + // Terminal/display + 'COLORTERM', + 'TERM', + 'TERM_PROGRAM', + 'DISPLAY', + 'FORCE_COLOR', + 'NO_COLOR', + // Temporary directories + 'TMP', + 'TEMP', + // Vercel specific + 'VERCEL', + 'VERCEL_*', + 'NEXT_*', + 'USE_OUTPUT_FOR_EDGE_FUNCTIONS', + 'NOW_BUILDER', + // Windows specific + 'APPDATA', + 'PROGRAMDATA', + 'SYSTEMROOT', + 'SYSTEMDRIVE', + 'USERPROFILE', + 'HOMEDRIVE', + 'HOMEPATH', + // IDE specific (exact matches) + 'ELECTRON_RUN_AS_NODE', + 'JB_INTERPRETER', + '_JETBRAINS_TEST_RUNNER_RUN_SCOPE_TYPE', + 'JB_IDE_*', + // VSCode specific + 'VSCODE_*', + // Docker specific + 'DOCKER_*', + 'BUILDKIT_*', + 'COMPOSE_*', + // Token patterns + '*_TOKEN', + // oxc specific + 'OXLINT_*', +].map(env => new Minimatch(env)); + +export function isPassThroughEnv(env: string) { + const upperEnv = env.toUpperCase(); + return DEFAULT_PASSTHROUGH_ENVS.some(pattern => pattern.match(upperEnv)); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f1e9c65014..80aa819a68 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,6 +36,9 @@ catalogs: cross-spawn: specifier: ^7.0.5 version: 7.0.6 + minimatch: + specifier: ^10.0.3 + version: 10.0.3 mri: specifier: ^1.2.0 version: 1.2.0 @@ -226,7 +229,11 @@ importers: specifier: 'catalog:' version: 0.15.1(typescript@5.9.2) - packages/tools: {} + packages/tools: + devDependencies: + minimatch: + specifier: 'catalog:' + version: 10.0.3 packages: @@ -567,6 +574,14 @@ packages: '@types/node': optional: true + '@isaacs/balanced-match@4.0.1': + resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} + engines: {node: 20 || >=22} + + '@isaacs/brace-expansion@5.0.0': + resolution: {integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==} + engines: {node: 20 || >=22} + '@jridgewell/gen-mapping@0.3.12': resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} @@ -2100,6 +2115,10 @@ packages: resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} engines: {node: '>=18'} + minimatch@10.0.3: + resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} + engines: {node: 20 || >=22} + minisearch@7.1.2: resolution: {integrity: sha512-R1Pd9eF+MD5JYDDSPAp/q1ougKglm14uEkPMvQ/05RGmx6G9wvmLTrTI/Q5iPNJLYqNdsDQ7qTGIcNWR+FrHmA==} @@ -2910,6 +2929,12 @@ snapshots: optionalDependencies: '@types/node': 24.5.2 + '@isaacs/balanced-match@4.0.1': {} + + '@isaacs/brace-expansion@5.0.0': + dependencies: + '@isaacs/balanced-match': 4.0.1 + '@jridgewell/gen-mapping@0.3.12': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -4182,6 +4207,10 @@ snapshots: mimic-function@5.0.1: {} + minimatch@10.0.3: + dependencies: + '@isaacs/brace-expansion': 5.0.0 + minisearch@7.1.2: {} mitt@3.0.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1aa3a02122..0d5f51c4bc 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -15,6 +15,7 @@ catalog: create-tsdown: 0.0.3 create-vite: ^8.0.0 cross-spawn: ^7.0.5 + minimatch: ^10.0.3 mri: ^1.2.0 next: ^15.4.3 oxc-minify: ^0.82.1 diff --git a/rfcs/add-remove-package-commands.md b/rfcs/add-remove-package-commands.md new file mode 100644 index 0000000000..f7d1185c02 --- /dev/null +++ b/rfcs/add-remove-package-commands.md @@ -0,0 +1,1489 @@ +# RFC: Vite+ Add and Remove Package Commands + +## Summary + +Add `vite add` and `vite remove` commands that automatically adapt to the detected package manager (pnpm/yarn/npm) for adding and removing packages, with support for multiple packages, common flags, and workspace-aware operations based on pnpm's API design. + +## Motivation + +Currently, developers must manually use package manager-specific commands: + +```bash +pnpm add react +yarn add react +npm install react +``` + +This creates friction in monorepo workflows and requires remembering different syntaxes. A unified interface would: + +1. **Simplify workflows**: One command works across all package managers +2. **Auto-detection**: Automatically uses the correct package manager +3. **Consistency**: Same syntax regardless of underlying tool +4. **Integration**: Works seamlessly with existing vite+ features + +### Current Pain Points + +```bash +# Developer needs to know which package manager is used +pnpm add -D typescript # pnpm project +yarn add --dev typescript # yarn project +npm install --save-dev typescript # npm project + +# Different remove commands +pnpm remove lodash +yarn remove lodash +npm uninstall lodash +``` + +### Proposed Solution + +```bash +# Works for all package managers +vite add typescript -D +vite remove lodash + +# Multiple packages +vite add react react-dom +vite remove axios lodash + +# Workspace operations +vite add react --filter app +vite add @myorg/utils --workspace --filter app +vite add lodash -w # Add to workspace root +``` + +## Proposed Solution + +### Command Syntax + +#### Add Command + +```bash +vite add ... [OPTIONS] +``` + +**Examples:** + +```bash +# Add production dependency +vite add react react-dom + +# Add dev dependency +vite add -D typescript @types/react + +# Add with exact version +vite add react -E + +# Add peer dependency +vite add --save-peer react + +# Add optional dependency +vite add -O sharp + +# Workspace operations +vite add react --filter app # Add to specific package +vite add @myorg/utils --workspace --filter app # Add workspace dependency +vite add lodash -w # Add to workspace root +vite add react --filter "app*" # Add to multiple packages (pattern) +vite add utils --filter "!@myorg/core" # Exclude packages +``` + +##### `vite install` Command with `PACKAGES` arguments + +To accommodate the user habits and experience of `npm install …`, `vite install ...` will be specially treated as an alias for the add command. + +The following commands will be automatically converted to the add command for processing: + +```bash +vite install ... [OPTIONS] + +-> vite add ... [OPTIONS] +``` + +##### Install global packages with npm cli only + +For global packages, we will use npm cli only. + +> Because yarn do not support global packages install on [version>=2.x](https://yarnpkg.com/migration/guide#use-yarn-dlx-instead-of-yarn-global), and pnpm global install has some bugs like `wrong bin file` issues. + +```bash +vite install -g ... +vite add -g ... + +-> npm install -g ... +``` + +#### Remove Command + +```bash +vite remove ... [OPTIONS] +vite rm ... [OPTIONS] # Alias +``` + +**Examples:** + +```bash +# Remove packages +vite remove lodash axios + +# Remove dev dependency +vite rm typescript + +# Alias support +vite rm old-package + +# Workspace operations +vite remove lodash --filter app # Remove from specific package +vite rm utils --filter "app*" # Remove from multiple packages +vite remove -g typescript # Remove global package +``` + +### Command Mapping + +#### Add Command Mapping + +- https://pnpm.io/cli/add#options +- https://yarnpkg.com/cli/add#options +- https://docs.npmjs.com/cli/v11/commands/npm-install#description + +| Vite+ Flag | pnpm | yarn | npm | Description | +| ------------------------------------ | ------------------------ | ----------------------------------------------- | ------------------------------- | ------------------------------------------------------- | +| `` | `add ` | `add ` | `install ` | Add packages | +| `--filter ` | `--filter add` | `workspaces foreach -A --include add` | `install --workspace ` | Target specific workspace package(s) | +| `-w, --workspace-root` | `-w` | `-W` for v1, v2+ N/A | `--include-workspace-root` | Add to workspace root (ignore-workspace-root-check) | +| `--workspace` | `--workspace` | N/A | N/A | Only add if package exists in workspace (pnpm-specific) | +| `-P, --save-prod` | `--save-prod` / `-P` | N/A | `--save-prod` / `-P` | Save to `dependencies`. The default behavior | +| `-D, --save-dev` | `-D` | `--dev` / `-D` | `--save-dev` / `-D` | Save to `devDependencies` | +| `--save-peer` | `--save-peer` | `--peer` / `-P` | `--save-peer` | Save to `peerDependencies` and `devDependencies` | +| `-O, --save-optional` | `-O` | `--optional` / `-O` | `--save-optional` / `-O` | Save to `optionalDependencies` | +| `-E, --save-exact` | `-E` | `--exact` / `-E` | `--save-exact` / `-E` | Save exact version | +| `-g, --global` | `-g` | `global add` | `--global` / `-g` | Install globally | +| `--save-catalog` | pnpm@10+ only | N/A | N/A | Save the new dependency to the default catalog | +| `--save-catalog-name ` | pnpm@10+ only | N/A | N/A | Save the new dependency to the specified catalog | +| `--allow-build ` | pnpm@10+ only | N/A | N/A | A list of package names allowed to run postinstall | + +**Note**: For pnpm, `--filter` must come before the command (e.g., `pnpm --filter app add react`). For yarn/npm, it's integrated into the command structure. + +#### Remove Command Mapping + +| Vite+ Flag | pnpm | yarn | npm | Description | +| ---------------------- | --------------------------- | ------------------------ | ----------------------------- | ---------------------------------------------- | +| `` | `remove ` | `remove ` | `uninstall ` | Remove packages | +| `-D, --save-dev` | `-D` | `--dev` / `-D` | `--save-dev` / `-D` | Only remove from `devDependencies` | +| `-O, --save-optional` | `-O` | `--optional` / `-O` | `--save-optional` / `-O` | Only remove from `optionalDependencies` | +| `-P, --save-prod` | `-P` | `--save-prod` / `-P` | `--save-prod` / `-P` | Only remove from `dependencies` | +| `--filter ` | `--filter remove` | `workspace remove` | `uninstall --workspace ` | Target specific workspace package(s) | +| `-w, --workspace-root` | `-w` | (default) | (default) | Remove from workspace root | +| `-r, --recursive` | `-r` | `--recursive` / `-r` | `--recursive` / `-r` | Remove recursively from all workspace packages | +| `-g, --global` | `-g` | `global remove` | `--global` / `-g` | Remove global packages | + +**Note**: Similar to add, `--filter` must precede the command for pnpm. + +**Aliases:** + +- `vite rm` = `vite remove` +- `vite un` = `vite remove` +- `vite uninstall` = `vite remove` + +#### Workspace Filter Patterns + +Based on pnpm's filter syntax: + +| Pattern | Description | Example | +| ------------ | ------------------------ | ------------------------------------------ | +| `` | Exact package name | `--filter app` | +| `*` | Wildcard match | `--filter "app*"` matches app, app-web | +| `@/*` | Scope match | `--filter "@myorg/*"` | +| `!` | Exclude pattern | `--filter "!test*"` excludes test packages | +| `...` | Package and dependencies | `--filter "app..."` | +| `...` | Package and dependents | `--filter "...utils"` | + +**Multiple Filters**: + +```bash +vite add react --filter app --filter web # Add to both app and web +vite add react --filter "app*" --filter "!app-test" # Add to app* except app-test +``` + +#### Pass-Through Arguments + +Additional parameters not covered by Vite+ can all be handled through pass-through arguments. + +All arguments after `--` will be passed through to the package manager. + +```bash +vite add react --allow-build=react,napi -- --use-stderr + +-> pnpm add --allow-build=react,napi --use-stderr react +-> yarn add --use-stderr react +-> npm install --use-stderr react +``` + +### Implementation Architecture + +#### 1. Command Structure + +**File**: `crates/vite_task/src/lib.rs` + +Add new command variants: + +```rust +#[derive(Subcommand, Debug)] +pub enum Commands { + // ... existing commands + + /// Add packages to dependencies + #[command(disable_help_flag = true)] + Add { + /// Packages to add + packages: Vec, + + /// Filter packages in monorepo (can be used multiple times) + #[arg(long, value_name = "PATTERN")] + filter: Vec, + + /// Add to workspace root (ignore-workspace-root-check) + #[arg(short = 'w', long)] + workspace_root: bool, + + /// Only add if package exists in workspace + #[arg(long)] + workspace: bool, + + /// Arguments to pass to package manager + #[arg(allow_hyphen_values = true, trailing_var_arg = true)] + args: Vec, + }, + + /// Remove packages from dependencies + #[command(disable_help_flag = true, alias = "rm", alias = "un", alias = "uninstall")] + Remove { + /// Packages to remove + packages: Vec, + + /// Filter packages in monorepo (can be used multiple times) + #[arg(long, value_name = "PATTERN")] + filter: Vec, + + /// Remove from workspace root + #[arg(short = 'w', long)] + workspace_root: bool, + + /// Arguments to pass to package manager + #[arg(allow_hyphen_values = true, trailing_var_arg = true)] + args: Vec, + }, +} +``` + +#### 2. Package Manager Adapter + +**File**: `crates/vite_package_manager/src/package_manager.rs` + +Add methods to translate commands: + +```rust +impl PackageManager { + /// Resolve add command for the package manager + pub fn resolve_add_command(&self) -> &'static str { + match self.client { + PackageManagerType::Pnpm => "add", + PackageManagerType::Yarn => "add", + PackageManagerType::Npm => "install", + } + } + + /// Resolve remove command for the package manager + pub fn resolve_remove_command(&self) -> &'static str { + match self.client { + PackageManagerType::Pnpm => "remove", + PackageManagerType::Yarn => "remove", + PackageManagerType::Npm => "uninstall", + } + } + + /// Build command arguments with workspace support + pub fn build_add_args( + &self, + packages: &[String], + filters: &[String], + workspace_root: bool, + workspace_only: bool, + extra_args: &[String], + ) -> Vec { + let mut args = Vec::new(); + + match self.client { + PackageManagerType::Pnpm => { + // pnpm: --filter must come before command + for filter in filters { + args.push("--filter".to_string()); + args.push(filter.clone()); + } + args.push("add".to_string()); + args.extend_from_slice(packages); + if workspace_root { + args.push("-w".to_string()); + } + if workspace_only { + args.push("--workspace".to_string()); + } + args.extend_from_slice(extra_args); + } + PackageManagerType::Yarn => { + // yarn: workspace add + if !filters.is_empty() { + // yarn workspace add + for filter in filters { + args.push("workspace".to_string()); + args.push(filter.clone()); + } + } + args.push("add".to_string()); + args.extend_from_slice(packages); + if workspace_root { + args.push("-W".to_string()); + } + args.extend_from_slice(extra_args); + } + PackageManagerType::Npm => { + // npm: --workspace must come before install + if !filters.is_empty() { + for filter in filters { + args.push("--workspace".to_string()); + args.push(filter.clone()); + } + } + args.push("install".to_string()); + args.extend_from_slice(packages); + if workspace_root { + args.push("-w".to_string()); + } + args.extend_from_slice(extra_args); + } + } + + args + } + + /// Build remove command arguments with workspace support + pub fn build_remove_args( + &self, + packages: &[String], + filters: &[String], + workspace_root: bool, + extra_args: &[String], + ) -> Vec { + let mut args = Vec::new(); + + match self.client { + PackageManagerType::Pnpm => { + for filter in filters { + args.push("--filter".to_string()); + args.push(filter.clone()); + } + args.push("remove".to_string()); + args.extend_from_slice(packages); + if workspace_root { + args.push("-w".to_string()); + } + args.extend_from_slice(extra_args); + } + PackageManagerType::Yarn => { + if !filters.is_empty() { + for filter in filters { + args.push("workspace".to_string()); + args.push(filter.clone()); + } + } + args.push("remove".to_string()); + args.extend_from_slice(packages); + args.extend_from_slice(extra_args); + } + PackageManagerType::Npm => { + if !filters.is_empty() { + for filter in filters { + args.push("--workspace".to_string()); + args.push(filter.clone()); + } + } + args.push("uninstall".to_string()); + args.extend_from_slice(packages); + args.extend_from_slice(extra_args); + } + } + + args + } +} +``` + +#### 3. Add Command Implementation + +**File**: `crates/vite_task/src/add.rs` (new file) + +```rust +pub struct AddCommand { + workspace_root: AbsolutePathBuf, +} + +impl AddCommand { + pub fn new(workspace_root: AbsolutePathBuf) -> Self { + Self { workspace_root } + } + + pub async fn execute( + self, + packages: Vec, + filters: Vec, + workspace_root: bool, + workspace_only: bool, + extra_args: Vec, + ) -> Result { + if packages.is_empty() { + return Err(Error::NoPackagesSpecified); + } + + let package_manager = PackageManager::builder(&self.workspace_root).build().await?; + let workspace = Workspace::partial_load(self.workspace_root)?; + + let resolve_command = package_manager.resolve_command(); + + // Build command with workspace support + let full_args = package_manager.build_add_args( + &packages, + &filters, + workspace_root, + workspace_only, + &extra_args, + ); + + let resolved_task = ResolvedTask::resolve_from_builtin_with_command_result( + &workspace, + "add", + full_args.iter().map(String::as_str), + ResolveCommandResult { + bin_path: resolve_command.bin_path, + envs: resolve_command.envs, + }, + false, + )?; + + let mut task_graph: StableGraph = Default::default(); + task_graph.add_node(resolved_task); + let summary = ExecutionPlan::plan(task_graph, false)?.execute(&workspace).await?; + workspace.unload().await?; + + Ok(summary) + } +} +``` + +#### 4. Remove Command Implementation + +**File**: `crates/vite_task/src/remove.rs` (new file) + +```rust +pub struct RemoveCommand { + workspace_root: AbsolutePathBuf, +} + +impl RemoveCommand { + pub fn new(workspace_root: AbsolutePathBuf) -> Self { + Self { workspace_root } + } + + pub async fn execute( + self, + packages: Vec, + filters: Vec, + workspace_root: bool, + extra_args: Vec, + ) -> Result { + if packages.is_empty() { + return Err(Error::NoPackagesSpecified); + } + + let package_manager = PackageManager::builder(&self.workspace_root).build().await?; + let workspace = Workspace::partial_load(self.workspace_root)?; + + let resolve_command = package_manager.resolve_command(); + + // Build command with workspace support + let full_args = package_manager.build_remove_args( + &packages, + &filters, + workspace_root, + &extra_args, + ); + + let resolved_task = ResolvedTask::resolve_from_builtin_with_command_result( + &workspace, + "remove", + full_args.iter().map(String::as_str), + ResolveCommandResult { + bin_path: resolve_command.bin_path, + envs: resolve_command.envs, + }, + false, + )?; + + let mut task_graph: StableGraph = Default::default(); + task_graph.add_node(resolved_task); + let summary = ExecutionPlan::plan(task_graph, false)?.execute(&workspace).await?; + workspace.unload().await?; + + Ok(summary) + } +} +``` + +### Special Handling + +#### 1. Global Packages + +Yarn requires different command structure for global operations: + +```rust +// pnpm/npm: add -g +// yarn: global add + +fn handle_global_flag(args: &[String], pm_type: PackageManagerType) -> (Vec, bool) { + let has_global = args.contains(&"-g".to_string()) || args.contains(&"--global".to_string()); + let filtered_args: Vec = args.iter() + .filter(|a| *a != "-g" && *a != "--global") + .cloned() + .collect(); + + (filtered_args, has_global) +} +``` + +#### 2. Workspace Filters + +pnpm uses `--filter` before command, yarn/npm use different approaches: + +```rust +fn build_workspace_command( + pm_type: PackageManagerType, + filters: &[String], + operation: &str, + packages: &[String], +) -> Vec { + match pm_type { + PackageManagerType::Pnpm => { + // pnpm --filter add + let mut args = Vec::new(); + for filter in filters { + args.push("--filter".to_string()); + args.push(filter.clone()); + } + args.push(operation.to_string()); + args.extend_from_slice(packages); + args + } + PackageManagerType::Yarn => { + // yarn workspace add + let mut args = Vec::new(); + if let Some(filter) = filters.first() { + args.push("workspace".to_string()); + args.push(filter.clone()); + } + args.push(operation.to_string()); + args.extend_from_slice(packages); + args + } + PackageManagerType::Npm => { + // npm install --workspace + let mut args = vec![operation.to_string()]; + args.extend_from_slice(packages); + for filter in filters { + args.push("--workspace".to_string()); + args.push(filter.clone()); + } + args + } + } +} +``` + +#### 3. Workspace Dependencies + +When adding workspace dependencies with `--workspace` flag: + +```bash +# pnpm: Adds with workspace: protocol +vite add @myorg/utils --workspace --filter app +# → pnpm --filter app add @myorg/utils --workspace +# → Adds: "@myorg/utils": "workspace:*" + +# Without --workspace: Tries to install from registry +vite add @myorg/utils --filter app +# → pnpm --filter app add @myorg/utils +# → Tries npm registry (may fail if not published) +``` + +## Design Decisions + +### 1. No Caching + +**Decision**: Do not cache add/remove operations. + +**Rationale**: + +- These commands modify package.json and lockfiles +- Side effects make caching inappropriate +- Each execution should run fresh +- Similar to how `vite install` works + +**Implementation**: Set `cacheable: false` or skip cache entirely. + +### 2. Pass-Through Arguments + +**Decision**: Pass all arguments after packages directly to package manager. + +**Rationale**: + +- Package managers have many flags (40+ for npm) +- Maintaining complete flag mapping is error-prone +- Pass-through allows accessing all features +- Only translate critical command name differences + +**Example**: + +```bash +vite add react --save-exact +# → pnpm add react --save-exact +# → yarn add react --save-exact +# → npm install react --save-exact +``` + +### 3. Common Flags Only + +**Decision**: Only explicitly support most common flags with automatic translation. + +**Common Flags**: + +- `-D, --save-dev` - universally supported +- `-g, --global` - needs special handling for yarn +- `-E, --save-exact` - universally supported +- `-P, --save-peer` - universally supported +- `-O, --save-optional` - universally supported + +**Advanced Flags**: Pass through as-is + +### 4. Command Aliases + +**Decision**: Support multiple aliases for remove command. + +**Aliases**: + +- `vite remove` (primary) +- `vite rm` (short) +- `vite un` (short, matches pnpm) +- `vite uninstall` (explicit, matches npm) + +**Rationale**: Matches user expectations from other tools. + +### 5. Multiple Package Support + +**Decision**: Allow specifying multiple packages in single command. + +**Example**: + +```bash +vite add react react-dom @types/react -D +vite remove lodash axios underscore +``` + +**Implementation**: Packages are positional arguments before flags. + +## Error Handling + +### No Packages Specified + +```bash +$ vite add +Error: No packages specified +Usage: vite add ... [OPTIONS] +``` + +### Package Manager Not Detected + +```bash +$ vite add react +Error: No package manager detected +Please run one of: + - vite install (to set up package manager) + - Add packageManager field to package.json +``` + +### Invalid Package Names + +Let the underlying package manager handle validation and provide clear errors. + +## User Experience + +### Success Output + +```bash +$ vite add react react-dom +Detected package manager: pnpm@10.15.0 +Running: pnpm add react react-dom + + WARN deprecated inflight@1.0.6: ... + +Packages: +2 +++ +Progress: resolved 150, reused 140, downloaded 10, added 2, done + +dependencies: ++ react 18.3.1 ++ react-dom 18.3.1 + +Done in 2.3s +``` + +### Error Output + +```bash +$ vite add invalid-package-that-does-not-exist +Detected package manager: pnpm@10.15.0 +Running: pnpm add invalid-package-that-does-not-exist + + ERR_PNPM_FETCH_404 GET https://registry.npmjs.org/invalid-package-that-does-not-exist: Not Found - 404 + +This error happened while installing the dependencies of undefined@undefined + +Error: Command failed with exit code 1 +``` + +## Alternative Designs Considered + +### Alternative 1: Flag Translation Layer + +Translate all flags to package manager-specific equivalents: + +```bash +vite add react --dev +# → pnpm add react -D +# → yarn add react --dev +# → npm install react --save-dev +``` + +**Rejected because**: + +- Maintenance burden (40+ npm flags) +- Package managers evolve with new flags +- Pass-through is simpler and more flexible +- Users can use native flags directly + +### Alternative 2: Separate Commands per Package Manager + +```bash +vite pnpm:add react +vite yarn:add react +vite npm:install react +``` + +**Rejected because**: + +- Defeats purpose of unified interface +- More verbose +- Doesn't leverage auto-detection + +### Alternative 3: Interactive Mode + +Prompt for packages and options interactively: + +```bash +$ vite add +? Which packages to add? react +? Add as dev dependency? Yes +``` + +**Rejected for initial version**: + +- Slower for experienced users +- Not scriptable +- Can be added later as optional mode + +## Implementation Plan + +### Phase 1: Core Functionality + +1. Add `Add` and `Remove` command variants to `Commands` enum +2. Create `add.rs` and `remove.rs` modules +3. Implement package manager command resolution +4. Add basic error handling + +### Phase 2: Special Cases + +1. Handle yarn global commands differently +2. Validate package names (optional) +3. Support workspace-specific operations + +### Phase 3: Testing + +1. Unit tests for command resolution +2. Integration tests with mock package managers +3. Manual testing with real package managers + +### Phase 4: Documentation + +1. Update CLI documentation +2. Add examples to README +3. Document flag compatibility matrix + +## Testing Strategy + +### Test Package Manager Versions + +- pnpm@9.x [WIP] +- pnpm@10.x +- yarn@1.x [WIP] +- yarn@4.x +- npm@10.x +- npm@11.x [WIP] + +### Unit Tests + +```rust +#[test] +fn test_add_command_resolution() { + let pm = PackageManager::mock(PackageManagerType::Pnpm); + assert_eq!(pm.resolve_add_command(), "add"); + + let pm = PackageManager::mock(PackageManagerType::Npm); + assert_eq!(pm.resolve_add_command(), "install"); +} + +#[test] +fn test_remove_command_resolution() { + let pm = PackageManager::mock(PackageManagerType::Pnpm); + assert_eq!(pm.resolve_remove_command(), "remove"); + + let pm = PackageManager::mock(PackageManagerType::Npm); + assert_eq!(pm.resolve_remove_command(), "uninstall"); +} + +#[test] +fn test_build_add_args_pnpm() { + let pm = PackageManager::mock(PackageManagerType::Pnpm); + let args = pm.build_add_args( + &["react".to_string()], + &["app".to_string()], + false, + false, + &[], + ); + assert_eq!(args, vec!["--filter", "app", "add", "react"]); +} + +#[test] +fn test_build_add_args_with_workspace_root() { + let pm = PackageManager::mock(PackageManagerType::Pnpm); + let args = pm.build_add_args( + &["typescript".to_string()], + &[], + true, // workspace_root + false, + &["-D".to_string()], + ); + assert_eq!(args, vec!["add", "typescript", "-w", "-D"]); +} + +#[test] +fn test_build_add_args_yarn_workspace() { + let pm = PackageManager::mock(PackageManagerType::Yarn); + let args = pm.build_add_args( + &["react".to_string()], + &["app".to_string()], + false, + false, + &[], + ); + assert_eq!(args, vec!["workspace", "app", "add", "react"]); +} + +#[test] +fn test_build_remove_args_with_filter() { + let pm = PackageManager::mock(PackageManagerType::Pnpm); + let args = pm.build_remove_args( + &["lodash".to_string()], + &["utils".to_string()], + false, + &[], + ); + assert_eq!(args, vec!["--filter", "utils", "remove", "lodash"]); +} +``` + +### Integration Tests + +Create fixtures for testing with each package manager: + +``` +fixtures/add-remove-test/ + pnpm-workspace.yaml + package.json + packages/ + app/ + package.json + utils/ + package.json + test-steps.json +``` + +Test cases: + +1. Add single package +2. Add multiple packages +3. Add with -D flag +4. Add with --filter to specific package +5. Add with --filter wildcard pattern +6. Add to workspace root with -w +7. Add workspace dependency with --workspace +8. Remove single package +9. Remove multiple packages +10. Remove with --filter +11. Error handling for invalid packages +12. Error handling for incompatible filters on yarn/npm + +## CLI Help Output + +### Add Command + +```bash +$ vite add --help +Add packages to dependencies + +Usage: vite add ... [OPTIONS] + +Arguments: + ... Packages to add + +Options: + --filter Filter packages in monorepo (can be used multiple times) + -w, --workspace-root Add to workspace root (ignore-workspace-root-check) + --workspace Only add if package exists in workspace + -D, --save-dev Add as dev dependency + -P, --save-peer Add as peer dependency + -O, --save-optional Add as optional dependency + -E, --save-exact Save exact version + -g, --global Install globally + -h, --help Print help + +Filter Patterns: + Exact package name match + * Wildcard match (pnpm only) + @/* Scope match (pnpm only) + ! Exclude pattern (pnpm only) + ... Package and dependencies (pnpm only) + ... Package and dependents (pnpm only) + +Examples: + vite add react react-dom + vite add -D typescript @types/react + vite add react --filter app + vite add react --filter "app*" --filter "!app-test" + vite add @myorg/utils --workspace --filter web + vite add lodash -w +``` + +### Remove Command + +```bash +$ vite remove --help +Remove packages from dependencies + +Usage: vite remove ... [OPTIONS] + +Aliases: rm, un, uninstall + +Arguments: + ... Packages to remove + +Options: + --filter Filter packages in monorepo (can be used multiple times) + -w, --workspace-root Remove from workspace root + -g, --global Remove global packages + -h, --help Print help + +Filter Patterns: + Exact package name match + * Wildcard match (pnpm only) + @/* Scope match (pnpm only) + ! Exclude pattern (pnpm only) + +Examples: + vite remove lodash + vite remove axios underscore lodash + vite rm lodash --filter app + vite remove utils --filter "app*" + vite rm old-package +``` + +## Performance Considerations + +1. **No Caching**: Operations run directly without cache overhead +2. **Single Execution**: Unlike task runner, these are one-off operations +3. **Pass-Through**: Minimal processing, just command translation +4. **Auto-Detection**: Reuses existing package manager detection (already cached) + +## Security Considerations + +1. **Package Name Validation**: Let package manager handle validation +2. **Lockfile Integrity**: Package manager ensures integrity +3. **No Code Execution**: Just passes through to trusted package manager +4. **Audit Flags**: Users can add `--audit` via pass-through + +## Backward Compatibility + +This is a new feature with no breaking changes: + +- Existing commands unaffected +- New commands are additive +- No changes to task configuration +- No changes to caching behavior + +## Migration Path + +### Adoption + +Users can start using immediately: + +```bash +# Old way +pnpm add react + +# New way (works with any package manager) +vite add react +``` + +### Discoverability + +Add to: + +- CLI help output +- Documentation +- VSCode extension suggestions +- Shell completions + +## Documentation Requirements + +### User Guide + +Add to CLI documentation: + +````markdown +### Adding Packages + +```bash +vite add ... [OPTIONS] +``` +```` + +Automatically uses the detected package manager (pnpm/yarn/npm). + +**Basic Examples:** + +- `vite add react` - Add production dependency +- `vite add -D typescript` - Add dev dependency +- `vite add react react-dom` - Add multiple packages + +**Workspace Examples:** + +- `vite add react --filter app` - Add to specific package +- `vite add react --filter "app*"` - Add to multiple packages (pnpm) +- `vite add @myorg/utils --workspace --filter web` - Add workspace dependency +- `vite add lodash -w` - Add to workspace root + +**Common Options:** + +- `--filter ` - Target specific workspace package(s) +- `-w, --workspace-root` - Add to workspace root +- `--workspace` - Add workspace dependency (pnpm) +- `-D, --save-dev` - Add as dev dependency +- `-E, --save-exact` - Save exact version +- `-P, --save-peer` - Add as peer dependency +- `-O, --save-optional` - Add as optional dependency +- `-g, --global` - Install globally + +### Removing Packages + +```bash +vite remove ... [OPTIONS] +vite rm ... [OPTIONS] +``` + +Aliases: `rm`, `un`, `uninstall` + +**Basic Examples:** + +- `vite remove lodash` - Remove package +- `vite rm axios underscore` - Remove multiple packages + +**Workspace Examples:** + +- `vite remove lodash --filter app` - Remove from specific package +- `vite rm utils --filter "app*"` - Remove from multiple packages (pnpm) +- `vite remove -g typescript` - Remove global package + +**Options:** + +- `--filter ` - Target specific workspace package(s) +- `-w, --workspace-root` - Remove from workspace root +- `-g, --global` - Remove global packages + +```` +### Package Manager Compatibility + +Document flag support matrix: + +| Flag | pnpm | yarn | npm | +|------|------|------|-----| +| `-D` | ✅ | ✅ | ✅ | +| `-E` | ✅ | ✅ | ✅ | +| `-P` | ✅ | ✅ | ✅ | +| `-O` | ✅ | ✅ | ✅ | +| `-g` | ✅ | ⚠️ (use global) | ✅ | + +## Workspace Operations Deep Dive + +### Filter Patterns (pnpm-inspired) + +Following pnpm's filter API: + +**Exact Match:** +```bash +vite add react --filter app +# → pnpm --filter app add react +```` + +**Wildcard Patterns:** + +```bash +vite add react --filter "app*" +# → pnpm --filter "app*" add react +# Matches: app, app-web, app-mobile +``` + +**Scope Patterns:** + +```bash +vite add lodash --filter "@myorg/*" +# → pnpm --filter "@myorg/*" add lodash +# Matches all packages in @myorg scope +``` + +**Exclusion Patterns:** + +```bash +vite add react --filter "!test*" +# → pnpm --filter "!test*" add react +# Adds to all packages EXCEPT those starting with test +``` + +**Multiple Filters:** + +```bash +vite add react --filter app --filter web +# → pnpm --filter app --filter web add react +# Adds to both app AND web packages +``` + +**Dependency Selectors:** + +```bash +# Add to package and all its dependencies +vite add lodash --filter "app..." +# → pnpm --filter "app..." add lodash + +# Add to package and all its dependents +vite add utils --filter "...core" +# → pnpm --filter "...core" add utils +``` + +### Workspace Root Operations + +Add dependencies to workspace root (requires special flag): + +```bash +vite add -D typescript -w +# → pnpm add -D typescript -w (pnpm) +# → yarn add -D typescript -W (yarn) +# → npm install -D typescript -w (npm) +``` + +**Why needed**: By default, package managers prevent adding to workspace root to encourage proper package structure. + +### Workspace Protocol + +For internal monorepo dependencies: + +```bash +# Add workspace dependency with workspace: protocol +vite add @myorg/utils --workspace --filter app +# → pnpm --filter app add @myorg/utils --workspace +# → Adds: "@myorg/utils": "workspace:*" + +# Specify version +vite add "@myorg/utils@workspace:^" --filter app +# → Adds: "@myorg/utils": "workspace:^" +``` + +### Package Manager Compatibility + +| Feature | pnpm | yarn | npm | Notes | +| -------------------------- | ------------------ | -------------------- | ---------------------- | ------------------------ | +| `--filter ` | ✅ Native | ⚠️ `workspace ` | ⚠️ `--workspace ` | Syntax differs | +| Multiple filters | ✅ Repeatable flag | ❌ Single only | ⚠️ Limited | pnpm most flexible | +| Wildcard patterns | ✅ Full support | ⚠️ Limited | ❌ No wildcards | pnpm best | +| Exclusion `!` | ✅ Supported | ❌ Not supported | ❌ Not supported | pnpm only | +| Dependency selectors `...` | ✅ Supported | ❌ Not supported | ❌ Not supported | pnpm only | +| `-w` (root) | ✅ `-w` | ✅ `-W` | ✅ `-w` | Slightly different flags | +| `--workspace` protocol | ✅ Supported | ❌ Manual | ❌ Manual | pnpm feature | + +**Graceful Degradation**: + +- Advanced pnpm features (wildcard, exclusion, selectors) will error on yarn/npm with helpful message +- Basic `--filter ` works across all package managers + +## Future Enhancements + +### 1. Enhanced Filter Support for yarn/npm + +Implement wildcard translation for yarn/npm: + +```bash +vite add react --filter "app*" +# → For yarn: Run `yarn workspace app add react` for each matching package +# → For npm: Run `npm install react --workspace app` for each matching package +``` + +### 2. Interactive Mode + +> Referer to ni's interactive mode https://github.com/antfu-collective/ni + +```bash +$ vite add --interactive +? Select for package > tsdown +❯ tsdown v0.15.7 - git+https://github.com/rolldown/tsdown.git + tsdown-config-silverwind v1.4.0 - git+https://github.com/silverwind/tsdown-config-silverwind.git + @storm-software/tsdown v0.45.0 - git+https://github.com/storm-software/storm-ops.git + create-tsdown v0.15.7 - git+https://github.com/rolldown/tsdown.git + shadcn-auv v0.0.1 - git+https://github.com/ohojs/shadcn-auv.git + ts-build-wizard v1.0.3 - git+https://github.com/Alireza-Tabatabaeian/react-app-registry.git + vite-plugin-shadcn-registry v0.0.6 - git+https://github.com/myshkouski/vite-plugin-shadcn-registry.git + @qds.dev/tools v0.3.3 - https://www.npmjs.com/package/@qds.dev/tools + feishu-bot-notify v0.1.3 - git+https://github.com/duowb/feishu-bot-notify.git + @memo28.pro/bundler v0.0.2 - https://www.npmjs.com/package/@memo28.pro/bundler + tsdown-jsr-exports-lint v0.1.4 - git+https://github.com/kazupon/tsdown-jsr-exports-lint.git + @miloas/tsdown v0.13.0 - git+https://github.com/rolldown/tsdown.git + @socket-synced-state/server v0.0.9 - https://www.npmjs.com/package/@socket-synced-state/server + @gamedev-sensei/tsdown-config v2.0.1 - git+ssh://git@github.com/gamedev-sensei/package-extras.git + ↓ 0xpresc-test v0.1.0 - https://www.npmjs.com/package/0xpresc-test + +? install tsdown as › - Use arrow-keys. Return to submit. +❯ prod + dev + peer +``` + +### 3. Upgrade Command + +```bash +vite upgrade react +vite upgrade --latest +vite upgrade --interactive +``` + +### 4. Smart Suggestions + +```bash +$ vite add react +Adding react... +💡 Suggestion: Install @types/react for TypeScript support? + Run: vite add -D @types/react +``` + +### 5. Dependency Analysis + +```bash +$ vite add react +Analyzing dependency impact... + Will add: + react@18.3.1 (85KB) + + scheduler@0.23.0 (5KB) + Total size: 90KB + +Proceed? (Y/n) +``` + +## Open Questions + +1. **Should we warn about peer dependency conflicts?** + - Proposed: Let package manager handle warnings + - Can be enhanced later with custom warnings + +2. **Should we support version specifiers?** + - Proposed: Yes, pass through to package manager + - Example: `vite add react@18.2.0` + +3. **Should we support scoped package shortcuts?** + - Proposed: No special handling, pass through as-is + - Example: `vite add @types/react` works naturally + +4. **Should we prevent adding to wrong dependency types?** + - Proposed: No validation, trust package manager + - Package managers handle this well already + +5. **How to handle pnpm-specific filter features on yarn/npm?** + - Proposed: For wildcards/exclusions on yarn/npm: + - Option A: Error with clear message explaining pnpm-only feature + - Option B: Resolve wildcard ourselves and run command for each package + - Recommendation: Start with Option A, add Option B later + +6. **Should we support workspace protocol configuration?** + - Proposed: Pass through to pnpm, document in .npmrc for users + - Example: `save-workspace-protocol=rolling` in .npmrc + - vite+ doesn't need to handle this explicitly + +7. **Should we validate that filtered packages exist?** + - Proposed: Let package manager validate + - Clearer error messages from native tools + - Avoids duplicating workspace parsing logic + +## Success Metrics + +1. **Adoption**: % of users using `vite add/remove` vs direct package manager +2. **Error Rate**: Track command failures vs package manager direct usage +3. **User Feedback**: Survey/issues about command ergonomics +4. **Performance**: Measure overhead vs direct package manager calls (<100ms target) + +## Implementation Timeline + +- **Week 1**: Core implementation (command parsing, package manager adapter) +- **Week 2**: Testing (unit tests, integration tests) +- **Week 3**: Documentation and examples +- **Week 4**: Review, polish, and release + +## Dependencies + +### New Dependencies + +None required - leverages existing: + +- `vite_package_manager` - package manager detection +- `clap` - command parsing +- Existing task execution infrastructure + +### Modified Files + +- `crates/vite_task/src/lib.rs` - Add command enum variants +- `crates/vite_task/src/add.rs` - New file +- `crates/vite_task/src/remove.rs` - New file +- `crates/vite_package_manager/src/package_manager.rs` - Add command resolution methods +- `docs/cli.md` - Documentation updates + +## Workspace Feature Implementation Priority + +### Phase 1: Core Functionality (MVP) + +- ✅ Basic add/remove without filters +- ✅ Multiple package support +- ✅ Auto package manager detection +- ✅ Common flags (-D, -E, -P, -O) + +### Phase 2: Workspace Support (pnpm-focused) + +- ✅ `--filter ` for all package managers +- ✅ `-w` flag for workspace root +- ✅ `--workspace` flag for workspace dependencies (pnpm) +- ✅ Wildcard patterns `*` (pnpm only, error on others) +- ✅ Scope patterns `@scope/*` (pnpm only) + +### Phase 3: Advanced Filters (pnpm-focused) + +- Exclusion patterns `!` (pnpm only) +- Dependency selectors `...` (pnpm only) +- Multiple filter support +- Graceful degradation for yarn/npm + +### Phase 4: Cross-PM Compatibility (optional) + +- Wildcard resolution for yarn/npm +- Run filtered command for each matching package +- Unified behavior across all package managers + +## Real-World Usage Examples + +### Monorepo Package Management + +```bash +# Add React to all frontend packages +vite add react react-dom --filter "@myorg/app-*" + +# Add testing library to all packages +vite add -D vitest --filter "*" + +# Add shared utils to app packages (workspace dependency) +vite add @myorg/shared-utils --workspace --filter "@myorg/app-*" + +# Remove deprecated package from all packages +vite remove moment --filter "*" + +# Add TypeScript to workspace root (shared config) +vite add -D typescript @types/node -w +``` + +### Development Workflow + +```bash +# Clone new monorepo +git clone +vite install + +# Add new feature dependencies to web app +cd packages/web +vite add axios react-query + +# Add development tool to specific package +vite add -D webpack-bundle-analyzer --filter web + +# Remove unused dependencies from utils package +vite rm lodash underscore --filter utils + +# Add workspace package as dependency +vite add @myorg/ui-components --workspace --filter web +``` + +### Migration from Direct Package Manager + +```bash +# Before (package manager specific) +pnpm --filter app add react +yarn workspace app add react +npm install react --workspace app + +# After (unified) +vite add react --filter app +``` + +## Conclusion + +This RFC proposes adding `vite add` and `vite remove` commands to provide a unified interface for package management across pnpm/yarn/npm. The design: + +- ✅ Automatically adapts to detected package manager +- ✅ Supports multiple packages in single command +- ✅ **Full workspace support following pnpm's API design** +- ✅ **Filter patterns for targeting specific packages** +- ✅ **Workspace root and workspace protocol support** +- ✅ Uses pass-through for maximum flexibility +- ✅ No caching overhead (as requested) +- ✅ Simple implementation leveraging existing infrastructure +- ✅ Graceful degradation for package manager-specific features +- ✅ Extensible for future enhancements + +The implementation follows pnpm's battle-tested workspace API design while providing graceful degradation for yarn/npm users. This provides immediate value to monorepo developers with a unified, intuitive interface.