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..a805c055e4 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 { + 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!( + " {: Result { + let link_path = get_bin_dir()?.join(tool); + let target = tokio::fs::read_link(&link_path).await?; + 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)] +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 fedab10c28..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 @@ -6,6 +6,12 @@ Linked 'npm-global-hint-cli' to /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 +/npm-global-hint-pkg/cli.js + Package: npm-global-hint-pkg + Source: npm + 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"