From 2be76845699ac7bd82acd8de11f8069e1496cc61 Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Sat, 23 May 2026 10:08:34 +0800 Subject: [PATCH 1/2] fix(global): prompt for node mismatch reinstall --- crates/vite_global_cli/src/cli.rs | 148 +++++++++++++++++- crates/vite_pm_cli/src/cli.rs | 8 + crates/vite_pm_cli/src/dispatch.rs | 2 + .../cli-helper-message/snap.txt | 2 + .../command-update-bun/snap.txt | 2 + .../command-update-node-mismatch/snap.txt | 23 +++ .../command-update-node-mismatch/steps.json | 28 ++++ .../command-update-pnpm10/snap.txt | 2 + .../command-update-pnpm11/snap.txt | 2 + .../snap-tests/command-update-pnpm10/snap.txt | 2 + 10 files changed, 213 insertions(+), 6 deletions(-) create mode 100644 packages/cli/snap-tests-global/command-update-node-mismatch/snap.txt create mode 100644 packages/cli/snap-tests-global/command-update-node-mismatch/steps.json diff --git a/crates/vite_global_cli/src/cli.rs b/crates/vite_global_cli/src/cli.rs index 509bab7a40..151931a0d8 100644 --- a/crates/vite_global_cli/src/cli.rs +++ b/crates/vite_global_cli/src/cli.rs @@ -3,7 +3,11 @@ //! This module defines the CLI structure using clap and routes commands //! to their appropriate handlers. -use std::{ffi::OsStr, process::ExitStatus}; +use std::{ + ffi::OsStr, + io::{IsTerminal, Write}, + process::ExitStatus, +}; use clap::{CommandFactory, FromArgMatches, Parser, Subcommand}; use clap_complete::ArgValueCompleter; @@ -15,7 +19,7 @@ use vite_shared::output; use crate::{ commands::{ self, - env::{global_install, package_metadata::PackageMetadata}, + env::{config::resolve_version, global_install, package_metadata::PackageMetadata}, }, error::Error, help, @@ -558,8 +562,22 @@ async fn run_package_manager_command( managed_uninstall(packages, dry_run).await } - PackageManagerCommand::Update { global: true, ref packages, concurrency, .. } => { - managed_update(packages, concurrency).await + PackageManagerCommand::Update { + global: true, + ref packages, + concurrency, + reinstall_node_mismatch, + ignore_node_mismatch, + .. + } => { + if reinstall_node_mismatch && ignore_node_mismatch { + output::error( + "--reinstall-node-mismatch and --ignore-node-mismatch cannot be used together", + ); + return Ok(exit_status(1)); + } + managed_update(packages, concurrency, reinstall_node_mismatch, ignore_node_mismatch) + .await } // `pm list -g` lists vite-plus-managed globals, not the underlying PM's. @@ -616,9 +634,27 @@ fn is_global_package_up_to_date(installed_version: &str, registry_version: &str) installed_version.trim() == registry_version.trim() } +fn is_same_node_version(installed_version: &str, current_version: &str) -> bool { + installed_version.trim().trim_start_matches('v') + == current_version.trim().trim_start_matches('v') +} + +fn display_node_version(version: &str) -> String { + let version = version.trim(); + if version.starts_with('v') { version.to_string() } else { format!("v{version}") } +} + +struct NodeMismatchPackage { + name: String, + spec: String, + installed_node: String, +} + async fn managed_update( packages: &[String], concurrency: Option, + reinstall_node_mismatch: bool, + ignore_node_mismatch: bool, ) -> Result { let all_packages = if packages.is_empty() { let all = PackageMetadata::list_all().await?; @@ -632,7 +668,9 @@ async fn managed_update( }; let mut to_update: Vec = Vec::new(); + let mut node_mismatches: Vec = Vec::new(); let mut skipped = 0usize; + let current_node_version = current_node_version().await?; if let Some(all) = all_packages { for metadata in all { @@ -640,6 +678,16 @@ async fn managed_update( Ok(latest_version) if is_global_package_up_to_date(&metadata.version, &latest_version) => { + if !is_same_node_version(&metadata.platform.node, ¤t_node_version) { + node_mismatches.push(NodeMismatchPackage { + name: metadata.name.clone(), + spec: metadata.name.clone(), + installed_node: metadata.platform.node.clone(), + }); + skipped += 1; + continue; + } + vite_shared::output::raw(&format!( "{} is already up to date (v{}).", metadata.name, metadata.version @@ -669,6 +717,16 @@ async fn managed_update( Ok(latest_version) if is_global_package_up_to_date(&metadata.version, &latest_version) => { + if !is_same_node_version(&metadata.platform.node, ¤t_node_version) { + node_mismatches.push(NodeMismatchPackage { + name: package_name.clone(), + spec: package.clone(), + installed_node: metadata.platform.node.clone(), + }); + skipped += 1; + continue; + } + vite_shared::output::raw(&format!( "{} is already up to date (v{}).", package_name, metadata.version @@ -688,6 +746,15 @@ async fn managed_update( } } + if should_reinstall_node_mismatches( + &node_mismatches, + ¤t_node_version, + reinstall_node_mismatch, + ignore_node_mismatch, + ) { + to_update.extend(node_mismatches.into_iter().map(|package| package.spec)); + } + if to_update.is_empty() { if skipped > 0 { vite_shared::output::raw("All global packages are up to date."); @@ -714,6 +781,63 @@ async fn managed_update( Ok(ExitStatus::default()) } +async fn current_node_version() -> Result { + let cwd = vite_path::current_dir().map_err(|error| { + Error::ConfigError(format!("Cannot get current directory: {error}").into()) + })?; + Ok(resolve_version(&cwd).await?.version) +} + +fn should_reinstall_node_mismatches( + packages: &[NodeMismatchPackage], + current_node_version: &str, + reinstall_node_mismatch: bool, + ignore_node_mismatch: bool, +) -> bool { + if packages.is_empty() || ignore_node_mismatch { + return false; + } + + if reinstall_node_mismatch { + return true; + } + + if !std::io::stdin().is_terminal() || std::env::var_os("CI").is_some() { + let package_names = + packages.iter().map(|package| package.name.as_str()).collect::>().join(", "); + output::warn(&format!( + "Skipping reinstall for global packages installed with a different Node.js version: {package_names}. Use --reinstall-node-mismatch to reinstall them." + )); + return false; + } + + prompt_reinstall_node_mismatches(packages, current_node_version) +} + +fn prompt_reinstall_node_mismatches( + packages: &[NodeMismatchPackage], + current_node_version: &str, +) -> bool { + output::raw("Some global packages were installed with a different Node.js version."); + output::raw(""); + output::raw(&format!("Current Node.js: {}", display_node_version(current_node_version))); + output::raw(""); + output::raw("Affected packages:"); + for package in packages { + output::raw(&format!( + "- {} (installed with {})", + package.name, + display_node_version(&package.installed_node) + )); + } + output::raw(""); + output::raw_inline("Reinstall them with the current Node.js version? (y/N) "); + let _ = std::io::stdout().flush(); + + let mut input = String::new(); + std::io::stdin().read_line(&mut input).is_ok() && input.trim().eq_ignore_ascii_case("y") +} + /// Run the CLI command. pub async fn run_command(cwd: AbsolutePathBuf, args: Args) -> Result { run_command_with_options(cwd, args, RenderOptions::default()).await @@ -956,8 +1080,8 @@ 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, - should_suppress_header_for_subcommand, + display_node_version, has_flag_before_terminator, is_global_package_up_to_date, + is_same_node_version, should_force_global_delegate, should_suppress_header_for_subcommand, }; #[test] @@ -970,6 +1094,18 @@ mod tests { assert!(!is_global_package_up_to_date("5.9.2", "5.9.3")); } + #[test] + fn detects_global_update_node_version_mismatch() { + assert!(is_same_node_version("21.0.0", "v21.0.0")); + assert!(!is_same_node_version("21.0.0", "25.0.0")); + } + + #[test] + fn displays_node_versions_with_v_prefix() { + assert_eq!(display_node_version("25.0.0"), "v25.0.0"); + assert_eq!(display_node_version("v25.0.0"), "v25.0.0"); + } + #[test] fn detects_flag_before_option_terminator() { assert!(has_flag_before_terminator( diff --git a/crates/vite_pm_cli/src/cli.rs b/crates/vite_pm_cli/src/cli.rs index 64c25cd54f..9e68a48b68 100644 --- a/crates/vite_pm_cli/src/cli.rs +++ b/crates/vite_pm_cli/src/cli.rs @@ -257,6 +257,14 @@ pub enum PackageManagerCommand { #[arg(long, requires = "global", value_parser = parse_positive_usize)] concurrency: Option, + /// Reinstall up-to-date global packages installed with a different Node.js version + #[arg(long, requires = "global")] + reinstall_node_mismatch: bool, + + /// Skip up-to-date global packages installed with a different Node.js version + #[arg(long, requires = "global")] + ignore_node_mismatch: bool, + /// Update recursively in all workspace packages #[arg(short = 'r', long)] recursive: bool, diff --git a/crates/vite_pm_cli/src/dispatch.rs b/crates/vite_pm_cli/src/dispatch.rs index 035b3c1c2b..56f2f2c0cd 100644 --- a/crates/vite_pm_cli/src/dispatch.rs +++ b/crates/vite_pm_cli/src/dispatch.rs @@ -169,6 +169,8 @@ pub async fn dispatch( latest, global: _, concurrency: _, + reinstall_node_mismatch: _, + ignore_node_mismatch: _, recursive, filter, workspace_root, diff --git a/packages/cli/snap-tests-global/cli-helper-message/snap.txt b/packages/cli/snap-tests-global/cli-helper-message/snap.txt index fbbed52b04..7351a04575 100644 --- a/packages/cli/snap-tests-global/cli-helper-message/snap.txt +++ b/packages/cli/snap-tests-global/cli-helper-message/snap.txt @@ -176,6 +176,8 @@ Options: -L, --latest Update to latest version (ignore semver range) -g, --global Update global packages --concurrency Number of global package updates to run in parallel (only with -g) + --reinstall-node-mismatch Reinstall up-to-date global packages installed with a different Node.js version + --ignore-node-mismatch Skip up-to-date global packages installed with a different Node.js version -r, --recursive Update recursively in all workspace packages --filter Filter packages in monorepo (can be used multiple times) -w, --workspace-root Include workspace root diff --git a/packages/cli/snap-tests-global/command-update-bun/snap.txt b/packages/cli/snap-tests-global/command-update-bun/snap.txt index 494778e3a1..0fcee283c4 100644 --- a/packages/cli/snap-tests-global/command-update-bun/snap.txt +++ b/packages/cli/snap-tests-global/command-update-bun/snap.txt @@ -11,6 +11,8 @@ Options: -L, --latest Update to latest version (ignore semver range) -g, --global Update global packages --concurrency Number of global package updates to run in parallel (only with -g) + --reinstall-node-mismatch Reinstall up-to-date global packages installed with a different Node.js version + --ignore-node-mismatch Skip up-to-date global packages installed with a different Node.js version -r, --recursive Update recursively in all workspace packages --filter Filter packages in monorepo (can be used multiple times) -w, --workspace-root Include workspace root diff --git a/packages/cli/snap-tests-global/command-update-node-mismatch/snap.txt b/packages/cli/snap-tests-global/command-update-node-mismatch/snap.txt new file mode 100644 index 0000000000..40393924f3 --- /dev/null +++ b/packages/cli/snap-tests-global/command-update-node-mismatch/snap.txt @@ -0,0 +1,23 @@ +> printf '22.22.0\n' > .node-version && vp install -g testnpm2@1.0.1 +> node -e "const fs=require('fs');const p=process.env.VP_HOME+'/packages/testnpm2.json';const m=JSON.parse(fs.readFileSync(p,'utf8'));m.platform.node='21.0.0';fs.writeFileSync(p,JSON.stringify(m,null,2)+'\n')" +> vp update -g testnpm2 # should warn and skip node mismatch reinstall in CI +warn: Skipping reinstall for global packages installed with a different Node.js version: testnpm2. Use --reinstall-node-mismatch to reinstall them. +All global packages are up to date. + +> cat $VP_HOME/packages/testnpm2.json | node -e "const m=JSON.parse(require('fs').readFileSync(0,'utf8')); console.log('Kept old Node:', m.platform.node === '21.0.0')" # should keep old Node version after CI skip +Kept old Node: true + +> vp update -g testnpm2 --ignore-node-mismatch # should explicitly skip node mismatch reinstall +All global packages are up to date. + +> cat $VP_HOME/packages/testnpm2.json | node -e "const m=JSON.parse(require('fs').readFileSync(0,'utf8')); console.log('Kept old Node:', m.platform.node === '21.0.0')" # should keep old Node version after explicit skip +Kept old Node: true + +> vp update -g testnpm2 --reinstall-node-mismatch +info: Updating 1 global package with Node.js +✓ Updated testnpm2 to + +> cat $VP_HOME/packages/testnpm2.json | node -e "const m=JSON.parse(require('fs').readFileSync(0,'utf8')); console.log('Reinstalled with current Node:', m.platform.node === '22.22.0')" # should reinstall with current Node version +Reinstalled with current Node: true + +> vp remove -g testnpm2 \ No newline at end of file diff --git a/packages/cli/snap-tests-global/command-update-node-mismatch/steps.json b/packages/cli/snap-tests-global/command-update-node-mismatch/steps.json new file mode 100644 index 0000000000..3af48ef18e --- /dev/null +++ b/packages/cli/snap-tests-global/command-update-node-mismatch/steps.json @@ -0,0 +1,28 @@ +{ + "ignoredPlatforms": ["win32"], + "serial": true, + "commands": [ + { + "command": "printf '22.22.0\\n' > .node-version && vp install -g testnpm2@1.0.1", + "ignoreOutput": true, + "timeout": 120000 + }, + { + "command": "node -e \"const fs=require('fs');const p=process.env.VP_HOME+'/packages/testnpm2.json';const m=JSON.parse(fs.readFileSync(p,'utf8'));m.platform.node='21.0.0';fs.writeFileSync(p,JSON.stringify(m,null,2)+'\\n')\"", + "ignoreOutput": true + }, + "vp update -g testnpm2 # should warn and skip node mismatch reinstall in CI", + "cat $VP_HOME/packages/testnpm2.json | node -e \"const m=JSON.parse(require('fs').readFileSync(0,'utf8')); console.log('Kept old Node:', m.platform.node === '21.0.0')\" # should keep old Node version after CI skip", + "vp update -g testnpm2 --ignore-node-mismatch # should explicitly skip node mismatch reinstall", + "cat $VP_HOME/packages/testnpm2.json | node -e \"const m=JSON.parse(require('fs').readFileSync(0,'utf8')); console.log('Kept old Node:', m.platform.node === '21.0.0')\" # should keep old Node version after explicit skip", + { + "command": "vp update -g testnpm2 --reinstall-node-mismatch", + "timeout": 120000 + }, + "cat $VP_HOME/packages/testnpm2.json | node -e \"const m=JSON.parse(require('fs').readFileSync(0,'utf8')); console.log('Reinstalled with current Node:', m.platform.node === '22.22.0')\" # should reinstall with current Node version", + { + "command": "vp remove -g testnpm2", + "ignoreOutput": true + } + ] +} diff --git a/packages/cli/snap-tests-global/command-update-pnpm10/snap.txt b/packages/cli/snap-tests-global/command-update-pnpm10/snap.txt index 8f6feaeed1..09e9493923 100644 --- a/packages/cli/snap-tests-global/command-update-pnpm10/snap.txt +++ b/packages/cli/snap-tests-global/command-update-pnpm10/snap.txt @@ -11,6 +11,8 @@ Options: -L, --latest Update to latest version (ignore semver range) -g, --global Update global packages --concurrency Number of global package updates to run in parallel (only with -g) + --reinstall-node-mismatch Reinstall up-to-date global packages installed with a different Node.js version + --ignore-node-mismatch Skip up-to-date global packages installed with a different Node.js version -r, --recursive Update recursively in all workspace packages --filter Filter packages in monorepo (can be used multiple times) -w, --workspace-root Include workspace root diff --git a/packages/cli/snap-tests-global/command-update-pnpm11/snap.txt b/packages/cli/snap-tests-global/command-update-pnpm11/snap.txt index bb6d6be63f..a4275aa835 100644 --- a/packages/cli/snap-tests-global/command-update-pnpm11/snap.txt +++ b/packages/cli/snap-tests-global/command-update-pnpm11/snap.txt @@ -11,6 +11,8 @@ Options: -L, --latest Update to latest version (ignore semver range) -g, --global Update global packages --concurrency Number of global package updates to run in parallel (only with -g) + --reinstall-node-mismatch Reinstall up-to-date global packages installed with a different Node.js version + --ignore-node-mismatch Skip up-to-date global packages installed with a different Node.js version -r, --recursive Update recursively in all workspace packages --filter Filter packages in monorepo (can be used multiple times) -w, --workspace-root Include workspace root diff --git a/packages/cli/snap-tests/command-update-pnpm10/snap.txt b/packages/cli/snap-tests/command-update-pnpm10/snap.txt index 65fdc8ed27..a1a2de64aa 100644 --- a/packages/cli/snap-tests/command-update-pnpm10/snap.txt +++ b/packages/cli/snap-tests/command-update-pnpm10/snap.txt @@ -11,6 +11,8 @@ Options: -L, --latest Update to latest version (ignore semver range) -g, --global Update global packages --concurrency Number of global package updates to run in parallel (only with -g) + --reinstall-node-mismatch Reinstall up-to-date global packages installed with a different Node.js version + --ignore-node-mismatch Skip up-to-date global packages installed with a different Node.js version -r, --recursive Update recursively in all workspace packages --filter Filter packages in monorepo (can be used multiple times) -w, --workspace-root Include workspace root From 53e88732f2b0d6c43b8226bea44198fdb1a567fa Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Sat, 23 May 2026 12:45:07 +0800 Subject: [PATCH 2/2] snapshots --- .../command-update-node-mismatch/snap.txt | 17 +++--------- .../command-update-node-mismatch/steps.json | 26 +++---------------- 2 files changed, 8 insertions(+), 35 deletions(-) diff --git a/packages/cli/snap-tests-global/command-update-node-mismatch/snap.txt b/packages/cli/snap-tests-global/command-update-node-mismatch/snap.txt index 40393924f3..057ca703ce 100644 --- a/packages/cli/snap-tests-global/command-update-node-mismatch/snap.txt +++ b/packages/cli/snap-tests-global/command-update-node-mismatch/snap.txt @@ -1,23 +1,14 @@ -> printf '22.22.0\n' > .node-version && vp install -g testnpm2@1.0.1 -> node -e "const fs=require('fs');const p=process.env.VP_HOME+'/packages/testnpm2.json';const m=JSON.parse(fs.readFileSync(p,'utf8'));m.platform.node='21.0.0';fs.writeFileSync(p,JSON.stringify(m,null,2)+'\n')" +> vp install -g --node 20 testnpm2 +info: Installing 1 global package with Node.js +✓ Installed testnpm2 + > vp update -g testnpm2 # should warn and skip node mismatch reinstall in CI warn: Skipping reinstall for global packages installed with a different Node.js version: testnpm2. Use --reinstall-node-mismatch to reinstall them. All global packages are up to date. -> cat $VP_HOME/packages/testnpm2.json | node -e "const m=JSON.parse(require('fs').readFileSync(0,'utf8')); console.log('Kept old Node:', m.platform.node === '21.0.0')" # should keep old Node version after CI skip -Kept old Node: true - > vp update -g testnpm2 --ignore-node-mismatch # should explicitly skip node mismatch reinstall All global packages are up to date. -> cat $VP_HOME/packages/testnpm2.json | node -e "const m=JSON.parse(require('fs').readFileSync(0,'utf8')); console.log('Kept old Node:', m.platform.node === '21.0.0')" # should keep old Node version after explicit skip -Kept old Node: true - > vp update -g testnpm2 --reinstall-node-mismatch info: Updating 1 global package with Node.js ✓ Updated testnpm2 to - -> cat $VP_HOME/packages/testnpm2.json | node -e "const m=JSON.parse(require('fs').readFileSync(0,'utf8')); console.log('Reinstalled with current Node:', m.platform.node === '22.22.0')" # should reinstall with current Node version -Reinstalled with current Node: true - -> vp remove -g testnpm2 \ No newline at end of file diff --git a/packages/cli/snap-tests-global/command-update-node-mismatch/steps.json b/packages/cli/snap-tests-global/command-update-node-mismatch/steps.json index 3af48ef18e..bf5e6061ba 100644 --- a/packages/cli/snap-tests-global/command-update-node-mismatch/steps.json +++ b/packages/cli/snap-tests-global/command-update-node-mismatch/steps.json @@ -1,28 +1,10 @@ { - "ignoredPlatforms": ["win32"], "serial": true, "commands": [ - { - "command": "printf '22.22.0\\n' > .node-version && vp install -g testnpm2@1.0.1", - "ignoreOutput": true, - "timeout": 120000 - }, - { - "command": "node -e \"const fs=require('fs');const p=process.env.VP_HOME+'/packages/testnpm2.json';const m=JSON.parse(fs.readFileSync(p,'utf8'));m.platform.node='21.0.0';fs.writeFileSync(p,JSON.stringify(m,null,2)+'\\n')\"", - "ignoreOutput": true - }, + "vp install -g --node 20 testnpm2", "vp update -g testnpm2 # should warn and skip node mismatch reinstall in CI", - "cat $VP_HOME/packages/testnpm2.json | node -e \"const m=JSON.parse(require('fs').readFileSync(0,'utf8')); console.log('Kept old Node:', m.platform.node === '21.0.0')\" # should keep old Node version after CI skip", "vp update -g testnpm2 --ignore-node-mismatch # should explicitly skip node mismatch reinstall", - "cat $VP_HOME/packages/testnpm2.json | node -e \"const m=JSON.parse(require('fs').readFileSync(0,'utf8')); console.log('Kept old Node:', m.platform.node === '21.0.0')\" # should keep old Node version after explicit skip", - { - "command": "vp update -g testnpm2 --reinstall-node-mismatch", - "timeout": 120000 - }, - "cat $VP_HOME/packages/testnpm2.json | node -e \"const m=JSON.parse(require('fs').readFileSync(0,'utf8')); console.log('Reinstalled with current Node:', m.platform.node === '22.22.0')\" # should reinstall with current Node version", - { - "command": "vp remove -g testnpm2", - "ignoreOutput": true - } - ] + "vp update -g testnpm2 --reinstall-node-mismatch" + ], + "after": ["vp remove -g testnpm2"] }