diff --git a/crates/vite_command/src/lib.rs b/crates/vite_command/src/lib.rs index 9f724e3aac..b4f88f9f59 100644 --- a/crates/vite_command/src/lib.rs +++ b/crates/vite_command/src/lib.rs @@ -331,6 +331,61 @@ mod tests { assert!(result.is_ok(), "Should run command successfully, but got error: {:?}", result); } + #[tokio::test] + async fn test_run_command_with_absolute_binary_path() { + let temp_dir = create_temp_dir(); + let temp_dir_path = + AbsolutePathBuf::new(temp_dir.path().canonicalize().unwrap().to_path_buf()) + .unwrap(); + + #[cfg(unix)] + let (bin_path, args) = { + let true_path = if std::path::Path::new("/bin/true").exists() { + "/bin/true" + } else { + "/usr/bin/true" + }; + (true_path.to_string(), Vec::<&str>::new()) + }; + + #[cfg(not(unix))] + let (bin_path, args) = { + let current_exe = std::env::current_exe().unwrap(); + (current_exe.to_string_lossy().to_string(), vec!["--help"]) + }; + + let envs = HashMap::new(); + let result = run_command(&bin_path, &args, &envs, &temp_dir_path).await; + assert!(result.is_ok(), "Should run absolute binary path, but got error: {:?}", result); + } + + #[cfg(unix)] + #[tokio::test] + async fn test_run_command_rejects_non_executable_absolute_binary_path() { + let temp_dir = create_temp_dir(); + let temp_dir_path = + AbsolutePathBuf::new(temp_dir.path().canonicalize().unwrap().to_path_buf()) + .unwrap(); + let script_path = temp_dir_path.join("test-bin"); + std::fs::write(&script_path, "#!/bin/sh\nexit 0\n").unwrap(); + let envs = HashMap::new(); + let result = run_command( + &script_path.as_path().display().to_string(), + &[] as &[&str], + &envs, + &temp_dir_path, + ) + .await; + assert!(result.is_err(), "Should reject non-executable absolute binary path"); + assert_eq!( + result.unwrap_err().to_string(), + format!( + "Cannot find binary path for command '{}'", + script_path.as_path().display() + ) + ); + } + #[tokio::test] async fn test_run_command_and_not_find_binary_path() { let temp_dir = create_temp_dir(); diff --git a/crates/vite_global_cli/src/commands/add.rs b/crates/vite_global_cli/src/commands/add.rs index 30b51cb1f0..bcd9804ce1 100644 --- a/crates/vite_global_cli/src/commands/add.rs +++ b/crates/vite_global_cli/src/commands/add.rs @@ -6,7 +6,7 @@ use vite_install::{ }; use vite_path::AbsolutePathBuf; -use super::prepend_js_runtime_to_path_env; +use super::{managed_npm_bin_for_global_command, prepend_js_runtime_to_path_env}; use crate::error::Error; /// Add command for adding packages to dependencies. @@ -35,7 +35,7 @@ impl AddCommand { allow_build: Option<&str>, pass_through_args: Option<&[String]>, ) -> Result { - prepend_js_runtime_to_path_env(&self.cwd).await?; + let node_bin_prefix = prepend_js_runtime_to_path_env(&self.cwd).await?; super::ensure_package_json(&self.cwd).await?; let add_command_options = AddCommandOptions { @@ -53,8 +53,15 @@ impl AddCommand { // Detect package manager let package_manager = PackageManager::builder(&self.cwd).build_with_default().await?; + let global_npm_bin_path = managed_npm_bin_for_global_command(global, &node_bin_prefix); - Ok(package_manager.run_add_command(&add_command_options, &self.cwd).await?) + Ok(package_manager + .run_add_command_with_global_npm_bin( + &add_command_options, + &self.cwd, + global_npm_bin_path.as_deref(), + ) + .await?) } } diff --git a/crates/vite_global_cli/src/commands/mod.rs b/crates/vite_global_cli/src/commands/mod.rs index 7d0f45a839..b95ba1d0b3 100644 --- a/crates/vite_global_cli/src/commands/mod.rs +++ b/crates/vite_global_cli/src/commands/mod.rs @@ -25,8 +25,10 @@ use std::{collections::HashMap, io::BufReader}; -use vite_install::package_manager::{PackageManager, PackageManagerType}; -use vite_path::AbsolutePath; +use vite_install::package_manager::{ + PackageManager, PackageManagerType, npm_bin_path_from_node_bin_prefix, +}; +use vite_path::{AbsolutePath, AbsolutePathBuf}; use vite_shared::{PrependOptions, prepend_to_path_env}; use crate::{error::Error, js_executor::JsExecutor}; @@ -86,7 +88,9 @@ pub async fn ensure_package_json(project_path: &AbsolutePath) -> Result<(), Erro /// /// If `project_path` contains a package.json, uses the project's runtime /// (based on devEngines.runtime). Otherwise, falls back to the CLI's runtime. -pub async fn prepend_js_runtime_to_path_env(project_path: &AbsolutePath) -> Result<(), Error> { +pub async fn prepend_js_runtime_to_path_env( + project_path: &AbsolutePath, +) -> Result { let mut executor = JsExecutor::new(None); // Use project runtime if package.json exists, otherwise use CLI runtime @@ -104,7 +108,33 @@ pub async fn prepend_js_runtime_to_path_env(project_path: &AbsolutePath) -> Resu tracing::debug!("Set PATH to include {:?}", node_bin_prefix); } - Ok(()) + Ok(node_bin_prefix) +} + +pub(crate) fn managed_npm_bin_for_global_command( + global: bool, + node_bin_prefix: &AbsolutePath, +) -> Option { + if !global { + return None; + } + + let npm_bin = npm_bin_path_from_node_bin_prefix(node_bin_prefix); + match vite_command::resolve_bin( + npm_bin.as_path().as_os_str().to_string_lossy().as_ref(), + None, + node_bin_prefix, + ) { + Ok(npm_bin) => Some(npm_bin), + Err(error) => { + tracing::debug!( + npm_bin = ?npm_bin, + ?error, + "Falling back to npm from PATH for global command" + ); + None + } + } } /// Build a PackageManager, converting PackageJsonNotFound into a friendly error message. @@ -201,6 +231,43 @@ mod tests { use super::*; + #[test] + fn test_managed_npm_bin_for_global_command_uses_executable_adjacent_npm() { + let temp_dir = tempfile::tempdir().unwrap(); + let node_bin_prefix = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + let expected = npm_bin_path_from_node_bin_prefix(&node_bin_prefix); + std::fs::write(&expected, "").unwrap(); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut permissions = std::fs::metadata(&expected).unwrap().permissions(); + permissions.set_mode(0o755); + std::fs::set_permissions(&expected, permissions).unwrap(); + } + + let npm_bin = managed_npm_bin_for_global_command(true, &node_bin_prefix).unwrap(); + assert_eq!(npm_bin, expected); + assert!(managed_npm_bin_for_global_command(false, &node_bin_prefix).is_none()); + } + + #[cfg(unix)] + #[test] + fn test_managed_npm_bin_for_global_command_falls_back_when_adjacent_npm_is_not_executable() { + let temp_dir = tempfile::tempdir().unwrap(); + let node_bin_prefix = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + let npm_bin = npm_bin_path_from_node_bin_prefix(&node_bin_prefix); + std::fs::write(&npm_bin, "").unwrap(); + + assert!(managed_npm_bin_for_global_command(true, &node_bin_prefix).is_none()); + } + + #[test] + fn test_managed_npm_bin_for_global_command_falls_back_when_adjacent_npm_is_missing() { + let temp_dir = tempfile::tempdir().unwrap(); + let node_bin_prefix = AbsolutePathBuf::new(temp_dir.path().to_path_buf()).unwrap(); + assert!(managed_npm_bin_for_global_command(true, &node_bin_prefix).is_none()); + } + #[test] fn test_has_vite_plus_in_dev_dependencies() { let temp_dir = tempfile::tempdir().unwrap(); diff --git a/crates/vite_global_cli/src/commands/outdated.rs b/crates/vite_global_cli/src/commands/outdated.rs index 725e8a1d67..47a5cb0a1f 100644 --- a/crates/vite_global_cli/src/commands/outdated.rs +++ b/crates/vite_global_cli/src/commands/outdated.rs @@ -3,7 +3,9 @@ use std::process::ExitStatus; use vite_install::commands::outdated::{Format, OutdatedCommandOptions}; use vite_path::AbsolutePathBuf; -use super::{build_package_manager, prepend_js_runtime_to_path_env}; +use super::{ + build_package_manager, managed_npm_bin_for_global_command, prepend_js_runtime_to_path_env, +}; use crate::error::Error; /// Outdated command for checking outdated packages. @@ -36,7 +38,7 @@ impl OutdatedCommand { global: bool, pass_through_args: Option<&[String]>, ) -> Result { - prepend_js_runtime_to_path_env(&self.cwd).await?; + let node_bin_prefix = prepend_js_runtime_to_path_env(&self.cwd).await?; let package_manager = build_package_manager(&self.cwd).await?; @@ -55,7 +57,14 @@ impl OutdatedCommand { global, pass_through_args, }; - Ok(package_manager.run_outdated_command(&outdated_command_options, &self.cwd).await?) + let global_npm_bin_path = managed_npm_bin_for_global_command(global, &node_bin_prefix); + Ok(package_manager + .run_outdated_command_with_global_npm_bin( + &outdated_command_options, + &self.cwd, + global_npm_bin_path.as_deref(), + ) + .await?) } } diff --git a/crates/vite_global_cli/src/commands/remove.rs b/crates/vite_global_cli/src/commands/remove.rs index d1b43e02ce..ea6aac7c64 100644 --- a/crates/vite_global_cli/src/commands/remove.rs +++ b/crates/vite_global_cli/src/commands/remove.rs @@ -3,7 +3,9 @@ use std::process::ExitStatus; use vite_install::commands::remove::RemoveCommandOptions; use vite_path::AbsolutePathBuf; -use super::{build_package_manager, prepend_js_runtime_to_path_env}; +use super::{ + build_package_manager, managed_npm_bin_for_global_command, prepend_js_runtime_to_path_env, +}; use crate::error::Error; /// Remove command for removing packages from dependencies. @@ -31,7 +33,7 @@ impl RemoveCommand { global: bool, pass_through_args: Option<&[String]>, ) -> Result { - prepend_js_runtime_to_path_env(&self.cwd).await?; + let node_bin_prefix = prepend_js_runtime_to_path_env(&self.cwd).await?; let package_manager = build_package_manager(&self.cwd).await?; @@ -46,7 +48,14 @@ impl RemoveCommand { save_prod, pass_through_args, }; - Ok(package_manager.run_remove_command(&remove_command_options, &self.cwd).await?) + let global_npm_bin_path = managed_npm_bin_for_global_command(global, &node_bin_prefix); + Ok(package_manager + .run_remove_command_with_global_npm_bin( + &remove_command_options, + &self.cwd, + global_npm_bin_path.as_deref(), + ) + .await?) } } diff --git a/crates/vite_global_cli/src/commands/update.rs b/crates/vite_global_cli/src/commands/update.rs index 9dada4a6d6..ad1c5ee00b 100644 --- a/crates/vite_global_cli/src/commands/update.rs +++ b/crates/vite_global_cli/src/commands/update.rs @@ -3,7 +3,9 @@ use std::process::ExitStatus; use vite_install::commands::update::UpdateCommandOptions; use vite_path::AbsolutePathBuf; -use super::{build_package_manager, prepend_js_runtime_to_path_env}; +use super::{ + build_package_manager, managed_npm_bin_for_global_command, prepend_js_runtime_to_path_env, +}; use crate::error::Error; /// Update command for updating packages to their latest versions. @@ -36,7 +38,7 @@ impl UpdateCommand { workspace_only: bool, pass_through_args: Option<&[String]>, ) -> Result { - prepend_js_runtime_to_path_env(&self.cwd).await?; + let node_bin_prefix = prepend_js_runtime_to_path_env(&self.cwd).await?; let package_manager = build_package_manager(&self.cwd).await?; @@ -55,7 +57,14 @@ impl UpdateCommand { workspace_only, pass_through_args, }; - Ok(package_manager.run_update_command(&update_command_options, &self.cwd).await?) + let global_npm_bin_path = managed_npm_bin_for_global_command(global, &node_bin_prefix); + Ok(package_manager + .run_update_command_with_global_npm_bin( + &update_command_options, + &self.cwd, + global_npm_bin_path.as_deref(), + ) + .await?) } } diff --git a/crates/vite_install/src/commands/add.rs b/crates/vite_install/src/commands/add.rs index e223e5f517..036523f171 100644 --- a/crates/vite_install/src/commands/add.rs +++ b/crates/vite_install/src/commands/add.rs @@ -7,6 +7,7 @@ use vite_shared::output; use crate::package_manager::{ PackageManager, PackageManagerType, ResolveCommandResult, format_path_env, + resolve_global_npm_bin_path, }; /// The type of dependency to save. @@ -46,7 +47,22 @@ impl PackageManager { options: &AddCommandOptions<'_>, cwd: impl AsRef, ) -> Result { - let resolve_command = self.resolve_add_command(options); + let resolve_command = self.resolve_add_command_with_global_npm_bin(options, None); + run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) + .await + } + + /// Run the add command with an explicit npm binary for global installs. + /// Return the exit status of the command. + #[must_use] + pub async fn run_add_command_with_global_npm_bin( + &self, + options: &AddCommandOptions<'_>, + cwd: impl AsRef, + global_npm_bin_path: Option<&AbsolutePath>, + ) -> Result { + let resolve_command = + self.resolve_add_command_with_global_npm_bin(options, global_npm_bin_path); run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) .await } @@ -54,13 +70,23 @@ impl PackageManager { /// Resolve the add command. #[must_use] pub fn resolve_add_command(&self, options: &AddCommandOptions) -> ResolveCommandResult { + self.resolve_add_command_with_global_npm_bin(options, None) + } + + /// Resolve the add command with an explicit npm binary for global installs. + #[must_use] + pub fn resolve_add_command_with_global_npm_bin( + &self, + options: &AddCommandOptions, + global_npm_bin_path: Option<&AbsolutePath>, + ) -> ResolveCommandResult { let bin_name: String; let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); let mut args: Vec = Vec::new(); // global packages should use npm cli only if options.global { - bin_name = "npm".into(); + bin_name = resolve_global_npm_bin_path(global_npm_bin_path); args.push("install".into()); args.push("--global".into()); if let Some(pass_through_args) = options.pass_through_args { @@ -600,4 +626,24 @@ mod tests { assert_eq!(result.args, vec!["add", "--allow-build=react,napi", "react"]); assert_eq!(result.bin_path, "pnpm"); } + + #[test] + fn test_global_add_uses_explicit_npm_bin() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let npm_bin = if cfg!(windows) { + AbsolutePathBuf::new("C:\\node\\npm.cmd".into()).unwrap() + } else { + AbsolutePathBuf::new("/node/bin/npm".into()).unwrap() + }; + let result = pm.resolve_add_command_with_global_npm_bin( + &AddCommandOptions { + packages: &["typescript".to_string()], + global: true, + ..Default::default() + }, + Some(&npm_bin), + ); + assert_eq!(result.args, vec!["install", "--global", "typescript"]); + assert_eq!(result.bin_path, npm_bin.as_path().display().to_string()); + } } diff --git a/crates/vite_install/src/commands/outdated.rs b/crates/vite_install/src/commands/outdated.rs index 6ac0964be3..d851014bc7 100644 --- a/crates/vite_install/src/commands/outdated.rs +++ b/crates/vite_install/src/commands/outdated.rs @@ -7,6 +7,7 @@ use vite_shared::output; use crate::package_manager::{ PackageManager, PackageManagerType, ResolveCommandResult, format_path_env, + resolve_global_npm_bin_path, }; /// Output format for the outdated command. @@ -77,7 +78,22 @@ impl PackageManager { options: &OutdatedCommandOptions<'_>, cwd: impl AsRef, ) -> Result { - let resolve_command = self.resolve_outdated_command(options); + let resolve_command = self.resolve_outdated_command_with_global_npm_bin(options, None); + run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) + .await + } + + /// Run the outdated command with an explicit npm binary for global checks. + /// Return the exit status of the command. + #[must_use] + pub async fn run_outdated_command_with_global_npm_bin( + &self, + options: &OutdatedCommandOptions<'_>, + cwd: impl AsRef, + global_npm_bin_path: Option<&AbsolutePath>, + ) -> Result { + let resolve_command = + self.resolve_outdated_command_with_global_npm_bin(options, global_npm_bin_path); run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) .await } @@ -87,6 +103,16 @@ impl PackageManager { pub fn resolve_outdated_command( &self, options: &OutdatedCommandOptions, + ) -> ResolveCommandResult { + self.resolve_outdated_command_with_global_npm_bin(options, None) + } + + /// Resolve the outdated command with an explicit npm binary for global checks. + #[must_use] + pub fn resolve_outdated_command_with_global_npm_bin( + &self, + options: &OutdatedCommandOptions, + global_npm_bin_path: Option<&AbsolutePath>, ) -> ResolveCommandResult { let bin_name: String; let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); @@ -94,7 +120,7 @@ impl PackageManager { // Global packages should use npm cli only if options.global { - bin_name = "npm".into(); + bin_name = resolve_global_npm_bin_path(global_npm_bin_path); Self::format_npm_outdated_args(&mut args, options); args.push("-g".into()); } else { @@ -484,6 +510,22 @@ mod tests { assert_eq!(result.args, vec!["outdated", "-g"]); } + #[test] + fn test_global_outdated_uses_explicit_npm_bin() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let npm_bin = if cfg!(windows) { + AbsolutePathBuf::new("C:\\node\\npm.cmd".into()).unwrap() + } else { + AbsolutePathBuf::new("/node/bin/npm".into()).unwrap() + }; + let result = pm.resolve_outdated_command_with_global_npm_bin( + &OutdatedCommandOptions { global: true, ..Default::default() }, + Some(&npm_bin), + ); + assert_eq!(result.bin_path, npm_bin.as_path().display().to_string()); + assert_eq!(result.args, vec!["outdated", "-g"]); + } + #[test] fn test_pnpm_outdated_with_workspace_root() { let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); diff --git a/crates/vite_install/src/commands/remove.rs b/crates/vite_install/src/commands/remove.rs index eb380760ee..e2cf49dfdd 100644 --- a/crates/vite_install/src/commands/remove.rs +++ b/crates/vite_install/src/commands/remove.rs @@ -7,6 +7,7 @@ use vite_shared::output; use crate::package_manager::{ PackageManager, PackageManagerType, ResolveCommandResult, format_path_env, + resolve_global_npm_bin_path, }; /// Options for the remove command. @@ -32,7 +33,22 @@ impl PackageManager { options: &RemoveCommandOptions<'_>, cwd: impl AsRef, ) -> Result { - let resolve_command = self.resolve_remove_command(options); + let resolve_command = self.resolve_remove_command_with_global_npm_bin(options, None); + run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) + .await + } + + /// Run the remove command with an explicit npm binary for global installs. + /// Return the exit status of the command. + #[must_use] + pub async fn run_remove_command_with_global_npm_bin( + &self, + options: &RemoveCommandOptions<'_>, + cwd: impl AsRef, + global_npm_bin_path: Option<&AbsolutePath>, + ) -> Result { + let resolve_command = + self.resolve_remove_command_with_global_npm_bin(options, global_npm_bin_path); run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) .await } @@ -40,14 +56,23 @@ impl PackageManager { /// Resolve the remove command. #[must_use] pub fn resolve_remove_command(&self, options: &RemoveCommandOptions) -> ResolveCommandResult { + self.resolve_remove_command_with_global_npm_bin(options, None) + } + + /// Resolve the remove command with an explicit npm binary for global installs. + #[must_use] + pub fn resolve_remove_command_with_global_npm_bin( + &self, + options: &RemoveCommandOptions, + global_npm_bin_path: Option<&AbsolutePath>, + ) -> ResolveCommandResult { let bin_name: String; let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); let mut args: Vec = Vec::new(); // global packages should use npm cli only if options.global { - // TODO(@fengmk2): Need to handle the case where the npm CLI does not exist in the PATH - bin_name = "npm".into(); + bin_name = resolve_global_npm_bin_path(global_npm_bin_path); args.push("uninstall".into()); args.push("--global".into()); if let Some(pass_through_args) = options.pass_through_args { @@ -443,6 +468,26 @@ mod tests { assert_eq!(result.bin_path, "npm"); } + #[test] + fn test_global_remove_uses_explicit_npm_bin() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm); + let npm_bin = if cfg!(windows) { + AbsolutePathBuf::new("C:\\node\\npm.cmd".into()).unwrap() + } else { + AbsolutePathBuf::new("/node/bin/npm".into()).unwrap() + }; + let result = pm.resolve_remove_command_with_global_npm_bin( + &RemoveCommandOptions { + packages: &["typescript".to_string()], + global: true, + ..Default::default() + }, + Some(&npm_bin), + ); + assert_eq!(result.args, vec!["uninstall", "--global", "typescript"]); + assert_eq!(result.bin_path, npm_bin.as_path().display().to_string()); + } + #[test] fn test_remove_multiple_packages() { let pm = create_mock_package_manager(PackageManagerType::Pnpm); diff --git a/crates/vite_install/src/commands/update.rs b/crates/vite_install/src/commands/update.rs index 9e1a46624f..45641323fc 100644 --- a/crates/vite_install/src/commands/update.rs +++ b/crates/vite_install/src/commands/update.rs @@ -7,6 +7,7 @@ use vite_shared::output; use crate::package_manager::{ PackageManager, PackageManagerType, ResolveCommandResult, format_path_env, + resolve_global_npm_bin_path, }; /// Options for the update command. @@ -36,7 +37,22 @@ impl PackageManager { options: &UpdateCommandOptions<'_>, cwd: impl AsRef, ) -> Result { - let resolve_command = self.resolve_update_command(options); + let resolve_command = self.resolve_update_command_with_global_npm_bin(options, None); + run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) + .await + } + + /// Run the update command with an explicit npm binary for global installs. + /// Return the exit status of the command. + #[must_use] + pub async fn run_update_command_with_global_npm_bin( + &self, + options: &UpdateCommandOptions<'_>, + cwd: impl AsRef, + global_npm_bin_path: Option<&AbsolutePath>, + ) -> Result { + let resolve_command = + self.resolve_update_command_with_global_npm_bin(options, global_npm_bin_path); run_command(&resolve_command.bin_path, &resolve_command.args, &resolve_command.envs, cwd) .await } @@ -44,13 +60,23 @@ impl PackageManager { /// Resolve the update command. #[must_use] pub fn resolve_update_command(&self, options: &UpdateCommandOptions) -> ResolveCommandResult { + self.resolve_update_command_with_global_npm_bin(options, None) + } + + /// Resolve the update command with an explicit npm binary for global installs. + #[must_use] + pub fn resolve_update_command_with_global_npm_bin( + &self, + options: &UpdateCommandOptions, + global_npm_bin_path: Option<&AbsolutePath>, + ) -> ResolveCommandResult { let bin_name: String; let envs = HashMap::from([("PATH".to_string(), format_path_env(self.get_bin_prefix()))]); let mut args: Vec = Vec::new(); // global packages should use npm cli only if options.global { - bin_name = "npm".into(); + bin_name = resolve_global_npm_bin_path(global_npm_bin_path); args.push("update".into()); args.push("--global".into()); if let Some(pass_through_args) = options.pass_through_args { @@ -551,6 +577,26 @@ mod tests { assert_eq!(result.bin_path, "npm"); } + #[test] + fn test_global_update_uses_explicit_npm_bin() { + let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); + let npm_bin = if cfg!(windows) { + AbsolutePathBuf::new("C:\\node\\npm.cmd".into()).unwrap() + } else { + AbsolutePathBuf::new("/node/bin/npm".into()).unwrap() + }; + let result = pm.resolve_update_command_with_global_npm_bin( + &UpdateCommandOptions { + packages: &["typescript".to_string()], + global: true, + ..Default::default() + }, + Some(&npm_bin), + ); + assert_eq!(result.args, vec!["update", "--global", "typescript"]); + assert_eq!(result.bin_path, npm_bin.as_path().display().to_string()); + } + #[test] fn test_pnpm_update_multiple_packages() { let pm = create_mock_package_manager(PackageManagerType::Pnpm, "10.0.0"); diff --git a/crates/vite_install/src/package_manager.rs b/crates/vite_install/src/package_manager.rs index bd7c16e4f3..410bf10a9c 100644 --- a/crates/vite_install/src/package_manager.rs +++ b/crates/vite_install/src/package_manager.rs @@ -66,6 +66,18 @@ pub struct ResolveCommandResult { pub envs: HashMap, } +#[must_use] +pub fn npm_bin_path_from_node_bin_prefix(node_bin_prefix: &AbsolutePath) -> AbsolutePathBuf { + if cfg!(windows) { node_bin_prefix.join("npm.cmd") } else { node_bin_prefix.join("npm") } +} + +#[must_use] +pub(crate) fn resolve_global_npm_bin_path(global_npm_bin_path: Option<&AbsolutePath>) -> String { + global_npm_bin_path + .map(|path| path.as_path().display().to_string()) + .unwrap_or_else(|| "npm".into()) +} + /// The package manager. /// Use `PackageManager::builder()` to create a package manager. /// Then use `PackageManager::resolve_command()` to resolve the command result. @@ -1014,6 +1026,22 @@ mod tests { .expect("Failed to write pnpm-workspace.yaml"); } + #[test] + fn test_npm_bin_path_from_node_bin_prefix() { + let node_bin_prefix = if cfg!(windows) { + AbsolutePathBuf::new("C:\\node".into()).unwrap() + } else { + AbsolutePathBuf::new("/node/bin".into()).unwrap() + }; + let npm_bin = npm_bin_path_from_node_bin_prefix(&node_bin_prefix); + let expected = if cfg!(windows) { + node_bin_prefix.join("npm.cmd") + } else { + node_bin_prefix.join("npm") + }; + assert_eq!(npm_bin, expected); + } + #[test] fn test_find_package_root() { let temp_dir = create_temp_dir();