Skip to content

Commit 089c12d

Browse files
committed
fix(build): exit watch mode cleanly on SIGINT
Both `scripts/build/main.mts` and `scripts/build/js.mts` had the same pattern in their watch-mode SIGINT handlers: set `process.exitCode = 0`, then `throw new Error('Watch mode interrupted')`. The throw happens inside an `async () => {}` registered with `process.on('SIGINT', ...)`, so it surfaces as an unhandled promise rejection — which Node exits non-zero on, negating the `exitCode = 0` — and even when the rejection is caught by the surrounding try/catch of `watchBuild`, the user sees `Watch mode failed: Watch mode interrupted` instead of a clean exit. Replaces the throw with `ctx.dispose().finally(() => process.exit(0))`, which tears down the esbuild context then exits with status 0 whether dispose succeeds or fails. Dev-loop Ctrl-C now closes without a stack trace.
1 parent 06d4528 commit 089c12d

2 files changed

Lines changed: 18 additions & 14 deletions

File tree

scripts/build/js.mts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -98,18 +98,19 @@ async function watchJS(): Promise<number> {
9898

9999
await ctx.watch()
100100

101-
// Keep process alive
102-
process.on('SIGINT', async () => {
101+
// On Ctrl-C, tear down the esbuild context and exit cleanly. Earlier
102+
// this handler threw inside an async callback, which surfaced as an
103+
// unhandled rejection and the outer try/catch rewrote the clean exit
104+
// into "Watch mode failed: Watch mode interrupted".
105+
process.on('SIGINT', () => {
103106
if (!isQuiet) {
104107
logger.log('\nStopping watch mode...')
105108
}
106-
await ctx.dispose()
107-
process.exitCode = 0
108-
throw new Error('Watch mode interrupted')
109+
ctx.dispose().finally(() => process.exit(0))
109110
})
110111

111-
// Wait indefinitely
112-
await new Promise(() => {})
112+
// Wait indefinitely — SIGINT is the only exit path.
113+
await new Promise<never>(() => {})
113114
} catch (error) {
114115
if (!isQuiet) {
115116
logger.error('Watch mode failed')

scripts/build/main.mts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -252,15 +252,18 @@ async function watchBuild(
252252
// Enable watch mode
253253
await ctx.watch()
254254

255-
// Keep the process alive
256-
process.on('SIGINT', async () => {
257-
await ctx.dispose()
258-
process.exitCode = 0
259-
throw new Error('Watch mode interrupted')
255+
// On Ctrl-C, tear down the esbuild context and exit cleanly. Earlier
256+
// this handler threw inside an async callback, which surfaced as an
257+
// unhandled rejection and the surrounding try/catch rewrote the clean
258+
// exit into "Watch mode failed: Watch mode interrupted".
259+
process.on('SIGINT', () => {
260+
ctx.dispose().finally(() => process.exit(0))
260261
})
261262

262-
// Wait indefinitely
263-
await new Promise(() => {})
263+
// Wait indefinitely — SIGINT is the only exit path.
264+
await new Promise<never>(() => {})
265+
// Unreachable; satisfies Promise<number> return type.
266+
return 0
264267
} catch (error) {
265268
if (!quiet) {
266269
logger.error('Watch mode failed:', error)

0 commit comments

Comments
 (0)