11#!/usr/bin/env node
2- // Sequential smoke runner for production-built Esmx examples.
2+ // Batched smoke runner for production-built Esmx examples.
33//
4- // For each example: spawn `pnpm start`, poll the configured port until 200,
5- // curl `/` once, assert the response is HTML and contains hydration markers
6- // (importmap + module script). Kill the server, move on.
7- //
8- // Hydration "browser" check is deferred to F2 (Playwright); this layer
9- // proves the SSR HTML wire-up is in place so the client bundle WILL hydrate.
4+ // Runs servers in two parallel groups (standalone + hub-federated micros):
5+ // - Group "standalone": 6 servers on ports 3000-3005, one set at a time
6+ // - Group "micro": hub (3000) + 15 remotes on 3001-3015, all at once
7+ // In each group all servers come up concurrently; we then curl each `/`,
8+ // assert it's a hydration-ready HTML document (status 200 + DOCTYPE +
9+ // `<script type="importmap">` + `<script type="module">`), and kill
10+ // the group. Wall-clock dominated by the slowest server in each group,
11+ // not by sum-of-all — what used to take ~25 min sequentially now finishes
12+ // in ~2 min.
1013
1114import { spawn } from 'node:child_process' ;
1215import { setTimeout as delay } from 'node:timers/promises' ;
@@ -107,9 +110,7 @@ const MICRO = [
107110 }
108111] ;
109112
110- const TARGETS = [ ...STANDALONE , ...MICRO ] ;
111-
112- const STARTUP_TIMEOUT_MS = 30_000 ;
113+ const STARTUP_TIMEOUT_MS = 60_000 ;
113114const POLL_INTERVAL_MS = 500 ;
114115
115116async function waitForReady ( port ) {
@@ -118,44 +119,37 @@ async function waitForReady(port) {
118119 while ( Date . now ( ) < deadline ) {
119120 try {
120121 const res = await fetch ( `http://127.0.0.1:${ port } /` , {
121- signal : AbortSignal . timeout ( 2000 )
122+ signal : AbortSignal . timeout ( 3000 )
122123 } ) ;
123124 if ( res . status === 200 ) {
124125 const body = await res . text ( ) ;
125- if ( / ^ \s * < ! D O C T Y P E / i. test ( body ) ) {
126- return { status : res . status , body } ;
127- }
128- lastErr = `200 but no DOCTYPE in body (len=${ body . length } )` ;
129- } else {
130- lastErr = `status=${ res . status } ` ;
126+ if ( / ^ \s * < ! D O C T Y P E / i. test ( body ) ) return body ;
131127 }
128+ lastErr = `status=${ res . status } ` ;
132129 } catch ( e ) {
133130 lastErr = e . message || String ( e ) ;
134131 }
135132 await delay ( POLL_INTERVAL_MS ) ;
136133 }
137- throw new Error (
138- `server on port ${ port } did not become ready in ${ STARTUP_TIMEOUT_MS } ms (last: ${ lastErr } )`
139- ) ;
134+ throw new Error ( `not ready in ${ STARTUP_TIMEOUT_MS } ms (last: ${ lastErr } )` ) ;
140135}
141136
142- function assertHydratable ( html , target ) {
137+ function assertHydratable ( html , name ) {
143138 const checks = [
144- { name : 'importmap script' , re : / < s c r i p t [ ^ > ] * t y p e = [ " ' ] i m p o r t m a p [ " ' ] / } ,
145- { name : 'module entry script' , re : / < s c r i p t [ ^ > ] * t y p e = [ " ' ] m o d u l e [ " ' ] / }
139+ { what : 'importmap script' , re : / < s c r i p t [ ^ > ] * t y p e = [ " ' ] i m p o r t m a p [ " ' ] / } ,
140+ { what : 'module entry script' , re : / < s c r i p t [ ^ > ] * t y p e = [ " ' ] m o d u l e [ " ' ] / }
146141 ] ;
147- const failures = checks
142+ const missing = checks
148143 . filter ( ( { re } ) => ! re . test ( html ) )
149- . map ( ( c ) => c . name ) ;
150- if ( failures . length ) {
144+ . map ( ( c ) => c . what ) ;
145+ if ( missing . length ) {
151146 throw new Error (
152- `${ target . name } : HTML missing hydration markers [${ failures . join ( ', ' ) } ]`
147+ `${ name } : missing hydration markers [${ missing . join ( ', ' ) } ]`
153148 ) ;
154149 }
155150}
156151
157- async function smokeOne ( target ) {
158- const start = Date . now ( ) ;
152+ function spawnServer ( target ) {
159153 const child = spawn ( 'pnpm' , [ '--filter' , `./${ target . dir } ` , 'start' ] , {
160154 stdio : [ 'ignore' , 'pipe' , 'pipe' ] ,
161155 env : {
@@ -164,47 +158,79 @@ async function smokeOne(target) {
164158 NODE_ENV : 'production'
165159 }
166160 } ) ;
167-
168- let stderr = '' ;
161+ child . _stderr = '' ;
169162 child . stderr . on ( 'data' , ( b ) => {
170- stderr += b . toString ( ) ;
163+ child . _stderr += b . toString ( ) ;
171164 } ) ;
165+ // Drain stdout so the child doesn't block on full pipe buffer.
166+ child . stdout . on ( 'data' , ( ) => { } ) ;
167+ return child ;
168+ }
169+
170+ async function killAll ( children ) {
171+ for ( const c of children ) {
172+ if ( ! c . killed ) c . kill ( 'SIGTERM' ) ;
173+ }
174+ await delay ( 500 ) ;
175+ for ( const c of children ) {
176+ if ( ! c . killed ) c . kill ( 'SIGKILL' ) ;
177+ }
178+ }
172179
180+ async function runGroup ( groupName , targets ) {
181+ console . log (
182+ `\n=== smoke group: ${ groupName } (${ targets . length } servers) ===`
183+ ) ;
184+ const start = Date . now ( ) ;
185+ const children = targets . map ( spawnServer ) ;
173186 try {
174- const { body : html } = await waitForReady ( target . port ) ;
175- assertHydratable ( html , target ) ;
187+ const results = await Promise . allSettled (
188+ targets . map ( async ( t ) => {
189+ const html = await waitForReady ( t . port ) ;
190+ assertHydratable ( html , t . name ) ;
191+ return { ok : true , target : t } ;
192+ } )
193+ ) ;
194+ let passed = 0 ;
195+ const failures = [ ] ;
196+ for ( let i = 0 ; i < results . length ; i ++ ) {
197+ const r = results [ i ] ;
198+ const t = targets [ i ] ;
199+ if ( r . status === 'fulfilled' ) {
200+ console . log ( `✓ ${ t . name } (:${ t . port } )` ) ;
201+ passed ++ ;
202+ } else {
203+ const err = r . reason ?. message || String ( r . reason ) ;
204+ console . error ( `✗ ${ t . name } (:${ t . port } ) ${ err } ` ) ;
205+ const child = children [ i ] ;
206+ if ( child . _stderr ) {
207+ console . error (
208+ ` stderr: ${ child . _stderr . trim ( ) . slice ( - 500 ) } `
209+ ) ;
210+ }
211+ failures . push ( { target : t , error : err } ) ;
212+ }
213+ }
176214 console . log (
177- `✓ ${ target . name } (: ${ target . port } ) ${ Date . now ( ) - start } ms`
215+ ` ${ passed } / ${ targets . length } passed ( ${ Date . now ( ) - start } ms) `
178216 ) ;
179- return { ok : true } ;
180- } catch ( e ) {
181- console . error ( `✗ ${ target . name } (:${ target . port } ) ${ e . message } ` ) ;
182- if ( stderr ) console . error ( ` stderr: ${ stderr . trim ( ) . slice ( - 500 ) } ` ) ;
183- return { ok : false , error : e . message } ;
217+ return failures ;
184218 } finally {
185- child . kill ( 'SIGTERM' ) ;
186- await delay ( 200 ) ;
187- if ( ! child . killed ) child . kill ( 'SIGKILL' ) ;
219+ await killAll ( children ) ;
188220 }
189221}
190222
191223async function main ( ) {
192- const filter = process . argv [ 2 ] ;
193- const targets = filter ? TARGETS . filter ( ( t ) => t . name === filter ) : TARGETS ;
194- if ( ! targets . length ) {
195- console . error ( `No targets match filter "${ filter } "` ) ;
196- process . exit ( 2 ) ;
197- }
198- const results = [ ] ;
199- for ( const t of targets ) {
200- results . push ( { target : t , ...( await smokeOne ( t ) ) } ) ;
201- }
202- const failed = results . filter ( ( r ) => ! r . ok ) ;
224+ const failures = [ ] ;
225+ failures . push ( ...( await runGroup ( 'standalone' , STANDALONE ) ) ) ;
226+ failures . push ( ...( await runGroup ( 'micro (hub + 15 remotes)' , MICRO ) ) ) ;
227+
203228 console . log (
204- `\nSmoke summary: ${ results . length - failed . length } / ${ results . length } passed `
229+ `\nSmoke summary: ${ failures . length === 0 ? 'PASS' : ` ${ failures . length } failure(s)` } `
205230 ) ;
206- if ( failed . length ) {
207- for ( const r of failed ) console . log ( ` - ${ r . target . name } : ${ r . error } ` ) ;
231+ if ( failures . length ) {
232+ for ( const f of failures )
233+ console . log ( ` - ${ f . target . name } : ${ f . error } ` ) ;
208234 process . exit ( 1 ) ;
209235 }
210236}
0 commit comments