1- import { spawnSync } from "node:child_process" ;
1+ import { spawn , spawnSync , type ChildProcess } from "node:child_process" ;
22import {
33 copyFileSync ,
44 existsSync ,
@@ -14,6 +14,7 @@ import { fileURLToPath } from "node:url";
1414
1515import { expect , test } from "@playwright/test" ;
1616import dedent from "dedent" ;
17+ import getPort from "get-port" ;
1718import semver from "semver" ;
1819
1920import { build , createProject , reactRouterConfig } from "./helpers/vite" ;
@@ -22,6 +23,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
2223const rootDirectory = path . resolve ( __dirname , ".." ) ;
2324const nodeBin = process . argv [ 0 ] ;
2425const reactRouterBin = "node_modules/@react-router/dev/dist/cli/index.js" ;
26+ const reactRouterPackageBinPath = "node_modules/@react-router/dev/bin.cjs" ;
2527const reactRouterPackageBin = path . join (
2628 rootDirectory ,
2729 "packages/react-router-dev/bin.cjs" ,
@@ -30,6 +32,121 @@ const reactRouterPackageBin = path.join(
3032const run = ( command : string [ ] , options : Parameters < typeof spawnSync > [ 2 ] ) =>
3133 spawnSync ( nodeBin , [ reactRouterBin , ...command ] , options ) ;
3234
35+ function bufferize ( stream : NodeJS . ReadableStream | null ) : ( ) => string {
36+ let buffer = "" ;
37+ stream ?. on ( "data" , ( data ) => ( buffer += data . toString ( ) ) ) ;
38+ return ( ) => buffer ;
39+ }
40+
41+ function delay ( ms : number ) {
42+ return new Promise ( ( resolve ) => setTimeout ( resolve , ms ) ) ;
43+ }
44+
45+ function restartCount ( output : string ) {
46+ return (
47+ output . match ( / \[ r e s t a r t \] R e l a u n c h i n g w i t h N O D E _ O P T I O N S : / g) ?. length ?? 0
48+ ) ;
49+ }
50+
51+ function getLogs ( stdout : string , stderr : string ) {
52+ return [
53+ `stdout:\n${ stdout || "<empty>" } ` ,
54+ `stderr:\n${ stderr || "<empty>" } ` ,
55+ ] . join ( "\n\n" ) ;
56+ }
57+
58+ async function waitForDevServer ( args : {
59+ port : number ;
60+ proc : ChildProcess ;
61+ stdout : ( ) => string ;
62+ stderr : ( ) => string ;
63+ } ) {
64+ let timeout = process . platform === "win32" ? 20_000 : 10_000 ;
65+ let start = Date . now ( ) ;
66+ let lastError : unknown ;
67+
68+ while ( Date . now ( ) - start < timeout ) {
69+ let stdout = args . stdout ( ) ;
70+ let stderr = args . stderr ( ) ;
71+
72+ if ( restartCount ( stdout ) > 1 ) {
73+ throw new Error (
74+ `Expected react-router dev to restart once, but it restarted ${ restartCount (
75+ stdout ,
76+ ) } times.\n\n${ getLogs ( stdout , stderr ) } `,
77+ ) ;
78+ }
79+
80+ if ( args . proc . exitCode !== null || args . proc . signalCode !== null ) {
81+ throw new Error (
82+ `react-router dev exited before the server started.\n\n${ getLogs (
83+ stdout ,
84+ stderr ,
85+ ) } `,
86+ ) ;
87+ }
88+
89+ try {
90+ let response = await fetch ( `http://127.0.0.1:${ args . port } /` , {
91+ signal : AbortSignal . timeout ( 1_000 ) ,
92+ } ) ;
93+ let html = await response . text ( ) ;
94+ if ( response . ok && html . includes ( "Welcome to React Router" ) ) {
95+ return ;
96+ }
97+ lastError = new Error ( `Unexpected response ${ response . status } : ${ html } ` ) ;
98+ } catch ( error ) {
99+ lastError = error ;
100+ }
101+
102+ await delay ( 100 ) ;
103+ }
104+
105+ throw new Error (
106+ [
107+ `Timed out waiting for react-router dev to start: ${ String ( lastError ) } ` ,
108+ getLogs ( args . stdout ( ) , args . stderr ( ) ) ,
109+ ] . join ( "\n\n" ) ,
110+ ) ;
111+ }
112+
113+ function waitForExit ( proc : ChildProcess , timeout : number ) {
114+ return new Promise < { code : number | null ; signal : NodeJS . Signals | null } > (
115+ ( resolve , reject ) => {
116+ if ( proc . exitCode !== null || proc . signalCode !== null ) {
117+ resolve ( { code : proc . exitCode , signal : proc . signalCode } ) ;
118+ return ;
119+ }
120+
121+ let timer = setTimeout ( ( ) => {
122+ reject ( new Error ( "Timed out waiting for react-router dev to exit" ) ) ;
123+ } , timeout ) ;
124+
125+ proc . once ( "exit" , ( code , signal ) => {
126+ clearTimeout ( timer ) ;
127+ resolve ( { code, signal } ) ;
128+ } ) ;
129+ } ,
130+ ) ;
131+ }
132+
133+ function killProcessGroup ( proc : ChildProcess ) {
134+ if ( proc . exitCode !== null || proc . signalCode !== null ) {
135+ return ;
136+ }
137+
138+ if ( proc . pid && process . platform !== "win32" ) {
139+ try {
140+ process . kill ( - proc . pid , "SIGKILL" ) ;
141+ return ;
142+ } catch {
143+ // Fall back to killing just the parent process below.
144+ }
145+ }
146+
147+ proc . kill ( "SIGKILL" ) ;
148+ }
149+
33150const getBinNodeEnv = ( command : string [ ] ) => {
34151 let cwd = mkdtempSync ( path . join ( tmpdir ( ) , "react-router-bin-" ) ) ;
35152 let env = { ...process . env } ;
@@ -162,6 +279,60 @@ test.describe("cli", () => {
162279 ) ;
163280 } ) ;
164281
282+ test ( "dev restarts with the development condition and starts the server" , async ( {
283+ browserName : _browserName ,
284+ } , { project } ) => {
285+ test . skip (
286+ project . name !== "chromium" ,
287+ "CLI smoke test only needs one browser project" ,
288+ ) ;
289+
290+ let cwd = await createProject ( ) ;
291+ let port = await getPort ( ) ;
292+ let proc = spawn (
293+ nodeBin ,
294+ [
295+ reactRouterPackageBinPath ,
296+ "dev" ,
297+ "--host" ,
298+ "127.0.0.1" ,
299+ "--port" ,
300+ String ( port ) ,
301+ "--strictPort" ,
302+ ] ,
303+ {
304+ cwd,
305+ detached : process . platform !== "win32" ,
306+ env : {
307+ ...process . env ,
308+ FORCE_COLOR : undefined ,
309+ NO_COLOR : "1" ,
310+ NODE_OPTIONS : "--no-warnings=ExperimentalWarning" ,
311+ } ,
312+ stdio : "pipe" ,
313+ } ,
314+ ) ;
315+ let stdout = bufferize ( proc . stdout ) ;
316+ let stderr = bufferize ( proc . stderr ) ;
317+
318+ try {
319+ await waitForDevServer ( { port, proc, stdout, stderr } ) ;
320+ expect ( restartCount ( stdout ( ) ) ) . toBe ( 1 ) ;
321+
322+ proc . kill ( "SIGTERM" ) ;
323+ await expect ( waitForExit ( proc , 5_000 ) ) . resolves . toBeDefined ( ) ;
324+ } catch ( error ) {
325+ throw new Error (
326+ [
327+ error instanceof Error ? error . message : String ( error ) ,
328+ getLogs ( stdout ( ) , stderr ( ) ) ,
329+ ] . join ( "\n\n" ) ,
330+ ) ;
331+ } finally {
332+ killProcessGroup ( proc ) ;
333+ }
334+ } ) ;
335+
165336 test ( "routes" , async ( ) => {
166337 const cwd = await createProject ( ) ;
167338 let { stdout, stderr, status } = run ( [ "routes" ] , { cwd } ) ;
0 commit comments