Skip to content

verify:typecheck-coverage cannot run on Windows — execFileSync('npx', …) is ENOENT, so validate/ci fail at step one #1939

Closed

Description

@olaservo

Version: v2 (main)
Affected client: repo tooling (scripts/verify-typecheck-coverage.mjs)

Symptom

On Windows, npm run validate (and therefore the mandatory npm run ci pre-push gate) fails at its first step. Every project harvest prints:

verify:typecheck-coverage — `tsc -p tsconfig.json` (in clients/cli) exited non-zero:
(no diagnostic captured)

…for every client/project, followed by 905 tracked source file(s) get no tsc pass — every TS file in the repo listed as uncovered.

Root cause

rawProjectFiles() (and projectDisablesChecking()) call execFileSync("npx", ["--no-install", "tsc", …]). On Windows there is no npx executable — only npx.cmd — and Node cannot spawn a .cmd without a shell (and refuses shell-less .cmd spawns since the CVE-2024-27980 hardening). The spawn throws ENOENT, the catch treats it as a tsc config error with empty stdout, every project's file set comes back empty, and the guard reports the whole repo as unclaimed.

Verified on Windows 11 / Node 22:

> node -e "require('child_process').execFileSync('npx',['--no-install','tsc','--version'])"
Error: spawnSync npx ENOENT

GitHub CI never sees this because its runners are Linux, so the gate is green there while being unrunnable for any Windows contributor.

Suggested fix

Resolve the real tsc entry instead of shelling through npx: spawn process.execPath with the client-local node_modules/typescript/lib/tsc.js (falling back up the tree), which is cross-platform, faster (no npx resolution), and keeps execFileSync shell-free. Alternatively spawn npx.cmd with shell: true on win32 and quote args — the approach scripts/pack-and-verify.mjs needs for the same reason — but the direct-node form avoids the shell-quoting hazards entirely. scripts/verify-typecheck-coverage.test.mjs should pin whichever contract is chosen with a Windows-shaped regression test (a spawner that throws ENOENT must surface as a hard "cannot measure" error, not as an empty file set silently reported as 905 uncovered files).

Context

Found while working #1852 on a Windows machine — the gate could not be run to validate that fix. Note the failure mode is doubly silent: the ENOENT is reported as "(no diagnostic captured)" and then converted into a misleading coverage report instead of an environment error.

Activity

  1. self-assigned this
    on Aug 6, 2026
  2. olaservo commented on Aug 6, 2026

    @olaservo
    MemberAuthor

    Same defect in scripts/verify-build-gate.mjs: on Windows npm run verify:build-gate fails immediately with

    verify:build-gate FAILED — could not run `vite build` (spawnSync npx ENOENT)
    

    Whatever spawn helper fixes this should be shared by both verifier scripts (and any other script spawning npx/npm via execFileSync/spawnSync without shell on win32 — scripts/pack-and-verify.mjs had the same class of problem).

  3. modified the milestones: v2.2.0, v2.3.0 on Aug 10, 2026
  4. olaservo commented on Aug 12, 2026

    @olaservo
    MemberAuthor

    Investigated on Windows against current v2/main (57d6e0d). Confirmed, with two additions to the report:

    Root cause: npx/npm are .cmd shims on Windows, and Node refuses shell-less .cmd spawns (the CVE-2024-27980 hardening) → ENOENT. CI runners are ubuntu-only, so CI never sees it. The failure is doubly silent in verify:typecheck-coverage: on a spawn failure err.stdout is undefined, the catch turns that into an empty file set, and the guard reports "918 tracked source file(s) get no tsc pass" instead of "cannot run tsc".

    Scope is wider than this script — seven call sites spawn npx/npm without a Windows-safe shell: both tsc spawns in verify-typecheck-coverage.mjs, the vite spawn in verify-build-gate.mjs, the npx tsc bootstrap in smoke-cli.mjs / smoke-tui.mjs / smoke-web-app.mjs, and the npm pack in pack-and-verify.mjs (whose other spawns already pass shell: process.platform === "win32"). verify-format-coverage and verify-dep-lockstep are unaffected (they only spawn git).

    Planned fix: a shared scripts/lib/resolve-node-bin.mjs that resolves a package's bin entry via its package.json (Vite 8's exports map blocks deep require.resolve) and spawns it with process.execPath — shell-free, cross-platform, and a resolution failure becomes a hard "cannot resolve — run npm install" error instead of a bogus coverage report. Prototyped: verify:typecheck-coverage goes green on Windows (all 918 files, ~28s) and verify:build-gate passes.

  5. added a commit that references this issue on Aug 17, 2026
  6. cliffhall commented on Aug 17, 2026

    @cliffhall
    Member

    Fixed by #1997, merged to v2/main as 7bda382. (Closing manually — Closes #N doesn't auto-fire for PRs targeting the non-default v2/main.)

    Beyond the original ENOENT fix, three rounds of review hardened the resolver against failures in the same family — a globally-installed TypeScript silently standing in for a missing repo install, a partial install whose declared bin file is gone, and a package whose exports map hides its own package.json. Each of those would have been green and wrong rather than loudly broken, which is the failure mode this issue was reported for.

  7. added a commit that references this issue on Aug 19, 2026
    e3a0268
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingv2Issues and PRs for v2

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions