diff --git a/crates/vite_global_cli/src/commands/global/install.rs b/crates/vite_global_cli/src/commands/global/install.rs index f750221a4d..0b09e52493 100644 --- a/crates/vite_global_cli/src/commands/global/install.rs +++ b/crates/vite_global_cli/src/commands/global/install.rs @@ -72,7 +72,9 @@ pub(crate) fn is_vp_shim_target( /// created for packages either, with one exception: `vp install -g corepack` /// may take BinConfig ownership of the corepack shim (see /// `create_package_shim`). -pub(crate) fn is_protected_shim(bin_name: &str) -> bool { +pub(crate) fn is_protected_shim(bin_name: &str, ignore_case: bool) -> bool { + let bin_name = + if cfg!(target_os = "linux") || !ignore_case { bin_name } else { &bin_name.to_lowercase() }; CORE_SHIMS.contains(&bin_name) || crate::commands::env::setup::SHIM_TOOLS.contains(&bin_name) } @@ -82,7 +84,7 @@ pub(crate) fn is_protected_shim(bin_name: &str) -> bool { /// resolution order. The exemption is scoped to the package name; any other /// package declaring a `corepack` bin must not take BinConfig ownership. pub(crate) fn package_may_own_bin(package_name: &str, bin_name: &str) -> bool { - !is_protected_shim(bin_name) || (bin_name == "corepack" && package_name == "corepack") + !is_protected_shim(bin_name, true) || (bin_name == "corepack" && package_name == "corepack") } /// Options for [`install`]. @@ -902,7 +904,7 @@ pub async fn uninstall(package_name: &str, dry_run: bool) -> Result<(), Error> { output::raw(&format!("Would uninstall {}:", package_name)); for bin_name in &bins { // Protected shims survive the real uninstall; keep dry-run honest. - if is_protected_shim(bin_name) { + if is_protected_shim(bin_name, false) { output::raw(&format!( " - shim: {} (kept: default shim)", bin_dir.join(bin_name).as_path().display() @@ -1093,7 +1095,7 @@ async fn remove_package_shim( // Don't remove protected shims (e.g., `vp remove -g corepack` must keep // the default corepack shim so it falls back to the Node-bundled or // auto-installed corepack). - if is_protected_shim(bin_name) { + if is_protected_shim(bin_name, false) { return Ok(()); } @@ -1228,6 +1230,13 @@ mod tests { // Regular bins are unrestricted assert!(package_may_own_bin("typescript", "tsc")); + + #[cfg(any(windows, target_os = "macos"))] + assert!(!package_may_own_bin("some-package", "NPM")); + #[cfg(any(windows, target_os = "macos"))] + assert!(!package_may_own_bin("some-package", "Node")); + #[cfg(any(windows, target_os = "macos"))] + assert!(!package_may_own_bin("some-package", "VP")); } #[tokio::test] diff --git a/crates/vite_global_cli/src/shim/dispatch.rs b/crates/vite_global_cli/src/shim/dispatch.rs index 5c4fecfa89..47776f7a31 100644 --- a/crates/vite_global_cli/src/shim/dispatch.rs +++ b/crates/vite_global_cli/src/shim/dispatch.rs @@ -268,7 +268,7 @@ fn check_npm_global_install_result( // Skip protected shims (core shims and default env shims). Tell // the user for the non-core names (e.g. `npm i -g corepack`): // npm installed the package, but the binary stays unlinked. - if is_protected_shim(&bin_name) { + if is_protected_shim(&bin_name, false) { if !crate::commands::global::CORE_SHIMS.contains(&bin_name.as_str()) { let hint = if bin_name == "corepack" { " Use `vp install -g corepack` to manage its version." @@ -530,7 +530,7 @@ fn remove_npm_global_uninstall_links(bin_entries: &[(String, String)], npm_prefi // Skip protected shims: a stale Npm BinConfig (e.g. a pre-default-shim // `npm install -g corepack`) must not let `npm uninstall -g` delete a // default shim that `vp env setup` now owns. - if is_protected_shim(bin_name) { + if is_protected_shim(bin_name, false) { continue; } diff --git a/crates/vite_global_cli/src/shim/mod.rs b/crates/vite_global_cli/src/shim/mod.rs index e602199107..d213901361 100644 --- a/crates/vite_global_cli/src/shim/mod.rs +++ b/crates/vite_global_cli/src/shim/mod.rs @@ -14,11 +14,15 @@ pub(crate) mod corepack; pub(crate) mod dispatch; pub(crate) mod exec; +use std::fs; + pub(crate) use cache::invalidate_cache; pub use dispatch::dispatch; pub(crate) use dispatch::find_system_tool; use vite_shared::env_vars; +use crate::commands::env::config::get_bin_dir; + /// Core shim tools (node, npm, npx). /// /// `corepack` is also a default shim (see `commands::env::setup::SHIM_TOOLS`) @@ -28,6 +32,7 @@ use vite_shared::env_vars; pub const CORE_SHIM_TOOLS: &[&str] = &["node", "npm", "npx"]; /// Extract the tool name from argv[0]. +/// We hope all bins should be put under $VP_HOME/bin /// /// Handles various formats: /// - `node` (Unix) @@ -36,11 +41,33 @@ pub const CORE_SHIM_TOOLS: &[&str] = &["node", "npm", "npx"]; /// - `C:\path\node.exe` (Windows full path) pub fn extract_tool_name(argv0: &str) -> String { let path = std::path::Path::new(argv0); - let stem = path.file_stem().unwrap_or_default().to_string_lossy(); // Handle Windows: strip .exe, .cmd extensions if present in stem // (file_stem already strips the extension) - stem.to_lowercase() + let stem = path.file_stem().unwrap_or_default().to_string_lossy().to_string(); + if cfg!(target_os = "linux") { + stem + } else { + let bin_dir = get_bin_dir(); + if let Ok(bin_dir) = bin_dir { + if let Ok(read_dir) = fs::read_dir(&bin_dir) { + for bin in read_dir.flatten() { + if bin.path().file_stem().unwrap_or_default().to_string_lossy().to_lowercase() + == stem.to_lowercase() + { + return bin + .path() + .file_stem() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + } + } + } + } + + stem + } } /// Check if the given tool name is a core shim tool (node/npm/npx). @@ -140,10 +167,10 @@ pub fn detect_shim_tool(argv0: &str) -> Option { // (so argv[0] would be "vp"), but the env var carries the real tool name. if let Some(tool) = env_tool { if !tool.is_empty() { - let tool_lower = tool.to_lowercase(); + let tool = extract_tool_name(&tool); // Accept any tool from env var (could be core or package binary) - if tool_lower != "vp" { - return Some(tool_lower); + if tool != "vp" { + return Some(tool); } } } diff --git a/packages/cli/snap-tests-global/env-install-mixed-case-bin/cli.js b/packages/cli/snap-tests-global/env-install-mixed-case-bin/cli.js new file mode 100644 index 0000000000..1414203c3c --- /dev/null +++ b/packages/cli/snap-tests-global/env-install-mixed-case-bin/cli.js @@ -0,0 +1 @@ +console.log('mixed-case bin executed'); diff --git a/packages/cli/snap-tests-global/env-install-mixed-case-bin/package.json b/packages/cli/snap-tests-global/env-install-mixed-case-bin/package.json new file mode 100644 index 0000000000..d696975b9e --- /dev/null +++ b/packages/cli/snap-tests-global/env-install-mixed-case-bin/package.json @@ -0,0 +1,7 @@ +{ + "name": "env-install-mixed-case-bin", + "version": "1.0.0", + "bin": { + "MixedCaseBin": "./cli.js" + } +} diff --git a/packages/cli/snap-tests-global/env-install-mixed-case-bin/snap.txt b/packages/cli/snap-tests-global/env-install-mixed-case-bin/snap.txt new file mode 100644 index 0000000000..eedf810cee --- /dev/null +++ b/packages/cli/snap-tests-global/env-install-mixed-case-bin/snap.txt @@ -0,0 +1,7 @@ +> vp install -g . +info: Installing 1 global package with Node.js +✓ Installed env-install-mixed-case-bin + Bins: MixedCaseBin + +> MixedCaseBin +mixed-case bin executed diff --git a/packages/cli/snap-tests-global/env-install-mixed-case-bin/steps.json b/packages/cli/snap-tests-global/env-install-mixed-case-bin/steps.json new file mode 100644 index 0000000000..6705dca3b0 --- /dev/null +++ b/packages/cli/snap-tests-global/env-install-mixed-case-bin/steps.json @@ -0,0 +1,4 @@ +{ + "commands": ["vp install -g .", "MixedCaseBin"], + "after": ["vp remove -g env-install-mixed-case-bin"] +}