diff --git a/npm/cli/lib/child.js b/npm/cli/lib/child.js index c0c0f5711..30e717b9a 100644 --- a/npm/cli/lib/child.js +++ b/npm/cli/lib/child.js @@ -1,7 +1,7 @@ import { spawn } from "node:child_process"; export async function runChild(command, args, options = {}) { - const child = spawn(command, args, { stdio: "inherit", ...options }); + const child = spawn(command, args, { stdio: "inherit", windowsHide: true, ...options }); const forward = (signal) => { if (!child.killed) { try { diff --git a/npm/cli/lib/update.js b/npm/cli/lib/update.js index 076a2a8ae..1a0a0fa74 100644 --- a/npm/cli/lib/update.js +++ b/npm/cli/lib/update.js @@ -57,10 +57,10 @@ export async function updateNpmInstall({ return { code: 0, signal: null }; } -function productionRunner() { +export function productionRunner() { return { output(command, args) { - const result = spawnSync(command, args, { encoding: "utf8" }); + const result = spawnSync(command, args, { encoding: "utf8", windowsHide: true }); if (result.error) { throw result.error; } diff --git a/npm/cli/package.json b/npm/cli/package.json index 07a239118..32d37f73d 100644 --- a/npm/cli/package.json +++ b/npm/cli/package.json @@ -15,7 +15,7 @@ "LICENSE" ], "scripts": { - "test": "node --test test/*.test.mjs" + "test": "node --experimental-test-module-mocks --test test/*.test.mjs" }, "engines": { "node": ">=22" diff --git a/npm/cli/test/windows-hide.test.mjs b/npm/cli/test/windows-hide.test.mjs new file mode 100644 index 000000000..e5e867f4a --- /dev/null +++ b/npm/cli/test/windows-hide.test.mjs @@ -0,0 +1,53 @@ +import assert from "node:assert/strict"; +import { EventEmitter } from "node:events"; +import { mock, test } from "node:test"; + +test("npm launcher hides native child consoles by default", async (t) => { + const spawnCalls = []; + const syncCalls = []; + mock.module("node:child_process", { + namedExports: { + spawn: (...args) => { + spawnCalls.push(args); + const child = new EventEmitter(); + queueMicrotask(() => child.emit("exit", 0, null)); + return child; + }, + spawnSync: (...args) => { + syncCalls.push(args); + return { status: 0, stdout: "output", stderr: "" }; + }, + }, + }); + t.after(() => mock.restoreAll()); + + const { runChild } = await import("../lib/child.js?windows-hide-child"); + const { productionRunner } = await import("../lib/update.js?windows-hide-update"); + + await runChild("mnemon", ["status"]); + const runner = productionRunner(); + assert.equal(runner.output("npm", ["--version"]), "output"); + + assert.equal(spawnCalls[0][2].windowsHide, true); + assert.equal(syncCalls[0][2].windowsHide, true); +}); + +test("explicit child options can still override the default", async () => { + const spawnCalls = []; + mock.module("node:child_process", { + namedExports: { + spawn: (...args) => { + spawnCalls.push(args); + const child = new EventEmitter(); + queueMicrotask(() => child.emit("exit", 0, null)); + return child; + }, + spawnSync: () => ({ status: 0, stdout: "", stderr: "" }), + }, + }); + + const { runChild } = await import("../lib/child.js?windows-hide-override"); + await runChild("mnemon", ["status"], { windowsHide: false }); + assert.equal(spawnCalls[0][2].windowsHide, false); + mock.restoreAll(); +});