diff --git a/Cargo.lock b/Cargo.lock index 8a84e67104..2b341e1660 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7545,6 +7545,9 @@ dependencies = [ "clap_complete", "crossterm", "directories", + "futures", + "indexmap", + "indicatif", "node-semver", "owo-colors", "oxc_resolver", diff --git a/crates/vite_global_cli/Cargo.toml b/crates/vite_global_cli/Cargo.toml index 90eecf1648..d1fdb3f08c 100644 --- a/crates/vite_global_cli/Cargo.toml +++ b/crates/vite_global_cli/Cargo.toml @@ -16,6 +16,7 @@ chrono = { workspace = true } clap = { workspace = true, features = ["derive"] } clap_complete = { workspace = true, features = ["unstable-dynamic"] } directories = { workspace = true } +futures = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } node-semver = { workspace = true } @@ -25,6 +26,8 @@ tracing = { workspace = true } owo-colors = { workspace = true } oxc_resolver = { workspace = true } crossterm = { workspace = true } +indexmap = { workspace = true } +indicatif = { workspace = true } vite_error = { workspace = true } vite_install = { workspace = true } vite_js_runtime = { workspace = true } diff --git a/crates/vite_global_cli/src/cli.rs b/crates/vite_global_cli/src/cli.rs index b2d5645dda..509bab7a40 100644 --- a/crates/vite_global_cli/src/cli.rs +++ b/crates/vite_global_cli/src/cli.rs @@ -8,10 +8,20 @@ use std::{ffi::OsStr, process::ExitStatus}; use clap::{CommandFactory, FromArgMatches, Parser, Subcommand}; use clap_complete::ArgValueCompleter; use tokio::runtime::Runtime; -use vite_path::{AbsolutePath, AbsolutePathBuf}; +use vite_path::AbsolutePathBuf; use vite_pm_cli::PackageManagerCommand; +use vite_shared::output; -use crate::{commands, error::Error, help}; +use crate::{ + commands::{ + self, + env::{global_install, package_metadata::PackageMetadata}, + }, + error::Error, + help, +}; + +const DEFAULT_GLOBAL_INSTALL_CONCURRENCY: usize = 5; #[derive(Clone, Copy, Debug)] pub struct RenderOptions { @@ -532,19 +542,24 @@ async fn run_package_manager_command( ) -> Result { match command { PackageManagerCommand::Install { - global: true, packages: Some(pkgs), node, force, .. - } if !pkgs.is_empty() => managed_install(&pkgs, node.as_deref(), force).await, + global: true, + packages: Some(pkgs), + node, + force, + concurrency, + .. + } if !pkgs.is_empty() => managed_install(&pkgs, node.as_deref(), force, concurrency).await, - PackageManagerCommand::Add { global: true, ref packages, ref node, .. } => { - managed_install(packages, node.as_deref(), false).await - } + PackageManagerCommand::Add { + global: true, ref packages, ref node, concurrency, .. + } => managed_install(packages, node.as_deref(), false, concurrency).await, PackageManagerCommand::Remove { global: true, ref packages, dry_run, .. } => { managed_uninstall(packages, dry_run).await } - PackageManagerCommand::Update { global: true, ref packages, .. } => { - managed_update(&cwd, packages).await + PackageManagerCommand::Update { global: true, ref packages, concurrency, .. } => { + managed_update(packages, concurrency).await } // `pm list -g` lists vite-plus-managed globals, not the underlying PM's. @@ -562,20 +577,28 @@ async fn run_package_manager_command( } } -// snap-test fixtures expect bare lines (no "error:"/"info:" prefix), so -// these helpers use `output::raw_stderr`/`output::raw` rather than the -// prefixed `output::error`/`output::info`. async fn managed_install( packages: &[String], node: Option<&str>, force: bool, + concurrency: Option, ) -> Result { - for package in packages { - if let Err(e) = crate::commands::env::global_install::install(package, node, force).await { - vite_shared::output::raw_stderr(&format!("Failed to install {package}: {e}")); - return Ok(exit_status(1)); - } + if let Err((package_name, error)) = crate::commands::env::global_install::install( + packages, + node, + force, + concurrency.unwrap_or(DEFAULT_GLOBAL_INSTALL_CONCURRENCY), + false, + ) + .await + { + output::error(&format!( + "Failed to install {}: {error}", + package_name.as_deref().unwrap_or("global packages") + )); + return Ok(exit_status(1)); } + Ok(ExitStatus::default()) } @@ -589,21 +612,14 @@ async fn managed_uninstall(packages: &[String], dry_run: bool) -> Result bool { +fn is_global_package_up_to_date(installed_version: &str, registry_version: &str) -> bool { installed_version.trim() == registry_version.trim() - && installed_node_version.trim() == target_node_version.trim() } -async fn managed_update(cwd: &AbsolutePath, packages: &[String]) -> Result { - use crate::commands::env::{ - config::resolve_version, global_install, package_metadata::PackageMetadata, - }; - +async fn managed_update( + packages: &[String], + concurrency: Option, +) -> Result { let all_packages = if packages.is_empty() { let all = PackageMetadata::list_all().await?; if all.is_empty() { @@ -615,27 +631,14 @@ async fn managed_update(cwd: &AbsolutePath, packages: &[String]) -> Result = Vec::new(); let mut skipped = 0usize; if let Some(all) = all_packages { for metadata in all { - if metadata.platform.node.trim() != target_node_version.trim() { - to_update.push(metadata.name.clone()); - continue; - } - - match global_install::latest_package_version(&metadata.name, Some(&target_node_version)) - .await - { + match global_install::latest_package_version(&metadata.name).await { Ok(latest_version) - if is_global_package_up_to_date( - &metadata.version, - &latest_version, - &metadata.platform.node, - &target_node_version, - ) => + if is_global_package_up_to_date(&metadata.version, &latest_version) => { vite_shared::output::raw(&format!( "{} is already up to date (v{}).", @@ -662,21 +665,9 @@ async fn managed_update(cwd: &AbsolutePath, packages: &[String]) -> Result + if is_global_package_up_to_date(&metadata.version, &latest_version) => { vite_shared::output::raw(&format!( "{} is already up to date (v{}).", @@ -704,11 +695,21 @@ async fn managed_update(cwd: &AbsolutePath, packages: &[String]) -> Result { + spec: &'a str, + staging_dir: Option, +} + +fn package_error(package_name: &str, error: impl Into) -> (Option, Error) { + (Some(package_name.to_string()), error.into()) +} + +/// Install global packages parallelly. /// /// If `node_version` is provided, uses that version. Otherwise, resolves from current directory. /// If `force` is true, auto-uninstalls conflicting packages. +/// Use `concurrency` to control the number of packages to install in parallel. pub async fn install( - package_spec: &str, + package_specs: &[String], node_version: Option<&str>, force: bool, -) -> Result<(), Error> { - // Parse package spec (e.g., "typescript", "typescript@5.0.0", "@scope/pkg") - let (package_name, _version_spec) = parse_package_spec(package_spec); + concurrency: usize, + update: bool, +) -> Result<(), (Option, Error)> { + if package_specs.is_empty() { + return Ok(()); + } - output::raw(&format!("Installing {} globally...", package_spec)); + let operation_progress = if update { "Updating" } else { "Installing" }; + let operation_past = if update { "Updated" } else { "Installed" }; // 1. Resolve Node.js version - let version = if let Some(v) = node_version { + let node_version = if let Some(v) = node_version { let provider = NodeProvider::new(); - resolve_version_alias(v, &provider).await? + match resolve_version_alias(v, &provider).await { + Ok(version) => version, + Err(error) => return Err((None, error)), + } } else { // Resolve from current directory - let cwd = current_dir().map_err(|e| { - Error::ConfigError(format!("Cannot get current directory: {}", e).into()) - })?; - let resolution = resolve_version(&cwd).await?; + let cwd = match current_dir() { + Ok(cwd) => cwd, + Err(error) => { + let error = + Error::ConfigError(format!("Cannot get current directory: {}", error).into()); + return Err((None, error)); + } + }; + let resolution = match resolve_version(&cwd).await { + Ok(resolution) => resolution, + Err(error) => return Err((None, error)), + }; resolution.version }; // 2. Ensure Node.js is installed - let runtime = - vite_js_runtime::download_runtime(vite_js_runtime::JsRuntimeType::Node, &version).await?; + let runtime = match vite_js_runtime::download_runtime( + vite_js_runtime::JsRuntimeType::Node, + &node_version, + ) + .await + { + Ok(runtime) => runtime, + Err(error) => { + let error = Error::RuntimeDownload(error); + return Err((None, error)); + } + }; let node_bin_dir = runtime.get_bin_prefix(); let npm_path = if cfg!(windows) { node_bin_dir.join("npm.cmd") } else { node_bin_dir.join("npm") }; - // 3. Create staging directory - let tmp_dir = get_tmp_dir()?; - let staging_dir = tmp_dir.join("packages").join(&package_name); - - // Clean up any previous failed install - if tokio::fs::try_exists(&staging_dir).await.unwrap_or(false) { - tokio::fs::remove_dir_all(&staging_dir).await?; + // 3. Install packages in parallel + let mut packages = IndexMap::::new(); + for package_spec in package_specs { + // Parse package spec (e.g., "typescript", "typescript@5.0.0", "@scope/pkg") + let (package_name, _version_spec) = parse_package_spec(package_spec); + packages.insert(package_name, Package { spec: package_spec, staging_dir: None }); + } + let packages_count = packages.len(); + + let concurrency = concurrency.max(1); + output::info(&format!( + "{} {} global {} with Node.js {}", + operation_progress, + packages_count, + if packages_count == 1 { "package" } else { "packages" }, + node_version + )); + + let progress = ProgressBar::new(packages_count as u64); + if std::io::stderr().is_terminal() && std::env::var_os("CI").is_none() { + let style = ProgressStyle::with_template("{spinner:.cyan} {msg} ({pos}/{len})") + .unwrap_or_else(|_| ProgressStyle::default_spinner()) + .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]); + progress.set_style(style); + progress.set_message(format!("{} global packages", operation_progress)); + progress.enable_steady_tick(Duration::from_millis(80)); + } else { + progress.set_draw_target(ProgressDrawTarget::hidden()); } - tokio::fs::create_dir_all(&staging_dir).await?; - // 4. Run npm install with prefix set to staging directory - // Pipe stdout/stderr so npm output is hidden on success, shown on failure - let output = Command::new(npm_path.as_path()) - .args(["install", "-g", "--no-fund", package_spec]) - .env("npm_config_prefix", staging_dir.as_path()) - .env("PATH", format_path_prepended(node_bin_dir.as_path())) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .await?; + // We have to clone it because we will modify `packages` to storage package names + let package_names = packages.keys().cloned().collect::>(); + let mut package_names = package_names.iter(); + + let mut installs = FuturesUnordered::new(); + loop { + while installs.len() < concurrency { + let Some(package_name) = package_names.next() else { break }; + let package = packages.get(package_name).unwrap(); + + installs.push(async { + ( + package_name.clone(), + install_one(package_name, package.spec, &npm_path, &node_bin_dir).await, + ) + }); + } - if !output.status.success() { - // Clean up staging directory - let _ = tokio::fs::remove_dir_all(&staging_dir).await; - // Show captured output to help debug the failure - let _ = std::io::stdout().write_all(&output.stdout); - let _ = std::io::stderr().write_all(&output.stderr); - return Err(Error::ConfigError( - format!("npm install failed with exit code: {:?}", output.status.code()).into(), - )); - } + if installs.is_empty() { + break; + } - // 5. Find installed package and extract metadata - let node_modules_dir = get_node_modules_dir(&staging_dir, &package_name); - let package_json_path = node_modules_dir.join("package.json"); + match installs.next().await { + Some((package_name, Ok(staging_dir))) => { + progress.inc(1); + packages.get_mut(&package_name).unwrap().staging_dir = Some(staging_dir) + } + Some((package_name, Err(error))) => { + // Cancel all tasks + installs.clear(); + progress.finish_and_clear(); + + // Clear the installed packages + packages.iter().for_each(|(_, package)| { + if let Some(staging_dir) = package.staging_dir.as_ref() { + let _ = std::fs::remove_dir_all(staging_dir); + } + }); - if !tokio::fs::try_exists(&package_json_path).await.unwrap_or(false) { - let _ = tokio::fs::remove_dir_all(&staging_dir).await; - return Err(Error::ConfigError( - format!( - "Package {} was not installed correctly, package.json not found at {}", - package_name, - package_json_path.as_path().display() - ) - .into(), - )); + return Err((Some(package_name), error)); + } + None => break, + } } + progress.finish_and_clear(); - // Read package.json to get version and binaries - let package_json_content = tokio::fs::read_to_string(&package_json_path).await?; - let package_json: serde_json::Value = serde_json::from_str(&package_json_content) - .map_err(|e| Error::ConfigError(format!("Failed to parse package.json: {}", e).into()))?; + // 4. Check the installed packages, move to final location and create shims. + let mut result = Ok(()); + for (index, (package_name, Package { spec: _, staging_dir })) in + packages.into_iter().enumerate() + { + // Packages must have staging dir + let staging_dir = staging_dir.unwrap(); - let installed_version = package_json["version"].as_str().unwrap_or("unknown").to_string(); + if result.is_err() { + let _ = std::fs::remove_dir_all(&staging_dir); + continue; + } - let binary_infos = extract_binaries(&package_json); + let node_modules_dir = get_node_modules_dir(&staging_dir, &package_name); + let package_json_path = node_modules_dir.join("package.json"); - // Detect which binaries are JavaScript files - let mut bin_names = Vec::new(); - let mut js_bins = HashSet::new(); - for info in &binary_infos { - bin_names.push(info.name.clone()); - let binary_path = node_modules_dir.join(&info.path); - if is_javascript_binary(&binary_path) { - js_bins.insert(info.name.clone()); + if !tokio::fs::try_exists(&package_json_path).await.unwrap_or(false) { + let _ = tokio::fs::remove_dir_all(&staging_dir).await; + result = Err(( + Some(package_name.clone()), + Error::ConfigError( + format!( + "Package was not installed correctly, package.json not found at {}", + package_json_path.as_path().display() + ) + .into(), + ), + )); + continue; } - } - - // 5b. Check for binary conflicts (before moving staging to final location) - let mut conflicts: Vec<(String, String)> = Vec::new(); // (bin_name, existing_package) - for bin_name in &bin_names { - if let Some(config) = BinConfig::load(bin_name).await? { - // Only conflict if owned by a different package - if config.package != package_name { - conflicts.push((bin_name.clone(), config.package.clone())); + let package_json_content = tokio::fs::read_to_string(&package_json_path) + .await + .map_err(|error| package_error(&package_name, error))?; + let package_json: serde_json::Value = match serde_json::from_str(&package_json_content) { + Ok(package_json) => package_json, + Err(error) => { + let _ = tokio::fs::remove_dir_all(&staging_dir).await; + result = Err(( + Some(package_name.clone()), + Error::ConfigError(format!("Failed to parse package.json: {}", error).into()), + )); + continue; + } + }; + + let installed_version = package_json["version"].as_str().unwrap_or("unknown").to_string(); + let binary_infos = extract_binaries(&package_json); + + let mut bin_names = Vec::new(); + let mut js_bins = HashSet::new(); + for info in &binary_infos { + bin_names.push(info.name.clone()); + let binary_path = node_modules_dir.join(&info.path); + if is_javascript_binary(&binary_path) { + js_bins.insert(info.name.clone()); } } - } - if !conflicts.is_empty() { - if force { - // Auto-uninstall conflicting packages - let packages_to_remove: HashSet<_> = - conflicts.iter().map(|(_, pkg)| pkg.clone()).collect(); - for pkg in packages_to_remove { - output::raw(&format!("Uninstalling {} (conflicts with {})...", pkg, package_name)); - // Use Box::pin to avoid recursive async type issues - Box::pin(uninstall(&pkg, false)).await?; + let mut conflicts = Vec::<(String, String)>::new(); + + for bin_name in &bin_names { + if let Some(config) = BinConfig::load(bin_name) + .await + .map_err(|error| package_error(&package_name, error))? + { + if config.package != package_name { + conflicts.push((bin_name.clone(), config.package.clone())); + } } - } else { - // Hard fail with clear error - // Clean up staging directory - let _ = tokio::fs::remove_dir_all(&staging_dir).await; - return Err(Error::BinaryConflict { - bin_name: conflicts[0].0.clone(), - existing_package: conflicts[0].1.clone(), - new_package: package_name.clone(), - }); } - } - // 6. Move staging to final location - let packages_dir = get_packages_dir()?; - let final_dir = packages_dir.join(&package_name); - - // Remove existing installation if present - if tokio::fs::try_exists(&final_dir).await.unwrap_or(false) { - tokio::fs::remove_dir_all(&final_dir).await?; - } + if !conflicts.is_empty() { + if force { + let packages_to_remove: HashSet<_> = + conflicts.iter().map(|(_, pkg)| pkg.clone()).collect(); + for pkg in packages_to_remove { + output::raw(&format!( + "Uninstalling {} (conflicts with {})...", + pkg, package_name + )); + Box::pin(uninstall(&pkg, false)) + .await + .map_err(|error| package_error(&package_name, error))?; + } + } else { + let _ = tokio::fs::remove_dir_all(&staging_dir).await; + result = Err(( + Some(package_name.clone()), + Error::BinaryConflict { + bin_name: conflicts[0].0.clone(), + existing_package: conflicts[0].1.clone(), + new_package: package_name.clone(), + }, + )); + continue; + } + } - // Create parent directory (handles scoped packages like @scope/pkg) - if let Some(parent) = final_dir.parent() { - tokio::fs::create_dir_all(parent).await?; - } - tokio::fs::rename(&staging_dir, &final_dir).await?; + let packages_dir = + get_packages_dir().map_err(|error| package_error(&package_name, error))?; + let final_dir = packages_dir.join(&package_name); - // 7. Save package metadata - let metadata = PackageMetadata::new( - package_name.clone(), - installed_version.clone(), - version.clone(), - None, // npm version - could extract from runtime - bin_names.clone(), - js_bins, - "npm".to_string(), - ); - metadata.save().await?; + if tokio::fs::try_exists(&final_dir).await.unwrap_or(false) { + tokio::fs::remove_dir_all(&final_dir) + .await + .map_err(|error| package_error(&package_name, error))?; + } - // 8. Create shims for binaries and save per-binary configs - let bin_dir = get_bin_dir()?; - for bin_name in &bin_names { - create_package_shim(&bin_dir, bin_name, &package_name).await?; + if let Some(parent) = final_dir.parent() { + tokio::fs::create_dir_all(parent) + .await + .map_err(|error| package_error(&package_name, error))?; + } + tokio::fs::rename(&staging_dir, &final_dir) + .await + .map_err(|error| package_error(&package_name, error))?; - // Write per-binary config - let bin_config = BinConfig::new( - bin_name.clone(), + let metadata = PackageMetadata::new( package_name.clone(), installed_version.clone(), - version.clone(), + node_version.clone(), + None, + bin_names.clone(), + js_bins, + "npm".to_string(), ); - bin_config.save().await?; + metadata.save().await.map_err(|error| package_error(&package_name, error))?; + + let bin_dir = get_bin_dir().map_err(|error| package_error(&package_name, error))?; + for bin_name in &bin_names { + create_package_shim(&bin_dir, bin_name, &package_name) + .await + .map_err(|error| package_error(&package_name, error))?; + + let bin_config = BinConfig::new( + bin_name.clone(), + package_name.clone(), + installed_version.clone(), + node_version.clone(), + ); + bin_config.save().await.map_err(|error| package_error(&package_name, error))?; + } + + output::success(&format!( + "{} {} {}{}", + operation_past, + package_name.bold(), + if update { "to " } else { "" }, + installed_version.bold() + )); + if !bin_names.is_empty() { + let bins = bin_names + .iter() + .map(|bin_name| bin_name.bold().to_string()) + .collect::>() + .join(", "); + output::raw(&format!(" Bins: {}", bins)); + } + if index + 1 < packages_count { + output::raw(""); + } } - output::raw(&format!("Installed {} v{}", package_name, installed_version)); - if !bin_names.is_empty() { - output::raw(&format!("Binaries: {}", bin_names.join(", "))); + result +} + +/// Install one package on the stage directory +/// Return (package_name, installed_version, bin_names) +async fn install_one( + package_name: &str, + package_spec: &str, + npm_path: &AbsolutePathBuf, + node_bin_dir: &AbsolutePathBuf, +) -> Result { + // 1. Create staging directory + let tmp_dir = get_tmp_dir()?; + let staging_dir = tmp_dir.join("packages").join(package_name); + + // Clean up any previous failed install + if tokio::fs::try_exists(&staging_dir).await.unwrap_or(false) { + tokio::fs::remove_dir_all(&staging_dir).await?; } + tokio::fs::create_dir_all(&staging_dir).await?; - Ok(()) + // 4. Run npm install with prefix set to staging directory + // Pipe stdout/stderr so npm output is hidden on success, shown on failure + let output = Command::new(npm_path.as_path()) + .args(["install", "-g", "--no-fund", &package_spec]) + .env("npm_config_prefix", staging_dir.as_path()) + .env("PATH", format_path_prepended(node_bin_dir.as_path())) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .kill_on_drop(true) + .output() + .await?; + + if !output.status.success() { + // Clean up staging directory + let _ = tokio::fs::remove_dir_all(&staging_dir).await; + + // Show captured output to help debug the failure + let _ = std::io::stdout().write_all(&output.stdout); + let _ = std::io::stderr().write_all(&output.stderr); + return Err(Error::ConfigError( + format!("npm install failed with exit code: {:?}", output.status.code()).into(), + )); + } + + Ok(staging_dir) } /// Uninstall a global package. @@ -272,23 +448,38 @@ pub async fn uninstall(package_name: &str, dry_run: bool) -> Result<(), Error> { /// `package_spec` may be a bare package name (`typescript`) or include a /// version/tag (`typescript@beta`, `@scope/pkg@1.0.0`). The command returns the /// version that npm resolves for that spec. -pub(crate) async fn latest_package_version( - package_spec: &str, - node_version: Option<&str>, -) -> Result { - let version = if let Some(v) = node_version { - let provider = NodeProvider::new(); - resolve_version_alias(v, &provider).await? - } else { - let cwd = current_dir().map_err(|e| { - Error::ConfigError(format!("Cannot get current directory: {}", e).into()) - })?; - let resolution = resolve_version(&cwd).await?; +pub(crate) async fn latest_package_version(package_spec: &str) -> Result { + // Resolve from current directory + let node_version = { + let cwd = match current_dir() { + Ok(cwd) => cwd, + Err(error) => { + let error = + Error::ConfigError(format!("Cannot get current directory: {}", error).into()); + return Err(error); + } + }; + let resolution = match resolve_version(&cwd).await { + Ok(resolution) => resolution, + Err(error) => return Err(error), + }; resolution.version }; - let runtime = - vite_js_runtime::download_runtime(vite_js_runtime::JsRuntimeType::Node, &version).await?; + // Ensure Node.js is installed + let runtime = match vite_js_runtime::download_runtime( + vite_js_runtime::JsRuntimeType::Node, + &node_version, + ) + .await + { + Ok(runtime) => runtime, + Err(error) => { + let error = Error::RuntimeDownload(error); + return Err(error); + } + }; + let node_bin_dir = runtime.get_bin_prefix(); let npm_path = if cfg!(windows) { node_bin_dir.join("npm.cmd") } else { node_bin_dir.join("npm") }; diff --git a/crates/vite_pm_cli/src/cli.rs b/crates/vite_pm_cli/src/cli.rs index 75975de99d..64c25cd54f 100644 --- a/crates/vite_pm_cli/src/cli.rs +++ b/crates/vite_pm_cli/src/cli.rs @@ -7,6 +7,14 @@ use clap::Subcommand; use vite_install::commands::{add::SaveDependencyType, outdated::Format}; +fn parse_positive_usize(value: &str) -> Result { + match value.parse::() { + Ok(value) if value > 0 => Ok(value), + Ok(_) => Err("value must be at least 1".to_string()), + Err(error) => Err(error.to_string()), + } +} + /// All package-manager subcommands. /// /// Variants intentionally mirror the original definitions in @@ -110,6 +118,10 @@ pub enum PackageManagerCommand { #[arg(long, requires = "global")] node: Option, + /// Number of global package installs to run in parallel (only with -g) + #[arg(long, requires = "global", value_parser = parse_positive_usize)] + concurrency: Option, + /// Packages to add (if provided, acts as `vp add`) #[arg(required = false)] packages: Option>, @@ -173,6 +185,10 @@ pub enum PackageManagerCommand { #[arg(long, requires = "global")] node: Option, + /// Number of global package installs to run in parallel (only with -g) + #[arg(long, requires = "global", value_parser = parse_positive_usize)] + concurrency: Option, + /// Packages to add #[arg(required = true)] packages: Vec, @@ -237,6 +253,10 @@ pub enum PackageManagerCommand { #[arg(short = 'g', long)] global: bool, + /// Number of global package updates to run in parallel (only with -g) + #[arg(long, requires = "global", value_parser = parse_positive_usize)] + concurrency: Option, + /// Update recursively in all workspace packages #[arg(short = 'r', long)] recursive: bool, @@ -1122,3 +1142,44 @@ pub enum DistTagCommands { tag: String, }, } + +#[cfg(test)] +mod tests { + use clap::FromArgMatches; + + use super::*; + + fn parse_pm_command(args: &[&str]) -> Result { + let mut command = PackageManagerCommand::augment_subcommands(clap::Command::new("vp")); + let matches = command.try_get_matches_from_mut(args)?; + PackageManagerCommand::from_arg_matches(&matches) + } + + #[test] + fn global_install_accepts_concurrency() { + let command = + parse_pm_command(&["vp", "install", "-g", "--concurrency", "2", "typescript"]) + .expect("expected command to parse"); + + assert!(matches!( + command, + PackageManagerCommand::Install { global: true, concurrency: Some(2), .. } + )); + } + + #[test] + fn concurrency_requires_global() { + let error = parse_pm_command(&["vp", "install", "--concurrency", "2", "typescript"]) + .expect_err("expected --concurrency without --global to fail"); + + assert_eq!(error.kind(), clap::error::ErrorKind::MissingRequiredArgument); + } + + #[test] + fn concurrency_rejects_zero() { + let error = parse_pm_command(&["vp", "update", "-g", "--concurrency", "0"]) + .expect_err("expected zero concurrency to fail"); + + assert_eq!(error.kind(), clap::error::ErrorKind::ValueValidation); + } +} diff --git a/crates/vite_pm_cli/src/dispatch.rs b/crates/vite_pm_cli/src/dispatch.rs index a16851e3d1..035b3c1c2b 100644 --- a/crates/vite_pm_cli/src/dispatch.rs +++ b/crates/vite_pm_cli/src/dispatch.rs @@ -44,6 +44,7 @@ pub async fn dispatch( save_catalog, global, node: _, + concurrency: _, packages, pass_through_args, } => { @@ -109,6 +110,7 @@ pub async fn dispatch( workspace, global, node: _, + concurrency: _, packages, pass_through_args, } => { @@ -166,6 +168,7 @@ pub async fn dispatch( PackageManagerCommand::Update { latest, global: _, + concurrency: _, recursive, filter, workspace_root, diff --git a/packages/cli/snap-tests-global/cli-helper-message/snap.txt b/packages/cli/snap-tests-global/cli-helper-message/snap.txt index 08992b73cf..fbbed52b04 100644 --- a/packages/cli/snap-tests-global/cli-helper-message/snap.txt +++ b/packages/cli/snap-tests-global/cli-helper-message/snap.txt @@ -82,30 +82,31 @@ Arguments: [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager Options: - -P, --prod Do not install devDependencies - -D, --dev Only install devDependencies (install) / Save to devDependencies (add) - --no-optional Do not install optionalDependencies - --frozen-lockfile Fail if lockfile needs to be updated (CI mode) - --no-frozen-lockfile Allow lockfile updates (opposite of --frozen-lockfile) - --lockfile-only Only update lockfile, don't install - --prefer-offline Use cached packages when available - --offline Only use packages already in cache - -f, --force Force reinstall all dependencies - --ignore-scripts Do not run lifecycle scripts - --no-lockfile Don't read or generate lockfile - --fix-lockfile Fix broken lockfile entries (pnpm and yarn@2+ only) - --shamefully-hoist Create flat `node_modules` (pnpm only) - --resolution-only Re-run resolution for peer dependency analysis (pnpm only) - --silent Suppress output (silent mode) - --filter Filter packages in monorepo (can be used multiple times) - -w, --workspace-root Install in workspace root only - -E, --save-exact Save exact version (only when adding packages) - --save-peer Save to peerDependencies (only when adding packages) - -O, --save-optional Save to optionalDependencies (only when adding packages) - --save-catalog Save the new dependency to the default catalog (only when adding packages) - -g, --global Install globally (requires package names) - --node Node.js version to use for global installation (only with -g) - -h, --help Print help + -P, --prod Do not install devDependencies + -D, --dev Only install devDependencies (install) / Save to devDependencies (add) + --no-optional Do not install optionalDependencies + --frozen-lockfile Fail if lockfile needs to be updated (CI mode) + --no-frozen-lockfile Allow lockfile updates (opposite of --frozen-lockfile) + --lockfile-only Only update lockfile, don't install + --prefer-offline Use cached packages when available + --offline Only use packages already in cache + -f, --force Force reinstall all dependencies + --ignore-scripts Do not run lifecycle scripts + --no-lockfile Don't read or generate lockfile + --fix-lockfile Fix broken lockfile entries (pnpm and yarn@2+ only) + --shamefully-hoist Create flat `node_modules` (pnpm only) + --resolution-only Re-run resolution for peer dependency analysis (pnpm only) + --silent Suppress output (silent mode) + --filter Filter packages in monorepo (can be used multiple times) + -w, --workspace-root Install in workspace root only + -E, --save-exact Save exact version (only when adding packages) + --save-peer Save to peerDependencies (only when adding packages) + -O, --save-optional Save to optionalDependencies (only when adding packages) + --save-catalog Save the new dependency to the default catalog (only when adding packages) + -g, --global Install globally (requires package names) + --node Node.js version to use for global installation (only with -g) + --concurrency Number of global package installs to run in parallel (only with -g) + -h, --help Print help Documentation: https://viteplus.dev/guide/install @@ -133,6 +134,7 @@ Options: --workspace Only add if package exists in workspace (pnpm-specific) -g, --global Install globally --node Node.js version to use for global installation (only with -g) + --concurrency Number of global package installs to run in parallel (only with -g) -h, --help Print help Documentation: https://viteplus.dev/guide/install @@ -171,18 +173,19 @@ Arguments: [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager Options: - -L, --latest Update to latest version (ignore semver range) - -g, --global Update global packages - -r, --recursive Update recursively in all workspace packages - --filter Filter packages in monorepo (can be used multiple times) - -w, --workspace-root Include workspace root - -D, --dev Update only devDependencies - -P, --prod Update only dependencies (production) - -i, --interactive Interactive mode - --no-optional Don't update optionalDependencies - --no-save Update lockfile only, don't modify package.json - --workspace Only update if package exists in workspace (pnpm-specific) - -h, --help Print help + -L, --latest Update to latest version (ignore semver range) + -g, --global Update global packages + --concurrency Number of global package updates to run in parallel (only with -g) + -r, --recursive Update recursively in all workspace packages + --filter Filter packages in monorepo (can be used multiple times) + -w, --workspace-root Include workspace root + -D, --dev Update only devDependencies + -P, --prod Update only dependencies (production) + -i, --interactive Interactive mode + --no-optional Don't update optionalDependencies + --no-save Update lockfile only, don't modify package.json + --workspace Only update if package exists in workspace (pnpm-specific) + -h, --help Print help Documentation: https://viteplus.dev/guide/install diff --git a/packages/cli/snap-tests-global/command-add-bun/snap.txt b/packages/cli/snap-tests-global/command-add-bun/snap.txt index a6128c99c7..8334324811 100644 --- a/packages/cli/snap-tests-global/command-add-bun/snap.txt +++ b/packages/cli/snap-tests-global/command-add-bun/snap.txt @@ -21,6 +21,7 @@ Options: --workspace Only add if package exists in workspace (pnpm-specific) -g, --global Install globally --node Node.js version to use for global installation (only with -g) + --concurrency Number of global package installs to run in parallel (only with -g) -h, --help Print help Documentation: https://viteplus.dev/guide/install diff --git a/packages/cli/snap-tests-global/command-add-npm10/snap.txt b/packages/cli/snap-tests-global/command-add-npm10/snap.txt index 6907aae2b1..bad4863af6 100644 --- a/packages/cli/snap-tests-global/command-add-npm10/snap.txt +++ b/packages/cli/snap-tests-global/command-add-npm10/snap.txt @@ -21,6 +21,7 @@ Options: --workspace Only add if package exists in workspace (pnpm-specific) -g, --global Install globally --node Node.js version to use for global installation (only with -g) + --concurrency Number of global package installs to run in parallel (only with -g) -h, --help Print help Documentation: https://viteplus.dev/guide/install diff --git a/packages/cli/snap-tests-global/command-add-npm11/snap.txt b/packages/cli/snap-tests-global/command-add-npm11/snap.txt index 84fbddc833..d0fa7a5916 100644 --- a/packages/cli/snap-tests-global/command-add-npm11/snap.txt +++ b/packages/cli/snap-tests-global/command-add-npm11/snap.txt @@ -21,6 +21,7 @@ Options: --workspace Only add if package exists in workspace (pnpm-specific) -g, --global Install globally --node Node.js version to use for global installation (only with -g) + --concurrency Number of global package installs to run in parallel (only with -g) -h, --help Print help Documentation: https://viteplus.dev/guide/install diff --git a/packages/cli/snap-tests-global/command-add-pnpm10/snap.txt b/packages/cli/snap-tests-global/command-add-pnpm10/snap.txt index cfd95712d8..81de193887 100644 --- a/packages/cli/snap-tests-global/command-add-pnpm10/snap.txt +++ b/packages/cli/snap-tests-global/command-add-pnpm10/snap.txt @@ -21,6 +21,7 @@ Options: --workspace Only add if package exists in workspace (pnpm-specific) -g, --global Install globally --node Node.js version to use for global installation (only with -g) + --concurrency Number of global package installs to run in parallel (only with -g) -h, --help Print help Documentation: https://viteplus.dev/guide/install diff --git a/packages/cli/snap-tests-global/command-add-pnpm11/snap.txt b/packages/cli/snap-tests-global/command-add-pnpm11/snap.txt index 3bae632c8c..1090f05929 100644 --- a/packages/cli/snap-tests-global/command-add-pnpm11/snap.txt +++ b/packages/cli/snap-tests-global/command-add-pnpm11/snap.txt @@ -21,6 +21,7 @@ Options: --workspace Only add if package exists in workspace (pnpm-specific) -g, --global Install globally --node Node.js version to use for global installation (only with -g) + --concurrency Number of global package installs to run in parallel (only with -g) -h, --help Print help Documentation: https://viteplus.dev/guide/install diff --git a/packages/cli/snap-tests-global/command-add-pnpm9/snap.txt b/packages/cli/snap-tests-global/command-add-pnpm9/snap.txt index f7be5d45c5..cfe3360253 100644 --- a/packages/cli/snap-tests-global/command-add-pnpm9/snap.txt +++ b/packages/cli/snap-tests-global/command-add-pnpm9/snap.txt @@ -21,6 +21,7 @@ Options: --workspace Only add if package exists in workspace (pnpm-specific) -g, --global Install globally --node Node.js version to use for global installation (only with -g) + --concurrency Number of global package installs to run in parallel (only with -g) -h, --help Print help Documentation: https://viteplus.dev/guide/install diff --git a/packages/cli/snap-tests-global/command-add-yarn4/snap.txt b/packages/cli/snap-tests-global/command-add-yarn4/snap.txt index 5a97f645f7..0dfe345f11 100644 --- a/packages/cli/snap-tests-global/command-add-yarn4/snap.txt +++ b/packages/cli/snap-tests-global/command-add-yarn4/snap.txt @@ -21,6 +21,7 @@ Options: --workspace Only add if package exists in workspace (pnpm-specific) -g, --global Install globally --node Node.js version to use for global installation (only with -g) + --concurrency Number of global package installs to run in parallel (only with -g) -h, --help Print help Documentation: https://viteplus.dev/guide/install diff --git a/packages/cli/snap-tests-global/command-env-install-conflict/snap.txt b/packages/cli/snap-tests-global/command-env-install-conflict/snap.txt index d5ea029c75..dd89567f74 100644 --- a/packages/cli/snap-tests-global/command-env-install-conflict/snap.txt +++ b/packages/cli/snap-tests-global/command-env-install-conflict/snap.txt @@ -1,17 +1,17 @@ > vp install -g ./conflict-pkg # Install package with conflicting binary name (uses cwd version) -Installing ./conflict-pkg globally... +info: Installing 1 global package with Node.js warn: Package './conflict-pkg' provides 'node' binary, but it conflicts with a core shim. Skipping. -Installed ./conflict-pkg v -Binaries: conflict-cli, node +✓ Installed ./conflict-pkg + Bins: conflict-cli, node > vp remove -g conflict-pkg # Cleanup Uninstalled conflict-pkg > vp install -g --node 20 ./conflict-pkg # Install with specific Node.js version -Installing ./conflict-pkg globally... +info: Installing 1 global package with Node.js warn: Package './conflict-pkg' provides 'node' binary, but it conflicts with a core shim. Skipping. -Installed ./conflict-pkg v -Binaries: conflict-cli, node +✓ Installed ./conflict-pkg + Bins: conflict-cli, node > vp remove -g conflict-pkg # Cleanup Uninstalled conflict-pkg diff --git a/packages/cli/snap-tests-global/command-env-install-fail/snap.txt b/packages/cli/snap-tests-global/command-env-install-fail/snap.txt index 698e831c53..226d6a6775 100644 --- a/packages/cli/snap-tests-global/command-env-install-fail/snap.txt +++ b/packages/cli/snap-tests-global/command-env-install-fail/snap.txt @@ -1,5 +1,5 @@ [1]> vp install -g voidzero-nonexistent-pkg-xyz-12345 # Install non-existent package -Installing voidzero-nonexistent-pkg-xyz-12345 globally... +info: Installing 1 global package with Node.js npm error code E404 npm error 404 Not Found - GET https://registry./voidzero-nonexistent-pkg-xyz-12345 - Not found npm error 404 @@ -8,4 +8,4 @@ npm error 404 npm error 404 Note that you can also install from a npm error 404 tarball, folder, http url, or git url. npm error A complete log of this run can be found in: /.npm/_logs/-debug.log -Failed to install voidzero-nonexistent-pkg-xyz-12345: Configuration error: npm install failed with exit code: Some(1) +error: Failed to install voidzero-nonexistent-pkg-xyz-12345: Configuration error: npm install failed with exit code: Some(1) diff --git a/packages/cli/snap-tests-global/command-env-install-node-version/snap.txt b/packages/cli/snap-tests-global/command-env-install-node-version/snap.txt index 01f2739ce5..40f1c040a8 100644 --- a/packages/cli/snap-tests-global/command-env-install-node-version/snap.txt +++ b/packages/cli/snap-tests-global/command-env-install-node-version/snap.txt @@ -1,7 +1,7 @@ > vp install -g --node 22 ./command-env-install-node-version-pkg # Install with Node.js 22 -Installing ./command-env-install-node-version-pkg globally... -Installed ./command-env-install-node-version-pkg v -Binaries: command-env-install-node-version-pkg-cli +info: Installing 1 global package with Node.js +✓ Installed ./command-env-install-node-version-pkg + Bins: command-env-install-node-version-pkg-cli > cat $VP_HOME/bins/command-env-install-node-version-pkg-cli.json | node -e "const d=JSON.parse(require('fs').readFileSync(0,'utf8')); console.log('Node major:', d.nodeVersion.split('.')[0])" # Verify Node 22 Node major: 22 @@ -10,9 +10,9 @@ Node major: 22 Uninstalled command-env-install-node-version-pkg > vp install -g --node 20 ./command-env-install-node-version-pkg # Install with Node.js 20 -Installing ./command-env-install-node-version-pkg globally... -Installed ./command-env-install-node-version-pkg v -Binaries: command-env-install-node-version-pkg-cli +info: Installing 1 global package with Node.js +✓ Installed ./command-env-install-node-version-pkg + Bins: command-env-install-node-version-pkg-cli > cat $VP_HOME/bins/command-env-install-node-version-pkg-cli.json | node -e "const d=JSON.parse(require('fs').readFileSync(0,'utf8')); console.log('Node major:', d.nodeVersion.split('.')[0])" # Verify Node 20 Node major: 20 diff --git a/packages/cli/snap-tests-global/command-env-install-parallel/parallel-pkg-a/cli.js b/packages/cli/snap-tests-global/command-env-install-parallel/parallel-pkg-a/cli.js new file mode 100644 index 0000000000..16b15b0474 --- /dev/null +++ b/packages/cli/snap-tests-global/command-env-install-parallel/parallel-pkg-a/cli.js @@ -0,0 +1,2 @@ +#!/usr/bin/env node +console.log('parallel-a ok'); diff --git a/packages/cli/snap-tests-global/command-env-install-parallel/parallel-pkg-a/package.json b/packages/cli/snap-tests-global/command-env-install-parallel/parallel-pkg-a/package.json new file mode 100644 index 0000000000..ecfd46a897 --- /dev/null +++ b/packages/cli/snap-tests-global/command-env-install-parallel/parallel-pkg-a/package.json @@ -0,0 +1,7 @@ +{ + "name": "parallel-pkg-a", + "version": "1.0.0", + "bin": { + "parallel-a": "./cli.js" + } +} diff --git a/packages/cli/snap-tests-global/command-env-install-parallel/parallel-pkg-b/cli.js b/packages/cli/snap-tests-global/command-env-install-parallel/parallel-pkg-b/cli.js new file mode 100644 index 0000000000..b1757459b8 --- /dev/null +++ b/packages/cli/snap-tests-global/command-env-install-parallel/parallel-pkg-b/cli.js @@ -0,0 +1,2 @@ +#!/usr/bin/env node +console.log('parallel-b ok'); diff --git a/packages/cli/snap-tests-global/command-env-install-parallel/parallel-pkg-b/package.json b/packages/cli/snap-tests-global/command-env-install-parallel/parallel-pkg-b/package.json new file mode 100644 index 0000000000..d8c16eb755 --- /dev/null +++ b/packages/cli/snap-tests-global/command-env-install-parallel/parallel-pkg-b/package.json @@ -0,0 +1,7 @@ +{ + "name": "parallel-pkg-b", + "version": "2.0.0", + "bin": { + "parallel-b": "./cli.js" + } +} diff --git a/packages/cli/snap-tests-global/command-env-install-parallel/snap.txt b/packages/cli/snap-tests-global/command-env-install-parallel/snap.txt new file mode 100644 index 0000000000..721dd1d80b --- /dev/null +++ b/packages/cli/snap-tests-global/command-env-install-parallel/snap.txt @@ -0,0 +1,15 @@ +> vp install -g --concurrency 1 ./parallel-pkg-a ./parallel-pkg-b # Install multiple global packages +info: Installing 2 global packages with Node.js +✓ Installed ./parallel-pkg-a + Bins: parallel-a + +✓ Installed ./parallel-pkg-b + Bins: parallel-b + +> parallel-a && parallel-b # Both binaries should be callable +parallel-a ok +parallel-b ok + +> vp remove -g parallel-pkg-a parallel-pkg-b # Cleanup +Uninstalled parallel-pkg-a +Uninstalled parallel-pkg-b diff --git a/packages/cli/snap-tests-global/command-env-install-parallel/steps.json b/packages/cli/snap-tests-global/command-env-install-parallel/steps.json new file mode 100644 index 0000000000..6e8afc2e24 --- /dev/null +++ b/packages/cli/snap-tests-global/command-env-install-parallel/steps.json @@ -0,0 +1,8 @@ +{ + "env": {}, + "commands": [ + "vp install -g --concurrency 1 ./parallel-pkg-a ./parallel-pkg-b # Install multiple global packages", + "parallel-a && parallel-b # Both binaries should be callable", + "vp remove -g parallel-pkg-a parallel-pkg-b # Cleanup" + ] +} diff --git a/packages/cli/snap-tests-global/command-env-install-version-alias/snap.txt b/packages/cli/snap-tests-global/command-env-install-version-alias/snap.txt index f9f6912575..58510ae5c6 100644 --- a/packages/cli/snap-tests-global/command-env-install-version-alias/snap.txt +++ b/packages/cli/snap-tests-global/command-env-install-version-alias/snap.txt @@ -1,7 +1,7 @@ > vp install -g --node lts ./command-env-install-version-alias-pkg # Install with LTS alias -Installing ./command-env-install-version-alias-pkg globally... -Installed ./command-env-install-version-alias-pkg v -Binaries: command-env-install-version-alias-pkg-cli +info: Installing 1 global package with Node.js +✓ Installed ./command-env-install-version-alias-pkg + Bins: command-env-install-version-alias-pkg-cli > cat $VP_HOME/bins/command-env-install-version-alias-pkg-cli.json | node -e "const d=JSON.parse(require('fs').readFileSync(0,'utf8')); const v=parseInt(d.nodeVersion.split('.')[0]); console.log('LTS major >= 20:', v >= 20)" # Verify LTS version LTS major >= 20: true @@ -10,9 +10,9 @@ LTS major >= 20: true Uninstalled command-env-install-version-alias-pkg > vp install -g --node latest ./command-env-install-version-alias-pkg # Install with latest alias -Installing ./command-env-install-version-alias-pkg globally... -Installed ./command-env-install-version-alias-pkg v -Binaries: command-env-install-version-alias-pkg-cli +info: Installing 1 global package with Node.js +✓ Installed ./command-env-install-version-alias-pkg + Bins: command-env-install-version-alias-pkg-cli > cat $VP_HOME/bins/command-env-install-version-alias-pkg-cli.json | node -e "const d=JSON.parse(require('fs').readFileSync(0,'utf8')); const v=parseInt(d.nodeVersion.split('.')[0]); console.log('Latest major >= 20:', v >= 20)" # Verify latest version Latest major >= 20: true diff --git a/packages/cli/snap-tests-global/command-env-which/snap.txt b/packages/cli/snap-tests-global/command-env-which/snap.txt index ef47f76c1b..a953d4da00 100644 --- a/packages/cli/snap-tests-global/command-env-which/snap.txt +++ b/packages/cli/snap-tests-global/command-env-which/snap.txt @@ -17,9 +17,9 @@ v20.18.0 Source: /.node-version > vp install -g cowsay@1.6.0 # Install a global package via vp -Installing cowsay@ globally... -Installed cowsay v -Binaries: cowsay, cowthink +info: Installing 1 global package with Node.js +✓ Installed cowsay + Bins: cowsay, cowthink > vp env which cowsay # Global package - shows binary path with metadata /packages/cowsay/lib/node_modules/cowsay/./cli.js diff --git a/packages/cli/snap-tests-global/command-update-bun/snap.txt b/packages/cli/snap-tests-global/command-update-bun/snap.txt index 328e17b1ea..494778e3a1 100644 --- a/packages/cli/snap-tests-global/command-update-bun/snap.txt +++ b/packages/cli/snap-tests-global/command-update-bun/snap.txt @@ -8,18 +8,19 @@ Arguments: [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager Options: - -L, --latest Update to latest version (ignore semver range) - -g, --global Update global packages - -r, --recursive Update recursively in all workspace packages - --filter Filter packages in monorepo (can be used multiple times) - -w, --workspace-root Include workspace root - -D, --dev Update only devDependencies - -P, --prod Update only dependencies (production) - -i, --interactive Interactive mode - --no-optional Don't update optionalDependencies - --no-save Update lockfile only, don't modify package.json - --workspace Only update if package exists in workspace (pnpm-specific) - -h, --help Print help + -L, --latest Update to latest version (ignore semver range) + -g, --global Update global packages + --concurrency Number of global package updates to run in parallel (only with -g) + -r, --recursive Update recursively in all workspace packages + --filter Filter packages in monorepo (can be used multiple times) + -w, --workspace-root Include workspace root + -D, --dev Update only devDependencies + -P, --prod Update only dependencies (production) + -i, --interactive Interactive mode + --no-optional Don't update optionalDependencies + --no-save Update lockfile only, don't modify package.json + --workspace Only update if package exists in workspace (pnpm-specific) + -h, --help Print help Documentation: https://viteplus.dev/guide/install diff --git a/packages/cli/snap-tests-global/command-update-pnpm10/snap.txt b/packages/cli/snap-tests-global/command-update-pnpm10/snap.txt index a86629c83b..8f6feaeed1 100644 --- a/packages/cli/snap-tests-global/command-update-pnpm10/snap.txt +++ b/packages/cli/snap-tests-global/command-update-pnpm10/snap.txt @@ -8,18 +8,19 @@ Arguments: [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager Options: - -L, --latest Update to latest version (ignore semver range) - -g, --global Update global packages - -r, --recursive Update recursively in all workspace packages - --filter Filter packages in monorepo (can be used multiple times) - -w, --workspace-root Include workspace root - -D, --dev Update only devDependencies - -P, --prod Update only dependencies (production) - -i, --interactive Interactive mode - --no-optional Don't update optionalDependencies - --no-save Update lockfile only, don't modify package.json - --workspace Only update if package exists in workspace (pnpm-specific) - -h, --help Print help + -L, --latest Update to latest version (ignore semver range) + -g, --global Update global packages + --concurrency Number of global package updates to run in parallel (only with -g) + -r, --recursive Update recursively in all workspace packages + --filter Filter packages in monorepo (can be used multiple times) + -w, --workspace-root Include workspace root + -D, --dev Update only devDependencies + -P, --prod Update only dependencies (production) + -i, --interactive Interactive mode + --no-optional Don't update optionalDependencies + --no-save Update lockfile only, don't modify package.json + --workspace Only update if package exists in workspace (pnpm-specific) + -h, --help Print help Documentation: https://viteplus.dev/guide/install diff --git a/packages/cli/snap-tests-global/command-update-pnpm11/snap.txt b/packages/cli/snap-tests-global/command-update-pnpm11/snap.txt index c059feab3b..bb6d6be63f 100644 --- a/packages/cli/snap-tests-global/command-update-pnpm11/snap.txt +++ b/packages/cli/snap-tests-global/command-update-pnpm11/snap.txt @@ -8,18 +8,19 @@ Arguments: [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager Options: - -L, --latest Update to latest version (ignore semver range) - -g, --global Update global packages - -r, --recursive Update recursively in all workspace packages - --filter Filter packages in monorepo (can be used multiple times) - -w, --workspace-root Include workspace root - -D, --dev Update only devDependencies - -P, --prod Update only dependencies (production) - -i, --interactive Interactive mode - --no-optional Don't update optionalDependencies - --no-save Update lockfile only, don't modify package.json - --workspace Only update if package exists in workspace (pnpm-specific) - -h, --help Print help + -L, --latest Update to latest version (ignore semver range) + -g, --global Update global packages + --concurrency Number of global package updates to run in parallel (only with -g) + -r, --recursive Update recursively in all workspace packages + --filter Filter packages in monorepo (can be used multiple times) + -w, --workspace-root Include workspace root + -D, --dev Update only devDependencies + -P, --prod Update only dependencies (production) + -i, --interactive Interactive mode + --no-optional Don't update optionalDependencies + --no-save Update lockfile only, don't modify package.json + --workspace Only update if package exists in workspace (pnpm-specific) + -h, --help Print help Documentation: https://viteplus.dev/guide/install diff --git a/packages/cli/snap-tests-global/env-install-binary-conflict/snap.txt b/packages/cli/snap-tests-global/env-install-binary-conflict/snap.txt index 90e23983e8..185390d749 100644 --- a/packages/cli/snap-tests-global/env-install-binary-conflict/snap.txt +++ b/packages/cli/snap-tests-global/env-install-binary-conflict/snap.txt @@ -1,7 +1,7 @@ > vp install -g ./env-binary-conflict-pkg-a # Install pkg-a which provides env-binary-conflict-cli binary -Installing ./env-binary-conflict-pkg-a globally... -Installed ./env-binary-conflict-pkg-a v -Binaries: env-binary-conflict-cli +info: Installing 1 global package with Node.js +✓ Installed ./env-binary-conflict-pkg-a + Bins: env-binary-conflict-cli > cat $VP_HOME/bins/env-binary-conflict-cli.json # Bin config should point to pkg-a { @@ -12,8 +12,8 @@ Binaries: env-binary-conflict-cli "source": "vp" } [1]> vp install -g ./env-binary-conflict-pkg-b # Try to install pkg-b without force - should fail -Installing ./env-binary-conflict-pkg-b globally... -Failed to install ./env-binary-conflict-pkg-b: Executable 'env-binary-conflict-cli' is already installed by ./env-binary-conflict-pkg-a +info: Installing 1 global package with Node.js +error: Failed to install ./env-binary-conflict-pkg-b: Executable 'env-binary-conflict-cli' is already installed by ./env-binary-conflict-pkg-a Please remove ./env-binary-conflict-pkg-a before installing ./env-binary-conflict-pkg-b, or use --force to auto-replace @@ -26,11 +26,11 @@ Please remove ./env-binary-conflict-pkg-a before installing ./env-binary-conflic "source": "vp" } > vp install -g --force ./env-binary-conflict-pkg-b # Force install pkg-b - should auto-uninstall pkg-a -Installing ./env-binary-conflict-pkg-b globally... +info: Installing 1 global package with Node.js Uninstalling ./env-binary-conflict-pkg-a (conflicts with ./env-binary-conflict-pkg-b)... Uninstalled ./env-binary-conflict-pkg-a -Installed ./env-binary-conflict-pkg-b v -Binaries: env-binary-conflict-cli +✓ Installed ./env-binary-conflict-pkg-b + Bins: env-binary-conflict-cli > cat $VP_HOME/bins/env-binary-conflict-cli.json # Bin config should now point to pkg-b { diff --git a/packages/cli/snap-tests-global/npm-global-install-already-linked/snap.txt b/packages/cli/snap-tests-global/npm-global-install-already-linked/snap.txt index 9c706343b5..9650a8d433 100644 --- a/packages/cli/snap-tests-global/npm-global-install-already-linked/snap.txt +++ b/packages/cli/snap-tests-global/npm-global-install-already-linked/snap.txt @@ -2,9 +2,9 @@ /../npm-global-lib-for-snap-tests > vp install -g ./npm-global-linked-pkg # First install via vp (creates managed shim) -Installing ./npm-global-linked-pkg globally... -Installed ./npm-global-linked-pkg v -Binaries: npm-global-linked-cli +info: Installing 1 global package with Node.js +✓ Installed ./npm-global-linked-pkg + Bins: npm-global-linked-cli > npm-global-linked-cli # Should be callable via the link npm-global-linked-cli works diff --git a/packages/cli/snap-tests-global/npm-global-uninstall-vp-managed/snap.txt b/packages/cli/snap-tests-global/npm-global-uninstall-vp-managed/snap.txt index 84fb5dbdc1..6d08f8a9a5 100644 --- a/packages/cli/snap-tests-global/npm-global-uninstall-vp-managed/snap.txt +++ b/packages/cli/snap-tests-global/npm-global-uninstall-vp-managed/snap.txt @@ -1,7 +1,7 @@ > vp install -g ./npm-global-vp-managed-pkg # Install via vp (creates managed shim) -Installing ./npm-global-vp-managed-pkg globally... -Installed ./npm-global-vp-managed-pkg v -Binaries: npm-global-vp-managed-cli +info: Installing 1 global package with Node.js +✓ Installed ./npm-global-vp-managed-pkg + Bins: npm-global-vp-managed-cli > npm install -g ./npm-global-vp-managed-pkg # npm install (should warn about conflict) diff --git a/packages/cli/snap-tests-global/shim-recursive-package-binary/snap.txt b/packages/cli/snap-tests-global/shim-recursive-package-binary/snap.txt index c9241fcbe8..a8a56719e0 100644 --- a/packages/cli/snap-tests-global/shim-recursive-package-binary/snap.txt +++ b/packages/cli/snap-tests-global/shim-recursive-package-binary/snap.txt @@ -1,7 +1,7 @@ > vp install -g ./recursive-cli-pkg # Install test package -Installing ./recursive-cli-pkg globally... -Installed ./recursive-cli-pkg v -Binaries: recursive-cli +info: Installing 1 global package with Node.js +✓ Installed ./recursive-cli-pkg + Bins: recursive-cli > recursive-cli # Outer call triggers recursive inner call through shim outer call diff --git a/packages/cli/snap-tests/command-add-pnpm10/snap.txt b/packages/cli/snap-tests/command-add-pnpm10/snap.txt index 3e50976eb5..e47c004a3d 100644 --- a/packages/cli/snap-tests/command-add-pnpm10/snap.txt +++ b/packages/cli/snap-tests/command-add-pnpm10/snap.txt @@ -34,6 +34,8 @@ Options: Install globally --node Node.js version to use for global installation (only with -g) + --concurrency + Number of global package installs to run in parallel (only with -g) -h, --help Print help diff --git a/packages/cli/snap-tests/command-update-pnpm10/snap.txt b/packages/cli/snap-tests/command-update-pnpm10/snap.txt index 65d899346f..65fdc8ed27 100644 --- a/packages/cli/snap-tests/command-update-pnpm10/snap.txt +++ b/packages/cli/snap-tests/command-update-pnpm10/snap.txt @@ -8,18 +8,19 @@ Arguments: [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager Options: - -L, --latest Update to latest version (ignore semver range) - -g, --global Update global packages - -r, --recursive Update recursively in all workspace packages - --filter Filter packages in monorepo (can be used multiple times) - -w, --workspace-root Include workspace root - -D, --dev Update only devDependencies - -P, --prod Update only dependencies (production) - -i, --interactive Interactive mode - --no-optional Don't update optionalDependencies - --no-save Update lockfile only, don't modify package.json - --workspace Only update if package exists in workspace (pnpm-specific) - -h, --help Print help + -L, --latest Update to latest version (ignore semver range) + -g, --global Update global packages + --concurrency Number of global package updates to run in parallel (only with -g) + -r, --recursive Update recursively in all workspace packages + --filter Filter packages in monorepo (can be used multiple times) + -w, --workspace-root Include workspace root + -D, --dev Update only devDependencies + -P, --prod Update only dependencies (production) + -i, --interactive Interactive mode + --no-optional Don't update optionalDependencies + --no-save Update lockfile only, don't modify package.json + --workspace Only update if package exists in workspace (pnpm-specific) + -h, --help Print help > vp update testnpm2 && cat package.json # should update package within semver range Packages: + diff --git a/packages/cli/snap-tests/npm-install-with-options/snap.txt b/packages/cli/snap-tests/npm-install-with-options/snap.txt index c16d851f41..bbec7e01e7 100644 --- a/packages/cli/snap-tests/npm-install-with-options/snap.txt +++ b/packages/cli/snap-tests/npm-install-with-options/snap.txt @@ -8,30 +8,31 @@ Arguments: [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager Options: - -P, --prod Do not install devDependencies - -D, --dev Only install devDependencies (install) / Save to devDependencies (add) - --no-optional Do not install optionalDependencies - --frozen-lockfile Fail if lockfile needs to be updated (CI mode) - --no-frozen-lockfile Allow lockfile updates (opposite of --frozen-lockfile) - --lockfile-only Only update lockfile, don't install - --prefer-offline Use cached packages when available - --offline Only use packages already in cache - -f, --force Force reinstall all dependencies - --ignore-scripts Do not run lifecycle scripts - --no-lockfile Don't read or generate lockfile - --fix-lockfile Fix broken lockfile entries (pnpm and yarn@2+ only) - --shamefully-hoist Create flat `node_modules` (pnpm only) - --resolution-only Re-run resolution for peer dependency analysis (pnpm only) - --silent Suppress output (silent mode) - --filter Filter packages in monorepo (can be used multiple times) - -w, --workspace-root Install in workspace root only - -E, --save-exact Save exact version (only when adding packages) - --save-peer Save to peerDependencies (only when adding packages) - -O, --save-optional Save to optionalDependencies (only when adding packages) - --save-catalog Save the new dependency to the default catalog (only when adding packages) - -g, --global Install globally (requires package names) - --node Node.js version to use for global installation (only with -g) - -h, --help Print help + -P, --prod Do not install devDependencies + -D, --dev Only install devDependencies (install) / Save to devDependencies (add) + --no-optional Do not install optionalDependencies + --frozen-lockfile Fail if lockfile needs to be updated (CI mode) + --no-frozen-lockfile Allow lockfile updates (opposite of --frozen-lockfile) + --lockfile-only Only update lockfile, don't install + --prefer-offline Use cached packages when available + --offline Only use packages already in cache + -f, --force Force reinstall all dependencies + --ignore-scripts Do not run lifecycle scripts + --no-lockfile Don't read or generate lockfile + --fix-lockfile Fix broken lockfile entries (pnpm and yarn@2+ only) + --shamefully-hoist Create flat `node_modules` (pnpm only) + --resolution-only Re-run resolution for peer dependency analysis (pnpm only) + --silent Suppress output (silent mode) + --filter Filter packages in monorepo (can be used multiple times) + -w, --workspace-root Install in workspace root only + -E, --save-exact Save exact version (only when adding packages) + --save-peer Save to peerDependencies (only when adding packages) + -O, --save-optional Save to optionalDependencies (only when adding packages) + --save-catalog Save the new dependency to the default catalog (only when adding packages) + -g, --global Install globally (requires package names) + --node Node.js version to use for global installation (only with -g) + --concurrency Number of global package installs to run in parallel (only with -g) + -h, --help Print help > vp run install # https://docs.npmjs.com/cli/v10/commands/npm-install $ vp install --prod --silent ⊘ cache disabled diff --git a/packages/cli/snap-tests/yarn-install-with-options/snap.txt b/packages/cli/snap-tests/yarn-install-with-options/snap.txt index 8282a8ac26..8e9649340a 100644 --- a/packages/cli/snap-tests/yarn-install-with-options/snap.txt +++ b/packages/cli/snap-tests/yarn-install-with-options/snap.txt @@ -8,30 +8,31 @@ Arguments: [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager Options: - -P, --prod Do not install devDependencies - -D, --dev Only install devDependencies (install) / Save to devDependencies (add) - --no-optional Do not install optionalDependencies - --frozen-lockfile Fail if lockfile needs to be updated (CI mode) - --no-frozen-lockfile Allow lockfile updates (opposite of --frozen-lockfile) - --lockfile-only Only update lockfile, don't install - --prefer-offline Use cached packages when available - --offline Only use packages already in cache - -f, --force Force reinstall all dependencies - --ignore-scripts Do not run lifecycle scripts - --no-lockfile Don't read or generate lockfile - --fix-lockfile Fix broken lockfile entries (pnpm and yarn@2+ only) - --shamefully-hoist Create flat `node_modules` (pnpm only) - --resolution-only Re-run resolution for peer dependency analysis (pnpm only) - --silent Suppress output (silent mode) - --filter Filter packages in monorepo (can be used multiple times) - -w, --workspace-root Install in workspace root only - -E, --save-exact Save exact version (only when adding packages) - --save-peer Save to peerDependencies (only when adding packages) - -O, --save-optional Save to optionalDependencies (only when adding packages) - --save-catalog Save the new dependency to the default catalog (only when adding packages) - -g, --global Install globally (requires package names) - --node Node.js version to use for global installation (only with -g) - -h, --help Print help + -P, --prod Do not install devDependencies + -D, --dev Only install devDependencies (install) / Save to devDependencies (add) + --no-optional Do not install optionalDependencies + --frozen-lockfile Fail if lockfile needs to be updated (CI mode) + --no-frozen-lockfile Allow lockfile updates (opposite of --frozen-lockfile) + --lockfile-only Only update lockfile, don't install + --prefer-offline Use cached packages when available + --offline Only use packages already in cache + -f, --force Force reinstall all dependencies + --ignore-scripts Do not run lifecycle scripts + --no-lockfile Don't read or generate lockfile + --fix-lockfile Fix broken lockfile entries (pnpm and yarn@2+ only) + --shamefully-hoist Create flat `node_modules` (pnpm only) + --resolution-only Re-run resolution for peer dependency analysis (pnpm only) + --silent Suppress output (silent mode) + --filter Filter packages in monorepo (can be used multiple times) + -w, --workspace-root Install in workspace root only + -E, --save-exact Save exact version (only when adding packages) + --save-peer Save to peerDependencies (only when adding packages) + -O, --save-optional Save to optionalDependencies (only when adding packages) + --save-catalog Save the new dependency to the default catalog (only when adding packages) + -g, --global Install globally (requires package names) + --node Node.js version to use for global installation (only with -g) + --concurrency Number of global package installs to run in parallel (only with -g) + -h, --help Print help > vp run install $ vp install --prod ⊘ cache disabled