On Windows npx/npm are .cmd shims a shell-free execFileSync/spawnSync cannot start (ENOENT since the CVE-2024-27980 hardening), so verify:typecheck-coverage died at validate's second step — doubly silently, echoing "(no diagnostic captured)" per project and then reporting all 918 tracked files as getting no tsc pass — and verify:build-gate, the smoke test-server bootstraps, and pack:verify's npm pack failed the same way. Add scripts/lib/resolve-node-bin.mjs: resolves the JS entry behind a package's bin from <pkg>/package.json (deep resolution is blocked by Vite 8's exports map) and spawns it via process.execPath — the same walk npx --no-install did, cross-platform and shell-free. Switch the six npx call sites to it; the npm pack call gets the existing shell-on-win32 idiom instead (npm has no in-tree package to resolve). An unresolvable tsc is now a hard "cannot measure" error with actionable stderr, pinned by a new main() regression test. Closes #1939 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
41 lines
2.1 KiB
JavaScript
41 lines
2.1 KiB
JavaScript
// Shared resolver for spawning a package's CLI cross-platform (#1939).
|
|
//
|
|
// On Windows, `npx`/`npm` are `.cmd` shims, not executables — a shell-free
|
|
// `execFileSync`/`spawnSync` cannot start one (Node refuses `.cmd`/`.bat`
|
|
// spawns without `shell: true` since the CVE-2024-27980 hardening) and throws
|
|
// `ENOENT`. GitHub CI runs Linux, so the gate stayed green there while being
|
|
// unrunnable for any Windows contributor. Instead of shelling through `npx`,
|
|
// resolve the JS entry behind the package's bin and run it with
|
|
// `process.execPath`: cross-platform, shell-free (no quoting hazards), faster
|
|
// (no npx resolution), and pinned to the locally installed package exactly as
|
|
// `npx --no-install` was.
|
|
|
|
import { readFileSync } from "node:fs";
|
|
import { createRequire } from "node:module";
|
|
import path from "node:path";
|
|
|
|
/**
|
|
* Absolute path of the JS entry behind a package's bin (e.g. typescript's
|
|
* `tsc`, vite's `vite`), resolved from `fromDir` up the node_modules tree —
|
|
* the same walk `npx --no-install` does, minus the `.cmd` shim a shell-free
|
|
* spawn can't start on Windows. Resolves `<pkg>/package.json` and reads its
|
|
* `bin` field (what npx itself does) rather than resolving the bin path
|
|
* directly, because an `exports` map blocks deep resolution — Vite 8 doesn't
|
|
* export `./bin/vite.js`, so `require.resolve("vite/bin/vite.js")` throws
|
|
* `ERR_PACKAGE_PATH_NOT_EXPORTED` (`./package.json` is always exported).
|
|
*
|
|
* Throws if the package isn't installed from `fromDir` or declares no such
|
|
* bin — the caller decides whether that's a hard "cannot measure" error or a
|
|
* fallback.
|
|
*/
|
|
export function resolveNodeBin(pkg, binName, fromDir) {
|
|
const pkgPath = createRequire(path.join(fromDir, "package.json")).resolve(
|
|
`${pkg}/package.json`,
|
|
);
|
|
const bin = JSON.parse(readFileSync(pkgPath, "utf8")).bin;
|
|
// A string-form `bin` names a single command (the package's own name).
|
|
const rel = typeof bin === "string" ? bin : bin?.[binName];
|
|
if (typeof rel !== "string")
|
|
throw new Error(`${pkg} declares no "${binName}" bin in its package.json`);
|
|
return path.join(path.dirname(pkgPath), rel);
|
|
}
|