1- import $ from "@david/dax " ;
1+ import { spawn } from "node:child_process " ;
22import { createWriteStream , type WriteStream } from "node:fs" ;
33import { join as joinPath } from "node:path" ;
4+ import process from "node:process" ;
5+ import { Readable } from "node:stream" ;
46import { printErrorMessage } from "../utils.ts" ;
57import { ensurePortReleased , killProcessOnPort } from "./port.ts" ;
68
@@ -45,20 +47,22 @@ export async function serverClosure<T>(
4547 await releasePort ?.( ) ;
4648
4749 const devCommand = cmd . split ( " " ) ;
48- const serverProcess = $ `${ devCommand } `
49- . cwd ( dir )
50- . env ( "PORT" , String ( defaultPort ) )
51- . stdin ( "null" )
52- . stdout ( "piped" )
53- . stderr ( "piped" )
54- . noThrow ( )
55- . spawn ( ) ;
50+ const child = spawn ( devCommand [ 0 ] , devCommand . slice ( 1 ) , {
51+ cwd : dir ,
52+ env : { ...process . env , PORT : String ( defaultPort ) } ,
53+ stdio : [ "ignore" , "pipe" , "pipe" ] ,
54+ detached : true , // creates a new process group so we can kill the tree
55+ } ) ;
56+
57+ // Prevent unhandled exception when the process is killed
58+ child . on ( "error" , ( ) => { } ) ;
5659
57- // Prevent unhandled rejection when the process is killed
58- serverProcess . catch ( ( ) => { } ) ;
60+ // Convert Node.js readable streams to Web ReadableStreams for tee()
61+ const stdoutWeb = Readable . toWeb ( child . stdout ! ) as ReadableStream < Uint8Array > ;
62+ const stderrWeb = Readable . toWeb ( child . stderr ! ) as ReadableStream < Uint8Array > ;
5963
60- const [ stdoutForFile , stdoutForPort ] = serverProcess . stdout ( ) . tee ( ) ;
61- const [ stderrForFile , stderrForPort ] = serverProcess . stderr ( ) . tee ( ) ;
64+ const [ stdoutForFile , stdoutForPort ] = stdoutWeb . tee ( ) ;
65+ const [ stderrForFile , stderrForPort ] = stderrWeb . tee ( ) ;
6266
6367 // Shared signal to cancel all background stream readers on cleanup
6468 const cleanup = new AbortController ( ) ;
@@ -82,8 +86,18 @@ export async function serverClosure<T>(
8286 } ) ;
8387 return await callback ( port ) ;
8488 } finally {
89+ // Kill the entire process group (tsx watch + all its children)
90+ try {
91+ if ( child . pid != null ) {
92+ process . kill ( - child . pid , "SIGKILL" ) ;
93+ }
94+ } catch {
95+ // Process group already exited
96+ }
97+
98+ // Also kill the child directly in case it wasn't in the group
8599 try {
86- serverProcess . kill ( "SIGKILL" ) ;
100+ child . kill ( "SIGKILL" ) ;
87101 } catch {
88102 // Process already exited
89103 }
0 commit comments