#!/usr/bin/env node // Boot-check (Gitea #92): after `npm run build`, nothing actually loaded the // built dist/ — a build that produces a broken bundle (bad base path, a 500 // from an asset, a malformed config.json) still went green. This script // serves dist/ the same way production does (vite preview) and makes a // handful of real HTTP requests against it, so a broken bundle fails CI // instead of surfacing after deploy. // // No Playwright here: playwright-core is not a project dependency (only the // browser binary caches happen to be present on this machine), so we don't // depend on it being installed. Plain fetch is enough to catch the class of // bug this check exists for — a page/asset/config that doesn't come back. import { spawn } from 'node:child_process'; const PORT = 4173; const HOST = '127.0.0.1'; const BASE_URL = `http://${HOST}:${PORT}`; const BOOT_TIMEOUT_MS = 30_000; function log(msg) { console.log(`[boot-check] ${msg}`); } function waitForPort(url, timeoutMs) { const deadline = Date.now() + timeoutMs; const attempt = async () => { try { const res = await fetch(url, { method: 'GET' }); return res; } catch { return null; } }; return new Promise((resolve, reject) => { const poll = async () => { const res = await attempt(); if (res) { resolve(); return; } if (Date.now() > deadline) { reject(new Error(`Timed out waiting for ${url} to come up`)); return; } setTimeout(poll, 300); }; poll(); }); } async function assert(condition, message) { if (!condition) { throw new Error(`Assertion failed: ${message}`); } log(`ok: ${message}`); } async function main() { const preview = spawn( 'npx', ['vite', 'preview', '--port', String(PORT), '--strictPort', '--host', HOST], { stdio: ['ignore', 'pipe', 'pipe'] }, ); let previewOutput = ''; preview.stdout.on('data', (d) => (previewOutput += d.toString())); preview.stderr.on('data', (d) => (previewOutput += d.toString())); const cleanup = () => { if (!preview.killed) { preview.kill('SIGTERM'); } }; process.on('exit', cleanup); process.on('SIGINT', () => { cleanup(); process.exit(1); }); process.on('SIGTERM', () => { cleanup(); process.exit(1); }); try { await waitForPort(BASE_URL, BOOT_TIMEOUT_MS); // 1. index page loads and contains the SPA mount point. const indexRes = await fetch(`${BASE_URL}/`); await assert(indexRes.status === 200, `GET / returns 200 (got ${indexRes.status})`); const indexHtml = await indexRes.text(); await assert(indexHtml.includes('
]+type="module"[^>]+src="([^"]+)"/); await assert(!!scriptMatch, 'index.html references a module script entry'); const mainScriptUrl = new URL(scriptMatch[1], BASE_URL).toString(); const scriptRes = await fetch(mainScriptUrl); await assert( scriptRes.status === 200, `GET ${scriptMatch[1]} returns 200 (got ${scriptRes.status})`, ); const scriptContentType = scriptRes.headers.get('content-type') || ''; await assert( /javascript/.test(scriptContentType), `GET ${scriptMatch[1]} has a JS content-type (got "${scriptContentType}")`, ); // 4. Element Call widget bundle is present. const callRes = await fetch(`${BASE_URL}/public/element-call/index.html`); await assert( callRes.status === 200, `GET /public/element-call/index.html returns 200 (got ${callRes.status})`, ); log('all checks passed'); } finally { cleanup(); } } main() .catch((err) => { console.error(`[boot-check] FAILED: ${err.message}`); process.exitCode = 1; }) .finally(() => { // The killed preview server's stdio pipes can keep the event loop alive // briefly; force the process down promptly with whatever exit code was set. process.exit(process.exitCode ?? 0); });