Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 142 additions & 6 deletions crates/vite_global_cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<usize>,
reinstall_node_mismatch: bool,
ignore_node_mismatch: bool,
) -> Result<ExitStatus, Error> {
let all_packages = if packages.is_empty() {
let all = PackageMetadata::list_all().await?;
Expand All @@ -632,14 +668,26 @@ async fn managed_update(
};

let mut to_update: Vec<String> = Vec::new();
let mut node_mismatches: Vec<NodeMismatchPackage> = Vec::new();
let mut skipped = 0usize;
let current_node_version = current_node_version().await?;

if let Some(all) = all_packages {
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) =>
{
if !is_same_node_version(&metadata.platform.node, &current_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
Expand Down Expand Up @@ -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, &current_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
Expand All @@ -688,6 +746,15 @@ async fn managed_update(
}
}

if should_reinstall_node_mismatches(
&node_mismatches,
&current_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.");
Expand All @@ -714,6 +781,63 @@ async fn managed_update(
Ok(ExitStatus::default())
}

async fn current_node_version() -> Result<String, Error> {
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::<Vec<_>>().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<ExitStatus, Error> {
run_command_with_options(cwd, args, RenderOptions::default()).await
Expand Down Expand Up @@ -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]
Expand All @@ -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(
Expand Down
8 changes: 8 additions & 0 deletions crates/vite_pm_cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,14 @@ pub enum PackageManagerCommand {
#[arg(long, requires = "global", value_parser = parse_positive_usize)]
concurrency: Option<usize>,

/// 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,
Expand Down
2 changes: 2 additions & 0 deletions crates/vite_pm_cli/src/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ pub async fn dispatch(
latest,
global: _,
concurrency: _,
reinstall_node_mismatch: _,
ignore_node_mismatch: _,
recursive,
filter,
workspace_root,
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/snap-tests-global/cli-helper-message/snap.txt
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ Options:
-L, --latest Update to latest version (ignore semver range)
-g, --global Update global packages
--concurrency <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 <PATTERN> Filter packages in monorepo (can be used multiple times)
-w, --workspace-root Include workspace root
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/snap-tests-global/command-update-bun/snap.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Options:
-L, --latest Update to latest version (ignore semver range)
-g, --global Update global packages
--concurrency <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 <PATTERN> Filter packages in monorepo (can be used multiple times)
-w, --workspace-root Include workspace root
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
> vp install -g --node 20 testnpm2
info: Installing 1 global package with Node.js <semver>
✓ Installed testnpm2 <semver>

> 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 <semver>
✓ Updated testnpm2 to <semver>
Original file line number Diff line number Diff line change
@@ -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"]
}
2 changes: 2 additions & 0 deletions packages/cli/snap-tests-global/command-update-pnpm10/snap.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Options:
-L, --latest Update to latest version (ignore semver range)
-g, --global Update global packages
--concurrency <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 <PATTERN> Filter packages in monorepo (can be used multiple times)
-w, --workspace-root Include workspace root
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/snap-tests-global/command-update-pnpm11/snap.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Options:
-L, --latest Update to latest version (ignore semver range)
-g, --global Update global packages
--concurrency <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 <PATTERN> Filter packages in monorepo (can be used multiple times)
-w, --workspace-root Include workspace root
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/snap-tests/command-update-pnpm10/snap.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Options:
-L, --latest Update to latest version (ignore semver range)
-g, --global Update global packages
--concurrency <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 <PATTERN> Filter packages in monorepo (can be used multiple times)
-w, --workspace-root Include workspace root
Expand Down
Loading