Skip to content
Merged
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
67 changes: 0 additions & 67 deletions crates/vite_global_cli/src/commands/env/package_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<Self>, 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).
Expand Down Expand Up @@ -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");
}
}
85 changes: 82 additions & 3 deletions crates/vite_global_cli/src/commands/env/which.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -55,8 +56,8 @@ pub async fn execute(cwd: AbsolutePathBuf, tool: &str) -> Result<ExitStatus, Err
}

// Check if this is a global package binary
if let Some(metadata) = PackageMetadata::find_by_binary(tool).await? {
return execute_package_binary(tool, &metadata).await;
if let Some(bin_config) = BinConfig::load(tool).await? {
return execute_bin_config_binary(tool, &bin_config).await;
Comment thread
liangmiQwQ marked this conversation as resolved.
}

// Unknown tool
Expand All @@ -66,6 +67,84 @@ pub async fn execute(cwd: AbsolutePathBuf, tool: &str) -> Result<ExitStatus, Err
Ok(exit_status(1))
}

async fn execute_bin_config_binary(
tool: &str,
bin_config: &BinConfig,
) -> Result<ExitStatus, Error> {
match bin_config.source {
Comment thread
liangmiQwQ marked this conversation as resolved.
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<ExitStatus, Error> {
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!(
" {:<LABEL_WIDTH$} {}",
"Package:".dimmed(),
bin_config.package.as_str().bright_blue()
);
println!(" {:<LABEL_WIDTH$} {}", "Source:".dimmed(), "npm".dimmed());
println!(" {:<LABEL_WIDTH$} {}", "Node:".dimmed(), bin_config.node_version.bright_green());

Ok(ExitStatus::default())
}

#[cfg(unix)]
async fn locate_npm_link_binary(tool: &str) -> Result<AbsolutePathBuf, Error> {
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<AbsolutePathBuf, Error> {
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ Linked 'npm-global-hint-cli' to <vite-plus-home>/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
<cwd>/npm-global-hint-pkg/cli.js
Package: npm-global-hint-pkg
Source: npm
Node: <semver>

> npm-global-hint-cli # Should be callable via the link
npm-global-hint-cli works

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading