Skip to content

Commit 052c7fe

Browse files
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>
1 parent a325940 commit 052c7fe

2 files changed

Lines changed: 46 additions & 0 deletions

File tree

browse/src/bun-polyfill.cjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ globalThis.Bun = {
7575
timeout: options.timeout,
7676
env: options.env,
7777
cwd: options.cwd,
78+
// Node defaults windowsHide to false, so every child opens a console
79+
// window on Windows. Bun.spawnSync has no such behaviour, so the
80+
// polyfill has to opt in to stay faithful to the API it's emulating.
81+
windowsHide: options.windowsHide !== false,
7882
});
7983

8084
return {
@@ -91,6 +95,10 @@ globalThis.Bun = {
9195
stdio,
9296
env: options.env,
9397
cwd: options.cwd,
98+
// See spawnSync above. This one is the visible offender: the detached
99+
// terminal-agent the browse watchdog respawns every 60s left an empty
100+
// console window on screen each time.
101+
windowsHide: options.windowsHide !== false,
94102
});
95103

96104
return {

browse/test/bun-polyfill.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,44 @@ describe('bun-polyfill', () => {
4848
expect(lines[2]).toBe('HAS_UNREF');
4949
});
5050

51+
// Regression: Node defaults windowsHide to false, so before this the browse
52+
// server flashed an empty console window on Windows for every child it
53+
// spawned -- most visibly the terminal-agent the watchdog respawns on a 60s
54+
// tick. Asserts the option reaches child_process rather than trying to
55+
// observe a window, so it is meaningful on every platform.
56+
test('Bun.spawn/spawnSync pass windowsHide to child_process', () => {
57+
const result = Bun.spawnSync(['node', '-e', `
58+
const cp = require('child_process');
59+
const realSpawn = cp.spawn, realSpawnSync = cp.spawnSync;
60+
const seen = {};
61+
cp.spawn = (c, a, o) => { seen.spawn = o.windowsHide; return realSpawn(c, a, o); };
62+
cp.spawnSync = (c, a, o) => { seen.spawnSync = o.windowsHide; return realSpawnSync(c, a, o); };
63+
require('${polyfillPath}');
64+
Bun.spawnSync([process.execPath, '-e', ''], { stdout: 'pipe' });
65+
Bun.spawn([process.execPath, '-e', ''], { stdio: ['ignore', 'ignore', 'ignore'] });
66+
console.log('spawnSync:' + seen.spawnSync);
67+
console.log('spawn:' + seen.spawn);
68+
`], { stdout: 'pipe', stderr: 'pipe' });
69+
const lines = result.stdout.toString().trim().split('\n');
70+
expect(lines[0]).toBe('spawnSync:true');
71+
expect(lines[1]).toBe('spawn:true');
72+
});
73+
74+
// An explicit windowsHide: false must still win, so a caller that genuinely
75+
// wants a visible console (e.g. debugging a child) keeps that escape hatch.
76+
test('Bun.spawn honours an explicit windowsHide: false', () => {
77+
const result = Bun.spawnSync(['node', '-e', `
78+
const cp = require('child_process');
79+
const realSpawn = cp.spawn;
80+
const seen = {};
81+
cp.spawn = (c, a, o) => { seen.spawn = o.windowsHide; return realSpawn(c, a, o); };
82+
require('${polyfillPath}');
83+
Bun.spawn([process.execPath, '-e', ''], { stdio: ['ignore', 'ignore', 'ignore'], windowsHide: false });
84+
console.log('spawn:' + seen.spawn);
85+
`], { stdout: 'pipe', stderr: 'pipe' });
86+
expect(result.stdout.toString().trim()).toBe('spawn:false');
87+
});
88+
5189
test('Bun.serve creates an HTTP server that responds', async () => {
5290
const result = Bun.spawnSync(['node', '-e', `
5391
require('${polyfillPath}');

0 commit comments

Comments
 (0)