Skip to content

fix(cli): stamp package-manager lifecycle env for vp run scripts - #2385

Open
tarikermis wants to merge 4 commits into
voidzero-dev:mainfrom
tarikermis:fix/pm-lifecycle-env
Open

fix(cli): stamp package-manager lifecycle env for vp run scripts#2385
tarikermis wants to merge 4 commits into
voidzero-dev:mainfrom
tarikermis:fix/pm-lifecycle-env

Conversation

@tarikermis

Copy link
Copy Markdown

What

vp run / vpr now stamp the package-manager lifecycle environment (npm_execpath, npm_config_user_agent, INIT_CWD, npm_node_execpath/NODE) into the process env before vt::Session::init snapshots it, mirroring what pnpm/npm/yarn set when they run package.json scripts themselves.

Closes #2317

Why

When pnpm runs a script it stamps these variables so child tooling can tell which package manager owns the run. vp run executes scripts via vite-task, which only prepends node_modules/.bin to PATH and sets VP_RUN=1 — the lifecycle variables stay unset. As a result, child runners such as npm-run-all2 (lib/run-task.js detects the package manager from npm_execpath / npm_config_user_agent) fall back to spawning npm run inside pnpm projects, which fails with EBADDEVENGINES when devEngines.packageManager requires pnpm.

How

  • crates/vp_pm_cli/src/lifecycle_env.rs (new): PackageManager::lifecycle_exec_path() and PackageManager::lifecycle_env_vars() build the session-constant subset of the lifecycle env from the resolved PackageManager. npm_execpath prefers the JS CLI entry (bin/pnpm.cjs, bin/npm-cli.js, bin/yarn.js — the same files create_shim_files points the shims at) because child runners execute .js/.cjs values through the current Node.js binary, which works on every platform, unlike extensionless shims on Windows; it falls back to pnpm.native[.exe] for pnpm ≥ 12 native installs and then to the bin shim (with a debug! log, since that path re-exposes the Windows problem). The user agent matches what the package managers emit — pnpm/11.20.0 npm/? node/v22.23.1 linux x64, npm/10.9.8 node/v22.23.1 linux x64 workspaces/false — verified against real pnpm 11.20.0 / npm 10.9.8 stamps. npm's workspaces/ flag stays false (it tracks npm's --workspaces flag, which vp run has no analogue of; checked against npm inside a workspace root). bun stamps nothing: what bun run sets is unverified, so its env is left untouched rather than guessed.
  • packages/cli/binding/src/cli/lifecycle_env.rs (new): stamps the vars into the process env in execute_vite_task_command, inside the existing PackageManager block that already prepends the PM bin dir to PATH, i.e. before Session::init snapshots the environment. INIT_CWD is the real process cwd (pnpm's semantics — unaffected by --cwd). PM resolution failure now logs at debug level instead of silently skipping.
  • nodeVersion: process.version and nodeExecPath: process.execPath are passed through the napi CliOptions so the user agent and npm_node_execpath/NODE carry the real host Node values (rather than deriving them from current_exe(), which would resolve version-manager symlinks and break under any non-napi entry point).

Deliberately out of scope: per-script variables (npm_lifecycle_event, npm_lifecycle_script, npm_package_*, PNPM_SCRIPT_SRC_DIR) name the running script or its owning package — they can't be stamped correctly once per session, so they belong in the task engine (vite-task), not here.

Verification

Repro project: package.json with devEngines.packageManager pinned to pnpm, a dumpenv script echoing the lifecycle variables, and check: run-p child:a child:b (npm-run-all2), plus a stubbed npm on PATH that fails loudly if invoked.

  • On main: all variables <unset>; run-p spawned the npm stub for its children.
  • With this change: variables set as pnpm sets them (npm_execpath=…/pnpm/11.20.0/pnpm/bin/pnpm.cjs, npm_config_user_agent=pnpm/11.20.0 npm/? node/v22.23.1 linux x64, …); run-p runs children through pnpm.cjs and check succeeds.

Checks run locally:

  • cargo test -p vp_pm_cli — 746 passed, 0 failed (10 new tests: exec-path selection incl. native pnpm, per-PM var sets, user-agent formats, bun opt-out, platform/arch spellings)
  • cargo test -p vite-plus-cli — 37 passed, 0 failed
  • cargo clippy -p vp_pm_cli -p vite-plus-cli --all-targets --all-features -- --deny warnings (with the just lint allow-list) — clean
  • cargo fmt --all -- --check — clean
  • pnpm fmt / pnpm lint — no findings in changed files (repo-wide vp lint reports 18 pre-existing type errors in untouched docs/.vitepress/* files in this environment)
  • The diff went through two independent-LLM review rounds: round-one findings (exec path must come from process.execPath, PNPM_SCRIPT_SRC_DIR is per-script and was dropped, silent fallbacks, SAFETY-comment wording, Windows test paths) were addressed; round two came back LGTM.

Notes / limitations

  • vp and vpr share the single run() call site (bin/vp/bin/vpr both load dist/bin.js), so both get the stamped env.
  • Windows not tested locally (the reporter was on Windows); the JS-entry choice for npm_execpath is specifically motivated by Windows shim execution, the pnpm ≥ 12 native path handles .exe, and path-sensitive tests pick Windows/Unix expectations via cfg!(windows).
  • Only the session-constant subset is stamped; per-script vars (incl. PNPM_SCRIPT_SRC_DIR) need vite-task (separate repo). Related follow-up: when vp run is itself launched from an outer PM lifecycle script, the outer npm_lifecycle_*/npm_package_* vars survive next to the rewritten npm_execpath — overwriting those also belongs to vite-task.
  • The unsafe { set_var } call inherits the risk profile of the existing PATH prepend right above it (same startup window, before task threads); the SAFETY comment says as much. Threading the values through SessionConfig as an explicit env overlay instead would remove the unsafe block, but that's a vite-task change.
  • Env values are OsString, so non-UTF-8 install paths survive unmangled.
  • Commit is unsigned (no GPG setup here).

@netlify

netlify Bot commented Aug 9, 2026

Copy link
Copy Markdown

Deploy Preview for viteplus-preview canceled.

Name Link
🔨 Latest commit 9cbc024
🔍 Latest deploy log https://app.netlify.com/projects/viteplus-preview/deploys/6a7a0ef1e3c42e000899f107

@fengmk2
fengmk2 requested a review from wan9chi August 10, 2026 06:24
@fengmk2 fengmk2 self-assigned this Aug 10, 2026
@fengmk2

fengmk2 commented Aug 10, 2026

Copy link
Copy Markdown
Member

@tarikermis can you also add a new snapshot test to cover this bug fix?

@tarikermis

Copy link
Copy Markdown
Author

Good call, on it - adding a snapshot test for the fix now. Best Regards, Tarik

Covers voidzero-dev#2317: snapshot the session-constant
lifecycle env computed for fixture package-manager install layouts
(pnpm/npm/yarn JS CLI entries, native pnpm binary, shim fallback, and
bun's empty stamp) so a regression that drops the stamp or changes
exec-path resolution fails the test.
@fengmk2

fengmk2 commented Aug 10, 2026

Copy link
Copy Markdown
Member

@tarikermis I mean this snapshot tests https://github.com/voidzero-dev/vite-plus/tree/main/crates/vp_cli_snapshots/tests/cli_snapshots .
Sorry I didn't explain clearly.

@tarikermis

Copy link
Copy Markdown
Author

Ah my bad, got it - adding a CLI snapshot test in vp_cli_snapshots now. Best Regards, Tarik

CLI-level regression test for voidzero-dev#2317: a fixture pnpm project runs a
package.json script via vp run that surfaces npm_execpath,
npm_config_user_agent, and INIT_CWD. Pre-fix all three were undefined in
the script process, so child tooling like npm-run-all fell back to npm.
A fake managed pnpm install under VP_HOME keeps the case offline.
---
source: crates/vp_pm_cli/src/lifecycle_env.rs
expression: "render_lifecycle_stamp(PackageManagerType::Pnpm, \"11.20.0\", &[])"
---

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tarikermis can you revert these snapshots?


if let Some(node_execpath) = &context.node_execpath {
vars.push(("npm_node_execpath", node_execpath.as_os_str().to_os_string()));
vars.push(("NODE", node_execpath.as_os_str().to_os_string()));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there source documentation for the naming of these environment variables? Or reference code? Need to add code comments to explain them.

@tarikermis

Copy link
Copy Markdown
Author

Sure - reverting the insta snapshots and adding source comments explaining the env var naming. Best Regards, Tarik

Revert the crate-level insta snapshot tests (the CLI-level
vp_cli_snapshots case covers the bug fix end to end), and document where
each lifecycle env var name and format comes from: npm's set-envs.js and
user-agent definition, pnpm's @pnpm/npm-lifecycle and config userAgent,
verified against pnpm 11.21.0 and npm 10.9.8.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

vpr appears not to preserve the pnpm lifecycle environment, causing npm-run-all2 to fall back to npm

2 participants