From 224dd80228052582af2a5f628783edc207ce65df Mon Sep 17 00:00:00 2001 From: wuyangfan <1102042793@qq.com> Date: Sat, 16 May 2026 13:27:55 +0800 Subject: [PATCH 1/2] fix(global): skip current packages on update --- crates/vite_global_cli/src/cli.rs | 73 +++++++++++++-- .../src/commands/env/global_install.rs | 89 ++++++++++++++++++- 2 files changed, 154 insertions(+), 8 deletions(-) diff --git a/crates/vite_global_cli/src/cli.rs b/crates/vite_global_cli/src/cli.rs index 4e864f555b..e8bf465d6c 100644 --- a/crates/vite_global_cli/src/cli.rs +++ b/crates/vite_global_cli/src/cli.rs @@ -573,21 +573,68 @@ async fn managed_uninstall(packages: &[String], dry_run: bool) -> Result bool { + installed_version.trim() == registry_version.trim() +} + async fn managed_update(packages: &[String]) -> Result { - use crate::commands::env::package_metadata::PackageMetadata; + use crate::commands::env::{global_install, package_metadata::PackageMetadata}; + + let mut to_update: Vec = Vec::new(); + let mut skipped = 0usize; - let to_update: Vec = if packages.is_empty() { + 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()); } - all.iter().map(|p| p.name.clone()).collect() + + for metadata in all { + let latest_version = global_install::latest_package_version( + &metadata.name, + Some(&metadata.platform.node), + ) + .await?; + 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; + } else { + to_update.push(metadata.name.clone()); + } + } } else { - packages.to_vec() - }; + for package in packages { + let (package_name, _) = global_install::parse_package_spec(package); + if let Some(metadata) = PackageMetadata::load(&package_name).await? { + let latest_version = + global_install::latest_package_version(package, Some(&metadata.platform.node)) + .await?; + 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; + } + } + to_update.push(package.clone()); + } + } + + if to_update.is_empty() { + if skipped > 0 { + vite_shared::output::raw("All global packages are up to date."); + } + return Ok(ExitStatus::default()); + } + for package in &to_update { - if let Err(e) = crate::commands::env::global_install::install(package, None, false).await { + if let Err(e) = global_install::install(package, None, false).await { vite_shared::output::raw_stderr(&format!("Failed to update {package}: {e}")); return Ok(exit_status(1)); } @@ -829,7 +876,19 @@ pub fn try_parse_args_from_with_options( #[cfg(test)] mod tests { - use super::{has_flag_before_terminator, should_force_global_delegate}; + use super::{ + has_flag_before_terminator, is_global_package_up_to_date, should_force_global_delegate, + }; + + #[test] + fn skips_global_update_when_registry_version_matches_installed_version() { + 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() { 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 327478d37c..0dcafdfe82 100644 --- a/crates/vite_global_cli/src/commands/env/global_install.rs +++ b/crates/vite_global_cli/src/commands/env/global_install.rs @@ -267,8 +267,71 @@ 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, + node_version: Option<&str>, +) -> Result { + let version = if let Some(v) = node_version { + let provider = NodeProvider::new(); + resolve_version_alias(v, &provider).await? + } else { + let cwd = current_dir().map_err(|e| { + Error::ConfigError(format!("Cannot get current directory: {}", e).into()) + })?; + let resolution = resolve_version(&cwd).await?; + resolution.version + }; + + let runtime = + vite_js_runtime::download_runtime(vite_js_runtime::JsRuntimeType::Node, &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") }; + + 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()), + } +} + /// Parse package spec into name and optional version. -fn parse_package_spec(spec: &str) -> (String, Option) { +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 @@ -699,6 +762,30 @@ 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_parse_package_spec_simple() { let (name, version) = parse_package_spec("typescript"); From ac4ed26b87995d4dcec429d13fc1e7654b5ec004 Mon Sep 17 00:00:00 2001 From: wuyangfan <1102042793@qq.com> Date: Sat, 16 May 2026 17:54:05 +0800 Subject: [PATCH 2/2] fix(global): handle update review edge cases --- crates/vite_global_cli/src/cli.rs | 128 +++++++++++++----- .../src/commands/env/global_install.rs | 31 +++++ 2 files changed, 125 insertions(+), 34 deletions(-) diff --git a/crates/vite_global_cli/src/cli.rs b/crates/vite_global_cli/src/cli.rs index e8bf465d6c..573684c62c 100644 --- a/crates/vite_global_cli/src/cli.rs +++ b/crates/vite_global_cli/src/cli.rs @@ -8,7 +8,7 @@ use std::{ffi::OsStr, process::ExitStatus}; use clap::{CommandFactory, FromArgMatches, Parser, Subcommand}; use clap_complete::ArgValueCompleter; use tokio::runtime::Runtime; -use vite_path::AbsolutePathBuf; +use vite_path::{AbsolutePath, AbsolutePathBuf}; use vite_pm_cli::PackageManagerCommand; use crate::{commands, error::Error, help}; @@ -528,7 +528,7 @@ async fn run_package_manager_command( } PackageManagerCommand::Update { global: true, ref packages, .. } => { - managed_update(packages).await + managed_update(&cwd, packages).await } // `pm list -g` lists vite-plus-managed globals, not the underlying PM's. @@ -573,54 +573,109 @@ async fn managed_uninstall(packages: &[String], dry_run: bool) -> Result bool { +fn is_global_package_up_to_date( + installed_version: &str, + registry_version: &str, + installed_node_version: &str, + target_node_version: &str, +) -> bool { installed_version.trim() == registry_version.trim() + && installed_node_version.trim() == target_node_version.trim() } -async fn managed_update(packages: &[String]) -> Result { - use crate::commands::env::{global_install, package_metadata::PackageMetadata}; - - let mut to_update: Vec = Vec::new(); - let mut skipped = 0usize; +async fn managed_update(cwd: &AbsolutePath, packages: &[String]) -> Result { + use crate::commands::env::{ + config::resolve_version, global_install, package_metadata::PackageMetadata, + }; - if packages.is_empty() { + let all_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 target_node_version = resolve_version(cwd).await?.version; + let mut to_update: Vec = Vec::new(); + let mut skipped = 0usize; + if let Some(all) = all_packages { for metadata in all { - let latest_version = global_install::latest_package_version( - &metadata.name, - Some(&metadata.platform.node), - ) - .await?; - 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; - } else { + if metadata.platform.node.trim() != target_node_version.trim() { to_update.push(metadata.name.clone()); + continue; + } + + match global_install::latest_package_version(&metadata.name, Some(&target_node_version)) + .await + { + Ok(latest_version) + if is_global_package_up_to_date( + &metadata.version, + &latest_version, + &metadata.platform.node, + &target_node_version, + ) => + { + vite_shared::output::raw(&format!( + "{} is already up to date (v{}).", + metadata.name, metadata.version + )); + skipped += 1; + } + Ok(_) => to_update.push(metadata.name.clone()), + 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()); + } } } } else { for package in packages { + if global_install::is_local_package_spec(package) { + to_update.push(package.clone()); + continue; + } + let (package_name, _) = global_install::parse_package_spec(package); if let Some(metadata) = PackageMetadata::load(&package_name).await? { - let latest_version = - global_install::latest_package_version(package, Some(&metadata.platform.node)) - .await?; - 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; + if metadata.platform.node.trim() != target_node_version.trim() { + to_update.push(package.clone()); continue; } + + match global_install::latest_package_version(package, Some(&target_node_version)) + .await + { + Ok(latest_version) + if is_global_package_up_to_date( + &metadata.version, + &latest_version, + &metadata.platform.node, + &target_node_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." + )); + } + } } to_update.push(package.clone()); } @@ -634,7 +689,7 @@ async fn managed_update(packages: &[String]) -> Result { } for package in &to_update { - if let Err(e) = global_install::install(package, None, false).await { + if let Err(e) = global_install::install(package, Some(&target_node_version), false).await { vite_shared::output::raw_stderr(&format!("Failed to update {package}: {e}")); return Ok(exit_status(1)); } @@ -881,13 +936,18 @@ mod tests { }; #[test] - fn skips_global_update_when_registry_version_matches_installed_version() { - assert!(is_global_package_up_to_date("5.9.3", "5.9.3")); + fn skips_global_update_when_registry_and_node_versions_match() { + assert!(is_global_package_up_to_date("5.9.3", "5.9.3", "20.18.0", "20.18.0")); } #[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")); + assert!(!is_global_package_up_to_date("5.9.2", "5.9.3", "20.18.0", "20.18.0")); + } + + #[test] + fn updates_global_package_when_node_version_differs_from_target_version() { + assert!(!is_global_package_up_to_date("5.9.3", "5.9.3", "20.18.0", "24.15.0")); } #[test] 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 0dcafdfe82..bbba413eb9 100644 --- a/crates/vite_global_cli/src/commands/env/global_install.rs +++ b/crates/vite_global_cli/src/commands/env/global_install.rs @@ -330,6 +330,20 @@ fn parse_npm_view_version(stdout: &[u8]) -> Result { } } +/// 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 @@ -786,6 +800,23 @@ mod tests { assert!(err.to_string().contains("empty version")); } + #[test] + fn test_is_local_package_spec_relative_paths() { + assert!(is_local_package_spec(".")); + assert!(is_local_package_spec("..")); + assert!(is_local_package_spec("./pkg")); + assert!(is_local_package_spec("../pkg")); + assert!(is_local_package_spec("file:../pkg")); + } + + #[test] + fn test_is_local_package_spec_registry_packages() { + assert!(!is_local_package_spec("typescript")); + assert!(!is_local_package_spec("typescript@5.9.3")); + assert!(!is_local_package_spec("@scope/pkg")); + assert!(!is_local_package_spec("@scope/pkg@1.0.0")); + } + #[test] fn test_parse_package_spec_simple() { let (name, version) = parse_package_spec("typescript");