Skip to content

finding: childEnv() forwards NODE_PATH to spawned children, so a spawned resolution pin that routes through CJS is as vacuous as the in-process one it replaced #11773

Description

@os-steve

Filed unassigned while measuring #11412. Recording only — not graded here.

The mechanism

A vitest worker runs with NODE_PATH set to pnpm's dirs, the last of which is the
hoisted store node_modules/.pnpm/node_modules — which holds everything
transitively reachable anywhere in the workspace.

packages/cli/test/helpers/serve-process.ts's childEnv() strips exactly the
vitest worker family (TEST, VITEST, VITEST_*) and forwards everything else,
so NODE_PATH rides into every spawned child. Measured from inside a vitest
worker: childEnv() keeps NODE_PATH and drops VITEST, and the child sees the
store.

Why it matters

Spawning a real Node child is the repo's remedy for #11412's base collapse. It
escapes Vite's rewrite — but not this. Measured in one spawned child anchored at
packages/types, NODE_PATH inherited:

esm chalk -> MISS (base preserved — ESM ignores NODE_PATH)
cjs chalk -> HIT (base erased — CJS honours it)
esm/cjs @os-fixture/... -> MISS, MISS (control: the probe could say either)

NODE_PATH is a fallback, not an override — the node_modules walk wins when
it hits, so the store can only turn a MISS into a HIT. The dangerous direction is
an ACCEPTANCE claim ("this base CAN reach X"): green because the store supplied X,
not because the base did.

The narrow escape, and why it is luck

test/serve-host-fallback-base.e2e.test.ts's CONTROL case is sound only
because createHostImporter's fallback leg is an ESM import()
. Had it been
CJS — as createHostRequire is — the inherited NODE_PATH would have kept it
green through the very ablation it exists to fail, i.e. the spawned remedy would
have reproduced the defect it was adopted to escape.

Not measured here

  • Whether childEnv() should strip NODE_PATH by default. ⚠️ Not obvious and
    not a tidy-up: real serve/dev children DO inherit NODE_PATH from pnpm
    bin shims — that is createHostRequire 认 NODE_PATH,于是 #4699 立下的「host app 必须自己声明」在 pnpm 工作区里根本没被强制 #4719's entire history — so an e2e reproducing production
    faithfully may want it kept. This looks like a per-test opt-out
    (childEnv({ NODE_PATH: undefined })) rather than a global strip, but that is a
    judgement, not a measurement.
  • Whether check:cli-test-child-env should flag a spawned CJS-resolution claim
    that does not strip it. Today's population of such pins is zero, so this is
    a guard against a future shape, not a live defect.

Today the hazard is recorded, not enforced: the mechanism and its controls are
in packages/cli/test/vitest-resolution-base-collapse.e2e.test.ts and referenced
from scripts/check-test-source-alias.mjs's header (PR #11771).

Related: #11412 (where it was measured) · #4719 (NODE_PATH reachability vs
declaration, the same variable one layer down) · #11595 · #11441 · #11464 (the
packages/cli/test spawner-env family; this is a different variable and a
different consequence)

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions