Skip to content
Open
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
55 changes: 55 additions & 0 deletions crates/vite_command/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
13 changes: 10 additions & 3 deletions crates/vite_global_cli/src/commands/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -35,7 +35,7 @@ impl AddCommand {
allow_build: Option<&str>,
pass_through_args: Option<&[String]>,
) -> Result<ExitStatus, Error> {
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 {
Expand All @@ -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?)
}
}

Expand Down
75 changes: 71 additions & 4 deletions crates/vite_global_cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<AbsolutePathBuf, Error> {
let mut executor = JsExecutor::new(None);

// Use project runtime if package.json exists, otherwise use CLI runtime
Expand All @@ -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<AbsolutePathBuf> {
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.
Expand Down Expand Up @@ -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();
Expand Down
15 changes: 12 additions & 3 deletions crates/vite_global_cli/src/commands/outdated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -36,7 +38,7 @@ impl OutdatedCommand {
global: bool,
pass_through_args: Option<&[String]>,
) -> Result<ExitStatus, Error> {
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?;

Expand All @@ -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?)
}
}

Expand Down
15 changes: 12 additions & 3 deletions crates/vite_global_cli/src/commands/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -31,7 +33,7 @@ impl RemoveCommand {
global: bool,
pass_through_args: Option<&[String]>,
) -> Result<ExitStatus, Error> {
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?;

Expand All @@ -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?)
}
}

Expand Down
15 changes: 12 additions & 3 deletions crates/vite_global_cli/src/commands/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -36,7 +38,7 @@ impl UpdateCommand {
workspace_only: bool,
pass_through_args: Option<&[String]>,
) -> Result<ExitStatus, Error> {
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?;

Expand All @@ -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?)
}
}

Expand Down
50 changes: 48 additions & 2 deletions crates/vite_install/src/commands/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -46,21 +47,46 @@ impl PackageManager {
options: &AddCommandOptions<'_>,
cwd: impl AsRef<AbsolutePath>,
) -> Result<ExitStatus, Error> {
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<AbsolutePath>,
global_npm_bin_path: Option<&AbsolutePath>,
) -> Result<ExitStatus, Error> {
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
}

/// 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<String> = 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 {
Expand Down Expand Up @@ -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());
}
}
Loading
Loading