From ef9a176289af9936a25db4f611ff451b4cf7113e Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Sun, 28 Jun 2026 13:58:26 +0800 Subject: [PATCH 1/4] fix(env): report npm-linked globals in which --- .../src/commands/env/package_metadata.rs | 67 ------------------- .../vite_global_cli/src/commands/env/which.rs | 51 +++++++++++++- .../npm-global-install-hint/snap.txt | 6 ++ .../npm-global-install-hint/steps.json | 1 + 4 files changed, 55 insertions(+), 70 deletions(-) diff --git a/crates/vite_global_cli/src/commands/env/package_metadata.rs b/crates/vite_global_cli/src/commands/env/package_metadata.rs index 8753e1dbbb..af979b06f8 100644 --- a/crates/vite_global_cli/src/commands/env/package_metadata.rs +++ b/crates/vite_global_cli/src/commands/env/package_metadata.rs @@ -164,21 +164,6 @@ impl PackageMetadata { packages.sort_by(|a, b| a.name.cmp(&b.name).then_with(|| a.version.cmp(&b.version))); Ok(packages) } - - /// Find the package that provides a given binary. - /// - /// Returns the package metadata if found, None otherwise. - pub async fn find_by_binary(binary_name: &str) -> Result, Error> { - let packages = Self::list_all().await?; - - for package in packages { - if package.bins.contains(&binary_name.to_string()) { - return Ok(Some(package)); - } - } - - Ok(None) - } } /// Recursively list packages in a directory (handles scoped packages in subdirs). @@ -389,56 +374,4 @@ mod tests { let names: Vec<_> = all.iter().map(|p| p.name.as_str()).collect(); assert_eq!(names, vec!["alpha", "zed"]); } - - #[tokio::test] - async fn test_find_by_binary() { - use tempfile::TempDir; - - let temp_dir = TempDir::new().unwrap(); - let temp_path = temp_dir.path().to_path_buf(); - let _guard = vite_shared::EnvConfig::test_guard( - vite_shared::EnvConfig::for_test_with_home(&temp_path), - ); - - // Create typescript package with tsc and tsserver binaries - let typescript = PackageMetadata::new( - "typescript".to_string(), - "5.0.0".to_string(), - "20.18.0".to_string(), - None, - vec!["tsc".to_string(), "tsserver".to_string()], - HashSet::from(["tsc".to_string(), "tsserver".to_string()]), - "npm".to_string(), - ); - typescript.save().await.unwrap(); - - // Create eslint package with eslint binary - let eslint = PackageMetadata::new( - "eslint".to_string(), - "9.0.0".to_string(), - "22.13.0".to_string(), - None, - vec!["eslint".to_string()], - HashSet::from(["eslint".to_string()]), - "npm".to_string(), - ); - eslint.save().await.unwrap(); - - // Find by binary should return the correct package - let found = PackageMetadata::find_by_binary("tsc").await.unwrap(); - assert!(found.is_some(), "Should find package providing tsc"); - assert_eq!(found.unwrap().name, "typescript"); - - let found = PackageMetadata::find_by_binary("tsserver").await.unwrap(); - assert!(found.is_some(), "Should find package providing tsserver"); - assert_eq!(found.unwrap().name, "typescript"); - - let found = PackageMetadata::find_by_binary("eslint").await.unwrap(); - assert!(found.is_some(), "Should find package providing eslint"); - assert_eq!(found.unwrap().name, "eslint"); - - // Non-existent binary should return None - let found = PackageMetadata::find_by_binary("nonexistent").await.unwrap(); - assert!(found.is_none(), "Should not find package for nonexistent binary"); - } } diff --git a/crates/vite_global_cli/src/commands/env/which.rs b/crates/vite_global_cli/src/commands/env/which.rs index 23c4f48771..3b57917f07 100644 --- a/crates/vite_global_cli/src/commands/env/which.rs +++ b/crates/vite_global_cli/src/commands/env/which.rs @@ -18,7 +18,8 @@ use vite_path::{AbsolutePath, AbsolutePathBuf}; use vite_shared::output; use super::{ - config::{VERSION_ENV_VAR, get_node_modules_dir, resolve_version}, + bin_config::{BinConfig, BinSource}, + config::{VERSION_ENV_VAR, get_bin_dir, get_node_modules_dir, resolve_version}, package_metadata::PackageMetadata, }; use crate::error::Error; @@ -55,8 +56,8 @@ pub async fn execute(cwd: AbsolutePathBuf, tool: &str) -> Result Result Result { + match bin_config.source { + BinSource::Vp => { + if let Some(metadata) = PackageMetadata::load(&bin_config.package).await? { + return execute_package_binary(tool, &metadata).await; + } + output::error(&format!("binary '{}' not found", tool.bold())); + eprintln!("Package {} may need to be reinstalled.", bin_config.package); + eprintln!("Run 'vp install -g {}' to reinstall.", bin_config.package); + Ok(exit_status(1)) + } + BinSource::Npm => execute_npm_link_binary(tool, bin_config).await, + } +} + +async fn execute_npm_link_binary(tool: &str, bin_config: &BinConfig) -> Result { + #[cfg(windows)] + let binary_path = get_bin_dir()?.join(format!("{tool}.cmd")); + + #[cfg(not(windows))] + let binary_path = get_bin_dir()?.join(tool); + + if !tokio::fs::try_exists(&binary_path).await.unwrap_or(false) { + output::error(&format!("binary '{}' not found", tool.bold())); + eprintln!("Package {} may need to be reinstalled.", bin_config.package); + eprintln!("Run 'npm install -g {}' to recreate the link.", bin_config.package); + return Ok(exit_status(1)); + } + + println!("{}", binary_path.as_path().display()); + println!( + " {:/bin/npm-global-hint-cli > test -L $VP_HOME/bin/npm-global-hint-cli && echo 'link created' # Link should exist link created +> vp env which npm-global-hint-cli # Should report npm-created link +/bin/npm-global-hint-cli + Package: npm-global-hint-pkg + Source: npm install -g + Node: + > npm-global-hint-cli # Should be callable via the link npm-global-hint-cli works diff --git a/packages/cli/snap-tests-global/npm-global-install-hint/steps.json b/packages/cli/snap-tests-global/npm-global-install-hint/steps.json index 98fc73ac8c..b25255b9d2 100644 --- a/packages/cli/snap-tests-global/npm-global-install-hint/steps.json +++ b/packages/cli/snap-tests-global/npm-global-install-hint/steps.json @@ -5,6 +5,7 @@ "commands": [ "npm install -g ./npm-global-hint-pkg # Should install and create link", "test -L $VP_HOME/bin/npm-global-hint-cli && echo 'link created' # Link should exist", + "vp env which npm-global-hint-cli # Should report npm-created link", "npm-global-hint-cli # Should be callable via the link", "rm -f $VP_HOME/bin/npm-global-hint-cli # Cleanup link", "npm uninstall -g npm-global-hint-pkg # Cleanup npm install" From 91b67aefbdc06f562a0a70a32423c53ee68dba99 Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Sun, 28 Jun 2026 14:08:01 +0800 Subject: [PATCH 2/4] small adjustment --- crates/vite_global_cli/src/commands/env/which.rs | 2 +- packages/cli/snap-tests-global/npm-global-install-hint/snap.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/vite_global_cli/src/commands/env/which.rs b/crates/vite_global_cli/src/commands/env/which.rs index 3b57917f07..685e3dcdaf 100644 --- a/crates/vite_global_cli/src/commands/env/which.rs +++ b/crates/vite_global_cli/src/commands/env/which.rs @@ -105,7 +105,7 @@ async fn execute_npm_link_binary(tool: &str, bin_config: &BinConfig) -> Result vp env which npm-global-hint-cli # Should report npm-created link /bin/npm-global-hint-cli Package: npm-global-hint-pkg - Source: npm install -g + Source: npm Node: > npm-global-hint-cli # Should be callable via the link From 0d47da9b424c6f3a92c8261281924da3a7f2d8e0 Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Sun, 28 Jun 2026 14:43:32 +0800 Subject: [PATCH 3/4] fix(env): resolve npm-linked which targets --- .../vite_global_cli/src/commands/env/which.rs | 55 +++++++++++++++---- .../npm-global-install-hint/snap.txt | 2 +- 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/crates/vite_global_cli/src/commands/env/which.rs b/crates/vite_global_cli/src/commands/env/which.rs index 685e3dcdaf..cc52d32b25 100644 --- a/crates/vite_global_cli/src/commands/env/which.rs +++ b/crates/vite_global_cli/src/commands/env/which.rs @@ -86,18 +86,15 @@ async fn execute_bin_config_binary( } async fn execute_npm_link_binary(tool: &str, bin_config: &BinConfig) -> Result { - #[cfg(windows)] - let binary_path = get_bin_dir()?.join(format!("{tool}.cmd")); - - #[cfg(not(windows))] - let binary_path = get_bin_dir()?.join(tool); - - if !tokio::fs::try_exists(&binary_path).await.unwrap_or(false) { - output::error(&format!("binary '{}' not found", tool.bold())); - eprintln!("Package {} may need to be reinstalled.", bin_config.package); - eprintln!("Run 'npm install -g {}' to recreate the link.", bin_config.package); - return Ok(exit_status(1)); - } + let binary_path = match locate_npm_link_binary(tool).await { + Ok(path) if tokio::fs::try_exists(&path).await.unwrap_or(false) => path, + _ => { + output::error(&format!("binary '{}' not found", tool.bold())); + eprintln!("Package {} may need to be reinstalled.", bin_config.package); + eprintln!("Run 'npm install -g {}' to recreate the link.", bin_config.package); + return Ok(exit_status(1)); + } + }; println!("{}", binary_path.as_path().display()); println!( @@ -111,6 +108,40 @@ async fn execute_npm_link_binary(tool: &str, bin_config: &BinConfig) -> Result Result { + let link_path = get_bin_dir()?.join(tool); + let target = tokio::fs::read_link(&link_path).await?; + if target.is_absolute() { + return AbsolutePathBuf::new(target) + .ok_or_else(|| Error::Other(format!("Invalid npm link target for {tool}").into())); + } + let parent = link_path + .parent() + .ok_or_else(|| Error::Other(format!("Invalid npm link path for {tool}").into()))?; + Ok(parent.join(target)) +} + +#[cfg(windows)] +async fn locate_npm_link_binary(tool: &str) -> Result { + let cmd_path = get_bin_dir()?.join(format!("{tool}.cmd")); + let content = tokio::fs::read_to_string(&cmd_path).await?; + let mut lines = content.lines(); + let source = match (lines.next(), lines.next(), lines.next(), lines.next()) { + (Some("@echo off"), Some(line), Some("exit /b %ERRORLEVEL%"), None) + if line.starts_with('"') && line.ends_with("\" %*") => + { + &line[1..line.len() - "\" %*".len()] + } + _ => { + return Err(Error::Other(format!("Invalid npm link wrapper for {tool}").into())); + } + }; + + AbsolutePathBuf::new(std::path::PathBuf::from(source)) + .ok_or_else(|| Error::Other(format!("Invalid npm link target for {tool}").into())) +} + async fn execute_package_manager_tool( cwd: &AbsolutePath, tool: &str, diff --git a/packages/cli/snap-tests-global/npm-global-install-hint/snap.txt b/packages/cli/snap-tests-global/npm-global-install-hint/snap.txt index 3f86dbf050..7754f89b60 100644 --- a/packages/cli/snap-tests-global/npm-global-install-hint/snap.txt +++ b/packages/cli/snap-tests-global/npm-global-install-hint/snap.txt @@ -7,7 +7,7 @@ Linked 'npm-global-hint-cli' to /bin/npm-global-hint-cli link created > vp env which npm-global-hint-cli # Should report npm-created link -/bin/npm-global-hint-cli +/../npm-global-lib-for-snap-tests/bin/npm-global-hint-cli Package: npm-global-hint-pkg Source: npm Node: From 9a1fa391c0ec38af3f8eadb5f9f58c4e5ff10810 Mon Sep 17 00:00:00 2001 From: Liang Mi Date: Sun, 28 Jun 2026 14:59:32 +0800 Subject: [PATCH 4/4] fix(env): canonicalize npm-linked which targets --- .../vite_global_cli/src/commands/env/which.rs | 19 +++++++++++-------- .../npm-global-install-hint/snap.txt | 2 +- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/crates/vite_global_cli/src/commands/env/which.rs b/crates/vite_global_cli/src/commands/env/which.rs index cc52d32b25..a805c055e4 100644 --- a/crates/vite_global_cli/src/commands/env/which.rs +++ b/crates/vite_global_cli/src/commands/env/which.rs @@ -112,14 +112,17 @@ async fn execute_npm_link_binary(tool: &str, bin_config: &BinConfig) -> Result Result { let link_path = get_bin_dir()?.join(tool); let target = tokio::fs::read_link(&link_path).await?; - if target.is_absolute() { - return AbsolutePathBuf::new(target) - .ok_or_else(|| Error::Other(format!("Invalid npm link target for {tool}").into())); - } - let parent = link_path - .parent() - .ok_or_else(|| Error::Other(format!("Invalid npm link path for {tool}").into()))?; - Ok(parent.join(target)) + let binary_path = if target.is_absolute() { + target + } else { + let parent = link_path + .parent() + .ok_or_else(|| Error::Other(format!("Invalid npm link path for {tool}").into()))?; + parent.join(target).into_path_buf() + }; + let canonical_path = tokio::fs::canonicalize(&binary_path).await?; + AbsolutePathBuf::new(canonical_path) + .ok_or_else(|| Error::Other(format!("Invalid npm link target for {tool}").into())) } #[cfg(windows)] diff --git a/packages/cli/snap-tests-global/npm-global-install-hint/snap.txt b/packages/cli/snap-tests-global/npm-global-install-hint/snap.txt index 7754f89b60..88a01b54b4 100644 --- a/packages/cli/snap-tests-global/npm-global-install-hint/snap.txt +++ b/packages/cli/snap-tests-global/npm-global-install-hint/snap.txt @@ -7,7 +7,7 @@ Linked 'npm-global-hint-cli' to /bin/npm-global-hint-cli link created > vp env which npm-global-hint-cli # Should report npm-created link -/../npm-global-lib-for-snap-tests/bin/npm-global-hint-cli +/npm-global-hint-pkg/cli.js Package: npm-global-hint-pkg Source: npm Node: