From 78f6084910760b4b32ba8b0af957cdc3d8e4f56a Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Fri, 22 May 2026 06:16:54 +0800 Subject: [PATCH 01/18] feat --- crates/vite_global_cli/src/cli.rs | 169 +++++++++++--- .../src/commands/env/global_install.rs | 102 --------- .../vite_global_cli/src/commands/env/mod.rs | 2 + .../src/commands/env/outdated.rs | 214 ++++++++++++++++++ .../src/commands/env/registry.rs | 145 ++++++++++++ 5 files changed, 499 insertions(+), 133 deletions(-) create mode 100644 crates/vite_global_cli/src/commands/env/outdated.rs create mode 100644 crates/vite_global_cli/src/commands/env/registry.rs diff --git a/crates/vite_global_cli/src/cli.rs b/crates/vite_global_cli/src/cli.rs index 509bab7a40..8dd6708c9b 100644 --- a/crates/vite_global_cli/src/cli.rs +++ b/crates/vite_global_cli/src/cli.rs @@ -3,7 +3,7 @@ //! This module defines the CLI structure using clap and routes commands //! to their appropriate handlers. -use std::{ffi::OsStr, process::ExitStatus}; +use std::{collections::HashMap, ffi::OsStr, process::ExitStatus}; use clap::{CommandFactory, FromArgMatches, Parser, Subcommand}; use clap_complete::ArgValueCompleter; @@ -562,6 +562,41 @@ async fn run_package_manager_command( managed_update(packages, concurrency).await } + PackageManagerCommand::Outdated { + global: true, + ref packages, + long, + format, + recursive, + ref filter, + workspace_root, + prod, + dev, + no_optional, + compatible, + ref sort_by, + ref pass_through_args, + } => { + warn_unsupported_global_outdated_flags( + recursive, + filter, + workspace_root, + prod, + dev, + no_optional, + compatible, + sort_by.as_deref(), + pass_through_args.as_deref(), + ); + crate::commands::env::outdated::execute( + packages, + long, + format, + DEFAULT_GLOBAL_INSTALL_CONCURRENCY, + ) + .await + } + // `pm list -g` lists vite-plus-managed globals, not the underlying PM's. PackageManagerCommand::Pm(vite_pm_cli::cli::PmCommands::List { global: true, @@ -612,6 +647,43 @@ async fn managed_uninstall(packages: &[String], dry_run: bool) -> Result>, + workspace_root: bool, + prod: bool, + dev: bool, + no_optional: bool, + compatible: bool, + sort_by: Option<&str>, + pass_through_args: Option<&[String]>, +) { + if recursive { + output::warn("--recursive is ignored with managed global packages"); + } + if filters.as_ref().is_some_and(|filters| !filters.is_empty()) { + output::warn("--filter is ignored with managed global packages"); + } + if workspace_root { + output::warn("--workspace-root is ignored with managed global packages"); + } + if prod || dev { + output::warn("--prod/--dev are ignored with managed global packages"); + } + if no_optional { + output::warn("--no-optional is ignored with managed global packages"); + } + if compatible { + output::warn("--compatible is ignored with managed global packages"); + } + if sort_by.is_some() { + output::warn("--sort-by is ignored with managed global packages"); + } + if pass_through_args.is_some_and(|args| !args.is_empty()) { + output::warn("pass-through arguments are ignored with managed global packages"); + } +} + fn is_global_package_up_to_date(installed_version: &str, registry_version: &str) -> bool { installed_version.trim() == registry_version.trim() } @@ -620,6 +692,7 @@ async fn managed_update( packages: &[String], concurrency: Option, ) -> Result { + let concurrency = concurrency.unwrap_or(DEFAULT_GLOBAL_INSTALL_CONCURRENCY); let all_packages = if packages.is_empty() { let all = PackageMetadata::list_all().await?; if all.is_empty() { @@ -635,10 +708,13 @@ async fn managed_update( let mut skipped = 0usize; if let Some(all) = all_packages { + let specs = all.iter().map(|metadata| metadata.name.clone()).collect::>(); + let latest_versions = latest_versions_by_spec(&specs, concurrency).await?; + for metadata in all { - match global_install::latest_package_version(&metadata.name).await { - Ok(latest_version) - if is_global_package_up_to_date(&metadata.version, &latest_version) => + match latest_versions.get(&metadata.name) { + Some(Ok(latest_version)) + if is_global_package_up_to_date(&metadata.version, latest_version) => { vite_shared::output::raw(&format!( "{} is already up to date (v{}).", @@ -646,17 +722,27 @@ async fn managed_update( )); skipped += 1; } - Ok(_) => to_update.push(metadata.name.clone()), - Err(e) => { + Some(Ok(_)) => to_update.push(metadata.name.clone()), + Some(Err(e)) => { vite_shared::output::raw_stderr(&format!( "Could not check latest version for {}: {e}; updating anyway.", metadata.name )); to_update.push(metadata.name.clone()); } + None => { + vite_shared::output::raw_stderr(&format!( + "Could not check latest version for {}; updating anyway.", + metadata.name + )); + to_update.push(metadata.name.clone()); + } } } } else { + let mut specs = Vec::new(); + let mut installed_packages = Vec::new(); + for package in packages { if global_install::is_local_package_spec(package) { to_update.push(package.clone()); @@ -665,23 +751,37 @@ async fn managed_update( let (package_name, _) = global_install::parse_package_spec(package); if let Some(metadata) = PackageMetadata::load(&package_name).await? { - match global_install::latest_package_version(package).await { - Ok(latest_version) - if is_global_package_up_to_date(&metadata.version, &latest_version) => - { - vite_shared::output::raw(&format!( - "{} is already up to date (v{}).", - package_name, metadata.version - )); - skipped += 1; - continue; - } - Ok(_) => {} - Err(e) => { - vite_shared::output::raw_stderr(&format!( - "Could not check latest version for {package}: {e}; updating anyway." - )); - } + specs.push(package.clone()); + installed_packages.push((package.clone(), package_name, metadata.version)); + } else { + to_update.push(package.clone()); + } + } + + let latest_versions = latest_versions_by_spec(&specs, concurrency).await?; + + for (package, package_name, installed_version) in installed_packages { + match latest_versions.get(&package) { + Some(Ok(latest_version)) + if is_global_package_up_to_date(&installed_version, latest_version) => + { + vite_shared::output::raw(&format!( + "{} is already up to date (v{}).", + package_name, installed_version + )); + skipped += 1; + continue; + } + Some(Ok(_)) => {} + Some(Err(e)) => { + vite_shared::output::raw_stderr(&format!( + "Could not check latest version for {package}: {e}; updating anyway." + )); + } + None => { + vite_shared::output::raw_stderr(&format!( + "Could not check latest version for {package}; updating anyway." + )); } } to_update.push(package.clone()); @@ -696,14 +796,8 @@ async fn managed_update( } // Call reinstall logic - if let Err((package_name, error)) = global_install::install( - &to_update, - None, - false, - concurrency.unwrap_or(DEFAULT_GLOBAL_INSTALL_CONCURRENCY), - true, - ) - .await + if let Err((package_name, error)) = + global_install::install(&to_update, None, false, concurrency, true).await { output::error(&format!( "Failed to update {}: {error}", @@ -714,6 +808,19 @@ async fn managed_update( Ok(ExitStatus::default()) } +async fn latest_versions_by_spec( + specs: &[String], + concurrency: usize, +) -> Result>, Error> { + let versions = + crate::commands::env::registry::latest_package_versions(specs, concurrency).await?; + let mut latest_versions = HashMap::with_capacity(versions.len()); + for version in versions { + latest_versions.insert(version.package_spec, version.version); + } + Ok(latest_versions) +} + /// Run the CLI command. pub async fn run_command(cwd: AbsolutePathBuf, args: Args) -> Result { run_command_with_options(cwd, args, RenderOptions::default()).await diff --git a/crates/vite_global_cli/src/commands/env/global_install.rs b/crates/vite_global_cli/src/commands/env/global_install.rs index b1d6c38b83..4118823163 100644 --- a/crates/vite_global_cli/src/commands/env/global_install.rs +++ b/crates/vite_global_cli/src/commands/env/global_install.rs @@ -443,84 +443,6 @@ pub async fn uninstall(package_name: &str, dry_run: bool) -> Result<(), Error> { Ok(()) } -/// Resolve the version currently published for a package spec. -/// -/// `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) -> 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 - }; - - // 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") }; - - let output = Command::new(npm_path.as_path()) - .args(["view", package_spec, "version", "--json"]) - .env("PATH", format_path_prepended(node_bin_dir.as_path())) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .output() - .await?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); - return Err(Error::ConfigError( - format!("npm view failed for {package_spec}: {stderr}").into(), - )); - } - - parse_npm_view_version(&output.stdout) -} - -fn parse_npm_view_version(stdout: &[u8]) -> Result { - let raw = String::from_utf8_lossy(stdout); - let trimmed = raw.trim(); - if trimmed.is_empty() { - return Err(Error::ConfigError("npm view returned an empty version".into())); - } - - match serde_json::from_str::(trimmed) { - Ok(serde_json::Value::String(version)) => Ok(version), - Ok(serde_json::Value::Array(versions)) => versions - .iter() - .rev() - .find_map(|version| version.as_str()) - .map(str::to_string) - .ok_or_else(|| Error::ConfigError("npm view returned an empty version list".into())), - _ => Ok(trimmed.to_string()), - } -} - /// Return true for package specs that refer to local filesystem content. pub(crate) fn is_local_package_spec(spec: &str) -> bool { spec == "." @@ -967,30 +889,6 @@ mod tests { } } - #[test] - fn test_parse_npm_view_version_json_string() { - let version = parse_npm_view_version(b"\"5.9.3\"\n").unwrap(); - assert_eq!(version, "5.9.3"); - } - - #[test] - fn test_parse_npm_view_version_plain_string() { - let version = parse_npm_view_version(b"5.9.3\n").unwrap(); - assert_eq!(version, "5.9.3"); - } - - #[test] - fn test_parse_npm_view_version_json_array_uses_latest_entry() { - let version = parse_npm_view_version(b"[\"5.9.2\", \"5.9.3\"]\n").unwrap(); - assert_eq!(version, "5.9.3"); - } - - #[test] - fn test_parse_npm_view_version_rejects_empty_output() { - let err = parse_npm_view_version(b"\n").unwrap_err(); - assert!(err.to_string().contains("empty version")); - } - #[test] fn test_is_local_package_spec_relative_paths() { assert!(is_local_package_spec(".")); diff --git a/crates/vite_global_cli/src/commands/env/mod.rs b/crates/vite_global_cli/src/commands/env/mod.rs index 6202039cb9..8d9a5bedef 100644 --- a/crates/vite_global_cli/src/commands/env/mod.rs +++ b/crates/vite_global_cli/src/commands/env/mod.rs @@ -14,9 +14,11 @@ mod list; mod list_remote; mod off; mod on; +pub mod outdated; pub mod package_metadata; pub mod packages; mod pin; +pub mod registry; mod setup; mod unpin; mod r#use; diff --git a/crates/vite_global_cli/src/commands/env/outdated.rs b/crates/vite_global_cli/src/commands/env/outdated.rs new file mode 100644 index 0000000000..09d549aec0 --- /dev/null +++ b/crates/vite_global_cli/src/commands/env/outdated.rs @@ -0,0 +1,214 @@ +//! Check managed global packages for newer registry versions. + +use std::{collections::HashMap, process::ExitStatus}; + +use owo_colors::OwoColorize; +use serde::Serialize; +use vite_install::commands::outdated::Format; + +use super::{ + global_install::parse_package_spec, + package_metadata::PackageMetadata, + registry::{self, PackageVersion}, +}; +use crate::error::Error; + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct OutdatedPackage { + name: String, + current: String, + latest: String, + node: String, + bins: Vec, +} + +pub async fn execute( + packages: &[String], + long: bool, + format: Option, + concurrency: usize, +) -> Result { + let installed = matching_packages(packages).await?; + if installed.is_empty() { + if packages.is_empty() { + print_empty(format, "No global packages installed."); + return Ok(ExitStatus::default()); + } + + let names = packages.join(", "); + print_empty(format, &format!("No matching global packages installed: {names}")); + return Ok(exit_status(1)); + } + + let specs = installed.iter().map(|package| package.name.clone()).collect::>(); + let versions = registry::latest_package_versions(&specs, concurrency).await?; + let mut latest_versions = HashMap::new(); + let mut failed = false; + + for PackageVersion { package_spec, version } in versions { + match version { + Ok(version) => { + latest_versions.insert(package_spec, version); + } + Err(error) => { + vite_shared::output::raw_stderr(&format!( + "Could not check latest version for {package_spec}: {error}" + )); + failed = true; + } + } + } + + let mut outdated = Vec::new(); + for package in installed { + let Some(latest) = latest_versions.remove(&package.name) else { continue }; + if package.version.trim() == latest.trim() { + continue; + } + + outdated.push(OutdatedPackage { + name: package.name, + current: package.version, + latest, + node: package.platform.node, + bins: package.bins, + }); + } + + if outdated.is_empty() { + print_empty(format, "All global packages are up to date."); + return Ok(if failed { exit_status(1) } else { ExitStatus::default() }); + } + + match format { + Some(Format::Json) => print_json(&outdated)?, + Some(Format::List) => print_list(&outdated, long), + _ => print_table(&outdated, long), + } + + Ok(exit_status(1)) +} + +async fn matching_packages(packages: &[String]) -> Result, Error> { + if packages.is_empty() { + return PackageMetadata::list_all().await; + } + + let mut installed = Vec::new(); + for package in packages { + let (package_name, _) = parse_package_spec(package); + if let Some(metadata) = PackageMetadata::load(&package_name).await? { + installed.push(metadata); + } + } + Ok(installed) +} + +fn print_empty(format: Option, message: &str) { + match format { + Some(Format::Json) => println!("[]"), + _ => println!("{message}"), + } +} + +fn print_json(packages: &[OutdatedPackage]) -> Result<(), Error> { + let json = serde_json::to_string_pretty(packages)?; + println!("{json}"); + Ok(()) +} + +fn print_list(packages: &[OutdatedPackage], long: bool) { + for package in packages { + if long { + println!( + "{}@{} -> {} (node {}, bins: {})", + package.name, + package.current, + package.latest, + package.node, + package.bins.join(", ") + ); + } else { + println!("{}@{} -> {}", package.name, package.current, package.latest); + } + } +} + +fn print_table(packages: &[OutdatedPackage], long: bool) { + let col_pkg = "Package"; + let col_current = "Current"; + let col_latest = "Latest"; + let col_node = "Node"; + let col_bins = "Bins"; + + let mut w_pkg = col_pkg.len(); + let mut w_current = col_current.len(); + let mut w_latest = col_latest.len(); + let mut w_node = col_node.len(); + + for package in packages { + w_pkg = w_pkg.max(package.name.len()); + w_current = w_current.max(package.current.len()); + w_latest = w_latest.max(package.latest.len()); + w_node = w_node.max(package.node.len()); + } + + let gap = 3; + if long { + println!( + "{:gap$}{:gap$}{:gap$}{:gap$}{}", + col_pkg, "", col_current, "", col_latest, "", col_node, "", col_bins + ); + println!( + "{:gap$}{:gap$}{:gap$}{:gap$}{}", + "---", "", "---", "", "---", "", "---", "", "---" + ); + } else { + println!( + "{:gap$}{:gap$}{}", + col_pkg, "", col_current, "", col_latest + ); + println!("{:gap$}{:gap$}---", "---", "", "---", ""); + } + + for package in packages { + if long { + println!( + "{}{:>gap$}{:gap$}{:gap$}{:gap$}{}", + format!("{:gap$}{:gap$}{}", + format!("{: ExitStatus { + #[cfg(unix)] + { + use std::os::unix::process::ExitStatusExt; + ExitStatus::from_raw(code << 8) + } + + #[cfg(windows)] + { + use std::os::windows::process::ExitStatusExt; + ExitStatus::from_raw(code as u32) + } +} diff --git a/crates/vite_global_cli/src/commands/env/registry.rs b/crates/vite_global_cli/src/commands/env/registry.rs new file mode 100644 index 0000000000..dbb9bbb8e9 --- /dev/null +++ b/crates/vite_global_cli/src/commands/env/registry.rs @@ -0,0 +1,145 @@ +//! npm registry queries for managed global packages. + +use std::process::Stdio; + +use futures::{StreamExt, stream::FuturesUnordered}; +use tokio::process::Command; +use vite_path::{AbsolutePathBuf, current_dir}; +use vite_shared::format_path_prepended; + +use super::config::resolve_version; +use crate::error::Error; + +#[derive(Debug)] +pub(crate) struct PackageVersion { + pub package_spec: String, + pub version: Result, +} + +struct NpmRegistry { + npm_path: AbsolutePathBuf, + node_bin_dir: AbsolutePathBuf, +} + +impl NpmRegistry { + async fn resolve() -> Result { + let cwd = current_dir().map_err(|error| { + Error::ConfigError(format!("Cannot get current directory: {error}").into()) + })?; + let resolution = resolve_version(&cwd).await?; + let runtime = vite_js_runtime::download_runtime( + vite_js_runtime::JsRuntimeType::Node, + &resolution.version, + ) + .await?; + + 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") }; + + Ok(Self { npm_path, node_bin_dir }) + } + + async fn latest_package_version(&self, package_spec: &str) -> Result { + let output = Command::new(self.npm_path.as_path()) + .args(["view", package_spec, "version", "--json"]) + .env("PATH", format_path_prepended(self.node_bin_dir.as_path())) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .await?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + return Err(Error::ConfigError( + format!("npm view failed for {package_spec}: {stderr}").into(), + )); + } + + parse_npm_view_version(&output.stdout) + } +} + +pub(crate) async fn latest_package_versions( + package_specs: &[String], + concurrency: usize, +) -> Result, Error> { + if package_specs.is_empty() { + return Ok(Vec::new()); + } + + let registry = NpmRegistry::resolve().await?; + let concurrency = concurrency.max(1); + let mut package_specs = package_specs.iter(); + let mut versions = Vec::with_capacity(package_specs.len()); + let mut queries = FuturesUnordered::new(); + + loop { + while queries.len() < concurrency { + let Some(package_spec) = package_specs.next() else { break }; + queries.push(async { + let package_spec = package_spec.clone(); + let version = registry.latest_package_version(&package_spec).await; + PackageVersion { package_spec, version } + }); + } + + if queries.is_empty() { + break; + } + + if let Some(version) = queries.next().await { + versions.push(version); + } + } + + Ok(versions) +} + +fn parse_npm_view_version(stdout: &[u8]) -> Result { + let raw = String::from_utf8_lossy(stdout); + let trimmed = raw.trim(); + if trimmed.is_empty() { + return Err(Error::ConfigError("npm view returned an empty version".into())); + } + + match serde_json::from_str::(trimmed) { + Ok(serde_json::Value::String(version)) => Ok(version), + Ok(serde_json::Value::Array(versions)) => { + let Some(version) = versions.iter().rev().find_map(|version| version.as_str()) else { + return Err(Error::ConfigError("npm view returned an empty version list".into())); + }; + Ok(version.to_string()) + } + _ => Ok(trimmed.to_string()), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_json_string_version() { + let version = parse_npm_view_version(br#""5.0.0""#).unwrap(); + assert_eq!(version, "5.0.0"); + } + + #[test] + fn parses_json_array_version() { + let version = parse_npm_view_version(br#"["4.9.5","5.0.0"]"#).unwrap(); + assert_eq!(version, "5.0.0"); + } + + #[test] + fn parses_plain_version() { + let version = parse_npm_view_version(b"5.0.0").unwrap(); + assert_eq!(version, "5.0.0"); + } + + #[test] + fn rejects_empty_output() { + let error = parse_npm_view_version(b"\n").unwrap_err(); + assert!(error.to_string().contains("empty version")); + } +} From b741fd8d6adb529ce68662f29269421e185ccd14 Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Fri, 22 May 2026 06:23:25 +0800 Subject: [PATCH 02/18] refactor --- crates/vite_global_cli/src/cli.rs | 111 +++--------------- .../vite_global_cli/src/commands/env/mod.rs | 4 - .../global_install.rs => global/install.rs} | 57 ++------- .../{env/registry.rs => global/mod.rs} | 66 +++++++++-- .../src/commands/{env => global}/outdated.rs | 38 +++--- .../src/commands/{env => global}/packages.rs | 3 +- crates/vite_global_cli/src/commands/mod.rs | 3 + crates/vite_global_cli/src/shim/dispatch.rs | 12 +- 8 files changed, 113 insertions(+), 181 deletions(-) rename crates/vite_global_cli/src/commands/{env/global_install.rs => global/install.rs} (95%) rename crates/vite_global_cli/src/commands/{env/registry.rs => global/mod.rs} (67%) rename crates/vite_global_cli/src/commands/{env => global}/outdated.rs (86%) rename crates/vite_global_cli/src/commands/{env => global}/packages.rs (96%) diff --git a/crates/vite_global_cli/src/cli.rs b/crates/vite_global_cli/src/cli.rs index 8dd6708c9b..8738a318d9 100644 --- a/crates/vite_global_cli/src/cli.rs +++ b/crates/vite_global_cli/src/cli.rs @@ -3,7 +3,7 @@ //! This module defines the CLI structure using clap and routes commands //! to their appropriate handlers. -use std::{collections::HashMap, ffi::OsStr, process::ExitStatus}; +use std::{ffi::OsStr, process::ExitStatus}; use clap::{CommandFactory, FromArgMatches, Parser, Subcommand}; use clap_complete::ArgValueCompleter; @@ -13,10 +13,7 @@ use vite_pm_cli::PackageManagerCommand; use vite_shared::output; use crate::{ - commands::{ - self, - env::{global_install, package_metadata::PackageMetadata}, - }, + commands::{self, env::package_metadata::PackageMetadata, global}, error::Error, help, }; @@ -533,7 +530,7 @@ fn run_tasks_completions(current: &OsStr) -> Vec { - warn_unsupported_global_outdated_flags( - recursive, - filter, - workspace_root, - prod, - dev, - no_optional, - compatible, - sort_by.as_deref(), - pass_through_args.as_deref(), - ); - crate::commands::env::outdated::execute( - packages, - long, - format, - DEFAULT_GLOBAL_INSTALL_CONCURRENCY, - ) - .await + PackageManagerCommand::Outdated { global: true, ref packages, long, format, .. } => { + global::outdated::execute(packages, long, format, DEFAULT_GLOBAL_INSTALL_CONCURRENCY) + .await } // `pm list -g` lists vite-plus-managed globals, not the underlying PM's. @@ -603,7 +570,7 @@ async fn run_package_manager_command( json, ref pattern, .. - }) => crate::commands::env::packages::execute(json, pattern.as_deref()).await, + }) => global::packages::execute(json, pattern.as_deref()).await, cmd => { commands::prepend_js_runtime_to_path_env(&cwd).await?; @@ -618,7 +585,7 @@ async fn managed_install( force: bool, concurrency: Option, ) -> Result { - if let Err((package_name, error)) = crate::commands::env::global_install::install( + if let Err((package_name, error)) = global::install::install( packages, node, force, @@ -639,7 +606,7 @@ async fn managed_install( async fn managed_uninstall(packages: &[String], dry_run: bool) -> Result { for package in packages { - if let Err(e) = crate::commands::env::global_install::uninstall(package, dry_run).await { + if let Err(e) = global::install::uninstall(package, dry_run).await { vite_shared::output::raw_stderr(&format!("Failed to uninstall {package}: {e}")); return Ok(exit_status(1)); } @@ -647,43 +614,6 @@ async fn managed_uninstall(packages: &[String], dry_run: bool) -> Result>, - workspace_root: bool, - prod: bool, - dev: bool, - no_optional: bool, - compatible: bool, - sort_by: Option<&str>, - pass_through_args: Option<&[String]>, -) { - if recursive { - output::warn("--recursive is ignored with managed global packages"); - } - if filters.as_ref().is_some_and(|filters| !filters.is_empty()) { - output::warn("--filter is ignored with managed global packages"); - } - if workspace_root { - output::warn("--workspace-root is ignored with managed global packages"); - } - if prod || dev { - output::warn("--prod/--dev are ignored with managed global packages"); - } - if no_optional { - output::warn("--no-optional is ignored with managed global packages"); - } - if compatible { - output::warn("--compatible is ignored with managed global packages"); - } - if sort_by.is_some() { - output::warn("--sort-by is ignored with managed global packages"); - } - if pass_through_args.is_some_and(|args| !args.is_empty()) { - output::warn("pass-through arguments are ignored with managed global packages"); - } -} - fn is_global_package_up_to_date(installed_version: &str, registry_version: &str) -> bool { installed_version.trim() == registry_version.trim() } @@ -709,7 +639,7 @@ async fn managed_update( if let Some(all) = all_packages { let specs = all.iter().map(|metadata| metadata.name.clone()).collect::>(); - let latest_versions = latest_versions_by_spec(&specs, concurrency).await?; + let latest_versions = global::latest_versions_by_spec(&specs, concurrency).await?; for metadata in all { match latest_versions.get(&metadata.name) { @@ -744,12 +674,12 @@ async fn managed_update( let mut installed_packages = Vec::new(); for package in packages { - if global_install::is_local_package_spec(package) { + if global::is_local_package_spec(package) { to_update.push(package.clone()); continue; } - let (package_name, _) = global_install::parse_package_spec(package); + let (package_name, _) = global::parse_package_spec(package); if let Some(metadata) = PackageMetadata::load(&package_name).await? { specs.push(package.clone()); installed_packages.push((package.clone(), package_name, metadata.version)); @@ -758,7 +688,7 @@ async fn managed_update( } } - let latest_versions = latest_versions_by_spec(&specs, concurrency).await?; + let latest_versions = global::latest_versions_by_spec(&specs, concurrency).await?; for (package, package_name, installed_version) in installed_packages { match latest_versions.get(&package) { @@ -797,7 +727,7 @@ async fn managed_update( // Call reinstall logic if let Err((package_name, error)) = - global_install::install(&to_update, None, false, concurrency, true).await + global::install::install(&to_update, None, false, concurrency, true).await { output::error(&format!( "Failed to update {}: {error}", @@ -808,19 +738,6 @@ async fn managed_update( Ok(ExitStatus::default()) } -async fn latest_versions_by_spec( - specs: &[String], - concurrency: usize, -) -> Result>, Error> { - let versions = - crate::commands::env::registry::latest_package_versions(specs, concurrency).await?; - let mut latest_versions = HashMap::with_capacity(versions.len()); - for version in versions { - latest_versions.insert(version.package_spec, version.version); - } - Ok(latest_versions) -} - /// Run the CLI command. pub async fn run_command(cwd: AbsolutePathBuf, args: Args) -> Result { run_command_with_options(cwd, args, RenderOptions::default()).await diff --git a/crates/vite_global_cli/src/commands/env/mod.rs b/crates/vite_global_cli/src/commands/env/mod.rs index 8d9a5bedef..f15251c3b5 100644 --- a/crates/vite_global_cli/src/commands/env/mod.rs +++ b/crates/vite_global_cli/src/commands/env/mod.rs @@ -9,16 +9,12 @@ mod current; mod default; mod doctor; mod exec; -pub mod global_install; mod list; mod list_remote; mod off; mod on; -pub mod outdated; pub mod package_metadata; -pub mod packages; mod pin; -pub mod registry; mod setup; mod unpin; mod r#use; diff --git a/crates/vite_global_cli/src/commands/env/global_install.rs b/crates/vite_global_cli/src/commands/global/install.rs similarity index 95% rename from crates/vite_global_cli/src/commands/env/global_install.rs rename to crates/vite_global_cli/src/commands/global/install.rs index 4118823163..97cea71480 100644 --- a/crates/vite_global_cli/src/commands/env/global_install.rs +++ b/crates/vite_global_cli/src/commands/global/install.rs @@ -16,15 +16,20 @@ use vite_js_runtime::NodeProvider; use vite_path::{AbsolutePath, AbsolutePathBuf, current_dir}; use vite_shared::{format_path_prepended, output}; -use super::{ - bin_config::BinConfig, - config::{ - get_bin_dir, get_node_modules_dir, get_packages_dir, get_tmp_dir, resolve_version, - resolve_version_alias, +use crate::{ + commands::{ + env::{ + bin_config::BinConfig, + config::{ + get_bin_dir, get_node_modules_dir, get_packages_dir, get_tmp_dir, resolve_version, + resolve_version_alias, + }, + package_metadata::PackageMetadata, + }, + global::{CORE_SHIMS, parse_package_spec}, }, - package_metadata::PackageMetadata, + error::Error, }; -use crate::error::Error; struct Package<'a> { spec: &'a str, @@ -443,40 +448,6 @@ pub async fn uninstall(package_name: &str, dry_run: bool) -> Result<(), Error> { Ok(()) } -/// Return true for package specs that refer to local filesystem content. -pub(crate) fn is_local_package_spec(spec: &str) -> bool { - spec == "." - || spec == ".." - || spec.starts_with("./") - || spec.starts_with("../") - || spec.starts_with('/') - || spec.starts_with("file:") - || (cfg!(windows) - && spec.len() >= 3 - && spec.as_bytes()[1] == b':' - && (spec.as_bytes()[2] == b'\\' || spec.as_bytes()[2] == b'/')) -} - -/// Parse package spec into name and optional version. -pub(crate) fn parse_package_spec(spec: &str) -> (String, Option) { - // Handle scoped packages: @scope/name@version - if spec.starts_with('@') { - // Find the second @ for version - if let Some(idx) = spec[1..].find('@') { - let idx = idx + 1; // Adjust for the skipped first char - return (spec[..idx].to_string(), Some(spec[idx + 1..].to_string())); - } - return (spec.to_string(), None); - } - - // Handle regular packages: name@version - if let Some(idx) = spec.find('@') { - return (spec[..idx].to_string(), Some(spec[idx + 1..].to_string())); - } - - (spec.to_string(), None) -} - /// Binary info extracted from package.json. struct BinaryInfo { /// Binary name (the command users will run) @@ -552,9 +523,6 @@ fn is_javascript_binary(path: &AbsolutePath) -> bool { false } -/// Core shims that should not be overwritten by package binaries. -pub(crate) const CORE_SHIMS: &[&str] = &["node", "npm", "npx", "vp"]; - /// Create a shim for a package binary. /// /// On Unix: Creates a symlink to ../current/bin/vp @@ -657,6 +625,7 @@ async fn remove_package_shim( #[cfg(test)] mod tests { use super::*; + use crate::commands::global::is_local_package_spec; /// RAII guard that sets `VP_TRAMPOLINE_PATH` to a fake binary on creation /// and clears it on drop. Ensures cleanup even on test panics. diff --git a/crates/vite_global_cli/src/commands/env/registry.rs b/crates/vite_global_cli/src/commands/global/mod.rs similarity index 67% rename from crates/vite_global_cli/src/commands/env/registry.rs rename to crates/vite_global_cli/src/commands/global/mod.rs index dbb9bbb8e9..d505e9a053 100644 --- a/crates/vite_global_cli/src/commands/env/registry.rs +++ b/crates/vite_global_cli/src/commands/global/mod.rs @@ -1,19 +1,25 @@ -//! npm registry queries for managed global packages. +//! Managed global package utilities. -use std::process::Stdio; +use std::{collections::HashMap, process::Stdio}; use futures::{StreamExt, stream::FuturesUnordered}; use tokio::process::Command; use vite_path::{AbsolutePathBuf, current_dir}; use vite_shared::format_path_prepended; -use super::config::resolve_version; -use crate::error::Error; +use crate::{commands::env::config::resolve_version, error::Error}; + +pub mod install; +pub mod outdated; +pub mod packages; + +/// Core shims that should not be overwritten by package binaries. +pub(crate) const CORE_SHIMS: &[&str] = &["node", "npm", "npx", "vp"]; #[derive(Debug)] -pub(crate) struct PackageVersion { - pub package_spec: String, - pub version: Result, +struct PackageVersion { + package_spec: String, + version: Result, } struct NpmRegistry { @@ -60,7 +66,7 @@ impl NpmRegistry { } } -pub(crate) async fn latest_package_versions( +async fn latest_package_versions( package_specs: &[String], concurrency: usize, ) -> Result, Error> { @@ -72,6 +78,7 @@ pub(crate) async fn latest_package_versions( let concurrency = concurrency.max(1); let mut package_specs = package_specs.iter(); let mut versions = Vec::with_capacity(package_specs.len()); + // Check packages in parallel let mut queries = FuturesUnordered::new(); loop { @@ -96,6 +103,49 @@ pub(crate) async fn latest_package_versions( Ok(versions) } +pub(crate) async fn latest_versions_by_spec( + specs: &[String], + concurrency: usize, +) -> Result>, Error> { + let versions = latest_package_versions(specs, concurrency).await?; + let mut latest_versions = HashMap::with_capacity(versions.len()); + for version in versions { + latest_versions.insert(version.package_spec, version.version); + } + Ok(latest_versions) +} + +/// Return true for package specs that refer to local filesystem content. +pub(crate) fn is_local_package_spec(spec: &str) -> bool { + spec == "." + || spec == ".." + || spec.starts_with("./") + || spec.starts_with("../") + || spec.starts_with('/') + || spec.starts_with("file:") + || (cfg!(windows) + && spec.len() >= 3 + && spec.as_bytes()[1] == b':' + && (spec.as_bytes()[2] == b'\\' || spec.as_bytes()[2] == b'/')) +} + +/// Parse package spec into name and optional version. +pub(crate) fn parse_package_spec(spec: &str) -> (String, Option) { + if spec.starts_with('@') { + if let Some(idx) = spec[1..].find('@') { + let idx = idx + 1; + return (spec[..idx].to_string(), Some(spec[idx + 1..].to_string())); + } + return (spec.to_string(), None); + } + + if let Some(idx) = spec.find('@') { + return (spec[..idx].to_string(), Some(spec[idx + 1..].to_string())); + } + + (spec.to_string(), None) +} + fn parse_npm_view_version(stdout: &[u8]) -> Result { let raw = String::from_utf8_lossy(stdout); let trimmed = raw.trim(); diff --git a/crates/vite_global_cli/src/commands/env/outdated.rs b/crates/vite_global_cli/src/commands/global/outdated.rs similarity index 86% rename from crates/vite_global_cli/src/commands/env/outdated.rs rename to crates/vite_global_cli/src/commands/global/outdated.rs index 09d549aec0..3fe6127537 100644 --- a/crates/vite_global_cli/src/commands/env/outdated.rs +++ b/crates/vite_global_cli/src/commands/global/outdated.rs @@ -1,17 +1,13 @@ //! Check managed global packages for newer registry versions. -use std::{collections::HashMap, process::ExitStatus}; +use std::process::ExitStatus; use owo_colors::OwoColorize; use serde::Serialize; use vite_install::commands::outdated::Format; -use super::{ - global_install::parse_package_spec, - package_metadata::PackageMetadata, - registry::{self, PackageVersion}, -}; -use crate::error::Error; +use super::{latest_versions_by_spec, parse_package_spec}; +use crate::{commands::env::package_metadata::PackageMetadata, error::Error}; #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] @@ -42,27 +38,27 @@ pub async fn execute( } let specs = installed.iter().map(|package| package.name.clone()).collect::>(); - let versions = registry::latest_package_versions(&specs, concurrency).await?; - let mut latest_versions = HashMap::new(); + let mut latest_versions = latest_versions_by_spec(&specs, concurrency).await?; let mut failed = false; - for PackageVersion { package_spec, version } in versions { - match version { - Ok(version) => { - latest_versions.insert(package_spec, version); - } - Err(error) => { - vite_shared::output::raw_stderr(&format!( - "Could not check latest version for {package_spec}: {error}" - )); - failed = true; - } + for (package_spec, version) in &latest_versions { + if let Err(error) = version { + vite_shared::output::raw_stderr(&format!( + "Could not check latest version for {package_spec}: {error}" + )); + failed = true; } } let mut outdated = Vec::new(); for package in installed { - let Some(latest) = latest_versions.remove(&package.name) else { continue }; + let Some(version) = latest_versions.remove(&package.name) else { + continue; + }; + let latest = match version { + Ok(version) => version, + Err(_) => continue, + }; if package.version.trim() == latest.trim() { continue; } diff --git a/crates/vite_global_cli/src/commands/env/packages.rs b/crates/vite_global_cli/src/commands/global/packages.rs similarity index 96% rename from crates/vite_global_cli/src/commands/env/packages.rs rename to crates/vite_global_cli/src/commands/global/packages.rs index 07ef5bda00..916585c162 100644 --- a/crates/vite_global_cli/src/commands/env/packages.rs +++ b/crates/vite_global_cli/src/commands/global/packages.rs @@ -4,8 +4,7 @@ use std::process::ExitStatus; use owo_colors::OwoColorize; -use super::package_metadata::PackageMetadata; -use crate::error::Error; +use crate::{commands::env::package_metadata::PackageMetadata, error::Error}; /// Execute the packages command. pub async fn execute(json: bool, pattern: Option<&str>) -> Result { diff --git a/crates/vite_global_cli/src/commands/mod.rs b/crates/vite_global_cli/src/commands/mod.rs index 9f2fc26932..18679f0de6 100644 --- a/crates/vite_global_cli/src/commands/mod.rs +++ b/crates/vite_global_cli/src/commands/mod.rs @@ -85,6 +85,9 @@ pub async fn prepend_js_runtime_to_path_env(project_path: &AbsolutePath) -> Resu Ok(()) } +// Global package management +pub mod global; + // Category B: JS Script Commands pub mod config; pub mod create; diff --git a/crates/vite_global_cli/src/shim/dispatch.rs b/crates/vite_global_cli/src/shim/dispatch.rs index 44813c261e..dbff81c55a 100644 --- a/crates/vite_global_cli/src/shim/dispatch.rs +++ b/crates/vite_global_cli/src/shim/dispatch.rs @@ -12,11 +12,13 @@ use super::{ cache::{self, ResolveCache, ResolveCacheEntry}, exec, is_core_shim_tool, }; -use crate::commands::env::{ - bin_config::{BinConfig, BinSource}, - config::{self, ShimMode}, - global_install::CORE_SHIMS, - package_metadata::PackageMetadata, +use crate::commands::{ + env::{ + bin_config::{BinConfig, BinSource}, + config::{self, ShimMode}, + package_metadata::PackageMetadata, + }, + global::CORE_SHIMS, }; /// Environment variable used to prevent infinite recursion in shim dispatch. From ca4a86a1dc363fbf1ad23db99a54fca1785750b6 Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Fri, 22 May 2026 20:55:10 +0800 Subject: [PATCH 03/18] tests --- .../src/commands/global/outdated.rs | 43 +++++++++++++++++-- .../command-outdated-global/package.json | 3 ++ .../command-outdated-global/snap.txt | 11 +++++ .../command-outdated-global/steps.json | 11 +++++ .../command-outdated-pnpm10/snap.txt | 3 -- .../command-outdated-pnpm10/steps.json | 3 +- .../command-outdated-pnpm11/snap.txt | 3 -- .../command-outdated-pnpm11/steps.json | 3 +- 8 files changed, 66 insertions(+), 14 deletions(-) create mode 100644 packages/cli/snap-tests-global/command-outdated-global/package.json create mode 100644 packages/cli/snap-tests-global/command-outdated-global/snap.txt create mode 100644 packages/cli/snap-tests-global/command-outdated-global/steps.json diff --git a/crates/vite_global_cli/src/commands/global/outdated.rs b/crates/vite_global_cli/src/commands/global/outdated.rs index 3fe6127537..66cfcc0db5 100644 --- a/crates/vite_global_cli/src/commands/global/outdated.rs +++ b/crates/vite_global_cli/src/commands/global/outdated.rs @@ -1,13 +1,19 @@ //! Check managed global packages for newer registry versions. -use std::process::ExitStatus; +use std::{collections::BTreeMap, process::ExitStatus}; use owo_colors::OwoColorize; use serde::Serialize; use vite_install::commands::outdated::Format; use super::{latest_versions_by_spec, parse_package_spec}; -use crate::{commands::env::package_metadata::PackageMetadata, error::Error}; +use crate::{ + commands::env::{ + config::{get_node_modules_dir, get_packages_dir}, + package_metadata::PackageMetadata, + }, + error::Error, +}; #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] @@ -19,6 +25,16 @@ struct OutdatedPackage { bins: Vec, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct OutdatedPackageJson { + current: String, + wanted: String, + latest: String, + dependent: &'static str, + location: String, +} + pub async fn execute( packages: &[String], long: bool, @@ -103,13 +119,32 @@ async fn matching_packages(packages: &[String]) -> Result, fn print_empty(format: Option, message: &str) { match format { - Some(Format::Json) => println!("[]"), + Some(Format::Json) => println!("{{}}"), _ => println!("{message}"), } } fn print_json(packages: &[OutdatedPackage]) -> Result<(), Error> { - let json = serde_json::to_string_pretty(packages)?; + let packages_dir = get_packages_dir()?; + let mut output = BTreeMap::new(); + + for package in packages { + let package_dir = packages_dir.join(&package.name); + let location = get_node_modules_dir(&package_dir, &package.name); + + output.insert( + package.name.clone(), + OutdatedPackageJson { + current: package.current.clone(), + wanted: package.latest.clone(), + latest: package.latest.clone(), + dependent: "global", + location: location.as_path().display().to_string(), + }, + ); + } + + let json = serde_json::to_string_pretty(&output)?; println!("{json}"); Ok(()) } diff --git a/packages/cli/snap-tests-global/command-outdated-global/package.json b/packages/cli/snap-tests-global/command-outdated-global/package.json new file mode 100644 index 0000000000..a6353dc52e --- /dev/null +++ b/packages/cli/snap-tests-global/command-outdated-global/package.json @@ -0,0 +1,3 @@ +{ + "name": "command-outdated-global" +} diff --git a/packages/cli/snap-tests-global/command-outdated-global/snap.txt b/packages/cli/snap-tests-global/command-outdated-global/snap.txt new file mode 100644 index 0000000000..7966796e3a --- /dev/null +++ b/packages/cli/snap-tests-global/command-outdated-global/snap.txt @@ -0,0 +1,11 @@ +> vp install -g testnpm2@1.0.0 # should prepare global outdated package +[1]> vp outdated testnpm2 -g --format json # should support global json output +{ + "testnpm2": { + "current": "1.0.0", + "wanted": "1.0.1", + "latest": "1.0.1", + "dependent": "global", + "location": "/packages/testnpm2/lib/node_modules/testnpm2" + } +} diff --git a/packages/cli/snap-tests-global/command-outdated-global/steps.json b/packages/cli/snap-tests-global/command-outdated-global/steps.json new file mode 100644 index 0000000000..9d1e0b0f7d --- /dev/null +++ b/packages/cli/snap-tests-global/command-outdated-global/steps.json @@ -0,0 +1,11 @@ +{ + "ignoredPlatforms": ["win32"], + "commands": [ + { + "command": "vp install -g testnpm2@1.0.0 # should prepare global outdated package", + "ignoreOutput": true + }, + "vp outdated testnpm2 -g --format json # should support global json output" + ], + "after": ["vp remove -g testnpm2"] +} diff --git a/packages/cli/snap-tests-global/command-outdated-pnpm10/snap.txt b/packages/cli/snap-tests-global/command-outdated-pnpm10/snap.txt index ec085ee577..d481255119 100644 --- a/packages/cli/snap-tests-global/command-outdated-pnpm10/snap.txt +++ b/packages/cli/snap-tests-global/command-outdated-pnpm10/snap.txt @@ -171,6 +171,3 @@ testnpm2 ├──────────────────────────────────────────┼─────────┼────────┤ │ testnpm2 │ │ └──────────────────────────────────────────┴─────────┴────────┘ - -> vp outdated testnpm2 -g --format json # should support global output -{} diff --git a/packages/cli/snap-tests-global/command-outdated-pnpm10/steps.json b/packages/cli/snap-tests-global/command-outdated-pnpm10/steps.json index b1e9e87cf3..5614bae240 100644 --- a/packages/cli/snap-tests-global/command-outdated-pnpm10/steps.json +++ b/packages/cli/snap-tests-global/command-outdated-pnpm10/steps.json @@ -16,7 +16,6 @@ "vp outdated --no-optional # should support no-optional output", "vp outdated --compatible # should compatible output nothing", "json-edit package.json '_.optionalDependencies[\"test-vite-plus-other-optional\"] = \"^1.0.0\"' && vp outdated --compatible # should support compatible output with optional dependencies", - "vp outdated --sort-by name # should support sort-by output", - "vp outdated testnpm2 -g --format json # should support global output" + "vp outdated --sort-by name # should support sort-by output" ] } diff --git a/packages/cli/snap-tests-global/command-outdated-pnpm11/snap.txt b/packages/cli/snap-tests-global/command-outdated-pnpm11/snap.txt index d1701b9216..a6285c1272 100644 --- a/packages/cli/snap-tests-global/command-outdated-pnpm11/snap.txt +++ b/packages/cli/snap-tests-global/command-outdated-pnpm11/snap.txt @@ -171,6 +171,3 @@ testnpm2 ├──────────────────────────────────────────┼─────────┼────────┤ │ testnpm2 │ │ └──────────────────────────────────────────┴─────────┴────────┘ - -> vp outdated testnpm2 -g --format json # should support global output -{} diff --git a/packages/cli/snap-tests-global/command-outdated-pnpm11/steps.json b/packages/cli/snap-tests-global/command-outdated-pnpm11/steps.json index b1e9e87cf3..5614bae240 100644 --- a/packages/cli/snap-tests-global/command-outdated-pnpm11/steps.json +++ b/packages/cli/snap-tests-global/command-outdated-pnpm11/steps.json @@ -16,7 +16,6 @@ "vp outdated --no-optional # should support no-optional output", "vp outdated --compatible # should compatible output nothing", "json-edit package.json '_.optionalDependencies[\"test-vite-plus-other-optional\"] = \"^1.0.0\"' && vp outdated --compatible # should support compatible output with optional dependencies", - "vp outdated --sort-by name # should support sort-by output", - "vp outdated testnpm2 -g --format json # should support global output" + "vp outdated --sort-by name # should support sort-by output" ] } From 2c4389eab881496b1cd57d2d2fe8a6f7679b308c Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Fri, 22 May 2026 20:57:00 +0800 Subject: [PATCH 04/18] revert --- crates/vite_pm_cli/src/handlers.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/vite_pm_cli/src/handlers.rs b/crates/vite_pm_cli/src/handlers.rs index 3ed2eec5a0..0b8fadc074 100644 --- a/crates/vite_pm_cli/src/handlers.rs +++ b/crates/vite_pm_cli/src/handlers.rs @@ -95,11 +95,7 @@ pub async fn run_outdated( cwd: &AbsolutePath, options: &OutdatedCommandOptions<'_>, ) -> Result { - let pm = if options.global { - default_npm_package_manager(cwd) - } else { - build_package_manager(cwd).await? - }; + let pm = build_package_manager(cwd).await?; Ok(pm.run_outdated_command(options, cwd).await?) } From f78fcfb73e0c41d081bfaa5737c7f1a51390ce98 Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Fri, 22 May 2026 21:15:00 +0800 Subject: [PATCH 05/18] wip --- .../src/commands/global/outdated.rs | 23 ++++++++++--------- .../command-outdated-global/snap.txt | 4 ++++ .../command-outdated-global/steps.json | 3 ++- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/crates/vite_global_cli/src/commands/global/outdated.rs b/crates/vite_global_cli/src/commands/global/outdated.rs index 66cfcc0db5..1674257d52 100644 --- a/crates/vite_global_cli/src/commands/global/outdated.rs +++ b/crates/vite_global_cli/src/commands/global/outdated.rs @@ -150,18 +150,19 @@ fn print_json(packages: &[OutdatedPackage]) -> Result<(), Error> { } fn print_list(packages: &[OutdatedPackage], long: bool) { - for package in packages { + for (index, package) in packages.iter().enumerate() { + if index > 0 { + println!(); + } + + println!("{} {}", package.name.bold(), "(global)".dimmed()); + println!("{} {} {}", package.current.dimmed(), "=>".dimmed(), package.latest.bold()); + if long { - println!( - "{}@{} -> {} (node {}, bins: {})", - package.name, - package.current, - package.latest, - package.node, - package.bins.join(", ") - ); - } else { - println!("{}@{} -> {}", package.name, package.current, package.latest); + println!("{} {}", "node".dimmed(), package.node); + if !package.bins.is_empty() { + println!("{} {}", "bins".dimmed(), package.bins.join(", ")); + } } } } diff --git a/packages/cli/snap-tests-global/command-outdated-global/snap.txt b/packages/cli/snap-tests-global/command-outdated-global/snap.txt index 7966796e3a..e8a59958ba 100644 --- a/packages/cli/snap-tests-global/command-outdated-global/snap.txt +++ b/packages/cli/snap-tests-global/command-outdated-global/snap.txt @@ -9,3 +9,7 @@ "location": "/packages/testnpm2/lib/node_modules/testnpm2" } } + +[1]> vp outdated testnpm2 -g --format list # should support global list output +testnpm2 (global) + => diff --git a/packages/cli/snap-tests-global/command-outdated-global/steps.json b/packages/cli/snap-tests-global/command-outdated-global/steps.json index 9d1e0b0f7d..1a886af041 100644 --- a/packages/cli/snap-tests-global/command-outdated-global/steps.json +++ b/packages/cli/snap-tests-global/command-outdated-global/steps.json @@ -5,7 +5,8 @@ "command": "vp install -g testnpm2@1.0.0 # should prepare global outdated package", "ignoreOutput": true }, - "vp outdated testnpm2 -g --format json # should support global json output" + "vp outdated testnpm2 -g --format json # should support global json output", + "vp outdated testnpm2 -g --format list # should support global list output" ], "after": ["vp remove -g testnpm2"] } From 869c80bcf48705f89f08c021f2176e3fc0b65c68 Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Fri, 22 May 2026 21:36:50 +0800 Subject: [PATCH 06/18] fix up --- crates/vite_global_cli/src/commands/env/mod.rs | 2 ++ crates/vite_global_cli/src/commands/global/install.rs | 10 +++++++--- crates/vite_pm_cli/src/cli.rs | 1 + 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/vite_global_cli/src/commands/env/mod.rs b/crates/vite_global_cli/src/commands/env/mod.rs index f15251c3b5..c4e8610592 100644 --- a/crates/vite_global_cli/src/commands/env/mod.rs +++ b/crates/vite_global_cli/src/commands/env/mod.rs @@ -22,6 +22,8 @@ mod which; use std::process::ExitStatus; +#[cfg(windows)] +pub(crate) use setup::{cleanup_legacy_windows_shim, get_trampoline_path, remove_or_rename_to_old}; use vite_path::AbsolutePathBuf; use crate::{ diff --git a/crates/vite_global_cli/src/commands/global/install.rs b/crates/vite_global_cli/src/commands/global/install.rs index 97cea71480..b28bdc39e2 100644 --- a/crates/vite_global_cli/src/commands/global/install.rs +++ b/crates/vite_global_cli/src/commands/global/install.rs @@ -564,21 +564,25 @@ async fn create_package_shim( #[cfg(windows)] { + use crate::commands::env::{ + cleanup_legacy_windows_shim, get_trampoline_path, remove_or_rename_to_old, + }; + let shim_path = bin_dir.join(format!("{}.exe", bin_name)); // Delete before overwrite; falls back to rename if the exe is locked. - super::setup::remove_or_rename_to_old(&shim_path).await; + remove_or_rename_to_old(&shim_path).await; // Copy the trampoline binary as .exe. // The trampoline detects the tool name from its own filename and sets // VP_SHIM_TOOL env var before spawning vp.exe. - let trampoline_src = super::setup::get_trampoline_path()?; + let trampoline_src = get_trampoline_path()?; tokio::fs::copy(trampoline_src.as_path(), &shim_path).await?; // Remove legacy .cmd and shell script wrappers from previous versions. // In Git Bash/MSYS, the extensionless script takes precedence over .exe, // so leftover wrappers would bypass the trampoline. - super::setup::cleanup_legacy_windows_shim(bin_dir, bin_name).await; + cleanup_legacy_windows_shim(bin_dir, bin_name).await; tracing::debug!("Created package trampoline shim {:?}", shim_path); } diff --git a/crates/vite_pm_cli/src/cli.rs b/crates/vite_pm_cli/src/cli.rs index 64c25cd54f..0427358152 100644 --- a/crates/vite_pm_cli/src/cli.rs +++ b/crates/vite_pm_cli/src/cli.rs @@ -524,6 +524,7 @@ impl PackageManagerCommand { Self::Install { global, .. } | Self::Add { global, .. } | Self::Remove { global, .. } + | Self::Outdated { global, .. } | Self::Update { global, .. } => *global, Self::Pm(PmCommands::List { global, .. }) => *global, _ => false, From aea343cbb1cb1083c7663941f771d914cfa1688b Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Fri, 22 May 2026 21:50:57 +0800 Subject: [PATCH 07/18] fix up --- .../cli/snap-tests-global/command-outdated-global/snap.txt | 3 +++ .../cli/snap-tests-global/command-outdated-global/steps.json | 1 + packages/cli/snap-tests/command-outdated-pnpm10/snap.txt | 3 --- packages/cli/snap-tests/command-outdated-pnpm10/steps.json | 3 +-- packages/cli/snap-tests/command-pm-no-package-json/snap.txt | 3 --- packages/cli/snap-tests/command-pm-no-package-json/steps.json | 1 - 6 files changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/cli/snap-tests-global/command-outdated-global/snap.txt b/packages/cli/snap-tests-global/command-outdated-global/snap.txt index e8a59958ba..3977103e5b 100644 --- a/packages/cli/snap-tests-global/command-outdated-global/snap.txt +++ b/packages/cli/snap-tests-global/command-outdated-global/snap.txt @@ -1,4 +1,7 @@ > vp install -g testnpm2@1.0.0 # should prepare global outdated package +[1]> vp outdated definitely-not-installed-vite-plus-snap-pkg -g --format json # should support empty global json output +{} + [1]> vp outdated testnpm2 -g --format json # should support global json output { "testnpm2": { diff --git a/packages/cli/snap-tests-global/command-outdated-global/steps.json b/packages/cli/snap-tests-global/command-outdated-global/steps.json index 1a886af041..27e1a9e88e 100644 --- a/packages/cli/snap-tests-global/command-outdated-global/steps.json +++ b/packages/cli/snap-tests-global/command-outdated-global/steps.json @@ -5,6 +5,7 @@ "command": "vp install -g testnpm2@1.0.0 # should prepare global outdated package", "ignoreOutput": true }, + "vp outdated definitely-not-installed-vite-plus-snap-pkg -g --format json # should support empty global json output", "vp outdated testnpm2 -g --format json # should support global json output", "vp outdated testnpm2 -g --format list # should support global list output" ], diff --git a/packages/cli/snap-tests/command-outdated-pnpm10/snap.txt b/packages/cli/snap-tests/command-outdated-pnpm10/snap.txt index e3dd24cf81..645d4f85d9 100644 --- a/packages/cli/snap-tests/command-outdated-pnpm10/snap.txt +++ b/packages/cli/snap-tests/command-outdated-pnpm10/snap.txt @@ -168,6 +168,3 @@ testnpm2 ├──────────────────────────────────────────┼─────────┼────────┤ │ testnpm2 │ │ └──────────────────────────────────────────┴─────────┴────────┘ - -> vp outdated testnpm2 -g --format json # should support global output -{} diff --git a/packages/cli/snap-tests/command-outdated-pnpm10/steps.json b/packages/cli/snap-tests/command-outdated-pnpm10/steps.json index b1e9e87cf3..5614bae240 100644 --- a/packages/cli/snap-tests/command-outdated-pnpm10/steps.json +++ b/packages/cli/snap-tests/command-outdated-pnpm10/steps.json @@ -16,7 +16,6 @@ "vp outdated --no-optional # should support no-optional output", "vp outdated --compatible # should compatible output nothing", "json-edit package.json '_.optionalDependencies[\"test-vite-plus-other-optional\"] = \"^1.0.0\"' && vp outdated --compatible # should support compatible output with optional dependencies", - "vp outdated --sort-by name # should support sort-by output", - "vp outdated testnpm2 -g --format json # should support global output" + "vp outdated --sort-by name # should support sort-by output" ] } diff --git a/packages/cli/snap-tests/command-pm-no-package-json/snap.txt b/packages/cli/snap-tests/command-pm-no-package-json/snap.txt index cca707ab25..f30fb0a9c3 100644 --- a/packages/cli/snap-tests/command-pm-no-package-json/snap.txt +++ b/packages/cli/snap-tests/command-pm-no-package-json/snap.txt @@ -10,9 +10,6 @@ error: No package.json found. [1]> vp why lodash # should show friendly error error: No package.json found. -> vp outdated definitely-not-installed-vite-plus-snap-pkg -g --format json # should not require package.json for global outdated -{} - [1]> vp why definitely-not-installed-vite-plus-snap-pkg -g --json # should not require package.json for global why npm error No dependencies found matching definitely-not-installed-vite-plus-snap-pkg { diff --git a/packages/cli/snap-tests/command-pm-no-package-json/steps.json b/packages/cli/snap-tests/command-pm-no-package-json/steps.json index f1c5f05d3c..49dc5cbff7 100644 --- a/packages/cli/snap-tests/command-pm-no-package-json/steps.json +++ b/packages/cli/snap-tests/command-pm-no-package-json/steps.json @@ -5,7 +5,6 @@ "vp pm prune # should show friendly error", "vp outdated # should show friendly error", "vp why lodash # should show friendly error", - "vp outdated definitely-not-installed-vite-plus-snap-pkg -g --format json # should not require package.json for global outdated", "vp why definitely-not-installed-vite-plus-snap-pkg -g --json # should not require package.json for global why" ] } From ad88b349690e50145a1b2a5b7f43e86ab3869f23 Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Fri, 22 May 2026 21:53:05 +0800 Subject: [PATCH 08/18] fix up --- .../src/commands/global/outdated.rs | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/crates/vite_global_cli/src/commands/global/outdated.rs b/crates/vite_global_cli/src/commands/global/outdated.rs index 1674257d52..f5c3c3596c 100644 --- a/crates/vite_global_cli/src/commands/global/outdated.rs +++ b/crates/vite_global_cli/src/commands/global/outdated.rs @@ -89,7 +89,7 @@ pub async fn execute( } if outdated.is_empty() { - print_empty(format, "All global packages are up to date."); + print_empty(format, empty_outdated_message(failed)); return Ok(if failed { exit_status(1) } else { ExitStatus::default() }); } @@ -124,6 +124,14 @@ fn print_empty(format: Option, message: &str) { } } +fn empty_outdated_message(failed: bool) -> &'static str { + if failed { + "Could not check all global packages for updates." + } else { + "All global packages are up to date." + } +} + fn print_json(packages: &[OutdatedPackage]) -> Result<(), Error> { let packages_dir = get_packages_dir()?; let mut output = BTreeMap::new(); @@ -244,3 +252,21 @@ fn exit_status(code: i32) -> ExitStatus { ExitStatus::from_raw(code as u32) } } + +#[cfg(test)] +mod tests { + use super::empty_outdated_message; + + #[test] + fn reports_lookup_failures_when_no_outdated_packages_are_found() { + assert_eq!( + empty_outdated_message(true), + "Could not check all global packages for updates." + ); + } + + #[test] + fn reports_up_to_date_only_when_all_lookups_succeed() { + assert_eq!(empty_outdated_message(false), "All global packages are up to date."); + } +} From 2f0959a422e20245fc68edc92ef02acb888977c3 Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Fri, 22 May 2026 22:29:32 +0800 Subject: [PATCH 09/18] feat --- crates/vite_global_cli/src/cli.rs | 21 +++++++++++++--- crates/vite_pm_cli/src/cli.rs | 4 +++ crates/vite_pm_cli/src/dispatch.rs | 1 + .../cli-helper-message/snap.txt | 25 ++++++++++--------- .../command-outdated-bun/snap.txt | 25 ++++++++++--------- .../command-outdated-global/snap.txt | 2 +- .../command-outdated-global/steps.json | 2 +- .../command-outdated-pnpm10/snap.txt | 25 ++++++++++--------- .../command-outdated-pnpm11/snap.txt | 25 ++++++++++--------- .../command-outdated-pnpm10/snap.txt | 25 ++++++++++--------- 10 files changed, 89 insertions(+), 66 deletions(-) diff --git a/crates/vite_global_cli/src/cli.rs b/crates/vite_global_cli/src/cli.rs index 8738a318d9..04510d48e3 100644 --- a/crates/vite_global_cli/src/cli.rs +++ b/crates/vite_global_cli/src/cli.rs @@ -19,6 +19,7 @@ use crate::{ }; const DEFAULT_GLOBAL_INSTALL_CONCURRENCY: usize = 5; +const DEFAULT_GLOBAL_VIEW_CONCURRENCY: usize = 3 * DEFAULT_GLOBAL_INSTALL_CONCURRENCY; #[derive(Clone, Copy, Debug)] pub struct RenderOptions { @@ -559,9 +560,21 @@ async fn run_package_manager_command( managed_update(packages, concurrency).await } - PackageManagerCommand::Outdated { global: true, ref packages, long, format, .. } => { - global::outdated::execute(packages, long, format, DEFAULT_GLOBAL_INSTALL_CONCURRENCY) - .await + PackageManagerCommand::Outdated { + global: true, + ref packages, + long, + format, + concurrency, + .. + } => { + global::outdated::execute( + packages, + long, + format, + concurrency.unwrap_or(DEFAULT_GLOBAL_VIEW_CONCURRENCY), + ) + .await } // `pm list -g` lists vite-plus-managed globals, not the underlying PM's. @@ -688,7 +701,7 @@ async fn managed_update( } } - let latest_versions = global::latest_versions_by_spec(&specs, concurrency).await?; + let latest_versions = global::latest_versions_by_spec(&specs, concurrency * 3).await?; for (package, package_name, installed_version) in installed_packages { match latest_versions.get(&package) { diff --git a/crates/vite_pm_cli/src/cli.rs b/crates/vite_pm_cli/src/cli.rs index 0427358152..ca196609e9 100644 --- a/crates/vite_pm_cli/src/cli.rs +++ b/crates/vite_pm_cli/src/cli.rs @@ -361,6 +361,10 @@ pub enum PackageManagerCommand { #[arg(short = 'g', long)] global: bool, + /// Number of global package checks to run in parallel (only with -g) + #[arg(long, requires = "global", value_parser = parse_positive_usize)] + concurrency: Option, + /// Additional arguments to pass through to the package manager #[arg(last = true, allow_hyphen_values = true)] pass_through_args: Option>, diff --git a/crates/vite_pm_cli/src/dispatch.rs b/crates/vite_pm_cli/src/dispatch.rs index 035b3c1c2b..344ccd17d5 100644 --- a/crates/vite_pm_cli/src/dispatch.rs +++ b/crates/vite_pm_cli/src/dispatch.rs @@ -217,6 +217,7 @@ pub async fn dispatch( compatible, sort_by, global, + concurrency: _, pass_through_args, } => { let options = OutdatedCommandOptions { 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 fbbed52b04..3a3c2993f1 100644 --- a/packages/cli/snap-tests-global/cli-helper-message/snap.txt +++ b/packages/cli/snap-tests-global/cli-helper-message/snap.txt @@ -246,18 +246,19 @@ Arguments: [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager Options: - --long Show extended information - --format Output format: table (default), list, or json - -r, --recursive Check recursively across all workspaces - --filter Filter packages in monorepo - -w, --workspace-root Include workspace root - -P, --prod Only production and optional dependencies - -D, --dev Only dev dependencies - --no-optional Exclude optional dependencies - --compatible Only show compatible versions - --sort-by Sort results by field - -g, --global Check globally installed packages - -h, --help Print help + --long Show extended information + --format Output format: table (default), list, or json + -r, --recursive Check recursively across all workspaces + --filter Filter packages in monorepo + -w, --workspace-root Include workspace root + -P, --prod Only production and optional dependencies + -D, --dev Only dev dependencies + --no-optional Exclude optional dependencies + --compatible Only show compatible versions + --sort-by Sort results by field + -g, --global Check globally installed packages + --concurrency Number of global package checks 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-outdated-bun/snap.txt b/packages/cli/snap-tests-global/command-outdated-bun/snap.txt index edc726829d..94e810797e 100644 --- a/packages/cli/snap-tests-global/command-outdated-bun/snap.txt +++ b/packages/cli/snap-tests-global/command-outdated-bun/snap.txt @@ -8,18 +8,19 @@ Arguments: [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager Options: - --long Show extended information - --format Output format: table (default), list, or json - -r, --recursive Check recursively across all workspaces - --filter Filter packages in monorepo - -w, --workspace-root Include workspace root - -P, --prod Only production and optional dependencies - -D, --dev Only dev dependencies - --no-optional Exclude optional dependencies - --compatible Only show compatible versions - --sort-by Sort results by field - -g, --global Check globally installed packages - -h, --help Print help + --long Show extended information + --format Output format: table (default), list, or json + -r, --recursive Check recursively across all workspaces + --filter Filter packages in monorepo + -w, --workspace-root Include workspace root + -P, --prod Only production and optional dependencies + -D, --dev Only dev dependencies + --no-optional Exclude optional dependencies + --compatible Only show compatible versions + --sort-by Sort results by field + -g, --global Check globally installed packages + --concurrency Number of global package checks 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-outdated-global/snap.txt b/packages/cli/snap-tests-global/command-outdated-global/snap.txt index 3977103e5b..30edb8e853 100644 --- a/packages/cli/snap-tests-global/command-outdated-global/snap.txt +++ b/packages/cli/snap-tests-global/command-outdated-global/snap.txt @@ -13,6 +13,6 @@ } } -[1]> vp outdated testnpm2 -g --format list # should support global list output +[1]> vp outdated testnpm2 -g --format list --concurrency 5 # should support global list output testnpm2 (global) => diff --git a/packages/cli/snap-tests-global/command-outdated-global/steps.json b/packages/cli/snap-tests-global/command-outdated-global/steps.json index 27e1a9e88e..b634d67ea3 100644 --- a/packages/cli/snap-tests-global/command-outdated-global/steps.json +++ b/packages/cli/snap-tests-global/command-outdated-global/steps.json @@ -7,7 +7,7 @@ }, "vp outdated definitely-not-installed-vite-plus-snap-pkg -g --format json # should support empty global json output", "vp outdated testnpm2 -g --format json # should support global json output", - "vp outdated testnpm2 -g --format list # should support global list output" + "vp outdated testnpm2 -g --format list --concurrency 5 # should support global list output" ], "after": ["vp remove -g testnpm2"] } diff --git a/packages/cli/snap-tests-global/command-outdated-pnpm10/snap.txt b/packages/cli/snap-tests-global/command-outdated-pnpm10/snap.txt index d481255119..b3ed2cbeed 100644 --- a/packages/cli/snap-tests-global/command-outdated-pnpm10/snap.txt +++ b/packages/cli/snap-tests-global/command-outdated-pnpm10/snap.txt @@ -8,18 +8,19 @@ Arguments: [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager Options: - --long Show extended information - --format Output format: table (default), list, or json - -r, --recursive Check recursively across all workspaces - --filter Filter packages in monorepo - -w, --workspace-root Include workspace root - -P, --prod Only production and optional dependencies - -D, --dev Only dev dependencies - --no-optional Exclude optional dependencies - --compatible Only show compatible versions - --sort-by Sort results by field - -g, --global Check globally installed packages - -h, --help Print help + --long Show extended information + --format Output format: table (default), list, or json + -r, --recursive Check recursively across all workspaces + --filter Filter packages in monorepo + -w, --workspace-root Include workspace root + -P, --prod Only production and optional dependencies + -D, --dev Only dev dependencies + --no-optional Exclude optional dependencies + --compatible Only show compatible versions + --sort-by Sort results by field + -g, --global Check globally installed packages + --concurrency Number of global package checks 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-outdated-pnpm11/snap.txt b/packages/cli/snap-tests-global/command-outdated-pnpm11/snap.txt index a6285c1272..2ed676e0ba 100644 --- a/packages/cli/snap-tests-global/command-outdated-pnpm11/snap.txt +++ b/packages/cli/snap-tests-global/command-outdated-pnpm11/snap.txt @@ -8,18 +8,19 @@ Arguments: [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager Options: - --long Show extended information - --format Output format: table (default), list, or json - -r, --recursive Check recursively across all workspaces - --filter Filter packages in monorepo - -w, --workspace-root Include workspace root - -P, --prod Only production and optional dependencies - -D, --dev Only dev dependencies - --no-optional Exclude optional dependencies - --compatible Only show compatible versions - --sort-by Sort results by field - -g, --global Check globally installed packages - -h, --help Print help + --long Show extended information + --format Output format: table (default), list, or json + -r, --recursive Check recursively across all workspaces + --filter Filter packages in monorepo + -w, --workspace-root Include workspace root + -P, --prod Only production and optional dependencies + -D, --dev Only dev dependencies + --no-optional Exclude optional dependencies + --compatible Only show compatible versions + --sort-by Sort results by field + -g, --global Check globally installed packages + --concurrency Number of global package checks to run in parallel (only with -g) + -h, --help Print help Documentation: https://viteplus.dev/guide/install diff --git a/packages/cli/snap-tests/command-outdated-pnpm10/snap.txt b/packages/cli/snap-tests/command-outdated-pnpm10/snap.txt index 645d4f85d9..71163b2b7f 100644 --- a/packages/cli/snap-tests/command-outdated-pnpm10/snap.txt +++ b/packages/cli/snap-tests/command-outdated-pnpm10/snap.txt @@ -8,18 +8,19 @@ Arguments: [PASS_THROUGH_ARGS]... Additional arguments to pass through to the package manager Options: - --long Show extended information - --format Output format: table (default), list, or json - -r, --recursive Check recursively across all workspaces - --filter Filter packages in monorepo - -w, --workspace-root Include workspace root - -P, --prod Only production and optional dependencies - -D, --dev Only dev dependencies - --no-optional Exclude optional dependencies - --compatible Only show compatible versions - --sort-by Sort results by field - -g, --global Check globally installed packages - -h, --help Print help + --long Show extended information + --format Output format: table (default), list, or json + -r, --recursive Check recursively across all workspaces + --filter Filter packages in monorepo + -w, --workspace-root Include workspace root + -P, --prod Only production and optional dependencies + -D, --dev Only dev dependencies + --no-optional Exclude optional dependencies + --compatible Only show compatible versions + --sort-by Sort results by field + -g, --global Check globally installed packages + --concurrency Number of global package checks to run in parallel (only with -g) + -h, --help Print help > vp install # should install packages first Packages: + From 549fbdbddaf223ae03d6e56367938572b09950bf Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Fri, 22 May 2026 22:56:45 +0800 Subject: [PATCH 10/18] refactor --- .../src/commands/global/outdated.rs | 77 ++++++++----------- 1 file changed, 32 insertions(+), 45 deletions(-) diff --git a/crates/vite_global_cli/src/commands/global/outdated.rs b/crates/vite_global_cli/src/commands/global/outdated.rs index f5c3c3596c..5f89251cdb 100644 --- a/crates/vite_global_cli/src/commands/global/outdated.rs +++ b/crates/vite_global_cli/src/commands/global/outdated.rs @@ -1,6 +1,9 @@ //! Check managed global packages for newer registry versions. -use std::{collections::BTreeMap, process::ExitStatus}; +use std::{ + collections::{BTreeMap, HashMap}, + process::ExitStatus, +}; use owo_colors::OwoColorize; use serde::Serialize; @@ -41,6 +44,8 @@ pub async fn execute( format: Option, concurrency: usize, ) -> Result { + // 1. Resolve the command arguments to vite-plus-managed global packages. + // A missing explicit package is a command result, not an internal error. let installed = matching_packages(packages).await?; if installed.is_empty() { if packages.is_empty() { @@ -53,44 +58,50 @@ pub async fn execute( return Ok(exit_status(1)); } + // 2. Query the registry for the latest version of each installed package. + // A registry setup failure is fatal. A package-level lookup failure is + // reported below and only affects the final exit status. let specs = installed.iter().map(|package| package.name.clone()).collect::>(); - let mut latest_versions = latest_versions_by_spec(&specs, concurrency).await?; - let mut failed = false; - for (package_spec, version) in &latest_versions { - if let Err(error) = version { - vite_shared::output::raw_stderr(&format!( - "Could not check latest version for {package_spec}: {error}" - )); - failed = true; - } + let mut latest_versions_map = HashMap::new(); + for (package_spec, version) in latest_versions_by_spec(&specs, concurrency).await? { + match version { + Ok(version) => latest_versions_map.insert(package_spec, version), + Err(error) => { + vite_shared::output::error(&format!( + "Could not check latest version for {package_spec}: {error}" + )); + return Err(error); + } + }; } + let mut latest_versions = latest_versions_map; + // 3. Compare installed metadata with registry versions. Packages whose + // registry lookup failed are skipped because there is no version to compare. let mut outdated = Vec::new(); for package in installed { let Some(version) = latest_versions.remove(&package.name) else { continue; }; - let latest = match version { - Ok(version) => version, - Err(_) => continue, - }; - if package.version.trim() == latest.trim() { + if package.version.trim() == version.trim() { continue; } outdated.push(OutdatedPackage { name: package.name, current: package.version, - latest, + latest: version, node: package.platform.node, bins: package.bins, }); } + // 4. Render the requested output format and return npm-compatible status: + // 0 means fully checked and up to date; 1 means outdated or incomplete. if outdated.is_empty() { - print_empty(format, empty_outdated_message(failed)); - return Ok(if failed { exit_status(1) } else { ExitStatus::default() }); + print_empty(format, "All global packages are up to date."); + return Ok(ExitStatus::default()); } match format { @@ -124,14 +135,6 @@ fn print_empty(format: Option, message: &str) { } } -fn empty_outdated_message(failed: bool) -> &'static str { - if failed { - "Could not check all global packages for updates." - } else { - "All global packages are up to date." - } -} - fn print_json(packages: &[OutdatedPackage]) -> Result<(), Error> { let packages_dir = get_packages_dir()?; let mut output = BTreeMap::new(); @@ -144,7 +147,9 @@ fn print_json(packages: &[OutdatedPackage]) -> Result<(), Error> { package.name.clone(), OutdatedPackageJson { current: package.current.clone(), - wanted: package.latest.clone(), + // npm always print current version as wanted. + // https://docs.npmjs.com/cli/v11/commands/npm-outdated + wanted: package.current.clone(), latest: package.latest.clone(), dependent: "global", location: location.as_path().display().to_string(), @@ -252,21 +257,3 @@ fn exit_status(code: i32) -> ExitStatus { ExitStatus::from_raw(code as u32) } } - -#[cfg(test)] -mod tests { - use super::empty_outdated_message; - - #[test] - fn reports_lookup_failures_when_no_outdated_packages_are_found() { - assert_eq!( - empty_outdated_message(true), - "Could not check all global packages for updates." - ); - } - - #[test] - fn reports_up_to_date_only_when_all_lookups_succeed() { - assert_eq!(empty_outdated_message(false), "All global packages are up to date."); - } -} From 77753e053479e95e2ad475b3664507d304583aef Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Fri, 22 May 2026 23:04:28 +0800 Subject: [PATCH 11/18] refactor --- crates/vite_global_cli/src/cli.rs | 4 +- .../src/commands/global/mod.rs | 47 ++++++++++--------- .../src/commands/global/outdated.rs | 4 +- 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/crates/vite_global_cli/src/cli.rs b/crates/vite_global_cli/src/cli.rs index 04510d48e3..c8735f43fc 100644 --- a/crates/vite_global_cli/src/cli.rs +++ b/crates/vite_global_cli/src/cli.rs @@ -652,7 +652,7 @@ async fn managed_update( if let Some(all) = all_packages { let specs = all.iter().map(|metadata| metadata.name.clone()).collect::>(); - let latest_versions = global::latest_versions_by_spec(&specs, concurrency).await?; + let latest_versions = global::latest_package_versions(&specs, concurrency).await?; for metadata in all { match latest_versions.get(&metadata.name) { @@ -701,7 +701,7 @@ async fn managed_update( } } - let latest_versions = global::latest_versions_by_spec(&specs, concurrency * 3).await?; + let latest_versions = global::latest_package_versions(&specs, concurrency * 3).await?; for (package, package_name, installed_version) in installed_packages { match latest_versions.get(&package) { diff --git a/crates/vite_global_cli/src/commands/global/mod.rs b/crates/vite_global_cli/src/commands/global/mod.rs index d505e9a053..4915a68d37 100644 --- a/crates/vite_global_cli/src/commands/global/mod.rs +++ b/crates/vite_global_cli/src/commands/global/mod.rs @@ -1,8 +1,9 @@ //! Managed global package utilities. -use std::{collections::HashMap, process::Stdio}; +use std::{collections::HashMap, io::IsTerminal, process::Stdio, time::Duration}; use futures::{StreamExt, stream::FuturesUnordered}; +use indicatif::{ProgressBar, ProgressDrawTarget, ProgressStyle}; use tokio::process::Command; use vite_path::{AbsolutePathBuf, current_dir}; use vite_shared::format_path_prepended; @@ -66,19 +67,31 @@ impl NpmRegistry { } } -async fn latest_package_versions( - package_specs: &[String], +pub(crate) async fn latest_package_versions( + specs: &[String], concurrency: usize, -) -> Result, Error> { - if package_specs.is_empty() { - return Ok(Vec::new()); +) -> Result>, Error> { + if specs.is_empty() { + return Ok(HashMap::new()); } let registry = NpmRegistry::resolve().await?; let concurrency = concurrency.max(1); - let mut package_specs = package_specs.iter(); - let mut versions = Vec::with_capacity(package_specs.len()); - // Check packages in parallel + let mut package_specs = specs.iter(); + let mut versions = HashMap::with_capacity(specs.len()); + + let progress = ProgressBar::new(specs.len() 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("Checking latest package versions"); + progress.enable_steady_tick(Duration::from_millis(80)); + } else { + progress.set_draw_target(ProgressDrawTarget::hidden()); + } + let mut queries = FuturesUnordered::new(); loop { @@ -96,25 +109,15 @@ async fn latest_package_versions( } if let Some(version) = queries.next().await { - versions.push(version); + progress.inc(1); + versions.insert(version.package_spec, version.version); } } + progress.finish_and_clear(); Ok(versions) } -pub(crate) async fn latest_versions_by_spec( - specs: &[String], - concurrency: usize, -) -> Result>, Error> { - let versions = latest_package_versions(specs, concurrency).await?; - let mut latest_versions = HashMap::with_capacity(versions.len()); - for version in versions { - latest_versions.insert(version.package_spec, version.version); - } - Ok(latest_versions) -} - /// Return true for package specs that refer to local filesystem content. pub(crate) fn is_local_package_spec(spec: &str) -> bool { spec == "." diff --git a/crates/vite_global_cli/src/commands/global/outdated.rs b/crates/vite_global_cli/src/commands/global/outdated.rs index 5f89251cdb..d526513f3e 100644 --- a/crates/vite_global_cli/src/commands/global/outdated.rs +++ b/crates/vite_global_cli/src/commands/global/outdated.rs @@ -9,7 +9,7 @@ use owo_colors::OwoColorize; use serde::Serialize; use vite_install::commands::outdated::Format; -use super::{latest_versions_by_spec, parse_package_spec}; +use super::{latest_package_versions, parse_package_spec}; use crate::{ commands::env::{ config::{get_node_modules_dir, get_packages_dir}, @@ -64,7 +64,7 @@ pub async fn execute( let specs = installed.iter().map(|package| package.name.clone()).collect::>(); let mut latest_versions_map = HashMap::new(); - for (package_spec, version) in latest_versions_by_spec(&specs, concurrency).await? { + for (package_spec, version) in latest_package_versions(&specs, concurrency).await? { match version { Ok(version) => latest_versions_map.insert(package_spec, version), Err(error) => { From 48d693cdaded21ac75c3bd9b9ed39f4d588bf9d0 Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Fri, 22 May 2026 23:09:10 +0800 Subject: [PATCH 12/18] test: update global outdated snapshot --- packages/cli/snap-tests-global/command-outdated-global/snap.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/snap-tests-global/command-outdated-global/snap.txt b/packages/cli/snap-tests-global/command-outdated-global/snap.txt index 30edb8e853..14f7e59cc9 100644 --- a/packages/cli/snap-tests-global/command-outdated-global/snap.txt +++ b/packages/cli/snap-tests-global/command-outdated-global/snap.txt @@ -6,7 +6,7 @@ { "testnpm2": { "current": "1.0.0", - "wanted": "1.0.1", + "wanted": "1.0.0", "latest": "1.0.1", "dependent": "global", "location": "/packages/testnpm2/lib/node_modules/testnpm2" From 0392b8ce96c0cd49897e7de977d8ce0b3ae53b1c Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Sat, 23 May 2026 15:06:47 +0800 Subject: [PATCH 13/18] wip --- crates/vite_global_cli/src/cli.rs | 110 +++--------------- .../src/commands/global/outdated.rs | 99 ++++++++-------- 2 files changed, 67 insertions(+), 142 deletions(-) diff --git a/crates/vite_global_cli/src/cli.rs b/crates/vite_global_cli/src/cli.rs index c8735f43fc..757b519725 100644 --- a/crates/vite_global_cli/src/cli.rs +++ b/crates/vite_global_cli/src/cli.rs @@ -627,114 +627,50 @@ async fn managed_uninstall(packages: &[String], dry_run: bool) -> Result bool { - installed_version.trim() == registry_version.trim() -} - async fn managed_update( packages: &[String], concurrency: Option, ) -> Result { let concurrency = concurrency.unwrap_or(DEFAULT_GLOBAL_INSTALL_CONCURRENCY); - let all_packages = if packages.is_empty() { + let mut to_update: Vec = Vec::new(); + + let packages = if packages.is_empty() { let all = PackageMetadata::list_all().await?; if all.is_empty() { vite_shared::output::raw("No global packages installed."); return Ok(ExitStatus::default()); } - Some(all) - } else { - None - }; - let mut to_update: Vec = Vec::new(); - let mut skipped = 0usize; - - if let Some(all) = all_packages { - let specs = all.iter().map(|metadata| metadata.name.clone()).collect::>(); - let latest_versions = global::latest_package_versions(&specs, concurrency).await?; - - for metadata in all { - match latest_versions.get(&metadata.name) { - Some(Ok(latest_version)) - if is_global_package_up_to_date(&metadata.version, latest_version) => - { - vite_shared::output::raw(&format!( - "{} is already up to date (v{}).", - metadata.name, metadata.version - )); - skipped += 1; - } - Some(Ok(_)) => to_update.push(metadata.name.clone()), - Some(Err(e)) => { - vite_shared::output::raw_stderr(&format!( - "Could not check latest version for {}: {e}; updating anyway.", - metadata.name - )); - to_update.push(metadata.name.clone()); - } - None => { - vite_shared::output::raw_stderr(&format!( - "Could not check latest version for {}; updating anyway.", - metadata.name - )); - to_update.push(metadata.name.clone()); - } - } - } + None } else { - let mut specs = Vec::new(); - let mut installed_packages = Vec::new(); + let mut managed_specs = Vec::new(); for package in packages { + // Always update local packages if global::is_local_package_spec(package) { to_update.push(package.clone()); continue; } let (package_name, _) = global::parse_package_spec(package); - if let Some(metadata) = PackageMetadata::load(&package_name).await? { - specs.push(package.clone()); - installed_packages.push((package.clone(), package_name, metadata.version)); + if PackageMetadata::load(&package_name).await?.is_some() { + managed_specs.push(package.clone()); } else { to_update.push(package.clone()); } } - let latest_versions = global::latest_package_versions(&specs, concurrency * 3).await?; - - for (package, package_name, installed_version) in installed_packages { - match latest_versions.get(&package) { - Some(Ok(latest_version)) - if is_global_package_up_to_date(&installed_version, latest_version) => - { - vite_shared::output::raw(&format!( - "{} is already up to date (v{}).", - package_name, installed_version - )); - skipped += 1; - continue; - } - Some(Ok(_)) => {} - Some(Err(e)) => { - vite_shared::output::raw_stderr(&format!( - "Could not check latest version for {package}: {e}; updating anyway." - )); - } - None => { - vite_shared::output::raw_stderr(&format!( - "Could not check latest version for {package}; updating anyway." - )); - } - } - to_update.push(package.clone()); - } - } + Some(managed_specs) + }; + to_update.extend( + global::outdated::get_outdated_packages(packages.as_deref(), concurrency * 3) + .await? + .into_iter() + .map(|package| package.name), + ); if to_update.is_empty() { - if skipped > 0 { - vite_shared::output::raw("All global packages are up to date."); - } + vite_shared::output::raw("All global packages are up to date."); return Ok(ExitStatus::default()); } @@ -993,20 +929,10 @@ pub fn try_parse_args_from_with_options( #[cfg(test)] mod tests { use super::{ - has_flag_before_terminator, is_global_package_up_to_date, should_force_global_delegate, + has_flag_before_terminator, should_force_global_delegate, should_suppress_header_for_subcommand, }; - #[test] - fn skips_global_update_when_registry_and_node_versions_match() { - assert!(is_global_package_up_to_date("5.9.3", "5.9.3")); - } - - #[test] - fn updates_global_package_when_registry_version_differs_from_installed_version() { - assert!(!is_global_package_up_to_date("5.9.2", "5.9.3")); - } - #[test] fn detects_flag_before_option_terminator() { assert!(has_flag_before_terminator( diff --git a/crates/vite_global_cli/src/commands/global/outdated.rs b/crates/vite_global_cli/src/commands/global/outdated.rs index d526513f3e..c7a5020f55 100644 --- a/crates/vite_global_cli/src/commands/global/outdated.rs +++ b/crates/vite_global_cli/src/commands/global/outdated.rs @@ -18,16 +18,17 @@ use crate::{ error::Error, }; -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -struct OutdatedPackage { - name: String, - current: String, - latest: String, +#[derive(Debug)] +pub struct OutdatedPackage { + pub name: String, + pub current: String, + pub latest: String, node: String, bins: Vec, } +/// For json output in `vp outdated` command +/// Use `npm outdated --json`'s data structure #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] struct OutdatedPackageJson { @@ -38,30 +39,36 @@ struct OutdatedPackageJson { location: String, } -pub async fn execute( - packages: &[String], - long: bool, - format: Option, +pub async fn get_outdated_packages( + packages: Option<&[String]>, concurrency: usize, -) -> Result { +) -> Result, Error> { // 1. Resolve the command arguments to vite-plus-managed global packages. // A missing explicit package is a command result, not an internal error. - let installed = matching_packages(packages).await?; - if installed.is_empty() { - if packages.is_empty() { - print_empty(format, "No global packages installed."); - return Ok(ExitStatus::default()); + let installed = if let Some(packages) = packages { + let mut installed = Vec::new(); + for package in packages { + let (package_name, _) = parse_package_spec(package); + if let Some(metadata) = PackageMetadata::load(&package_name).await? { + installed.push((metadata, Some(package.clone()))); + } } + installed + } else { + PackageMetadata::list_all().await?.into_iter().map(|package| (package, None)).collect() + }; - let names = packages.join(", "); - print_empty(format, &format!("No matching global packages installed: {names}")); - return Ok(exit_status(1)); + if installed.is_empty() { + return Ok(Vec::new()); } - // 2. Query the registry for the latest version of each installed package. + // 2. Query the registry for the latest version of each matching package. // A registry setup failure is fatal. A package-level lookup failure is - // reported below and only affects the final exit status. - let specs = installed.iter().map(|package| package.name.clone()).collect::>(); + // returned as an error because there is no version to compare. + let specs = installed + .iter() + .map(|(package, spec)| spec.clone().unwrap_or_else(|| package.name.clone())) + .collect::>(); let mut latest_versions_map = HashMap::new(); for (package_spec, version) in latest_package_versions(&specs, concurrency).await? { @@ -80,8 +87,9 @@ pub async fn execute( // 3. Compare installed metadata with registry versions. Packages whose // registry lookup failed are skipped because there is no version to compare. let mut outdated = Vec::new(); - for package in installed { - let Some(version) = latest_versions.remove(&package.name) else { + for (package, spec) in installed { + let key = spec.unwrap_or_else(|| package.name.clone()); + let Some(version) = latest_versions.remove(&key) else { continue; }; if package.version.trim() == version.trim() { @@ -97,10 +105,23 @@ pub async fn execute( }); } - // 4. Render the requested output format and return npm-compatible status: - // 0 means fully checked and up to date; 1 means outdated or incomplete. + Ok(outdated) +} + +pub async fn execute( + packages: &[String], + long: bool, + format: Option, + concurrency: usize, +) -> Result { + let outdated = get_outdated_packages(Some(packages), concurrency).await?; + + // Exit code 0 means fully checked and up to date; 1 means outdated or incomplete. + // To follow other pms' behavior, we do not print message there. if outdated.is_empty() { - print_empty(format, "All global packages are up to date."); + if let Some(Format::Json) = format { + vite_shared::output::raw("{}"); + } return Ok(ExitStatus::default()); } @@ -113,28 +134,6 @@ pub async fn execute( Ok(exit_status(1)) } -async fn matching_packages(packages: &[String]) -> Result, Error> { - if packages.is_empty() { - return PackageMetadata::list_all().await; - } - - let mut installed = Vec::new(); - for package in packages { - let (package_name, _) = parse_package_spec(package); - if let Some(metadata) = PackageMetadata::load(&package_name).await? { - installed.push(metadata); - } - } - Ok(installed) -} - -fn print_empty(format: Option, message: &str) { - match format { - Some(Format::Json) => println!("{{}}"), - _ => println!("{message}"), - } -} - fn print_json(packages: &[OutdatedPackage]) -> Result<(), Error> { let packages_dir = get_packages_dir()?; let mut output = BTreeMap::new(); @@ -149,7 +148,7 @@ fn print_json(packages: &[OutdatedPackage]) -> Result<(), Error> { current: package.current.clone(), // npm always print current version as wanted. // https://docs.npmjs.com/cli/v11/commands/npm-outdated - wanted: package.current.clone(), + wanted: package.latest.clone(), latest: package.latest.clone(), dependent: "global", location: location.as_path().display().to_string(), From 4346b1310d298bfc6666ef0361acbca360debfe7 Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Sat, 23 May 2026 15:10:36 +0800 Subject: [PATCH 14/18] enhance --- .../src/commands/global/outdated.rs | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/vite_global_cli/src/commands/global/outdated.rs b/crates/vite_global_cli/src/commands/global/outdated.rs index c7a5020f55..9cf528590a 100644 --- a/crates/vite_global_cli/src/commands/global/outdated.rs +++ b/crates/vite_global_cli/src/commands/global/outdated.rs @@ -75,9 +75,6 @@ pub async fn get_outdated_packages( match version { Ok(version) => latest_versions_map.insert(package_spec, version), Err(error) => { - vite_shared::output::error(&format!( - "Could not check latest version for {package_spec}: {error}" - )); return Err(error); } }; @@ -114,13 +111,24 @@ pub async fn execute( format: Option, concurrency: usize, ) -> Result { - let outdated = get_outdated_packages(Some(packages), concurrency).await?; + let outdated = match get_outdated_packages(Some(packages), concurrency).await { + Ok(outdated) => outdated, + Err(error) => { + if let Some(Format::Json) = format { + vite_shared::output::raw("{}"); + } else { + vite_shared::output::error(&format!("Could not get outdated packages: {error}")); + } + return Err(error); + } + }; // Exit code 0 means fully checked and up to date; 1 means outdated or incomplete. - // To follow other pms' behavior, we do not print message there. if outdated.is_empty() { if let Some(Format::Json) = format { vite_shared::output::raw("{}"); + } else { + vite_shared::output::info("All global packages are up to date."); } return Ok(ExitStatus::default()); } @@ -146,8 +154,6 @@ fn print_json(packages: &[OutdatedPackage]) -> Result<(), Error> { package.name.clone(), OutdatedPackageJson { current: package.current.clone(), - // npm always print current version as wanted. - // https://docs.npmjs.com/cli/v11/commands/npm-outdated wanted: package.latest.clone(), latest: package.latest.clone(), dependent: "global", From 6ded1ea862e6696a09f0815e87d64be5c34d872b Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Sat, 23 May 2026 15:14:26 +0800 Subject: [PATCH 15/18] adjust --- .../cli/snap-tests-global/command-outdated-global/snap.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/snap-tests-global/command-outdated-global/snap.txt b/packages/cli/snap-tests-global/command-outdated-global/snap.txt index 14f7e59cc9..2e610fa165 100644 --- a/packages/cli/snap-tests-global/command-outdated-global/snap.txt +++ b/packages/cli/snap-tests-global/command-outdated-global/snap.txt @@ -1,12 +1,12 @@ > vp install -g testnpm2@1.0.0 # should prepare global outdated package -[1]> vp outdated definitely-not-installed-vite-plus-snap-pkg -g --format json # should support empty global json output +> vp outdated definitely-not-installed-vite-plus-snap-pkg -g --format json # should support empty global json output {} [1]> vp outdated testnpm2 -g --format json # should support global json output { "testnpm2": { "current": "1.0.0", - "wanted": "1.0.0", + "wanted": "1.0.1", "latest": "1.0.1", "dependent": "global", "location": "/packages/testnpm2/lib/node_modules/testnpm2" From 7b905c693f7ee027a5940c55f1a0d263259a2049 Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Sun, 24 May 2026 21:20:58 +0800 Subject: [PATCH 16/18] fix --- crates/vite_global_cli/src/cli.rs | 2 +- crates/vite_global_cli/src/commands/global/outdated.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/vite_global_cli/src/cli.rs b/crates/vite_global_cli/src/cli.rs index 757b519725..437b259913 100644 --- a/crates/vite_global_cli/src/cli.rs +++ b/crates/vite_global_cli/src/cli.rs @@ -663,7 +663,7 @@ async fn managed_update( Some(managed_specs) }; to_update.extend( - global::outdated::get_outdated_packages(packages.as_deref(), concurrency * 3) + global::outdated::get_outdated_packages(&packages.unwrap_or(Vec::new()), concurrency * 3) .await? .into_iter() .map(|package| package.name), diff --git a/crates/vite_global_cli/src/commands/global/outdated.rs b/crates/vite_global_cli/src/commands/global/outdated.rs index 9cf528590a..08e3b7d630 100644 --- a/crates/vite_global_cli/src/commands/global/outdated.rs +++ b/crates/vite_global_cli/src/commands/global/outdated.rs @@ -40,12 +40,12 @@ struct OutdatedPackageJson { } pub async fn get_outdated_packages( - packages: Option<&[String]>, + packages: &[String], concurrency: usize, ) -> Result, Error> { // 1. Resolve the command arguments to vite-plus-managed global packages. // A missing explicit package is a command result, not an internal error. - let installed = if let Some(packages) = packages { + let installed = if !packages.is_empty() { let mut installed = Vec::new(); for package in packages { let (package_name, _) = parse_package_spec(package); @@ -111,7 +111,7 @@ pub async fn execute( format: Option, concurrency: usize, ) -> Result { - let outdated = match get_outdated_packages(Some(packages), concurrency).await { + let outdated = match get_outdated_packages(packages, concurrency).await { Ok(outdated) => outdated, Err(error) => { if let Some(Format::Json) = format { From ca51504314e6e5dff0b49f15d2030470c3cd5afa Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Sun, 24 May 2026 21:33:45 +0800 Subject: [PATCH 17/18] fix --- crates/vite_global_cli/src/cli.rs | 12 ++++++++---- .../vite_global_cli/src/commands/global/outdated.rs | 13 +++++++++---- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/crates/vite_global_cli/src/cli.rs b/crates/vite_global_cli/src/cli.rs index 437b259913..258680395c 100644 --- a/crates/vite_global_cli/src/cli.rs +++ b/crates/vite_global_cli/src/cli.rs @@ -663,10 +663,14 @@ async fn managed_update( Some(managed_specs) }; to_update.extend( - global::outdated::get_outdated_packages(&packages.unwrap_or(Vec::new()), concurrency * 3) - .await? - .into_iter() - .map(|package| package.name), + global::outdated::get_outdated_packages( + &packages.unwrap_or(Vec::new()), + concurrency * 3, + true, + ) + .await? + .into_iter() + .map(|package| package.name), ); if to_update.is_empty() { diff --git a/crates/vite_global_cli/src/commands/global/outdated.rs b/crates/vite_global_cli/src/commands/global/outdated.rs index 08e3b7d630..eacab55bfa 100644 --- a/crates/vite_global_cli/src/commands/global/outdated.rs +++ b/crates/vite_global_cli/src/commands/global/outdated.rs @@ -42,6 +42,7 @@ struct OutdatedPackageJson { pub async fn get_outdated_packages( packages: &[String], concurrency: usize, + error_on_fail: bool, ) -> Result, Error> { // 1. Resolve the command arguments to vite-plus-managed global packages. // A missing explicit package is a command result, not an internal error. @@ -73,11 +74,15 @@ pub async fn get_outdated_packages( let mut latest_versions_map = HashMap::new(); for (package_spec, version) in latest_package_versions(&specs, concurrency).await? { match version { - Ok(version) => latest_versions_map.insert(package_spec, version), + Ok(version) => { + latest_versions_map.insert(package_spec, version); + } Err(error) => { - return Err(error); + if error_on_fail { + return Err(error); + } } - }; + } } let mut latest_versions = latest_versions_map; @@ -111,7 +116,7 @@ pub async fn execute( format: Option, concurrency: usize, ) -> Result { - let outdated = match get_outdated_packages(packages, concurrency).await { + let outdated = match get_outdated_packages(packages, concurrency, false).await { Ok(outdated) => outdated, Err(error) => { if let Some(Format::Json) = format { From a50830e0f4cd47faa23d9b8287a81c90ef818c77 Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Sun, 24 May 2026 21:39:04 +0800 Subject: [PATCH 18/18] fix --- crates/vite_global_cli/src/cli.rs | 2 +- crates/vite_global_cli/src/commands/global/outdated.rs | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/vite_global_cli/src/cli.rs b/crates/vite_global_cli/src/cli.rs index 258680395c..4835ea917d 100644 --- a/crates/vite_global_cli/src/cli.rs +++ b/crates/vite_global_cli/src/cli.rs @@ -670,7 +670,7 @@ async fn managed_update( ) .await? .into_iter() - .map(|package| package.name), + .map(|package| package.spec.unwrap_or(package.name)), ); if to_update.is_empty() { diff --git a/crates/vite_global_cli/src/commands/global/outdated.rs b/crates/vite_global_cli/src/commands/global/outdated.rs index eacab55bfa..233473caf0 100644 --- a/crates/vite_global_cli/src/commands/global/outdated.rs +++ b/crates/vite_global_cli/src/commands/global/outdated.rs @@ -23,6 +23,7 @@ pub struct OutdatedPackage { pub name: String, pub current: String, pub latest: String, + pub spec: Option, node: String, bins: Vec, } @@ -90,8 +91,9 @@ pub async fn get_outdated_packages( // registry lookup failed are skipped because there is no version to compare. let mut outdated = Vec::new(); for (package, spec) in installed { - let key = spec.unwrap_or_else(|| package.name.clone()); - let Some(version) = latest_versions.remove(&key) else { + let default_key = package.name.clone(); + let key = spec.as_deref().unwrap_or(&default_key); + let Some(version) = latest_versions.remove(key) else { continue; }; if package.version.trim() == version.trim() { @@ -102,6 +104,7 @@ pub async fn get_outdated_packages( name: package.name, current: package.version, latest: version, + spec, node: package.platform.node, bins: package.bins, });