From 3e93ba85da5814884185b88f98e414e1a4ecd191 Mon Sep 17 00:00:00 2001 From: Oscar-Williams <319719771+Oscar-Williams@users.noreply.github.com> Date: Wed, 23 Sep 2026 23:38:40 +0800 Subject: [PATCH 1/2] fix(npm): hide native CLI consoles on Windows Pass windowsHide to both native launcher paths so GUI-hosted harnesses do not flash a console on each invocation. Add Node test coverage for the default and explicit override behavior. --- npm/cli/lib/child.js | 2 +- npm/cli/lib/update.js | 4 +-- npm/cli/package.json | 2 +- npm/cli/test/windows-hide.test.mjs | 53 ++++++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 npm/cli/test/windows-hide.test.mjs 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..157520704 --- /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", { + exports: { + 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", { + exports: { + 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(); +}); From 49d999247542b2130e452046f903261095ef926c Mon Sep 17 00:00:00 2001 From: Oscar-Williams <319719771+Oscar-Williams@users.noreply.github.com> Date: Thu, 24 Sep 2026 15:50:03 +0800 Subject: [PATCH 2/2] test: use Node 22 module mock option --- npm/cli/test/windows-hide.test.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/npm/cli/test/windows-hide.test.mjs b/npm/cli/test/windows-hide.test.mjs index 157520704..e5e867f4a 100644 --- a/npm/cli/test/windows-hide.test.mjs +++ b/npm/cli/test/windows-hide.test.mjs @@ -6,7 +6,7 @@ test("npm launcher hides native child consoles by default", async (t) => { const spawnCalls = []; const syncCalls = []; mock.module("node:child_process", { - exports: { + namedExports: { spawn: (...args) => { spawnCalls.push(args); const child = new EventEmitter(); @@ -35,7 +35,7 @@ test("npm launcher hides native child consoles by default", async (t) => { test("explicit child options can still override the default", async () => { const spawnCalls = []; mock.module("node:child_process", { - exports: { + namedExports: { spawn: (...args) => { spawnCalls.push(args); const child = new EventEmitter();