@@ -3,24 +3,129 @@ import child_process from "node:child_process";
33import fs from "node:fs" ;
44import { on } from "node:events" ;
55import path from "node:path" ;
6+ import readline from "node:readline" ;
67
8+ // Third party imports
9+ import { getPort } from "get-port-please" ;
10+
11+ // Local imports
712import { appMode } from "./app_mode.js" ;
813
14+ const BYTES_PER_KIBIBYTE = 1024 ;
15+ const MAX_ERROR_BUFFER_KIBIBYTES = 64 ;
16+ const MAX_ERROR_BUFFER_BYTES = MAX_ERROR_BUFFER_KIBIBYTES * BYTES_PER_KIBIBYTE ;
17+
18+ function getAvailablePort ( ) {
19+ return getPort ( {
20+ host : "localhost" ,
21+ random : true ,
22+ } ) ;
23+ }
24+
925function commandExistsSync ( execName ) {
1026 const envPath = process . env . PATH || "" ;
11- return envPath . split ( path . delimiter ) . some ( ( dir ) => {
12- const filePath = path . join ( dir , execName ) ;
27+ return envPath . split ( path . delimiter ) . some ( ( directory ) => {
28+ const filePath = path . join ( directory , execName ) ;
1329 return fs . existsSync ( filePath ) && fs . statSync ( filePath ) . isFile ( ) ;
1430 } ) ;
1531}
1632
17- async function waitForReady ( child , expectedResponse ) {
18- for await ( const [ data ] of on ( child . stdout , "data" ) ) {
19- if ( data . toString ( ) . includes ( expectedResponse ) ) {
20- return child ;
33+ const encoder = new TextEncoder ( ) ;
34+
35+ function byteLength ( str ) {
36+ return encoder . encode ( str ) . byteLength ;
37+ }
38+
39+ // oxlint-disable-next-line max-lines-per-function
40+ function waitForReady ( child , expectedResponse , signal ) {
41+ // oxlint-disable-next-line promise/avoid-new
42+ return new Promise ( ( resolve , reject ) => {
43+ const readlineStdout = readline . createInterface ( { input : child . stdout } ) ;
44+ const readlineStderr = readline . createInterface ( { input : child . stderr } ) ;
45+
46+ let recentOutput = "" ;
47+
48+ function recordOutput ( lineOutput ) {
49+ const safeLine =
50+ byteLength ( lineOutput ) > MAX_ERROR_BUFFER_BYTES / 2
51+ ? `${ lineOutput . slice ( 0 , MAX_ERROR_BUFFER_BYTES / 2 ) } …[truncated]`
52+ : lineOutput ;
53+
54+ recentOutput = `${ recentOutput } ${ safeLine } \n` ;
55+
56+ while ( byteLength ( recentOutput ) > MAX_ERROR_BUFFER_BYTES ) {
57+ const newline = recentOutput . indexOf ( "\n" ) ;
58+ if ( newline === - 1 ) {
59+ recentOutput = "" ;
60+ break ;
61+ }
62+ recentOutput = recentOutput . slice ( newline + 1 ) ;
63+ }
2164 }
22- }
23- throw new Error ( "Process closed before signal" ) ;
65+
66+ function cleanup ( ) {
67+ readlineStdout . removeListener ( "line" , onLine ) ;
68+ readlineStderr . removeListener ( "line" , onErrLine ) ;
69+ child . removeListener ( "error" , onError ) ;
70+ child . removeListener ( "close" , onClose ) ;
71+ if ( signal ) {
72+ signal . removeEventListener ( "abort" , onAbort ) ;
73+ }
74+ }
75+
76+ function onLine ( lineOutput ) {
77+ console . log ( `[${ child . name } ] ${ lineOutput } ` ) ;
78+ recordOutput ( lineOutput ) ;
79+ if ( lineOutput . includes ( expectedResponse ) ) {
80+ cleanup ( ) ;
81+ readlineStdout . on ( "line" , ( line ) => {
82+ console . log ( `[${ child . name } ] ${ line } ` ) ;
83+ } ) ;
84+ readlineStderr . on ( "line" , ( line ) => {
85+ console . log ( `[${ child . name } ] ${ line } ` ) ;
86+ } ) ;
87+ child . once ( "close" , ( code ) => {
88+ console . log ( `[${ child . name } ] exited with code ${ code } ` ) ;
89+ } ) ;
90+ resolve ( child ) ;
91+ }
92+ }
93+
94+ function onErrLine ( line ) {
95+ console . log ( `[${ child . name } ] ${ line } ` ) ;
96+ recordOutput ( line ) ;
97+ }
98+
99+ function onError ( err ) {
100+ cleanup ( ) ;
101+ reject ( err ) ;
102+ }
103+
104+ function onClose ( code ) {
105+ console . log ( `[${ child . name } ] exited with code ${ code } ` ) ;
106+ cleanup ( ) ;
107+ reject (
108+ new Error (
109+ `[${ child . name } ] exited with code ${ code } before becoming ready.${
110+ recentOutput ? `\nRecent output:\n${ recentOutput } ` : ""
111+ } `,
112+ ) ,
113+ ) ;
114+ }
115+
116+ function onAbort ( ) {
117+ cleanup ( ) ;
118+ reject ( new Error ( `[${ child . name } ] timed out waiting for "${ expectedResponse } "` ) ) ;
119+ }
120+
121+ readlineStdout . on ( "line" , onLine ) ;
122+ readlineStderr . on ( "line" , onErrLine ) ;
123+ child . once ( "error" , onError ) ;
124+ child . once ( "close" , onClose ) ;
125+ if ( signal ) {
126+ signal . addEventListener ( "abort" , onAbort , { once : true } ) ;
127+ }
128+ } ) ;
24129}
25130
26131async function waitNuxt ( nuxtProcess ) {
@@ -49,11 +154,17 @@ async function waitNuxt(nuxtProcess) {
49154async function runBrowser ( scriptName ) {
50155 process . env . MODE = appMode . BROWSER ;
51156
157+ const port = await getAvailablePort ( ) ;
158+
52159 const nuxtProcess = child_process . spawn ( "npm" , [ "run" , scriptName ] , {
53160 shell : true ,
54161 FORCE_COLOR : true ,
162+ env : {
163+ ...process . env ,
164+ PORT : port ,
165+ } ,
55166 } ) ;
56167 return await waitNuxt ( nuxtProcess ) ;
57168}
58169
59- export { runBrowser , waitForReady , commandExistsSync } ;
170+ export { commandExistsSync , getAvailablePort , runBrowser , waitForReady } ;
0 commit comments