Skip to content

Commit 1da5bfa

Browse files
committed
fix: respect packageManager in pm shims
1 parent d5e8e61 commit 1da5bfa

6 files changed

Lines changed: 431 additions & 48 deletions

File tree

crates/vite_global_cli/src/commands/env/current.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ use std::process::ExitStatus;
66

77
use owo_colors::OwoColorize;
88
use serde::Serialize;
9+
use vite_install::package_manager::{
10+
package_manager_bin_path, package_manager_install_dir,
11+
resolve_package_manager_from_package_json,
12+
};
913
use vite_path::AbsolutePathBuf;
1014

1115
use super::config::resolve_version;
@@ -20,6 +24,8 @@ struct CurrentEnvInfo {
2024
project_root: Option<String>,
2125
node_path: String,
2226
tool_paths: ToolPaths,
27+
#[serde(skip_serializing_if = "Option::is_none")]
28+
package_manager: Option<PackageManagerInfo>,
2329
}
2430

2531
#[derive(Serialize)]
@@ -29,6 +35,16 @@ struct ToolPaths {
2935
npx: String,
3036
}
3137

38+
#[derive(Clone, Serialize)]
39+
struct PackageManagerInfo {
40+
name: String,
41+
version: String,
42+
source: String,
43+
source_path: String,
44+
project_root: String,
45+
bin_path: String,
46+
}
47+
3248
fn accent(text: &str) -> String {
3349
if help::should_style_help() { text.bright_blue().to_string() } else { text.to_string() }
3450
}
@@ -45,6 +61,7 @@ fn print_rows(title: &str, rows: &[(&str, String)]) {
4561
/// Execute the current command.
4662
pub async fn execute(cwd: AbsolutePathBuf, json: bool) -> Result<ExitStatus, Error> {
4763
let resolution = resolve_version(&cwd).await?;
64+
let package_manager = resolve_package_manager_info(&cwd);
4865

4966
// Get the home directory for this version
5067
let home_dir =
@@ -77,6 +94,7 @@ pub async fn execute(cwd: AbsolutePathBuf, json: bool) -> Result<ExitStatus, Err
7794
npm: npm_path.as_path().display().to_string(),
7895
npx: npx_path.as_path().display().to_string(),
7996
},
97+
package_manager: package_manager.clone(),
8098
};
8199

82100
let json_str = serde_json::to_string_pretty(&info)?;
@@ -101,7 +119,37 @@ pub async fn execute(cwd: AbsolutePathBuf, json: bool) -> Result<ExitStatus, Err
101119
("npx", npx_path.as_path().display().to_string()),
102120
],
103121
);
122+
if let Some(package_manager) = package_manager {
123+
println!();
124+
print_rows(
125+
"Package Manager",
126+
&[
127+
("Name", package_manager.name),
128+
("Version", package_manager.version),
129+
("Source", package_manager.source),
130+
("Source Path", package_manager.source_path),
131+
("Project Root", package_manager.project_root),
132+
("Bin Path", package_manager.bin_path),
133+
],
134+
);
135+
}
104136
}
105137

106138
Ok(ExitStatus::default())
107139
}
140+
141+
fn resolve_package_manager_info(cwd: &AbsolutePathBuf) -> Option<PackageManagerInfo> {
142+
let resolution = resolve_package_manager_from_package_json(cwd).ok()??;
143+
let install_dir =
144+
package_manager_install_dir(resolution.package_manager_type, &resolution.version)?;
145+
let name = resolution.package_manager_type.to_string();
146+
let bin_path = package_manager_bin_path(&install_dir, &name);
147+
Some(PackageManagerInfo {
148+
name,
149+
version: resolution.version.to_string(),
150+
source: resolution.source.to_string(),
151+
source_path: resolution.source_path.as_path().display().to_string(),
152+
project_root: resolution.project_root.as_path().display().to_string(),
153+
bin_path: bin_path.as_path().display().to_string(),
154+
})
155+
}

crates/vite_global_cli/src/commands/env/which.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ use std::process::ExitStatus;
1010

1111
use chrono::Local;
1212
use owo_colors::OwoColorize;
13+
use vite_install::package_manager::{
14+
PackageManagerType, package_manager_bin_path, package_manager_install_dir,
15+
resolve_package_manager_from_package_json,
16+
};
1317
use vite_path::{AbsolutePath, AbsolutePathBuf};
1418
use vite_shared::output;
1519

@@ -27,6 +31,10 @@ const LABEL_WIDTH: usize = 10;
2731

2832
/// Execute the which command.
2933
pub async fn execute(cwd: AbsolutePathBuf, tool: &str) -> Result<ExitStatus, Error> {
34+
if let Some(status) = execute_package_manager_tool(&cwd, tool)? {
35+
return Ok(status);
36+
}
37+
3038
// Check if this is a core tool
3139
if CORE_TOOLS.contains(&tool) {
3240
return execute_core_tool(cwd, tool).await;
@@ -44,6 +52,53 @@ pub async fn execute(cwd: AbsolutePathBuf, tool: &str) -> Result<ExitStatus, Err
4452
Ok(exit_status(1))
4553
}
4654

55+
fn execute_package_manager_tool(
56+
cwd: &AbsolutePath,
57+
tool: &str,
58+
) -> Result<Option<ExitStatus>, Error> {
59+
let Some(expected_type) = package_manager_type_for_tool(tool) else {
60+
return Ok(None);
61+
};
62+
let Some(resolution) = resolve_package_manager_from_package_json(cwd)? else {
63+
return Ok(None);
64+
};
65+
if resolution.package_manager_type != expected_type {
66+
return Ok(None);
67+
}
68+
69+
let Some(install_dir) =
70+
package_manager_install_dir(resolution.package_manager_type, &resolution.version)
71+
else {
72+
return Ok(None);
73+
};
74+
let name = resolution.package_manager_type.to_string();
75+
let tool_path = package_manager_bin_path(&install_dir, &name);
76+
77+
println!("{}", tool_path.as_path().display());
78+
println!(
79+
" {:<LABEL_WIDTH$} {}",
80+
"Package:".dimmed(),
81+
format!("{}@{}", name, resolution.version).bright_blue()
82+
);
83+
println!(
84+
" {:<LABEL_WIDTH$} {}",
85+
"Source:".dimmed(),
86+
resolution.source_path.as_path().display().to_string().dimmed()
87+
);
88+
89+
Ok(Some(ExitStatus::default()))
90+
}
91+
92+
fn package_manager_type_for_tool(tool: &str) -> Option<PackageManagerType> {
93+
match tool {
94+
"npm" => Some(PackageManagerType::Npm),
95+
"pnpm" => Some(PackageManagerType::Pnpm),
96+
"yarn" => Some(PackageManagerType::Yarn),
97+
"bun" => Some(PackageManagerType::Bun),
98+
_ => None,
99+
}
100+
}
101+
47102
/// Execute which for a core tool (node, npm, npx).
48103
async fn execute_core_tool(cwd: AbsolutePathBuf, tool: &str) -> Result<ExitStatus, Error> {
49104
// Resolve version for current directory

crates/vite_global_cli/src/shim/dispatch.rs

Lines changed: 153 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,25 @@
55
//! 2. Node.js installation (if needed)
66
//! 3. Tool execution (core tools and package binaries)
77
8+
use vite_install::package_manager::{
9+
PackageManagerType, download_package_manager, package_manager_bin_path,
10+
resolve_package_manager_from_package_json,
11+
};
812
use vite_path::{AbsolutePath, AbsolutePathBuf, current_dir};
913
use vite_shared::{PrependOptions, env_vars, output, prepend_to_path_env};
1014

1115
use super::{
1216
cache::{self, ResolveCache, ResolveCacheEntry},
1317
exec, is_core_shim_tool,
1418
};
15-
use crate::commands::env::{
16-
bin_config::{BinConfig, BinSource},
17-
config::{self, ShimMode},
18-
global_install::CORE_SHIMS,
19-
package_metadata::PackageMetadata,
19+
use crate::{
20+
commands::env::{
21+
bin_config::{BinConfig, BinSource},
22+
config::{self, ShimMode},
23+
global_install::CORE_SHIMS,
24+
package_metadata::PackageMetadata,
25+
},
26+
error::Error,
2027
};
2128

2229
/// Environment variable used to prevent infinite recursion in shim dispatch.
@@ -33,6 +40,25 @@ fn is_package_manager_tool(tool: &str) -> bool {
3340
PACKAGE_MANAGER_TOOLS.contains(&tool)
3441
}
3542

43+
fn package_manager_type_for_tool(tool: &str) -> Option<PackageManagerType> {
44+
match tool {
45+
"npm" => Some(PackageManagerType::Npm),
46+
"pnpm" => Some(PackageManagerType::Pnpm),
47+
"yarn" => Some(PackageManagerType::Yarn),
48+
"bun" => Some(PackageManagerType::Bun),
49+
_ => None,
50+
}
51+
}
52+
53+
fn package_manager_bin_name(tool: &str, package_manager_type: PackageManagerType) -> &str {
54+
match (tool, package_manager_type) {
55+
(_, PackageManagerType::Npm) => "npm",
56+
(_, PackageManagerType::Pnpm) => "pnpm",
57+
(_, PackageManagerType::Yarn) => "yarn",
58+
(_, PackageManagerType::Bun) => "bun",
59+
}
60+
}
61+
3662
/// Parsed npm global command (install or uninstall).
3763
struct NpmGlobalCommand {
3864
/// Package names/specs extracted from args (e.g., ["codex", "typescript@5"])
@@ -654,6 +680,37 @@ fn resolve_npm_prefix(
654680
get_npm_global_prefix(npm_path, node_dir)
655681
}
656682

683+
/// Resolve a matching package-manager binary from the current project's explicit
684+
/// `packageManager` field.
685+
///
686+
/// The match is intentionally strict to avoid translating commands: `npm` only uses
687+
/// `npm@...`, `pnpm` only uses `pnpm@...`, etc.
688+
async fn resolve_matching_package_manager_tool(
689+
cwd: &AbsolutePath,
690+
tool: &str,
691+
) -> Result<Option<AbsolutePathBuf>, Error> {
692+
let Some(expected_type) = package_manager_type_for_tool(tool) else {
693+
return Ok(None);
694+
};
695+
696+
let Some(resolution) = resolve_package_manager_from_package_json(cwd)? else {
697+
return Ok(None);
698+
};
699+
700+
if resolution.package_manager_type != expected_type {
701+
return Ok(None);
702+
}
703+
704+
let (install_dir, _, _) = download_package_manager(
705+
resolution.package_manager_type,
706+
&resolution.version,
707+
resolution.hash.as_deref(),
708+
)
709+
.await?;
710+
let bin_name = package_manager_bin_name(tool, resolution.package_manager_type);
711+
Ok(Some(package_manager_bin_path(&install_dir, bin_name)))
712+
}
713+
657714
/// Main shim dispatch entry point.
658715
///
659716
/// Called when the binary is invoked as node, npm, npx, or a package binary.
@@ -757,24 +814,58 @@ pub async fn dispatch(tool: &str, args: &[String]) -> i32 {
757814
return 1;
758815
}
759816

760-
// Locate tool binary
761-
let tool_path = match locate_tool(&resolution.version, tool) {
817+
// Locate the Node binary for PATH preparation. Package-manager shims can use
818+
// their own declared version, but JS-based package managers still need the
819+
// project-resolved Node.js runtime to execute.
820+
let node_path = match locate_tool(&resolution.version, "node") {
762821
Ok(p) => p,
763822
Err(e) => {
764-
eprintln!("vp: Tool '{tool}' not found: {e}");
823+
eprintln!("vp: Node not found: {e}");
765824
return 1;
766825
}
767826
};
768827

769-
// Save original PATH before we modify it — needed for npm global install check.
828+
// Locate tool binary. If the current project explicitly pins the invoked
829+
// package manager in `packageManager`, prefer that managed package-manager
830+
// binary over the tool bundled with Node.js.
831+
let package_manager_tool_path = match resolve_matching_package_manager_tool(&cwd, tool).await {
832+
Ok(path) => path,
833+
Err(e) => {
834+
eprintln!("vp: Failed to resolve package manager for '{tool}': {e}");
835+
return 1;
836+
}
837+
};
838+
let tool_path = match package_manager_tool_path {
839+
Some(path) => path,
840+
None => match locate_tool(&resolution.version, tool) {
841+
Ok(p) => p,
842+
Err(e) => {
843+
eprintln!("vp: Tool '{tool}' not found: {e}");
844+
return 1;
845+
}
846+
},
847+
};
848+
849+
if !tool_path.as_path().exists() {
850+
eprintln!("vp: Tool '{tool}' not found: {}", tool_path.as_path().display());
851+
return 1;
852+
}
853+
854+
// Save original PATH before we modify it - needed for npm global install check.
770855
// Only captured for npm to avoid unnecessary work on node/npx hot path.
771856
let original_path = if tool == "npm" { std::env::var_os("PATH") } else { None };
772857

773-
// Prepare environment for recursive invocations
774-
// Prepend real node bin dir to PATH so child processes use the correct version
775-
let node_bin_dir = tool_path.parent().expect("Tool has no parent directory");
776-
// Use dedupe_anywhere=false to only check if it's first in PATH (original behavior)
858+
// Prepare environment for recursive invocations. Keep the project Node.js
859+
// bin dir available for JS package-manager shims, and when a package-manager
860+
// version was selected from `packageManager`, put that PM bin dir first so
861+
// nested invocations see the same PM version while recursion prevention is set.
862+
let node_bin_dir = node_path.parent().expect("Node has no parent directory");
777863
let _ = prepend_to_path_env(node_bin_dir, PrependOptions::default());
864+
if let Some(pm_bin_dir) = tool_path.parent()
865+
&& pm_bin_dir != node_bin_dir
866+
{
867+
let _ = prepend_to_path_env(pm_bin_dir, PrependOptions::default());
868+
}
778869

779870
// Optional debug env vars
780871
if std::env::var(env_vars::VP_DEBUG_SHIM).is_ok() {
@@ -842,6 +933,55 @@ pub async fn dispatch(tool: &str, args: &[String]) -> i32 {
842933
/// Finds the package that provides this binary and executes it with the
843934
/// Node.js version that was used to install the package.
844935
async fn dispatch_package_binary(tool: &str, args: &[String]) -> i32 {
936+
if package_manager_type_for_tool(tool).is_some() {
937+
let cwd = match current_dir() {
938+
Ok(path) => path,
939+
Err(e) => {
940+
eprintln!("vp: Failed to get current directory: {e}");
941+
return 1;
942+
}
943+
};
944+
945+
match resolve_matching_package_manager_tool(&cwd, tool).await {
946+
Ok(Some(tool_path)) => {
947+
let node_version = match resolve_with_cache(&cwd).await {
948+
Ok(resolution) => resolution.version,
949+
Err(_) => match find_package_for_binary(tool).await {
950+
Ok(Some(metadata)) => metadata.platform.node,
951+
_ => String::new(),
952+
},
953+
};
954+
955+
if !node_version.is_empty() {
956+
if let Err(e) = ensure_installed(&node_version).await {
957+
eprintln!("vp: Failed to install Node {}: {e}", node_version);
958+
return 1;
959+
}
960+
if let Ok(node_path) = locate_tool(&node_version, "node")
961+
&& let Some(node_bin_dir) = node_path.parent()
962+
{
963+
let _ = prepend_to_path_env(node_bin_dir, PrependOptions::default());
964+
}
965+
}
966+
967+
if let Some(pm_bin_dir) = tool_path.parent() {
968+
let _ = prepend_to_path_env(pm_bin_dir, PrependOptions::default());
969+
}
970+
971+
// SAFETY: Setting env vars at this point before exec is safe
972+
unsafe {
973+
std::env::set_var(RECURSION_ENV_VAR, "1");
974+
}
975+
return exec::exec_tool(&tool_path, args);
976+
}
977+
Ok(None) => {}
978+
Err(e) => {
979+
eprintln!("vp: Failed to resolve package manager for '{tool}': {e}");
980+
return 1;
981+
}
982+
}
983+
}
984+
845985
// Find which package provides this binary
846986
let package_metadata = match find_package_for_binary(tool).await {
847987
Ok(Some(metadata)) => metadata,

0 commit comments

Comments
 (0)