diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0811fb336a..ae7ca51eb4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 ` must download 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 @@ -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 ` must download 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 diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index e63f1a51f1..6afd471de6 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -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: diff --git a/.github/workflows/test-vp-create.yml b/.github/workflows/test-vp-create.yml index 2a2973cc30..979ec5b210 100644 --- a/.github/workflows/test-vp-create.yml +++ b/.github/workflows/test-vp-create.yml @@ -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: diff --git a/crates/vite_global_cli/src/shim/dispatch.rs b/crates/vite_global_cli/src/shim/dispatch.rs index 47776f7a31..988776478d 100644 --- a/crates/vite_global_cli/src/shim/dispatch.rs +++ b/crates/vite_global_cli/src/shim/dispatch.rs @@ -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, 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, @@ -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"); @@ -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; @@ -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() { diff --git a/crates/vite_setup/src/install.rs b/crates/vite_setup/src/install.rs index 879af4f4a5..e497f017b5 100644 --- a/crates/vite_setup/src/install.rs +++ b/crates/vite_setup/src/install.rs @@ -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); @@ -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(); @@ -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)] diff --git a/packages/cli/snap-tests-global/shim-npm-ignores-mismatched-package-manager/.node-version b/packages/cli/snap-tests-global/shim-npm-blocks-mismatched-package-manager/.node-version similarity index 100% rename from packages/cli/snap-tests-global/shim-npm-ignores-mismatched-package-manager/.node-version rename to packages/cli/snap-tests-global/shim-npm-blocks-mismatched-package-manager/.node-version diff --git a/packages/cli/snap-tests-global/shim-npm-ignores-mismatched-package-manager/package.json b/packages/cli/snap-tests-global/shim-npm-blocks-mismatched-package-manager/package.json similarity index 58% rename from packages/cli/snap-tests-global/shim-npm-ignores-mismatched-package-manager/package.json rename to packages/cli/snap-tests-global/shim-npm-blocks-mismatched-package-manager/package.json index 904d8ea55a..49bcac5efc 100644 --- a/packages/cli/snap-tests-global/shim-npm-ignores-mismatched-package-manager/package.json +++ b/packages/cli/snap-tests-global/shim-npm-blocks-mismatched-package-manager/package.json @@ -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" diff --git a/packages/cli/snap-tests-global/shim-npm-blocks-mismatched-package-manager/snap.txt b/packages/cli/snap-tests-global/shim-npm-blocks-mismatched-package-manager/snap.txt new file mode 100644 index 0000000000..c128d824e8 --- /dev/null +++ b/packages/cli/snap-tests-global/shim-npm-blocks-mismatched-package-manager/snap.txt @@ -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 /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 diff --git a/packages/cli/snap-tests-global/shim-npm-ignores-mismatched-package-manager/steps.json b/packages/cli/snap-tests-global/shim-npm-blocks-mismatched-package-manager/steps.json similarity index 70% rename from packages/cli/snap-tests-global/shim-npm-ignores-mismatched-package-manager/steps.json rename to packages/cli/snap-tests-global/shim-npm-blocks-mismatched-package-manager/steps.json index 87795302a5..8974cd5a6a 100644 --- a/packages/cli/snap-tests-global/shim-npm-ignores-mismatched-package-manager/steps.json +++ b/packages/cli/snap-tests-global/shim-npm-blocks-mismatched-package-manager/steps.json @@ -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\"" ] } diff --git a/packages/cli/snap-tests-global/shim-npm-ignores-mismatched-package-manager/snap.txt b/packages/cli/snap-tests-global/shim-npm-ignores-mismatched-package-manager/snap.txt deleted file mode 100644 index 72b32bee58..0000000000 --- a/packages/cli/snap-tests-global/shim-npm-ignores-mismatched-package-manager/snap.txt +++ /dev/null @@ -1,8 +0,0 @@ -> vp env exec node --version # Ensure Node.js is installed first -v20.18.0 - -> test "$(npm --version)" != "10.19.0" && echo "npm shim ignored mismatched packageManager" -npm shim ignored mismatched packageManager - -> test "$(npx --version)" != "10.19.0" && echo "npx shim ignored mismatched packageManager" -npx shim ignored mismatched packageManager