diff --git a/crates/vite_global_cli/src/commands/global/install.rs b/crates/vite_global_cli/src/commands/global/install.rs index c998dd5ab2..f559bb65a2 100644 --- a/crates/vite_global_cli/src/commands/global/install.rs +++ b/crates/vite_global_cli/src/commands/global/install.rs @@ -195,6 +195,16 @@ pub async fn install( continue; }; let mut backup = backup; + let stale_bin_names = match stale_bin_names_for_package(&package_name, &bin_names).await { + Ok(bin_names) => bin_names, + Err(error) => { + let _ = cleanup_failed_install(&package_name, backup.take()).await; + if first_error.is_none() { + first_error = Some(package_error(&package_name, error)); + } + continue; + } + }; let mut conflicts = Vec::<(String, String)>::new(); let mut finalize_blocked = false; @@ -336,12 +346,35 @@ pub async fn install( continue; } - // 4.5 Commit the install by discarding the backup and reporting the installed bins. + // 4.5 Remove shims for binaries the package used to expose but no longer declares. + for bin_name in stale_bin_names { + let result = async { + remove_package_shim(&bin_dir, &bin_name).await?; + BinConfig::delete(&bin_name).await?; + Ok::<(), Error>(()) + } + .await; + + if let Err(error) = result.map_err(|error| package_error(&package_name, error)) { + let _ = cleanup_failed_install(&package_name, backup.take()).await; + if first_error.is_none() { + first_error = Some(error); + } + finalized = false; + break; + } + } + + if !finalized { + continue; + } + + // 4.6 Commit the install by discarding the backup and reporting the installed bins. if let Some(backup) = backup { backup.discard().await; } - // 4.6 Print success message + // 4.7 Print success message output::success(&format!( "{} {} {}{}", operation_past, @@ -552,6 +585,23 @@ async fn cleanup_installed_package(package_name: &str) -> Result<(), Error> { Ok(()) } +async fn stale_bin_names_for_package( + package_name: &str, + current_bin_names: &[String], +) -> Result, Error> { + let current_bin_names: HashSet<_> = current_bin_names.iter().cloned().collect(); + let mut previous_bin_names = HashSet::new(); + + if let Some(metadata) = PackageMetadata::load(package_name).await? { + previous_bin_names.extend(metadata.bins); + } + + previous_bin_names.extend(BinConfig::find_by_package(package_name).await?); + previous_bin_names.retain(|bin_name| !current_bin_names.contains(bin_name)); + + Ok(previous_bin_names.into_iter().collect()) +} + /// Uninstall a global package. /// /// Uses two-phase uninstall: diff --git a/packages/cli/snap-tests-global/env-install-stale-bin/check-stale-binary.js b/packages/cli/snap-tests-global/env-install-stale-bin/check-stale-binary.js new file mode 100644 index 0000000000..747f500736 --- /dev/null +++ b/packages/cli/snap-tests-global/env-install-stale-bin/check-stale-binary.js @@ -0,0 +1,9 @@ +const fs = require('fs'); +const path = require('path'); + +const shimSuffix = process.platform === 'win32' ? '.exe' : ''; +const shim = path.join(process.env.VP_HOME, 'bin', 'env-install-stale-drop' + shimSuffix); +const config = path.join(process.env.VP_HOME, 'bins/env-install-stale-drop.json'); + +console.log(fs.existsSync(shim) ? 'stale shim exists' : 'stale shim removed'); +console.log(fs.existsSync(config) ? 'stale config exists' : 'stale config removed'); diff --git a/packages/cli/snap-tests-global/env-install-stale-bin/env-install-stale-bin-pkg-v1/cli-drop.js b/packages/cli/snap-tests-global/env-install-stale-bin/env-install-stale-bin-pkg-v1/cli-drop.js new file mode 100644 index 0000000000..1007ca7b47 --- /dev/null +++ b/packages/cli/snap-tests-global/env-install-stale-bin/env-install-stale-bin-pkg-v1/cli-drop.js @@ -0,0 +1,2 @@ +#!/usr/bin/env node +console.log('env-install-stale-drop ok'); diff --git a/packages/cli/snap-tests-global/env-install-stale-bin/env-install-stale-bin-pkg-v1/cli-keep.js b/packages/cli/snap-tests-global/env-install-stale-bin/env-install-stale-bin-pkg-v1/cli-keep.js new file mode 100644 index 0000000000..8524f7cc95 --- /dev/null +++ b/packages/cli/snap-tests-global/env-install-stale-bin/env-install-stale-bin-pkg-v1/cli-keep.js @@ -0,0 +1,2 @@ +#!/usr/bin/env node +console.log('env-install-stale-keep ok'); diff --git a/packages/cli/snap-tests-global/env-install-stale-bin/env-install-stale-bin-pkg-v1/package.json b/packages/cli/snap-tests-global/env-install-stale-bin/env-install-stale-bin-pkg-v1/package.json new file mode 100644 index 0000000000..bc1de7dce4 --- /dev/null +++ b/packages/cli/snap-tests-global/env-install-stale-bin/env-install-stale-bin-pkg-v1/package.json @@ -0,0 +1,8 @@ +{ + "name": "env-install-stale-bin-pkg", + "version": "1.0.0", + "bin": { + "env-install-stale-drop": "./cli-drop.js", + "env-install-stale-keep": "./cli-keep.js" + } +} diff --git a/packages/cli/snap-tests-global/env-install-stale-bin/env-install-stale-bin-pkg-v2/cli-keep.js b/packages/cli/snap-tests-global/env-install-stale-bin/env-install-stale-bin-pkg-v2/cli-keep.js new file mode 100644 index 0000000000..8524f7cc95 --- /dev/null +++ b/packages/cli/snap-tests-global/env-install-stale-bin/env-install-stale-bin-pkg-v2/cli-keep.js @@ -0,0 +1,2 @@ +#!/usr/bin/env node +console.log('env-install-stale-keep ok'); diff --git a/packages/cli/snap-tests-global/env-install-stale-bin/env-install-stale-bin-pkg-v2/package.json b/packages/cli/snap-tests-global/env-install-stale-bin/env-install-stale-bin-pkg-v2/package.json new file mode 100644 index 0000000000..f6086a76aa --- /dev/null +++ b/packages/cli/snap-tests-global/env-install-stale-bin/env-install-stale-bin-pkg-v2/package.json @@ -0,0 +1,7 @@ +{ + "name": "env-install-stale-bin-pkg", + "version": "2.0.0", + "bin": { + "env-install-stale-keep": "./cli-keep.js" + } +} diff --git a/packages/cli/snap-tests-global/env-install-stale-bin/snap.txt b/packages/cli/snap-tests-global/env-install-stale-bin/snap.txt new file mode 100644 index 0000000000..ddf6aa1e7c --- /dev/null +++ b/packages/cli/snap-tests-global/env-install-stale-bin/snap.txt @@ -0,0 +1,20 @@ +> vp install -g ./env-install-stale-bin-pkg-v1 # Install package with two binaries +info: Installing 1 global package with Node.js +✓ Installed env-install-stale-bin-pkg + Bins: env-install-stale-drop, env-install-stale-keep + +> env-install-stale-keep && env-install-stale-drop # Both binaries should be callable +env-install-stale-keep ok +env-install-stale-drop ok + +> vp install -g ./env-install-stale-bin-pkg-v2 # Reinstall package version that removed one binary +info: Installing 1 global package with Node.js +✓ Installed env-install-stale-bin-pkg + Bins: env-install-stale-keep + +> env-install-stale-keep # Remaining binary should still be callable +env-install-stale-keep ok + +> node check-stale-binary.js +stale shim removed +stale config removed diff --git a/packages/cli/snap-tests-global/env-install-stale-bin/steps.json b/packages/cli/snap-tests-global/env-install-stale-bin/steps.json new file mode 100644 index 0000000000..8e2adf1354 --- /dev/null +++ b/packages/cli/snap-tests-global/env-install-stale-bin/steps.json @@ -0,0 +1,11 @@ +{ + "env": {}, + "commands": [ + "vp install -g ./env-install-stale-bin-pkg-v1 # Install package with two binaries", + "env-install-stale-keep && env-install-stale-drop # Both binaries should be callable", + "vp install -g ./env-install-stale-bin-pkg-v2 # Reinstall package version that removed one binary", + "env-install-stale-keep # Remaining binary should still be callable", + "node check-stale-binary.js" + ], + "after": ["vp remove -g env-install-stale-bin-pkg"] +}