From f4b525cdf193a5f5e280a61f39a6b925119247db Mon Sep 17 00:00:00 2001 From: leohara Date: Tue, 19 May 2026 22:09:35 +0900 Subject: [PATCH 1/2] fix(cli): make env shims use the active vp executable --- .../vite_global_cli/src/commands/env/setup.rs | 241 ++++++++++++++++-- .../assert-shims.mjs | 14 + .../command-env-setup-external-vp/snap.txt | 45 ++++ .../command-env-setup-external-vp/steps.json | 13 + 4 files changed, 285 insertions(+), 28 deletions(-) create mode 100644 packages/cli/snap-tests-global/command-env-setup-external-vp/assert-shims.mjs create mode 100644 packages/cli/snap-tests-global/command-env-setup-external-vp/snap.txt create mode 100644 packages/cli/snap-tests-global/command-env-setup-external-vp/steps.json diff --git a/crates/vite_global_cli/src/commands/env/setup.rs b/crates/vite_global_cli/src/commands/env/setup.rs index bd37fa311f..9c3c078a61 100644 --- a/crates/vite_global_cli/src/commands/env/setup.rs +++ b/crates/vite_global_cli/src/commands/env/setup.rs @@ -5,8 +5,8 @@ //! - ~/.vite-plus/current/ - Contains the actual vp CLI binary //! //! On Unix: -//! - bin/vp is a symlink to ../current/bin/vp -//! - bin/node, bin/npm, bin/npx are symlinks to ../current/bin/vp +//! - bin/vp is a symlink to the active vp binary +//! - bin/node, bin/npm, bin/npx are symlinks to the active vp binary //! - Symlinks preserve argv[0], allowing tool detection via the symlink name //! //! On Windows: @@ -88,7 +88,7 @@ pub async fn execute(refresh: bool, env_only: bool) -> Result .map_err(|e| Error::ConfigError(format!("Cannot find current executable: {e}").into()))?; // Create wrapper script in bin/ - setup_vp_wrapper(&bin_dir, refresh).await?; + setup_vp_wrapper(¤t_exe, &bin_dir, refresh).await?; // Create shims for node, npm, npx let mut created = Vec::new(); @@ -144,30 +144,44 @@ pub async fn execute(refresh: bool, env_only: bool) -> Result Ok(ExitStatus::default()) } -/// Create symlink in bin/ that points to current/bin/vp. -async fn setup_vp_wrapper(bin_dir: &vite_path::AbsolutePath, refresh: bool) -> Result<(), Error> { +/// Create symlink in bin/ that points to the active vp binary. +async fn setup_vp_wrapper( + current_exe: &std::path::Path, + bin_dir: &vite_path::AbsolutePath, + refresh: bool, +) -> Result<(), Error> { #[cfg(unix)] { let bin_vp = bin_dir.join("vp"); - - // Create symlink bin/vp -> ../current/bin/vp - let should_create_symlink = refresh - || !tokio::fs::try_exists(&bin_vp).await.unwrap_or(false) - || !is_symlink(&bin_vp).await; // Replace non-symlink with symlink + let target = resolve_unix_vp_shim_target(current_exe, bin_dir).await?; + let existing = tokio::fs::symlink_metadata(&bin_vp).await.ok(); + + let should_create_symlink = match existing.as_ref() { + Some(metadata) if refresh || !metadata.file_type().is_symlink() => true, + Some(_) => { + let broken_symlink = !std::fs::exists(bin_vp.as_path()).unwrap_or(false); + let wrong_target = tokio::fs::read_link(&bin_vp) + .await + .map(|existing_target| existing_target != target) + .unwrap_or(true); + broken_symlink || wrong_target + } + None => true, + }; if should_create_symlink { // Remove existing if present (could be old wrapper script or file) - if tokio::fs::try_exists(&bin_vp).await.unwrap_or(false) { + if existing.is_some() { tokio::fs::remove_file(&bin_vp).await?; } - // Create relative symlink - tokio::fs::symlink("../current/bin/vp", &bin_vp).await?; - tracing::debug!("Created symlink {:?} -> ../current/bin/vp", bin_vp); + tokio::fs::symlink(&target, &bin_vp).await?; + tracing::debug!("Created symlink {:?} -> {:?}", bin_vp, target); } } #[cfg(windows)] { + let _ = current_exe; let bin_vp_exe = bin_dir.join("vp.exe"); // Create trampoline bin/vp.exe that forwards to current\bin\vp.exe @@ -195,13 +209,23 @@ async fn setup_vp_wrapper(bin_dir: &vite_path::AbsolutePath, refresh: bool) -> R Ok(()) } -/// Check if a path is a symlink. #[cfg(unix)] -async fn is_symlink(path: &vite_path::AbsolutePath) -> bool { - match tokio::fs::symlink_metadata(path).await { - Ok(m) => m.file_type().is_symlink(), - Err(_) => false, +pub(crate) async fn resolve_unix_vp_shim_target( + current_exe: &std::path::Path, + bin_dir: &vite_path::AbsolutePath, +) -> Result { + if let Some(vite_plus_home) = bin_dir.parent() { + let standalone_vp = vite_plus_home.join("current").join("bin").join("vp"); + if tokio::fs::try_exists(&standalone_vp).await.unwrap_or(false) { + let standalone_vp = tokio::fs::canonicalize(&standalone_vp).await.ok(); + let current_exe = tokio::fs::canonicalize(current_exe).await.ok(); + if standalone_vp.is_some() && standalone_vp == current_exe { + return Ok(std::path::PathBuf::from("../current/bin/vp")); + } + } } + + Ok(current_exe.to_path_buf()) } /// Create a single shim for node/npm/npx. @@ -215,9 +239,31 @@ async fn create_shim( ) -> Result { let shim_path = bin_dir.join(shim_filename(tool)); - // Check if shim already exists - if tokio::fs::try_exists(&shim_path).await.unwrap_or(false) { - if !refresh { + #[cfg(unix)] + let desired_target = resolve_unix_vp_shim_target(source, bin_dir).await?; + + let existing = tokio::fs::symlink_metadata(&shim_path).await.ok(); + if existing.is_some() { + let should_replace = if refresh { + true + } else { + #[cfg(unix)] + { + existing.as_ref().is_some_and(|metadata| metadata.file_type().is_symlink()) + && (!std::fs::exists(shim_path.as_path()).unwrap_or(false) + || tokio::fs::read_link(&shim_path) + .await + .map(|existing_target| existing_target != desired_target) + .unwrap_or(true)) + } + + #[cfg(windows)] + { + false + } + }; + + if !should_replace { return Ok(false); } #[cfg(windows)] @@ -255,19 +301,22 @@ fn shim_filename(tool: &str) -> String { } } -/// Create a Unix shim using symlink to ../current/bin/vp. +/// Create a Unix shim using symlink to the active vp binary. /// /// Symlinks preserve argv[0], allowing the vp binary to detect which tool /// was invoked. This is the same pattern used by Volta. #[cfg(unix)] async fn create_unix_shim( - _source: &std::path::Path, + source: &std::path::Path, shim_path: &vite_path::AbsolutePath, - _tool: &str, + tool: &str, ) -> Result<(), Error> { - // Create symlink to ../current/bin/vp (relative path) - tokio::fs::symlink("../current/bin/vp", shim_path).await?; - tracing::debug!("Created symlink shim at {:?} -> ../current/bin/vp", shim_path); + let bin_dir = shim_path.parent().ok_or_else(|| { + Error::ConfigError(format!("Cannot find parent directory for {tool} shim").into()) + })?; + let target = resolve_unix_vp_shim_target(source, bin_dir).await?; + tokio::fs::symlink(&target, shim_path).await?; + tracing::debug!("Created symlink shim at {:?} -> {:?}", shim_path, target); Ok(()) } @@ -1086,6 +1135,142 @@ mod tests { assert!(fresh_home.join("env.ps1").exists(), "env.ps1 file should be created"); } + #[tokio::test] + #[cfg(unix)] + async fn test_unix_vp_shim_target_prefers_standalone_layout_for_current_exe() { + let temp_dir = TempDir::new().unwrap(); + let home = AbsolutePathBuf::new(temp_dir.path().join(".vite-plus")).unwrap(); + let bin_dir = home.join("bin"); + let standalone_vp = home.join("current").join("bin").join("vp"); + + tokio::fs::create_dir_all(standalone_vp.parent().unwrap()).await.unwrap(); + tokio::fs::create_dir_all(&bin_dir).await.unwrap(); + tokio::fs::write(&standalone_vp, b"vp").await.unwrap(); + + let target = resolve_unix_vp_shim_target(standalone_vp.as_path(), &bin_dir).await.unwrap(); + + assert_eq!(target, std::path::Path::new("../current/bin/vp")); + } + + #[tokio::test] + #[cfg(unix)] + async fn test_unix_vp_shim_target_uses_current_exe_when_standalone_is_stale() { + let temp_dir = TempDir::new().unwrap(); + let home = AbsolutePathBuf::new(temp_dir.path().join(".vite-plus")).unwrap(); + let bin_dir = home.join("bin"); + let standalone_vp = home.join("current").join("bin").join("vp"); + let external_vp = temp_dir.path().join("external-vp"); + + tokio::fs::create_dir_all(standalone_vp.parent().unwrap()).await.unwrap(); + tokio::fs::create_dir_all(&bin_dir).await.unwrap(); + tokio::fs::write(&standalone_vp, b"stale-vp").await.unwrap(); + tokio::fs::write(&external_vp, b"active-vp").await.unwrap(); + + let target = resolve_unix_vp_shim_target(&external_vp, &bin_dir).await.unwrap(); + + assert_eq!(target, external_vp); + } + + #[tokio::test] + #[cfg(unix)] + async fn test_unix_vp_shim_target_uses_current_exe_without_standalone_layout() { + let temp_dir = TempDir::new().unwrap(); + let home = AbsolutePathBuf::new(temp_dir.path().join(".vite-plus")).unwrap(); + let bin_dir = home.join("bin"); + let external_vp = temp_dir.path().join("external-vp"); + + tokio::fs::create_dir_all(&bin_dir).await.unwrap(); + tokio::fs::write(&external_vp, b"vp").await.unwrap(); + + let target = resolve_unix_vp_shim_target(&external_vp, &bin_dir).await.unwrap(); + + assert_eq!(target, external_vp); + } + + #[tokio::test] + #[cfg(unix)] + async fn test_create_shim_replaces_stale_unix_symlink_without_refresh() { + let temp_dir = TempDir::new().unwrap(); + let home = AbsolutePathBuf::new(temp_dir.path().join(".vite-plus")).unwrap(); + let bin_dir = home.join("bin"); + let standalone_vp = home.join("current").join("bin").join("vp"); + let external_vp = temp_dir.path().join("external-vp"); + let node_shim = bin_dir.join("node"); + + tokio::fs::create_dir_all(standalone_vp.parent().unwrap()).await.unwrap(); + tokio::fs::create_dir_all(&bin_dir).await.unwrap(); + tokio::fs::write(&standalone_vp, b"stale-vp").await.unwrap(); + tokio::fs::write(&external_vp, b"active-vp").await.unwrap(); + tokio::fs::symlink("../current/bin/vp", &node_shim).await.unwrap(); + + let created = create_shim(&external_vp, &bin_dir, "node", false).await.unwrap(); + let target = tokio::fs::read_link(&node_shim).await.unwrap(); + + assert!(created, "stale shims should be recreated"); + assert_eq!(target, external_vp); + } + + #[tokio::test] + #[cfg(unix)] + async fn test_create_shim_replaces_broken_unix_symlink_without_refresh() { + let temp_dir = TempDir::new().unwrap(); + let home = AbsolutePathBuf::new(temp_dir.path().join(".vite-plus")).unwrap(); + let bin_dir = home.join("bin"); + let external_vp = temp_dir.path().join("external-vp"); + let node_shim = bin_dir.join("node"); + + tokio::fs::create_dir_all(&bin_dir).await.unwrap(); + tokio::fs::write(&external_vp, b"vp").await.unwrap(); + tokio::fs::symlink("../current/bin/vp", &node_shim).await.unwrap(); + + let created = create_shim(&external_vp, &bin_dir, "node", false).await.unwrap(); + let target = tokio::fs::read_link(&node_shim).await.unwrap(); + + assert!(created, "broken shims should be recreated"); + assert_eq!(target, external_vp); + } + + #[tokio::test] + #[cfg(unix)] + async fn test_setup_vp_wrapper_replaces_stale_unix_symlink_without_refresh() { + let temp_dir = TempDir::new().unwrap(); + let home = AbsolutePathBuf::new(temp_dir.path().join(".vite-plus")).unwrap(); + let bin_dir = home.join("bin"); + let standalone_vp = home.join("current").join("bin").join("vp"); + let external_vp = temp_dir.path().join("external-vp"); + let vp_shim = bin_dir.join("vp"); + + tokio::fs::create_dir_all(standalone_vp.parent().unwrap()).await.unwrap(); + tokio::fs::create_dir_all(&bin_dir).await.unwrap(); + tokio::fs::write(&standalone_vp, b"stale-vp").await.unwrap(); + tokio::fs::write(&external_vp, b"active-vp").await.unwrap(); + tokio::fs::symlink("../current/bin/vp", &vp_shim).await.unwrap(); + + setup_vp_wrapper(&external_vp, &bin_dir, false).await.unwrap(); + let target = tokio::fs::read_link(&vp_shim).await.unwrap(); + + assert_eq!(target, external_vp); + } + + #[tokio::test] + #[cfg(unix)] + async fn test_setup_vp_wrapper_replaces_broken_unix_symlink_without_refresh() { + let temp_dir = TempDir::new().unwrap(); + let home = AbsolutePathBuf::new(temp_dir.path().join(".vite-plus")).unwrap(); + let bin_dir = home.join("bin"); + let external_vp = temp_dir.path().join("external-vp"); + let vp_shim = bin_dir.join("vp"); + + tokio::fs::create_dir_all(&bin_dir).await.unwrap(); + tokio::fs::write(&external_vp, b"vp").await.unwrap(); + tokio::fs::symlink("../current/bin/vp", &vp_shim).await.unwrap(); + + setup_vp_wrapper(&external_vp, &bin_dir, false).await.unwrap(); + let target = tokio::fs::read_link(&vp_shim).await.unwrap(); + + assert_eq!(target, external_vp); + } + #[tokio::test] async fn test_create_env_files_contains_dynamic_completion() { let temp_dir = TempDir::new().unwrap(); diff --git a/packages/cli/snap-tests-global/command-env-setup-external-vp/assert-shims.mjs b/packages/cli/snap-tests-global/command-env-setup-external-vp/assert-shims.mjs new file mode 100644 index 0000000000..02797c70f5 --- /dev/null +++ b/packages/cli/snap-tests-global/command-env-setup-external-vp/assert-shims.mjs @@ -0,0 +1,14 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +const expected = path.resolve('external/vp'); + +for (const shim of ['vp', 'node', 'npm', 'npx', 'vpx', 'vpr']) { + const shimPath = path.join('home', 'bin', shim); + const target = fs.readlinkSync(shimPath); + if (target !== expected) { + throw new Error(`${shim} points to ${target}, expected ${expected}`); + } +} + +console.log('all shims point to external vp'); diff --git a/packages/cli/snap-tests-global/command-env-setup-external-vp/snap.txt b/packages/cli/snap-tests-global/command-env-setup-external-vp/snap.txt new file mode 100644 index 0000000000..8be0311cff --- /dev/null +++ b/packages/cli/snap-tests-global/command-env-setup-external-vp/snap.txt @@ -0,0 +1,45 @@ +> mkdir -p external home # Prepare isolated external install and VP_HOME +> cp "$(command -v vp)" external/vp && chmod +x external/vp # Simulate a Homebrew-style vp outside VP_HOME +> printf '22.18.0\n' > .node-version # Project Node.js version +> mkdir -p home/js_runtime/node/22.18.0/bin && printf '#!/bin/sh\necho v22.18.0\n' > home/js_runtime/node/22.18.0/bin/node && chmod +x home/js_runtime/node/22.18.0/bin/node # Preinstall managed Node runtime +> VP_HOME="$(pwd)/home" ./external/vp env setup # Setup shims from external vp +Setup: + Preparing vite-plus environment. + +Created Shims: + /home/bin/node + /home/bin/npm + /home/bin/npx + /home/bin/vpx + /home/bin/vpr + +Next Steps: + Add to your shell profile (~/.zshrc, ~/.bashrc, etc.): + + . "/home/env" + + For fish shell, add to ~/.config/fish/config.fish: + + source "/home/env.fish" + + For Nushell, add to ~/.config/nushell/config.nu: + + source '/home/env.nu' + + For PowerShell, add to your $PROFILE: + + . "/home/env.ps1" + + For IDE support (VS Code, Cursor), ensure bin directory is in system PATH: + - macOS: Add to ~/.profile or use launchd + + Restart your terminal and IDE, then run `vp env doctor` to verify. + +> node assert-shims.mjs # Shims should point to external vp, not VP_HOME/current/bin/vp +all shims point to external vp + +> VP_HOME="$(pwd)/home" PATH="$(pwd)/home/bin:$PATH" command -v node # node resolves to the new shim +/home/bin/node + +> VP_HOME="$(pwd)/home" PATH="$(pwd)/home/bin:$PATH" node -v # node shim uses the project version +v22.18.0 diff --git a/packages/cli/snap-tests-global/command-env-setup-external-vp/steps.json b/packages/cli/snap-tests-global/command-env-setup-external-vp/steps.json new file mode 100644 index 0000000000..024adb63cb --- /dev/null +++ b/packages/cli/snap-tests-global/command-env-setup-external-vp/steps.json @@ -0,0 +1,13 @@ +{ + "ignoredPlatforms": ["win32"], + "commands": [ + "mkdir -p external home # Prepare isolated external install and VP_HOME", + "cp \"$(command -v vp)\" external/vp && chmod +x external/vp # Simulate a Homebrew-style vp outside VP_HOME", + "printf '22.18.0\\n' > .node-version # Project Node.js version", + "mkdir -p home/js_runtime/node/22.18.0/bin && printf '#!/bin/sh\\necho v22.18.0\\n' > home/js_runtime/node/22.18.0/bin/node && chmod +x home/js_runtime/node/22.18.0/bin/node # Preinstall managed Node runtime", + "VP_HOME=\"$(pwd)/home\" ./external/vp env setup # Setup shims from external vp", + "node assert-shims.mjs # Shims should point to external vp, not VP_HOME/current/bin/vp", + "VP_HOME=\"$(pwd)/home\" PATH=\"$(pwd)/home/bin:$PATH\" command -v node # node resolves to the new shim", + "VP_HOME=\"$(pwd)/home\" PATH=\"$(pwd)/home/bin:$PATH\" node -v # node shim uses the project version" + ] +} From 1d90836afd70dfc246378a7bc6452fe083d4ede5 Mon Sep 17 00:00:00 2001 From: leohara Date: Wed, 20 May 2026 23:09:10 +0900 Subject: [PATCH 2/2] test(cli): fix external vp setup snap --- .../command-env-setup-external-vp/snap.txt | 11 ++++------- .../command-env-setup-external-vp/steps.json | 7 +++---- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/packages/cli/snap-tests-global/command-env-setup-external-vp/snap.txt b/packages/cli/snap-tests-global/command-env-setup-external-vp/snap.txt index 8be0311cff..e31b747f83 100644 --- a/packages/cli/snap-tests-global/command-env-setup-external-vp/snap.txt +++ b/packages/cli/snap-tests-global/command-env-setup-external-vp/snap.txt @@ -1,7 +1,7 @@ > mkdir -p external home # Prepare isolated external install and VP_HOME -> cp "$(command -v vp)" external/vp && chmod +x external/vp # Simulate a Homebrew-style vp outside VP_HOME +> cp "$VP_HOME/bin/vp" external/vp && chmod +x external/vp # Simulate a Homebrew-style vp outside VP_HOME > printf '22.18.0\n' > .node-version # Project Node.js version -> mkdir -p home/js_runtime/node/22.18.0/bin && printf '#!/bin/sh\necho v22.18.0\n' > home/js_runtime/node/22.18.0/bin/node && chmod +x home/js_runtime/node/22.18.0/bin/node # Preinstall managed Node runtime +> mkdir -p home/js_runtime/node/22.18.0/bin && printf '#!/bin/sh\necho vp-managed-node-22.18.0\n' > home/js_runtime/node/22.18.0/bin/node && chmod +x home/js_runtime/node/22.18.0/bin/node # Preinstall managed Node runtime > VP_HOME="$(pwd)/home" ./external/vp env setup # Setup shims from external vp Setup: Preparing vite-plus environment. @@ -35,11 +35,8 @@ Next Steps: Restart your terminal and IDE, then run `vp env doctor` to verify. -> node assert-shims.mjs # Shims should point to external vp, not VP_HOME/current/bin/vp +> VP_BYPASS="$VP_HOME/bin" node assert-shims.mjs # Shims should point to external vp, not VP_HOME/current/bin/vp all shims point to external vp -> VP_HOME="$(pwd)/home" PATH="$(pwd)/home/bin:$PATH" command -v node # node resolves to the new shim -/home/bin/node - > VP_HOME="$(pwd)/home" PATH="$(pwd)/home/bin:$PATH" node -v # node shim uses the project version -v22.18.0 +vp-managed-node-22.18.0 diff --git a/packages/cli/snap-tests-global/command-env-setup-external-vp/steps.json b/packages/cli/snap-tests-global/command-env-setup-external-vp/steps.json index 024adb63cb..b4489ee772 100644 --- a/packages/cli/snap-tests-global/command-env-setup-external-vp/steps.json +++ b/packages/cli/snap-tests-global/command-env-setup-external-vp/steps.json @@ -2,12 +2,11 @@ "ignoredPlatforms": ["win32"], "commands": [ "mkdir -p external home # Prepare isolated external install and VP_HOME", - "cp \"$(command -v vp)\" external/vp && chmod +x external/vp # Simulate a Homebrew-style vp outside VP_HOME", + "cp \"$VP_HOME/bin/vp\" external/vp && chmod +x external/vp # Simulate a Homebrew-style vp outside VP_HOME", "printf '22.18.0\\n' > .node-version # Project Node.js version", - "mkdir -p home/js_runtime/node/22.18.0/bin && printf '#!/bin/sh\\necho v22.18.0\\n' > home/js_runtime/node/22.18.0/bin/node && chmod +x home/js_runtime/node/22.18.0/bin/node # Preinstall managed Node runtime", + "mkdir -p home/js_runtime/node/22.18.0/bin && printf '#!/bin/sh\\necho vp-managed-node-22.18.0\\n' > home/js_runtime/node/22.18.0/bin/node && chmod +x home/js_runtime/node/22.18.0/bin/node # Preinstall managed Node runtime", "VP_HOME=\"$(pwd)/home\" ./external/vp env setup # Setup shims from external vp", - "node assert-shims.mjs # Shims should point to external vp, not VP_HOME/current/bin/vp", - "VP_HOME=\"$(pwd)/home\" PATH=\"$(pwd)/home/bin:$PATH\" command -v node # node resolves to the new shim", + "VP_BYPASS=\"$VP_HOME/bin\" node assert-shims.mjs # Shims should point to external vp, not VP_HOME/current/bin/vp", "VP_HOME=\"$(pwd)/home\" PATH=\"$(pwd)/home/bin:$PATH\" node -v # node shim uses the project version" ] }