Skip to content
Closed
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ jobs:
# Force-reinstall the published `latest` version rather than the local
# build's version: mid-release-cycle the build can be ahead of the
# registry, and `vp upgrade <version>` must download <version> from npm.
$version = "$(npm view vite-plus version)".Trim()
$version = "$(pnpm view vite-plus version)".Trim()
if (-not $version) {
Write-Error "Failed to resolve latest published vite-plus version"
exit 1
Expand Down Expand Up @@ -605,7 +605,7 @@ jobs:
# Force-reinstall the published `latest` version rather than the local
# build's version: mid-release-cycle the build can be ahead of the
# registry, and `vp upgrade <version>` must download <version> from npm.
version=$(npm view vite-plus version)
version=$(pnpm view vite-plus version)
if [ -z "$version" ]; then
echo "Failed to resolve latest published vite-plus version"
exit 1
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/e2e-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ jobs:
# A release commit can leave `packages/{core,cli}` at a published
# version (e.g. 0.1.22), which would make `pnpm pack` emit
# `*-0.1.22.tgz` instead. Pin both to 0.0.0 so the names stay stable.
(cd packages/core && npm pkg set version=0.0.0)
(cd packages/cli && npm pkg set version=0.0.0)
(cd packages/core && pnpm pkg set version=0.0.0)
(cd packages/cli && pnpm pkg set version=0.0.0)
cd packages/core && pnpm pack --pack-destination ../../tmp/tgz && cd ../..
cd packages/cli && pnpm pack --pack-destination ../../tmp/tgz && cd ../..
# Bun is uniquely strict about peer-dep resolution:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test-vp-create.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ jobs:
# A release commit can leave `packages/{core,cli}` at a published
# version (e.g. 0.1.22), which would make `pnpm pack` emit
# `*-0.1.22.tgz` instead. Pin both to 0.0.0 so the names stay stable.
(cd packages/core && npm pkg set version=0.0.0)
(cd packages/cli && npm pkg set version=0.0.0)
(cd packages/core && pnpm pkg set version=0.0.0)
(cd packages/cli && pnpm pkg set version=0.0.0)
cd packages/core && pnpm pack --pack-destination ../../tmp/tgz && cd ../..
cd packages/cli && pnpm pack --pack-destination ../../tmp/tgz && cd ../..
# Bun is uniquely strict about peer-dep resolution:
Expand Down
83 changes: 75 additions & 8 deletions crates/vite_global_cli/src/shim/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,26 @@ async fn resolve_matching_package_manager_tool(
Ok(Some(package_manager_bin_path(&install_dir, bin_name)))
}

fn npm_package_manager_mismatch_message(cwd: &AbsolutePath) -> Result<Option<String>, Error> {
let Some(resolution) = resolve_package_manager_from_package_json(cwd)? else {
return Ok(None);
};

if resolution.package_manager_type == PackageManagerType::Npm {
return Ok(None);
}

let package_manager = resolution.package_manager_type;
let path = resolution.source_path.as_path().display();
let source = resolution.source;
Ok(Some(
vite_str::format!(
"This project is configured to use {package_manager} because {path} has a \"{source}\" field\n\nUse `{package_manager}` instead of `npm`."
)
.to_string(),
))
}

async fn prepend_js_child_process_path_env(
cwd: &AbsolutePath,
node_bin_dir: &AbsolutePath,
Expand Down Expand Up @@ -762,14 +782,6 @@ pub async fn dispatch(tool: &str, args: &[String]) -> i32 {
return crate::commands::vpr::execute_vpr(args, &cwd).await;
}

// Check recursion prevention - if already in a shim context, passthrough directly
// Only applies to core tools (node/npm/npx) whose bin dir is prepended to PATH.
// Package binaries are always resolved via metadata lookup, so they can't loop.
if std::env::var(RECURSION_ENV_VAR).is_ok() && is_core_shim_tool(tool) {
tracing::debug!("recursion prevention enabled for core tool");
return passthrough_to_system(tool, args);
}

// Check bypass mode (explicit environment variable)
if std::env::var(env_vars::VP_BYPASS).is_ok() {
tracing::debug!("bypass mode enabled");
Expand Down Expand Up @@ -813,6 +825,37 @@ pub async fn dispatch(tool: &str, args: &[String]) -> i32 {
return super::corepack::dispatch_corepack(args).await;
}

// Check recursion prevention - if already in a shim context, passthrough directly
// Only applies to core tools (node/npm/npx) whose bin dir is prepended to PATH.
// Package binaries are always resolved via metadata lookup, so they can't loop.
if std::env::var(RECURSION_ENV_VAR).is_ok() && is_core_shim_tool(tool) {
tracing::debug!("recursion prevention enabled for core tool");
return passthrough_to_system(tool, args);
}

// Top-level npm should not run against a project that explicitly declares
// a different package manager.
if tool == "npm" {
let cwd = match current_dir() {
Ok(path) => path,
Err(e) => {
eprintln!("vp: Failed to get current directory: {e}");
return 1;
}
};
match npm_package_manager_mismatch_message(&cwd) {
Ok(Some(message)) => {
output::raw_stderr(&message);
return 1;
}
Ok(None) => {}
Err(e) => {
eprintln!("vp: Failed to resolve package manager for '{tool}': {e}");
return 1;
}
}
}

// Check if this is a package binary (not node/npm/npx)
if !is_core_shim_tool(tool) {
return dispatch_package_binary(tool, args).await;
Expand Down Expand Up @@ -1503,6 +1546,30 @@ mod tests {
}
}

#[test]
fn test_package_manager_tool_mismatch_blocks_npm_for_pnpm_project() {
let temp = TempDir::new().unwrap();
let cwd = AbsolutePathBuf::new(temp.path().to_path_buf()).unwrap();
std::fs::write(
cwd.join("package.json"),
r#"{"name":"fixture","packageManager":"pnpm@10.33.2"}"#,
)
.unwrap();

let message = npm_package_manager_mismatch_message(&cwd).unwrap().unwrap();

assert!(message.contains("This project is configured to use pnpm"), "got: {message}");
assert!(message.contains("\"packageManager\" field"), "got: {message}");
assert!(message.contains("Use `pnpm` instead of `npm`."), "got: {message}");

std::fs::write(
cwd.join("package.json"),
r#"{"name":"fixture","packageManager":"npm@10.9.4"}"#,
)
.unwrap();
assert!(npm_package_manager_mismatch_message(&cwd).unwrap().is_none());
}

#[tokio::test]
#[serial]
async fn test_resolve_with_cache_bypasses_stale_lts_after_dev_engines_is_added() {
Expand Down
8 changes: 6 additions & 2 deletions crates/vite_setup/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,8 @@ async fn run_pnpm_install(
.args(args)
.current_dir(version_dir)
.env("CI", "true")
.env("PATH", path);
.env("PATH", path)
.env(vite_shared::env_vars::VP_TOOL_RECURSION, "1");

if let Some(registry_url) = registry {
cmd.env(vite_shared::env_vars::NPM_CONFIG_REGISTRY, registry_url);
Expand Down Expand Up @@ -873,7 +874,7 @@ mod tests {
let node_binary = node_bin.join("node");
tokio::fs::write(
&node_binary,
"#!/bin/sh\nprintf '%s\\n' \"$@\" > invocation.txt\nprintf '%s' \"$PATH\" > path.txt\nprintf '%s' \"$npm_config_registry\" > registry.txt\n",
"#!/bin/sh\nprintf '%s\\n' \"$@\" > invocation.txt\nprintf '%s' \"$PATH\" > path.txt\nprintf '%s' \"$npm_config_registry\" > registry.txt\nprintf '%s' \"$VP_TOOL_RECURSION\" > recursion.txt\n",
)
.await
.unwrap();
Expand Down Expand Up @@ -913,6 +914,9 @@ mod tests {
// falls back to its default registry.
let registry = tokio::fs::read_to_string(version_dir.join("registry.txt")).await.unwrap();
assert_eq!(registry, "");

let recursion = tokio::fs::read_to_string(version_dir.join("recursion.txt")).await.unwrap();
assert_eq!(recursion, "1");
}

#[cfg(unix)]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "shim-npm-ignores-mismatched-package-manager",
"name": "shim-npm-blocks-mismatched-package-manager",
"version": "1.0.0",
"private": true,
"packageManager": "pnpm@10.19.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
> vp env exec node --version # Ensure Node.js is installed first
v20.18.0

> npm --version || echo "npm shim blocked mismatched packageManager"
This project is configured to use pnpm because <cwd>/package.json has a "packageManager" field

Use `pnpm` instead of `npm`.
npm shim blocked mismatched packageManager

> test "$(npx --version)" != "10.19.0" && echo "npx shim ignored mismatched packageManager"
npx shim ignored mismatched packageManager
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"env": {},
"commands": [
"vp env exec node --version # Ensure Node.js is installed first",
"test \"$(npm --version)\" != \"10.19.0\" && echo \"npm shim ignored mismatched packageManager\"",
"npm --version || echo \"npm shim blocked mismatched packageManager\"",
"test \"$(npx --version)\" != \"10.19.0\" && echo \"npx shim ignored mismatched packageManager\""
]
}

This file was deleted.

Loading