Skip to content

Commit 9c9aec4

Browse files
authored
Merge branch 'main' into feat/vp-shell-env
2 parents e1ab2e0 + 9168712 commit 9c9aec4

25 files changed

Lines changed: 635 additions & 69 deletions

File tree

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

Lines changed: 53 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+
PackageManagerResolution, 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,33 @@ 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+
48+
impl PackageManagerInfo {
49+
fn from_resolution(resolution: PackageManagerResolution) -> Option<Self> {
50+
let install_dir =
51+
package_manager_install_dir(resolution.package_manager_type, &resolution.version)?;
52+
let name = resolution.package_manager_type.to_string();
53+
let bin_path = package_manager_bin_path(&install_dir, &name);
54+
Some(Self {
55+
name,
56+
version: resolution.version.to_string(),
57+
source: resolution.source.to_string(),
58+
source_path: resolution.source_path.as_path().display().to_string(),
59+
project_root: resolution.project_root.as_path().display().to_string(),
60+
bin_path: bin_path.as_path().display().to_string(),
61+
})
62+
}
63+
}
64+
3265
fn accent(text: &str) -> String {
3366
if help::should_style_help() { text.bright_blue().to_string() } else { text.to_string() }
3467
}
@@ -45,6 +78,7 @@ fn print_rows(title: &str, rows: &[(&str, String)]) {
4578
/// Execute the current command.
4679
pub async fn execute(cwd: AbsolutePathBuf, json: bool) -> Result<ExitStatus, Error> {
4780
let resolution = resolve_version(&cwd).await?;
81+
let package_manager = resolve_package_manager_info(&cwd);
4882

4983
// Get the home directory for this version
5084
let home_dir =
@@ -77,6 +111,7 @@ pub async fn execute(cwd: AbsolutePathBuf, json: bool) -> Result<ExitStatus, Err
77111
npm: npm_path.as_path().display().to_string(),
78112
npx: npx_path.as_path().display().to_string(),
79113
},
114+
package_manager: package_manager.clone(),
80115
};
81116

82117
let json_str = serde_json::to_string_pretty(&info)?;
@@ -101,7 +136,25 @@ pub async fn execute(cwd: AbsolutePathBuf, json: bool) -> Result<ExitStatus, Err
101136
("npx", npx_path.as_path().display().to_string()),
102137
],
103138
);
139+
if let Some(package_manager) = package_manager {
140+
println!();
141+
print_rows(
142+
"Package Manager",
143+
&[
144+
("Name", package_manager.name),
145+
("Version", package_manager.version),
146+
("Source", package_manager.source),
147+
("Source Path", package_manager.source_path),
148+
("Project Root", package_manager.project_root),
149+
("Bin Path", package_manager.bin_path),
150+
],
151+
);
152+
}
104153
}
105154

106155
Ok(ExitStatus::default())
107156
}
157+
158+
fn resolve_package_manager_info(cwd: &AbsolutePathBuf) -> Option<PackageManagerInfo> {
159+
PackageManagerInfo::from_resolution(resolve_package_manager_from_package_json(cwd).ok()??)
160+
}

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

Lines changed: 50 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).await? {
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,48 @@ pub async fn execute(cwd: AbsolutePathBuf, tool: &str) -> Result<ExitStatus, Err
4452
Ok(exit_status(1))
4553
}
4654

55+
async fn execute_package_manager_tool(
56+
cwd: &AbsolutePath,
57+
tool: &str,
58+
) -> Result<Option<ExitStatus>, Error> {
59+
let Some(expected_type) = PackageManagerType::from_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) = package_manager_install_dir(expected_type, &resolution.version) else {
70+
return Ok(None);
71+
};
72+
let bin_name = expected_type.bin_name_for_tool(tool);
73+
let tool_path = package_manager_bin_path(&install_dir, bin_name);
74+
75+
if !tokio::fs::try_exists(&tool_path).await.unwrap_or(false) {
76+
output::error(&format!("{} not found", tool.bold()));
77+
eprintln!("{expected_type} {} is not installed.", resolution.version);
78+
eprintln!("Run 'vp install' inside the project to download it.");
79+
return Ok(Some(exit_status(1)));
80+
}
81+
82+
println!("{}", tool_path.as_path().display());
83+
println!(
84+
" {:<LABEL_WIDTH$} {}",
85+
"Package:".dimmed(),
86+
format!("{}@{}", expected_type, resolution.version).bright_blue()
87+
);
88+
println!(
89+
" {:<LABEL_WIDTH$} {}",
90+
"Source:".dimmed(),
91+
resolution.source_path.as_path().display().to_string().dimmed()
92+
);
93+
94+
Ok(Some(ExitStatus::default()))
95+
}
96+
4797
/// Execute which for a core tool (node, npm, npx).
4898
async fn execute_core_tool(cwd: AbsolutePathBuf, tool: &str) -> Result<ExitStatus, Error> {
4999
// Resolve version for current directory

crates/vite_global_cli/src/shim/dispatch.rs

Lines changed: 151 additions & 18 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+
package_manager_install_dir, 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.
@@ -25,12 +32,14 @@ use crate::commands::env::{
2532
/// directly using the current PATH (passthrough mode).
2633
const RECURSION_ENV_VAR: &str = env_vars::VP_TOOL_RECURSION;
2734

28-
/// Package manager tools that should resolve Node.js version from the project context
29-
/// rather than using the install-time version.
30-
const PACKAGE_MANAGER_TOOLS: &[&str] = &["pnpm", "yarn", "bun"];
31-
35+
/// Package-manager tools whose Node.js runtime should be resolved from the
36+
/// project context rather than the install-time version.
37+
///
38+
/// Intentionally excludes `npm`/`npx`: those are core shims (see
39+
/// `is_core_shim_tool`) and never reach `dispatch_package_binary`, so they are
40+
/// handled by the main `dispatch` path instead.
3241
fn is_package_manager_tool(tool: &str) -> bool {
33-
PACKAGE_MANAGER_TOOLS.contains(&tool)
42+
matches!(PackageManagerType::from_tool(tool), Some(t) if t != PackageManagerType::Npm)
3443
}
3544

3645
/// Parsed npm global command (install or uninstall).
@@ -654,6 +663,48 @@ fn resolve_npm_prefix(
654663
get_npm_global_prefix(npm_path, node_dir)
655664
}
656665

666+
/// Resolve a matching package-manager binary from the current project's explicit
667+
/// `packageManager` field.
668+
///
669+
/// The match is intentionally strict to avoid translating commands: `npm` only uses
670+
/// `npm@...`, `pnpm` only uses `pnpm@...`, etc.
671+
async fn resolve_matching_package_manager_tool(
672+
cwd: &AbsolutePath,
673+
tool: &str,
674+
) -> Result<Option<AbsolutePathBuf>, Error> {
675+
let Some(expected_type) = PackageManagerType::from_tool(tool) else {
676+
return Ok(None);
677+
};
678+
679+
let Some(resolution) = resolve_package_manager_from_package_json(cwd)? else {
680+
return Ok(None);
681+
};
682+
683+
if resolution.package_manager_type != expected_type {
684+
return Ok(None);
685+
}
686+
687+
let bin_name = expected_type.bin_name_for_tool(tool);
688+
689+
// Fast path: if the managed install already exists, skip download_package_manager
690+
// entirely. The slow path stats three files (`bin`, `.cmd`, `.ps1`) on every
691+
// invocation, which adds up on the shim hot path.
692+
if let Some(install_dir) = package_manager_install_dir(expected_type, &resolution.version) {
693+
let bin_path = package_manager_bin_path(&install_dir, bin_name);
694+
if bin_path.as_path().exists() {
695+
return Ok(Some(bin_path));
696+
}
697+
}
698+
699+
let (install_dir, _, _) = download_package_manager(
700+
resolution.package_manager_type,
701+
&resolution.version,
702+
resolution.hash.as_deref(),
703+
)
704+
.await?;
705+
Ok(Some(package_manager_bin_path(&install_dir, bin_name)))
706+
}
707+
657708
/// Main shim dispatch entry point.
658709
///
659710
/// Called when the binary is invoked as node, npm, npx, or a package binary.
@@ -757,24 +808,53 @@ pub async fn dispatch(tool: &str, args: &[String]) -> i32 {
757808
return 1;
758809
}
759810

760-
// Locate tool binary
761-
let tool_path = match locate_tool(&resolution.version, tool) {
811+
// Locate the Node binary for PATH preparation. Package-manager shims can use
812+
// their own declared version, but JS-based package managers still need the
813+
// project-resolved Node.js runtime to execute.
814+
let node_path = match locate_tool(&resolution.version, "node") {
762815
Ok(p) => p,
763816
Err(e) => {
764-
eprintln!("vp: Tool '{tool}' not found: {e}");
817+
eprintln!("vp: Node not found: {e}");
818+
return 1;
819+
}
820+
};
821+
822+
// Locate tool binary. If the current project explicitly pins the invoked
823+
// package manager in `packageManager`, prefer that managed package-manager
824+
// binary over the tool bundled with Node.js.
825+
let package_manager_tool_path = match resolve_matching_package_manager_tool(&cwd, tool).await {
826+
Ok(path) => path,
827+
Err(e) => {
828+
eprintln!("vp: Failed to resolve package manager for '{tool}': {e}");
765829
return 1;
766830
}
767831
};
832+
let tool_path = match package_manager_tool_path {
833+
Some(path) => path,
834+
None => match locate_tool(&resolution.version, tool) {
835+
Ok(p) => p,
836+
Err(e) => {
837+
eprintln!("vp: Tool '{tool}' not found: {e}");
838+
return 1;
839+
}
840+
},
841+
};
768842

769-
// Save original PATH before we modify it needed for npm global install check.
843+
// Save original PATH before we modify it - needed for npm global install check.
770844
// Only captured for npm to avoid unnecessary work on node/npx hot path.
771845
let original_path = if tool == "npm" { std::env::var_os("PATH") } else { None };
772846

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)
847+
// Prepare environment for recursive invocations. Keep the project Node.js
848+
// bin dir available for JS package-manager shims, and when a package-manager
849+
// version was selected from `packageManager`, put that PM bin dir first so
850+
// nested invocations see the same PM version while recursion prevention is set.
851+
let node_bin_dir = node_path.parent().expect("Node has no parent directory");
777852
let _ = prepend_to_path_env(node_bin_dir, PrependOptions::default());
853+
if let Some(pm_bin_dir) = tool_path.parent()
854+
&& pm_bin_dir != node_bin_dir
855+
{
856+
let _ = prepend_to_path_env(pm_bin_dir, PrependOptions::default());
857+
}
778858

779859
// Optional debug env vars
780860
if std::env::var(env_vars::VP_DEBUG_SHIM).is_ok() {
@@ -842,6 +922,59 @@ pub async fn dispatch(tool: &str, args: &[String]) -> i32 {
842922
/// Finds the package that provides this binary and executes it with the
843923
/// Node.js version that was used to install the package.
844924
async fn dispatch_package_binary(tool: &str, args: &[String]) -> i32 {
925+
if let Some(pm_family) = PackageManagerType::from_tool(tool) {
926+
let cwd = match current_dir() {
927+
Ok(path) => path,
928+
Err(e) => {
929+
eprintln!("vp: Failed to get current directory: {e}");
930+
return 1;
931+
}
932+
};
933+
934+
match resolve_matching_package_manager_tool(&cwd, tool).await {
935+
Ok(Some(tool_path)) => {
936+
// Bun is a native binary and does not need a Node.js runtime on PATH;
937+
// JS-based PMs (npm/pnpm/yarn) do.
938+
if pm_family != PackageManagerType::Bun {
939+
let node_version = match resolve_with_cache(&cwd).await {
940+
Ok(resolution) => resolution.version,
941+
Err(_) => match find_package_for_binary(tool).await {
942+
Ok(Some(metadata)) => metadata.platform.node,
943+
_ => String::new(),
944+
},
945+
};
946+
947+
if !node_version.is_empty() {
948+
if let Err(e) = ensure_installed(&node_version).await {
949+
eprintln!("vp: Failed to install Node {}: {e}", node_version);
950+
return 1;
951+
}
952+
if let Ok(node_path) = locate_tool(&node_version, "node")
953+
&& let Some(node_bin_dir) = node_path.parent()
954+
{
955+
let _ = prepend_to_path_env(node_bin_dir, PrependOptions::default());
956+
}
957+
}
958+
}
959+
960+
if let Some(pm_bin_dir) = tool_path.parent() {
961+
let _ = prepend_to_path_env(pm_bin_dir, PrependOptions::default());
962+
}
963+
964+
// SAFETY: Setting env vars at this point before exec is safe
965+
unsafe {
966+
std::env::set_var(RECURSION_ENV_VAR, "1");
967+
}
968+
return exec::exec_tool(&tool_path, args);
969+
}
970+
Ok(None) => {}
971+
Err(e) => {
972+
eprintln!("vp: Failed to resolve package manager for '{tool}': {e}");
973+
return 1;
974+
}
975+
}
976+
}
977+
845978
// Find which package provides this binary
846979
let package_metadata = match find_package_for_binary(tool).await {
847980
Ok(Some(metadata)) => metadata,

0 commit comments

Comments
 (0)