1010 * governing permissions and limitations under the License.
1111 */
1212import { fileURLToPath } from 'url' ;
13- import { execSync , spawnSync } from 'child_process' ;
13+ import { execSync , spawn } from 'child_process' ;
1414import path from 'path' ;
1515import fse from 'fs-extra' ;
1616import * as esbuild from 'esbuild' ;
@@ -249,19 +249,52 @@ export default class EdgeESBuildBundler extends BaseBundler {
249249 }
250250 }
251251
252+ /**
253+ * Waits for a server to become ready by polling an HTTP endpoint.
254+ * @param {string } url - The URL to poll
255+ * @param {number } timeout - Maximum time to wait in ms
256+ * @param {number } interval - Polling interval in ms
257+ * @returns {Promise<boolean> } True if server is ready, false if timeout
258+ */
259+ // eslint-disable-next-line class-methods-use-this
260+ async waitForServer ( url , timeout = 10000 , interval = 500 ) {
261+ const start = Date . now ( ) ;
262+ // eslint-disable-next-line no-await-in-loop
263+ while ( Date . now ( ) - start < timeout ) {
264+ try {
265+ // eslint-disable-next-line no-await-in-loop
266+ const response = await fetch ( url ) ;
267+ if ( response . ok || response . status < 500 ) {
268+ return true ;
269+ }
270+ } catch {
271+ // Server not ready yet, continue polling
272+ }
273+ // eslint-disable-next-line no-await-in-loop
274+ await new Promise ( ( resolve ) => {
275+ setTimeout ( resolve , interval ) ;
276+ } ) ;
277+ }
278+ return false ;
279+ }
280+
252281 /**
253282 * Validates the edge bundle using wrangler (Cloudflare) or viceroy (Fastly) if available.
254- * This helps catch runtime issues before deployment.
283+ * Starts a local server, makes an HTTP request to validate, then stops the server.
284+ * @returns {Promise<{wrangler?: boolean, fastly?: boolean}> } Validation results
255285 */
256286 async validateBundle ( ) {
257287 const { cfg } = this ;
258288 const bundlePath = cfg . edgeBundle ;
259289 const bundleDir = path . dirname ( path . resolve ( cfg . cwd , bundlePath ) ) ;
290+ const results = { } ;
260291
261- // Try wrangler validation first (Cloudflare)
292+ // Try wrangler validation (Cloudflare)
262293 const hasWrangler = this . isCommandAvailable ( 'wrangler' ) ;
263294 if ( hasWrangler ) {
264295 cfg . log . info ( '--: validating edge bundle with wrangler...' ) ;
296+ let wranglerProcess = null ;
297+ const wranglerPort = 8787 + Math . floor ( Math . random ( ) * 1000 ) ;
265298 try {
266299 // Create a minimal wrangler.toml for validation
267300 const wranglerToml = path . join ( bundleDir , 'wrangler.toml' ) ;
@@ -273,68 +306,109 @@ export default class EdgeESBuildBundler extends BaseBundler {
273306 ] . join ( '\n' ) ;
274307 await fse . writeFile ( wranglerToml , wranglerConfig ) ;
275308
276- // Run wrangler deploy --dry-run to validate without deploying
277- const result = spawnSync ( 'wrangler' , [ 'deploy ' , '--dry-run' ] , {
309+ // Start wrangler dev server
310+ wranglerProcess = spawn ( 'wrangler' , [ 'dev ' , '--local' , '--port' , String ( wranglerPort ) ] , {
278311 cwd : bundleDir ,
279312 stdio : 'pipe' ,
280- timeout : 30000 ,
281313 } ) ;
282314
283- // Clean up temporary wrangler.toml
284- await fse . remove ( wranglerToml ) ;
285-
286- if ( result . status === 0 ) {
287- cfg . log . info ( chalk `{green ok:} wrangler validation passed` ) ;
315+ // Wait for server to be ready
316+ const serverReady = await this . waitForServer ( `http://127.0.0.1:${ wranglerPort } /` ) ;
317+
318+ if ( serverReady ) {
319+ // Make a validation request
320+ const response = await fetch ( `http://127.0.0.1:${ wranglerPort } /` ) ;
321+ if ( response . ok || response . status < 500 ) {
322+ cfg . log . info ( chalk `{green ok:} wrangler validation passed (status: ${ response . status } )` ) ;
323+ results . wrangler = true ;
324+ } else {
325+ cfg . log . warn ( chalk `{yellow warn:} wrangler validation returned status ${ response . status } ` ) ;
326+ results . wrangler = false ;
327+ }
288328 } else {
289- const stderr = result . stderr ?. toString ( ) || '' ;
290- cfg . log . warn ( chalk `{yellow warn:} wrangler validation issues: ${ stderr } ` ) ;
329+ cfg . log . warn ( chalk `{yellow warn:} wrangler server failed to start within timeout` ) ;
330+ results . wrangler = false ;
291331 }
292332 } catch ( err ) {
293333 cfg . log . warn ( chalk `{yellow warn:} wrangler validation failed: ${ err . message } ` ) ;
334+ results . wrangler = false ;
335+ } finally {
336+ // Kill wrangler process
337+ if ( wranglerProcess ) {
338+ wranglerProcess . kill ( 'SIGTERM' ) ;
339+ }
340+ // Clean up temporary wrangler.toml
341+ const wranglerToml = path . join ( bundleDir , 'wrangler.toml' ) ;
342+ await fse . remove ( wranglerToml ) . catch ( ( ) => { } ) ;
294343 }
295344 }
296345
297346 // Try Fastly validation (via fastly CLI which uses viceroy)
298347 const hasFastly = this . isCommandAvailable ( 'fastly' ) ;
299348 if ( hasFastly ) {
300349 cfg . log . info ( '--: validating edge bundle with fastly (viceroy)...' ) ;
350+ let fastlyProcess = null ;
351+ const fastlyPort = 7676 + Math . floor ( Math . random ( ) * 1000 ) ;
301352 try {
302353 // Create a minimal fastly.toml for validation
303354 const fastlyToml = path . join ( bundleDir , 'fastly.toml' ) ;
304355 const fastlyConfig = [
305- 'manifest_version = 2 ' ,
356+ 'manifest_version = 3 ' ,
306357 'name = "validation-test"' ,
307358 'language = "javascript"' ,
359+ 'service_id = ""' ,
360+ '' ,
308361 '[scripts]' ,
309362 'build = ""' ,
310363 ] . join ( '\n' ) ;
311364 await fse . writeFile ( fastlyToml , fastlyConfig ) ;
312365
313- // Run fastly compute serve with --skip-build to validate
314- // Use a short timeout and immediately kill to just check if bundle loads
315- const result = spawnSync ( 'fastly' , [ 'compute' , 'serve' , '--skip-build' , '--file' , path . basename ( bundlePath ) ] , {
366+ // Start fastly compute serve
367+ fastlyProcess = spawn ( 'fastly' , [
368+ 'compute' , 'serve' ,
369+ '--skip-build' ,
370+ '--file' , path . basename ( bundlePath ) ,
371+ '--addr' , `127.0.0.1:${ fastlyPort } ` ,
372+ ] , {
316373 cwd : bundleDir ,
317374 stdio : 'pipe' ,
318- timeout : 5000 ,
319375 } ) ;
320376
321- // Clean up temporary fastly.toml
322- await fse . remove ( fastlyToml ) ;
323-
324- // Check if it started successfully (will timeout, but no errors means valid)
325- const stderr = result . stderr ?. toString ( ) || '' ;
326- if ( ! stderr . includes ( 'error' ) && ! stderr . includes ( 'Error' ) ) {
327- cfg . log . info ( chalk `{green ok:} fastly validation passed` ) ;
377+ // Wait for server to be ready
378+ const serverReady = await this . waitForServer ( `http://127.0.0.1:${ fastlyPort } /` ) ;
379+
380+ if ( serverReady ) {
381+ // Make a validation request
382+ const response = await fetch ( `http://127.0.0.1:${ fastlyPort } /` ) ;
383+ if ( response . ok || response . status < 500 ) {
384+ cfg . log . info ( chalk `{green ok:} fastly validation passed (status: ${ response . status } )` ) ;
385+ results . fastly = true ;
386+ } else {
387+ cfg . log . warn ( chalk `{yellow warn:} fastly validation returned status ${ response . status } ` ) ;
388+ results . fastly = false ;
389+ }
328390 } else {
329- cfg . log . warn ( chalk `{yellow warn:} fastly validation issues: ${ stderr } ` ) ;
391+ cfg . log . warn ( chalk `{yellow warn:} fastly server failed to start within timeout` ) ;
392+ results . fastly = false ;
330393 }
331394 } catch ( err ) {
332395 cfg . log . warn ( chalk `{yellow warn:} fastly validation failed: ${ err . message } ` ) ;
396+ results . fastly = false ;
397+ } finally {
398+ // Kill fastly process
399+ if ( fastlyProcess ) {
400+ fastlyProcess . kill ( 'SIGTERM' ) ;
401+ }
402+ // Clean up temporary fastly.toml
403+ const fastlyToml = path . join ( bundleDir , 'fastly.toml' ) ;
404+ await fse . remove ( fastlyToml ) . catch ( ( ) => { } ) ;
333405 }
334406 }
335407
336408 if ( ! hasWrangler && ! hasFastly ) {
337409 cfg . log . info ( '--: skipping bundle validation (neither wrangler nor fastly CLI installed)' ) ;
338410 }
411+
412+ return results ;
339413 }
340414}
0 commit comments