1- import { spawn , type ChildProcess } from 'node:child_process'
1+ import { spawn , execSync , type ChildProcess } from 'node:child_process'
22import { existsSync } from 'node:fs'
3+ import { createServer } from 'node:net'
34import { resolve } from 'node:path'
45
56/**
@@ -69,6 +70,41 @@ export function resolveWranglerConfig(cwd: string, configPath?: string): string
6970 )
7071}
7172
73+ /**
74+ * Check if a port is free. If not, kill whatever is holding it
75+ * (likely a stale wrangler from a previous run).
76+ */
77+ async function ensurePortFree ( port : number ) : Promise < void > {
78+ const isFree = await new Promise < boolean > ( ( resolve_ ) => {
79+ const server = createServer ( )
80+ server . once ( 'error' , ( ) => resolve_ ( false ) )
81+ server . once ( 'listening' , ( ) => {
82+ server . close ( )
83+ resolve_ ( true )
84+ } )
85+ server . listen ( port )
86+ } )
87+
88+ if ( isFree ) return
89+
90+ // Port is taken — find and kill the process holding it
91+ try {
92+ const output = execSync ( `lsof -ti tcp:${ port } ` , { encoding : 'utf-8' } )
93+ const pids = output . trim ( ) . split ( '\n' ) . filter ( Boolean )
94+ for ( const pid of pids ) {
95+ try {
96+ process . kill ( parseInt ( pid , 10 ) , 'SIGTERM' )
97+ } catch { /* already dead */ }
98+ }
99+ // Wait briefly for port to be released
100+ await new Promise ( ( r ) => setTimeout ( r , 500 ) )
101+ } catch {
102+ throw new Error (
103+ `Port ${ port } is in use and could not be freed. Kill the process manually or use a different port.` ,
104+ )
105+ }
106+ }
107+
72108/**
73109 * Spawn `npx wrangler dev` and wait for "Ready on" output.
74110 * Returns the child process.
@@ -81,7 +117,7 @@ export function startWrangler(opts: {
81117 const cwd = opts . cwd ?? process . cwd ( )
82118 const configPath = resolveWranglerConfig ( cwd , opts . wrangler )
83119
84- return new Promise ( ( resolve_ , reject ) => {
120+ return ensurePortFree ( opts . port ) . then ( ( ) => new Promise ( ( resolve_ , reject ) => {
85121 const wranglerArgs = [
86122 'wrangler' ,
87123 'dev' ,
@@ -102,14 +138,19 @@ export function startWrangler(opts: {
102138 30000 ,
103139 )
104140
141+ const stderrChunks : string [ ] = [ ]
142+
105143 const onReady = ( data : Buffer ) => {
106144 if ( data . toString ( ) . includes ( 'Ready on' ) ) {
107145 clearTimeout ( timeout )
108146 resolve_ ( wrangler )
109147 }
110148 }
111149
112- wrangler . stderr ?. on ( 'data' , onReady )
150+ wrangler . stderr ?. on ( 'data' , ( data : Buffer ) => {
151+ stderrChunks . push ( data . toString ( ) )
152+ onReady ( data )
153+ } )
113154 wrangler . stdout ?. on ( 'data' , onReady )
114155
115156 wrangler . on ( 'error' , ( err ) => {
@@ -119,7 +160,11 @@ export function startWrangler(opts: {
119160
120161 wrangler . on ( 'exit' , ( code ) => {
121162 clearTimeout ( timeout )
122- if ( code !== 0 ) reject ( new Error ( `Wrangler exited with code ${ code } ` ) )
163+ if ( code !== 0 ) {
164+ const stderr = stderrChunks . join ( '' ) . trim ( )
165+ const detail = stderr ? `:\n${ stderr } ` : ''
166+ reject ( new Error ( `Wrangler exited with code ${ code } ${ detail } ` ) )
167+ }
123168 } )
124- } )
169+ } ) )
125170}
0 commit comments