diff --git a/Cargo.lock b/Cargo.lock index cd8d2022af..4d2a0ba05a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1477,6 +1477,18 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "dialoguer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f104b501bf2364e78d0d3974cbc774f738f5865306ed128e1e0d7499c0ad96" +dependencies = [ + "console", + "shell-words", + "tempfile", + "zeroize", +] + [[package]] name = "diff" version = "0.1.13" @@ -5164,7 +5176,6 @@ dependencies = [ "commondir", "cow-utils", "dashmap", - "dunce", "futures", "indexmap", "insta", @@ -5637,7 +5648,6 @@ version = "0.1.0" dependencies = [ "arcstr", "phf", - "rolldown_common", "rolldown_plugin", "rolldown_utils", ] @@ -6524,6 +6534,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f" +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + [[package]] name = "shlex" version = "1.3.0" @@ -7623,6 +7639,7 @@ dependencies = [ "clap", "clap_complete", "crossterm", + "dialoguer", "directories", "flate2", "futures", diff --git a/Cargo.toml b/Cargo.toml index 5c0978ea4e..33abbb0dc3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -187,6 +187,7 @@ css-module-lexer = "0.0.15" dashmap = "6.2.1" derive_more = { version = "2.0.1", features = ["debug"] } directories = "6.0.0" +dialoguer = "0.12.0" dunce = "1.0.5" fast-glob = "1.0.0" flate2 = { version = "=1.1.9", features = ["zlib-rs"] } diff --git a/crates/vite_global_cli/Cargo.toml b/crates/vite_global_cli/Cargo.toml index 656558b306..6b050c1501 100644 --- a/crates/vite_global_cli/Cargo.toml +++ b/crates/vite_global_cli/Cargo.toml @@ -16,6 +16,7 @@ chrono = { workspace = true } clap = { workspace = true, features = ["derive"] } clap_complete = { workspace = true, features = ["unstable-dynamic"] } directories = { workspace = true } +dialoguer = { workspace = true } futures = { workspace = true } flate2 = { workspace = true } serde = { workspace = true } diff --git a/crates/vite_global_cli/src/cli.rs b/crates/vite_global_cli/src/cli.rs index 741c3e9a9a..24f0bfcead 100644 --- a/crates/vite_global_cli/src/cli.rs +++ b/crates/vite_global_cli/src/cli.rs @@ -3,17 +3,23 @@ //! This module defines the CLI structure using clap and routes commands //! to their appropriate handlers. -use std::{ffi::OsStr, process::ExitStatus}; +use std::{collections::HashSet, ffi::OsStr, io::IsTerminal, process::ExitStatus}; use clap::{CommandFactory, FromArgMatches, Parser, Subcommand}; use clap_complete::ArgValueCompleter; +use dialoguer::{Confirm, theme::ColorfulTheme}; +use owo_colors::OwoColorize; use tokio::runtime::Runtime; use vite_path::AbsolutePathBuf; use vite_pm_cli::PackageManagerCommand; use vite_shared::output; use crate::{ - commands::{self, env::package_metadata::PackageMetadata, global}, + commands::{ + self, + env::{config::resolve_version, package_metadata::PackageMetadata}, + global, + }, error::Error, help, }; @@ -575,8 +581,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 } PackageManagerCommand::Outdated { @@ -646,12 +666,32 @@ async fn managed_uninstall(packages: &[String], dry_run: bool) -> Result 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 concurrency = concurrency.unwrap_or(DEFAULT_GLOBAL_INSTALL_CONCURRENCY); let mut to_update: Vec = Vec::new(); + let mut node_mismatches: Vec = Vec::new(); + let current_node_version; let packages = if packages.is_empty() { let all = PackageMetadata::list_all().await?; @@ -659,10 +699,22 @@ async fn managed_update( vite_shared::output::raw("No global packages installed."); return Ok(ExitStatus::default()); } + current_node_version = get_current_node_version().await?; + + for metadata in &all { + 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(), + }); + } + } None } else { let mut managed_specs = Vec::new(); + current_node_version = get_current_node_version().await?; for package in packages { // Always update local packages @@ -673,7 +725,14 @@ async fn managed_update( // It is not a local package, so `parse_package_spec` there won't return `Err()` let (package_name, _) = global::parse_package_spec(package).unwrap(); - if PackageMetadata::load(&package_name).await?.is_some() { + if let Some(metadata) = PackageMetadata::load(&package_name).await? { + if !is_same_node_version(&metadata.platform.node, ¤t_node_version) { + node_mismatches.push(NodeMismatchPackage { + name: package_name, + spec: package.clone(), + installed_node: metadata.platform.node, + }); + } managed_specs.push(package.clone()); } else { to_update.push(package.clone()); @@ -682,16 +741,26 @@ async fn managed_update( Some(managed_specs) }; - to_update.extend( - global::outdated::get_outdated_packages( - &packages.unwrap_or(Vec::new()), - concurrency * 3, - true, - ) - .await? - .into_iter() - .map(|package| package.spec.unwrap_or(package.name)), - ); + + let outdated = global::outdated::get_outdated_packages( + &packages.unwrap_or_default(), + concurrency * 3, + true, + ) + .await?; + to_update.extend(outdated.into_iter().map(|package| package.spec.unwrap_or(package.name))); + + let to_update_set = to_update.iter().map(String::as_str).collect::>(); + node_mismatches.retain(|package| !to_update_set.contains(package.spec.as_str())); + + 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() { vite_shared::output::raw("All global packages are up to date."); @@ -700,7 +769,8 @@ 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, Some(¤t_node_version), false, concurrency, true) + .await { output::error(&format!( "Failed to update {}: {error}", @@ -711,6 +781,63 @@ async fn managed_update( Ok(ExitStatus::default()) } +async fn get_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::info("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).bold())); + output::raw(""); + output::raw("Affected packages:"); + for package in packages { + output::raw(&format!( + "- {} (installed with {})", + package.name.bold(), + display_node_version(&package.installed_node).bold() + )); + } + output::raw(""); + Confirm::with_theme(&ColorfulTheme::default()) + .with_prompt("Reinstall them with the current Node.js version?") + .default(false) + .interact() + .unwrap_or(false) +} + /// Run the CLI command. pub async fn run_command(cwd: AbsolutePathBuf, args: Args) -> Result { run_command_with_options(cwd, args, RenderOptions::default()).await @@ -953,10 +1080,22 @@ pub fn try_parse_args_from_with_options( #[cfg(test)] mod tests { use super::{ - has_flag_before_terminator, should_force_global_delegate, - should_suppress_header_for_subcommand, + display_node_version, has_flag_before_terminator, is_same_node_version, + should_force_global_delegate, should_suppress_header_for_subcommand, }; + #[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 399026ab4e..2e74a81647 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 344ccd17d5..922982a9c1 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 17a92b38c9..97fa4b0692 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 5e06654736..71af4f3440 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..057ca703ce --- /dev/null +++ b/packages/cli/snap-tests-global/command-update-node-mismatch/snap.txt @@ -0,0 +1,14 @@ +> 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. + +> vp update -g testnpm2 --ignore-node-mismatch # should explicitly skip node mismatch reinstall +All global packages are up to date. + +> vp update -g testnpm2 --reinstall-node-mismatch +info: Updating 1 global package with Node.js +✓ Updated testnpm2 to 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..bf5e6061ba --- /dev/null +++ b/packages/cli/snap-tests-global/command-update-node-mismatch/steps.json @@ -0,0 +1,10 @@ +{ + "serial": true, + "commands": [ + "vp install -g --node 20 testnpm2", + "vp update -g testnpm2 # should warn and skip node mismatch reinstall in CI", + "vp update -g testnpm2 --ignore-node-mismatch # should explicitly skip node mismatch reinstall", + "vp update -g testnpm2 --reinstall-node-mismatch" + ], + "after": ["vp remove -g testnpm2"] +} 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