@@ -7,7 +7,7 @@ import { rm } from 'node:fs/promises'
77import { join , resolve } from 'node:path'
88
99import { fileURLToPath } from 'node:url'
10- import { getPort , waitForPort } from 'get-port-please'
10+ import { checkPort , getPort , waitForPort } from 'get-port-please'
1111import { isCI } from 'std-env'
1212import { WebSocket } from 'undici'
1313import { it as _it , afterAll , beforeAll , describe , expect , vi } from 'vitest'
@@ -108,6 +108,7 @@ describe.sequential.each(runtimes)('dev server (%s)', (runtimeName) => {
108108 server = await startDevServer ( {
109109 cwd,
110110 runtime : runtimeName ,
111+ basePort : 3100 + runtimes . indexOf ( runtimeName ) * 10 ,
111112 } )
112113 } )
113114
@@ -261,11 +262,13 @@ interface DevServerInstance {
261262async function startDevServer ( options : {
262263 cwd : string
263264 port ?: number
265+ /** Port to prefer, if free. Each runtime gets its own range so a server that is slow to release its listener cannot collide with the next one. */
266+ basePort ?: number
264267 runtime ?: 'node' | 'bun' | 'deno'
265268 env ?: Record < string , string >
266269} ) : Promise < DevServerInstance > {
267- const { cwd, port : preferredPort , runtime = 'node' , env = { } } = options
268- const port = preferredPort || await getPort ( { port : 3100 } )
270+ const { cwd, port : preferredPort , basePort = 3100 , runtime = 'node' , env = { } } = options
271+ const port = preferredPort || await getPort ( { port : basePort , host : '127.0.0.1' } )
269272 const host = '127.0.0.1'
270273 const url = `http://${ host } :${ port } `
271274
@@ -328,19 +331,43 @@ async function startDevServer(options: {
328331 url,
329332 port,
330333 close : async ( ) => {
331- return new Promise < void > ( ( resolve ) => {
332- child . kill ( 'SIGTERM' )
333- setTimeout ( ( ) => {
334- if ( ! child . killed ) {
334+ await new Promise < void > ( ( resolve ) => {
335+ if ( child . exitCode !== null || child . signalCode !== null ) {
336+ resolve ( )
337+ return
338+ }
339+
340+ // `child.killed` only records that a signal was sent, so escalation has to
341+ // look at whether the process has actually gone away.
342+ const force = setTimeout ( ( ) => {
343+ if ( child . exitCode === null && child . signalCode === null ) {
335344 child . kill ( 'SIGKILL' )
336345 }
337346 } , 5000 )
338- child . on ( 'exit' , ( ) => resolve ( ) )
347+ force . unref ?.( )
348+
349+ child . on ( 'exit' , ( ) => {
350+ clearTimeout ( force )
351+ resolve ( )
352+ } )
353+ child . kill ( 'SIGTERM' )
339354 } )
355+
356+ await waitForPortRelease ( port , host )
340357 } ,
341358 }
342359}
343360
361+ /** Wait for a dev server's listener to be released, so the next runtime can bind it. */
362+ async function waitForPortRelease ( port : number , host : string , retries = 50 , delay = 100 ) : Promise < void > {
363+ for ( let i = 0 ; i < retries ; i ++ ) {
364+ if ( await checkPort ( port , host ) ) {
365+ return
366+ }
367+ await new Promise ( resolve => setTimeout ( resolve , delay ) )
368+ }
369+ }
370+
344371interface WebSocketTestOptions {
345372 url : string
346373 timeout ?: number
0 commit comments