diff --git a/changelog.d/1057-windows-npm-dep0190.md b/changelog.d/1057-windows-npm-dep0190.md new file mode 100644 index 000000000..1e8e1da55 --- /dev/null +++ b/changelog.d/1057-windows-npm-dep0190.md @@ -0,0 +1,4 @@ +- On Windows, `npm run doctor` and the Pinokio install no longer print Node's + DEP0190 warning ("Passing args to a child process with shell option true can + lead to security vulnerabilities"): npm.cmd's fixed subcommand is now part of + the shell command string instead of an argument array (quanticsoul4772, #1057). diff --git a/scripts/pinokio-install.mjs b/scripts/pinokio-install.mjs index 01a26ccce..4e52bf3af 100644 --- a/scripts/pinokio-install.mjs +++ b/scripts/pinokio-install.mjs @@ -24,8 +24,8 @@ export function runChecked(command, args, { shell = false } = {}) { export function installPinokioDependencies() { applyPinokioEnvironment(); rmSync(READY_FILE, { force: true }); - const npm = npmProcessSpec(); - runChecked(npm.command, ['ci'], { shell: npm.shell }); + const npm = npmProcessSpec(['ci']); + runChecked(npm.command, npm.args, { shell: npm.shell }); // Pinokio starts Vite directly and loads only its ENVIRONMENT file plus the // normal dotenv ladder. Unlike dev-fresh.sh, it does not import macOS diff --git a/scripts/setup-doctor.mjs b/scripts/setup-doctor.mjs index 49c6722c2..01e048091 100644 --- a/scripts/setup-doctor.mjs +++ b/scripts/setup-doctor.mjs @@ -76,10 +76,15 @@ export function hasRequiredDependencies(rootDir = ROOT) { } } -/** Return the npm command and spawn mode required by the target platform. */ -export function npmProcessSpec(platform = process.platform) { - const windows = platform === 'win32'; - return { command: windows ? 'npm.cmd' : 'npm', shell: windows }; +/** + * Return the npm command, arguments and spawn mode for the target platform. + * Windows must run npm.cmd through a shell, and Node 24 deprecates passing an + * argument array with `shell: true` (DEP0190), so there the arguments, which + * are fixed npm subcommands, join the command string. + */ +export function npmProcessSpec(args, platform = process.platform) { + if (platform !== 'win32') return { command: 'npm', args, shell: false }; + return { command: ['npm.cmd', ...args].join(' '), args: [], shell: true }; } /** Read one key from Vite's dotenv file ladder without depending on Vite. */ @@ -190,8 +195,8 @@ export function buildCapabilitySummary( export function inspectSetup({ includeKeychain = true, authoritativeEnvironment = false, rootDir = ROOT } = {}) { const node = classifyNodeVersion(); - const npm = npmProcessSpec(); - const npmResult = spawnSync(npm.command, ['--version'], { + const npm = npmProcessSpec(['--version']); + const npmResult = spawnSync(npm.command, npm.args, { encoding: 'utf8', shell: npm.shell, }); diff --git a/src/setupDoctor.test.mjs b/src/setupDoctor.test.mjs index 71c6e44af..26d695ba1 100644 --- a/src/setupDoctor.test.mjs +++ b/src/setupDoctor.test.mjs @@ -69,12 +69,22 @@ test('placeholder values are never counted as configured credentials', () => { }); test('doctor selects a Windows-safe npm process without changing Unix behavior', () => { - assert.deepEqual(npmProcessSpec('win32'), { - command: 'npm.cmd', + // Windows passes no argument array with shell: true (Node DEP0190). + assert.deepEqual(npmProcessSpec(['ci'], 'win32'), { + command: 'npm.cmd ci', + args: [], shell: true, }); - assert.deepEqual(npmProcessSpec('darwin'), { command: 'npm', shell: false }); - assert.deepEqual(npmProcessSpec('linux'), { command: 'npm', shell: false }); + assert.deepEqual(npmProcessSpec(['--version'], 'darwin'), { + command: 'npm', + args: ['--version'], + shell: false, + }); + assert.deepEqual(npmProcessSpec(['ci'], 'linux'), { + command: 'npm', + args: ['ci'], + shell: false, + }); }); test('doctor recognizes every OpenSky OAuth keychain alias used by dev-fresh', () => {