Copilot review round 3, all three real: - `require.resolve.paths()` also appends Node's GLOBAL FOLDERS ($HOME/.node_modules, $HOME/.node_libraries, $PREFIX/lib/node) and any NODE_PATH entries, so round 2's lookup could satisfy a missing repo install from a GLOBALLY installed typescript/vite — measuring the programs with the wrong compiler instead of producing the actionable "run npm install" this helper promises. Filter to the node_modules directories on fromDir's own ancestor chain. - The quoting predicate missed characters cmd.exe still parses, notably parentheses (`C:\Temp(1)`), and quoting cannot suppress `%VAR%` at all — expansion happens before quotes are processed. Extracted to lib/win-shell-args.mjs with a broad predicate and a hard error on `%` rather than a silently different path. `platform` is a parameter so the Windows behavior is exercised on the POSIX machines that run the suite. - AGENTS.md claimed pack-and-verify spawns via `process.execPath`; it does not, and cannot — its children are `npm` and the installed `.bin` shim. State the exception and why. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01387r3MotGk52hrkfjzHFsN Signed-off-by: cliffhall <cliff@futurescale.com>
58 lines
2.8 KiB
JavaScript
58 lines
2.8 KiB
JavaScript
// Quoting for the few spawns that still need a Windows shell (#1939).
|
|
//
|
|
// Most call sites avoid the shell entirely — `resolve-node-bin.mjs` finds a
|
|
// package's JS entry and runs it with `process.execPath`. That is not available
|
|
// for `npm`/`npx` themselves: they are not resolvable from `node_modules`, and
|
|
// locating npm's own CLI relative to `process.execPath` differs across system,
|
|
// nvm, and volta installs — trading a quoting bug for a resolution bug. So the
|
|
// `npm` calls keep `shell: true`, which on Windows is the ONLY way to start the
|
|
// `.cmd` shim at all (Node has refused shell-free `.cmd`/`.bat` spawns since the
|
|
// CVE-2024-27980 hardening).
|
|
//
|
|
// With a shell, Node hands `cmd.exe` one space-joined string rather than an argv
|
|
// array, so any argument holding a space or a `cmd.exe` metacharacter is
|
|
// re-parsed as syntax. That is not hypothetical for these callers: every path
|
|
// they pass is generated under `tmpdir()`, which on Windows sits beneath the
|
|
// user profile — `C:\Users\First Last\AppData\Local\Temp` splits in two, and a
|
|
// profile name like `C:\Temp(1)` is read as grouping syntax.
|
|
|
|
/**
|
|
* `cmd.exe` characters that make an argument parse as syntax rather than text.
|
|
* Deliberately broad — parentheses group, `^` escapes, `!` expands under
|
|
* delayed expansion, and a bare space splits. Anything not plainly inert is
|
|
* quoted; over-quoting is free, since `cmd.exe` and the CRT argument parser
|
|
* behind it both strip the quotes back off before the child sees them.
|
|
*/
|
|
const NEEDS_QUOTING = /[\s&|<>^()!"'`,;=[\]]/;
|
|
|
|
/**
|
|
* A single argument, safe to pass through `cmd.exe`.
|
|
*
|
|
* Throws on `%`, which quoting genuinely cannot handle: `cmd.exe` expands
|
|
* `%NAME%` *before* it processes quotes, so `"%TEMP%"` is rewritten inside the
|
|
* quotes and the child receives a different path than we passed. There is no
|
|
* escape for it outside a batch file. A `%` in one of these generated temp
|
|
* paths is far-fetched, but silently running against the wrong directory is the
|
|
* exact class of failure this work exists to remove — so say so instead.
|
|
*/
|
|
export function quoteWinArg(arg) {
|
|
if (arg.includes("%")) {
|
|
throw new Error(
|
|
`cannot safely pass an argument containing "%" through cmd.exe (it is ` +
|
|
`expanded before quotes are processed): ${arg}`,
|
|
);
|
|
}
|
|
if (!NEEDS_QUOTING.test(arg)) return arg;
|
|
// A literal `"` inside a quoted string is doubled, per the CRT parser.
|
|
return `"${arg.replace(/"/g, '""')}"`;
|
|
}
|
|
|
|
/**
|
|
* `args`, quoted for `cmd.exe` when spawning with `shell: true` on Windows and
|
|
* returned untouched everywhere else — POSIX shells are not in play here, since
|
|
* these call sites only set `shell` on win32.
|
|
*/
|
|
export function winShellArgs(args, platform = process.platform) {
|
|
if (platform !== "win32") return args;
|
|
return args.map(quoteWinArg);
|
|
}
|