fix(browse): stop the Bun polyfill flashing console windows on Windows

The Node bun-polyfill forwards Bun.spawn/spawnSync to child_process
without windowsHide. Node defaults that option to false, so on Windows
every child the browse server starts opens a console window -- Bun
itself does not behave this way, so the polyfill was drifting from the
API it emulates.

The visible symptom is an empty black bun.exe window appearing every
~60s: server.ts's agent watchdog respawns a crashed terminal-agent on
each tick, and the agent is spawned with stdio ['ignore','ignore',
'ignore'], so the window it opens has nothing in it. The tasklist,
powershell and chrome --version helpers pop windows the same way.

Fixed at the polyfill rather than the call sites, since it is the one
choke point every Bun.spawn under Node passes through. An explicit
windowsHide: false is still honoured for callers that want a visible
console.

Verified against the unpatched polyfill (spawn/spawnSync both received
undefined) and the patched one (both receive true, explicit false still
wins).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Wim van den Heijkant 2026-07-17 11:59:06 +02:00
parent a3259400a3
commit 052c7fe010
2 changed files with 46 additions and 0 deletions

View File

@ -75,6 +75,10 @@ globalThis.Bun = {
timeout: options.timeout,
env: options.env,
cwd: options.cwd,
// Node defaults windowsHide to false, so every child opens a console
// window on Windows. Bun.spawnSync has no such behaviour, so the
// polyfill has to opt in to stay faithful to the API it's emulating.
windowsHide: options.windowsHide !== false,
});
return {
@ -91,6 +95,10 @@ globalThis.Bun = {
stdio,
env: options.env,
cwd: options.cwd,
// See spawnSync above. This one is the visible offender: the detached
// terminal-agent the browse watchdog respawns every 60s left an empty
// console window on screen each time.
windowsHide: options.windowsHide !== false,
});
return {

View File

@ -48,6 +48,44 @@ describe('bun-polyfill', () => {
expect(lines[2]).toBe('HAS_UNREF');
});
// Regression: Node defaults windowsHide to false, so before this the browse
// server flashed an empty console window on Windows for every child it
// spawned -- most visibly the terminal-agent the watchdog respawns on a 60s
// tick. Asserts the option reaches child_process rather than trying to
// observe a window, so it is meaningful on every platform.
test('Bun.spawn/spawnSync pass windowsHide to child_process', () => {
const result = Bun.spawnSync(['node', '-e', `
const cp = require('child_process');
const realSpawn = cp.spawn, realSpawnSync = cp.spawnSync;
const seen = {};
cp.spawn = (c, a, o) => { seen.spawn = o.windowsHide; return realSpawn(c, a, o); };
cp.spawnSync = (c, a, o) => { seen.spawnSync = o.windowsHide; return realSpawnSync(c, a, o); };
require('${polyfillPath}');
Bun.spawnSync([process.execPath, '-e', ''], { stdout: 'pipe' });
Bun.spawn([process.execPath, '-e', ''], { stdio: ['ignore', 'ignore', 'ignore'] });
console.log('spawnSync:' + seen.spawnSync);
console.log('spawn:' + seen.spawn);
`], { stdout: 'pipe', stderr: 'pipe' });
const lines = result.stdout.toString().trim().split('\n');
expect(lines[0]).toBe('spawnSync:true');
expect(lines[1]).toBe('spawn:true');
});
// An explicit windowsHide: false must still win, so a caller that genuinely
// wants a visible console (e.g. debugging a child) keeps that escape hatch.
test('Bun.spawn honours an explicit windowsHide: false', () => {
const result = Bun.spawnSync(['node', '-e', `
const cp = require('child_process');
const realSpawn = cp.spawn;
const seen = {};
cp.spawn = (c, a, o) => { seen.spawn = o.windowsHide; return realSpawn(c, a, o); };
require('${polyfillPath}');
Bun.spawn([process.execPath, '-e', ''], { stdio: ['ignore', 'ignore', 'ignore'], windowsHide: false });
console.log('spawn:' + seen.spawn);
`], { stdout: 'pipe', stderr: 'pipe' });
expect(result.stdout.toString().trim()).toBe('spawn:false');
});
test('Bun.serve creates an HTTP server that responds', async () => {
const result = Bun.spawnSync(['node', '-e', `
require('${polyfillPath}');