Skip to content
Draft
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
6 changes: 4 additions & 2 deletions .github/workflows/e2e-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -327,8 +327,10 @@ jobs:
node-version: 22
command: |
# scripts/bootstrap.mjs spawns `pnpm build` via tinyexec and needs
# pnpm on PATH (not exposed by the vp install itself).
vp i -g pnpm
# pnpm on PATH (not exposed by the vp install itself). corepack
# enable creates a pnpm launcher in the vp bin dir that resolves
# the project's pinned packageManager version (pnpm@9.15.9).
corepack enable
node scripts/bootstrap.mjs
# Report-only: oxlint 1.68.0 surfaces ts1038 ("A 'declare' modifier
# cannot be used in an already ambient context.") on varlet's
Expand Down
15 changes: 11 additions & 4 deletions crates/vite_global_cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ impl Commands {
#[command(after_help = "\
Examples:
Setup:
vp env setup # Create shims for node, npm, npx
vp env setup # Create shims for node, npm, npx, corepack
vp env on # Use vite-plus managed Node.js
vp env print # Print shell snippet for this session

Expand Down Expand Up @@ -666,6 +666,7 @@ async fn managed_install(
force,
concurrency.unwrap_or(DEFAULT_GLOBAL_INSTALL_CONCURRENCY),
false,
None,
)
.await
{
Expand Down Expand Up @@ -791,9 +792,15 @@ async fn managed_update(
}

// Call reinstall logic
if let Err((package_name, error)) =
global::install::install(&to_update, Some(&current_node_version), false, concurrency, true)
.await
if let Err((package_name, error)) = global::install::install(
&to_update,
Some(&current_node_version),
false,
concurrency,
true,
None,
)
.await
{
output::error(&format!(
"Failed to update {}: {error}",
Expand Down
2 changes: 1 addition & 1 deletion crates/vite_global_cli/src/commands/env/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ mod off;
mod on;
pub mod package_metadata;
mod pin;
mod setup;
pub(crate) mod setup;
mod unpin;
mod r#use;
mod which;
Expand Down
6 changes: 6 additions & 0 deletions crates/vite_global_cli/src/commands/env/package_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ pub struct PackageMetadata {
/// Binary names that are JavaScript files (need Node.js to run).
#[serde(default)]
pub js_bins: HashSet<String>,
/// Whether `bins` was deliberately restricted to a subset of the bins the
/// package declares (e.g., the corepack shim auto-install links only
/// `corepack`). Updates keep the restriction; explicit installs reset it.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub bins_restricted: bool,
/// Package manager used for installation (npm, yarn, pnpm)
pub manager: String,
/// Installation timestamp
Expand Down Expand Up @@ -57,6 +62,7 @@ impl PackageMetadata {
platform: Platform { node: node_version, npm: npm_version },
bins,
js_bins,
bins_restricted: false,
manager,
installed_at: Utc::now(),
}
Expand Down
47 changes: 39 additions & 8 deletions crates/vite_global_cli/src/commands/env/setup.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
//! Setup command implementation for creating bin directory and shims.
//!
//! Creates the following structure:
//! - ~/.vite-plus/bin/ - Contains vp symlink and node/npm/npx shims
//! - ~/.vite-plus/bin/ - Contains vp symlink and node/npm/npx/corepack shims
//! - ~/.vite-plus/current/ - Contains the actual vp CLI binary
//!
//! On Unix:
//! - bin/vp is a symlink to the active vp binary
//! - bin/node, bin/npm, bin/npx are symlinks to the active vp binary
//! - bin/node, bin/npm, bin/npx, bin/corepack are symlinks to the active vp binary
//! - Symlinks preserve argv[0], allowing tool detection via the symlink name
//!
//! On Windows:
//! - bin/vp.exe, bin/node.exe, bin/npm.exe, bin/npx.exe are trampoline executables
//! - bin/vp.exe, bin/node.exe, bin/npm.exe, bin/npx.exe, bin/corepack.exe are trampoline executables
//! - Each trampoline detects its tool name from its own filename and spawns
//! current\bin\vp.exe with VP_SHIM_TOOL env var set
//! - This avoids the "Terminate batch job (Y/N)?" prompt from .cmd wrappers
Expand Down Expand Up @@ -43,8 +43,8 @@ impl EnvShell {
}
}

/// Tools to create shims for (node, npm, npx, vpx, vpr)
pub(crate) const SHIM_TOOLS: &[&str] = &["node", "npm", "npx", "vpx", "vpr"];
/// Tools to create shims for (node, npm, npx, corepack, vpx, vpr)
pub(crate) const SHIM_TOOLS: &[&str] = &["node", "npm", "npx", "corepack", "vpx", "vpr"];

fn accent_command(command: &str) -> String {
if help::should_style_help() {
Expand Down Expand Up @@ -90,7 +90,7 @@ pub async fn execute(refresh: bool, env_only: bool) -> Result<ExitStatus, Error>
// Create wrapper script in bin/
setup_vp_wrapper(&current_exe, &bin_dir, refresh).await?;

// Create shims for node, npm, npx
// Create shims for node, npm, npx, corepack
let mut created = Vec::new();
let mut skipped = Vec::new();

Expand All @@ -101,6 +101,22 @@ pub async fn execute(refresh: bool, env_only: bool) -> Result<ExitStatus, Error>
} else {
skipped.push(*tool);
}

// Remove corepack-written .cmd/.ps1/extensionless launchers that
// would shadow an existing trampoline .exe in PowerShell/Git Bash
// (create_shim skips existing shims without cleaning siblings).
#[cfg(windows)]
cleanup_legacy_windows_shim(&bin_dir, tool).await;

// Drop stale `npm install -g` link configs for default shim names
// (e.g. a pre-default-shim `npm i -g corepack`): the link itself is
// replaced by the shim above, and a leftover Npm-sourced BinConfig
// would let a later `npm uninstall -g` delete the default shim.
if let Ok(Some(config)) = super::bin_config::BinConfig::load(tool).await
&& config.source == super::bin_config::BinSource::Npm
{
let _ = super::bin_config::BinConfig::delete(tool).await;
}
}

#[cfg(windows)]
Expand Down Expand Up @@ -228,10 +244,10 @@ pub(crate) async fn resolve_unix_vp_shim_target(
Ok(current_exe.to_path_buf())
}

/// Create a single shim for node/npm/npx.
/// Create a single shim for a default shim tool (node/npm/npx/corepack/vpx/vpr).
///
/// Returns `true` if the shim was created, `false` if it already exists.
async fn create_shim(
pub(crate) async fn create_shim(
source: &std::path::Path,
bin_dir: &vite_path::AbsolutePath,
tool: &str,
Expand Down Expand Up @@ -484,6 +500,12 @@ pub(crate) async fn cleanup_legacy_windows_shim(bin_dir: &vite_path::AbsolutePat
let cmd_path = bin_dir.join(format!("{tool}.cmd"));
let _ = tokio::fs::remove_file(&cmd_path).await;

// Remove .ps1 launchers (corepack's cmd-shim writes them; PowerShell
// resolves `<tool>.ps1` ahead of `<tool>.exe`, so a leftover would shadow
// the trampoline). Vite+ never creates per-tool .ps1 files in bin.
let ps1_path = bin_dir.join(format!("{tool}.ps1"));
let _ = tokio::fs::remove_file(&ps1_path).await;

// Remove old shell script wrapper (extensionless, for Git Bash)
// Only remove if it starts with #!/bin/sh (not a binary or other file)
// Read only the first 9 bytes to avoid loading large files into memory
Expand Down Expand Up @@ -847,6 +869,15 @@ mod tests {

use super::*;

#[test]
fn test_shim_tools_contains_default_shims() {
// corepack is a default shim (#858, #1309). It must NOT be a core
// shim: `vp install -g corepack` stays allowed (CORE_SHIMS guard) and
// dispatch uses a dedicated resolution path instead of the core one.
assert!(SHIM_TOOLS.contains(&"corepack"));
assert!(!crate::commands::global::CORE_SHIMS.contains(&"corepack"));
}

/// Helper: create a test_guard with user_home set to the given path.
fn home_guard(home: impl Into<std::path::PathBuf>) -> vite_shared::TestEnvGuard {
vite_shared::EnvConfig::test_guard(vite_shared::EnvConfig {
Expand Down
39 changes: 31 additions & 8 deletions crates/vite_global_cli/src/commands/env/which.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
//!
//! Shows the path to the tool binary that would be executed.
//!
//! For core tools (node, npm, npx), shows the resolved Node.js binary path
//! along with version and resolution source.
//! For core tools (node, npm, npx, corepack), shows the resolved Node.js
//! binary path along with version and resolution source.
//! For global packages, shows the binary path plus package metadata.

use std::process::ExitStatus;
Expand All @@ -23,8 +23,8 @@ use super::{
};
use crate::error::Error;

/// Core tools (node, npm, npx)
const CORE_TOOLS: &[&str] = &["node", "npm", "npx"];
/// Core tools (node, npm, npx, corepack)
const CORE_TOOLS: &[&str] = &["node", "npm", "npx", "corepack"];

/// Column width for left-side labels in aligned metadata output
const LABEL_WIDTH: usize = 10;
Expand All @@ -37,6 +37,17 @@ pub async fn execute(cwd: AbsolutePathBuf, tool: &str) -> Result<ExitStatus, Err

// Check if this is a core tool
if CORE_TOOLS.contains(&tool) {
// corepack: a vp-managed global install wins over the Node-bundled
// copy. Mirror the shim dispatch: BinConfig-based lookup, falling
// back to the bundled copy when the managed state is unusable
// (dispatch warns and falls back the same way), so the diagnostic
// matches what actually runs.
if tool == "corepack"
&& let Ok(Some(metadata)) = crate::shim::dispatch::find_package_for_binary(tool).await
&& locate_package_binary(&metadata.name, tool).is_ok()
{
return execute_package_binary(tool, &metadata).await;
}
return execute_core_tool(cwd, tool).await;
}

Expand All @@ -47,7 +58,7 @@ pub async fn execute(cwd: AbsolutePathBuf, tool: &str) -> Result<ExitStatus, Err

// Unknown tool
output::error(&format!("tool '{}' not found", tool.bold()));
eprintln!("Not a core tool (node, npm, npx) or installed global package.");
eprintln!("Not a core tool (node, npm, npx, corepack) or installed global package.");
eprintln!("Run 'vp list -g' to see installed packages.");
Ok(exit_status(1))
}
Expand Down Expand Up @@ -94,7 +105,7 @@ async fn execute_package_manager_tool(
Ok(Some(ExitStatus::default()))
}

/// Execute which for a core tool (node, npm, npx).
/// Execute which for a core tool (node, npm, npx, corepack).
async fn execute_core_tool(cwd: AbsolutePathBuf, tool: &str) -> Result<ExitStatus, Error> {
// Resolve version for current directory
let resolution = resolve_version(&cwd).await?;
Expand All @@ -116,8 +127,20 @@ async fn execute_core_tool(cwd: AbsolutePathBuf, tool: &str) -> Result<ExitStatu
// Check if the tool exists
if !tokio::fs::try_exists(&tool_path).await.unwrap_or(false) {
output::error(&format!("{} not found", tool.bold()));
eprintln!("Node.js {} is not installed.", resolution.version);
eprintln!("Run 'vp env install {}' to install it.", resolution.version);
// corepack is no longer bundled starting with Node.js 25 (and a
// bundled copy may have been removed); only print that hint when the
// Node.js installation itself is present.
if tool == "corepack"
&& crate::shim::dispatch::locate_tool(&resolution.version, "node").is_ok()
{
eprintln!("corepack is not available for Node.js {}.", resolution.version);
eprintln!(
"It is installed automatically on first use, or run 'vp install -g corepack'."
);
} else {
eprintln!("Node.js {} is not installed.", resolution.version);
eprintln!("Run 'vp env install {}' to install it.", resolution.version);
}
return Ok(exit_status(1));
}

Expand Down
Loading
Loading