Skip to content

Commit 9d274bd

Browse files
committed
fix(bundler): improve validateBundle to start server and make HTTP request
- Replace dry-run validation with actual server startup - Start wrangler dev --local for Cloudflare validation - Start fastly compute serve for Fastly validation - Add waitForServer() helper to poll for server readiness - Make HTTP request to validate bundle works at runtime - Kill server processes and clean up after validation - Return validation results object 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> Signed-off-by: Lars Trieloff <[email protected]>
1 parent e337df5 commit 9d274bd

File tree

1 file changed

+100
-26
lines changed

1 file changed

+100
-26
lines changed

src/EdgeESBuildBundler.js

Lines changed: 100 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212
import { fileURLToPath } from 'url';
13-
import { execSync, spawnSync } from 'child_process';
13+
import { execSync, spawn } from 'child_process';
1414
import path from 'path';
1515
import fse from 'fs-extra';
1616
import * 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

Comments
 (0)