import { spawn } from 'node:child_process'; import { resolve } from 'node:path'; const rootDir = process.cwd(); const viteBin = resolve(rootDir, 'node_modules/vite/bin/vite.js'); const serveScript = resolve(rootDir, 'scripts/serve-app-dist.mjs'); const appDistDir = '/tmp/ai-code-test-app-dist'; const buildEnv = { ...process.env, VITE_FILTER_PUBLIC_DIR: 'true', VITE_DISABLE_MODULE_PRELOAD: 'true', }; const serveEnv = { ...process.env, APP_DIST_DIR: appDistDir, }; function log(message) { console.log(`[preview:test-app:watch] ${new Date().toISOString()} ${message}`); } function logError(message) { console.error(`[preview:test-app:watch] ${new Date().toISOString()} ${message}`); } function runNodeScript(args, env, options = {}) { return spawn(process.execPath, args, { cwd: rootDir, env, stdio: 'inherit', ...options, }); } function pipePrefixedLines(stream, writer, prefix, onLine) { if (!stream) { return; } let buffer = ''; stream.setEncoding('utf8'); stream.on('data', (chunk) => { buffer += chunk; const lines = buffer.split(/\r?\n/u); buffer = lines.pop() ?? ''; for (const line of lines) { onLine?.(line); writer.write(`${prefix}${line}\n`); } }); stream.on('end', () => { if (!buffer) { return; } onLine?.(buffer); writer.write(`${prefix}${buffer}\n`); buffer = ''; }); } function attachWatchBuildLogging(child) { let rebuildStartedAt = null; let serviceWorkerBuildPending = false; const handleLine = (line) => { const normalized = line.replace(/\u001b\[[0-9;]*m/g, '').trim(); if (!normalized) { return; } if (normalized.startsWith('Building src/sw.js service worker')) { serviceWorkerBuildPending = true; return; } if (normalized.includes('building client environment for production')) { if (serviceWorkerBuildPending) { serviceWorkerBuildPending = false; return; } rebuildStartedAt = Date.now(); log('watch rebuild started'); return; } const builtMatch = normalized.match(/^built in (\d+)ms\.$/u); if (!builtMatch) { return; } const reportedMs = Number.parseInt(builtMatch[1], 10); const wallMs = rebuildStartedAt === null ? null : Date.now() - rebuildStartedAt; const durationText = wallMs === null ? `vite=${reportedMs}ms` : `vite=${reportedMs}ms, wall=${wallMs}ms`; log(`watch rebuild completed (${durationText})`); rebuildStartedAt = null; }; pipePrefixedLines(child.stdout, process.stdout, '[preview:watch:vite] ', handleLine); pipePrefixedLines(child.stderr, process.stderr, '[preview:watch:vite] ', handleLine); } function waitForExit(child) { return new Promise((resolveExit, rejectExit) => { child.once('error', rejectExit); child.once('exit', (code, signal) => { if (code === 0) { resolveExit(); return; } rejectExit(new Error(`process exited with code ${code ?? 'null'}${signal ? ` (signal: ${signal})` : ''}`)); }); }); } function terminateChild(child, signal = 'SIGTERM') { if (child.killed) { return; } try { child.kill(signal); } catch { // Ignore termination races during shutdown. } } const backgroundChildren = []; let shuttingDown = false; function shutdown(exitCode = 0) { if (shuttingDown) { return; } shuttingDown = true; backgroundChildren.forEach((child) => terminateChild(child)); setTimeout(() => { backgroundChildren.forEach((child) => terminateChild(child, 'SIGKILL')); process.exit(exitCode); }, 5_000).unref(); } process.on('SIGINT', () => shutdown(0)); process.on('SIGTERM', () => shutdown(0)); try { log('initial build started'); await waitForExit(runNodeScript([viteBin, 'build', '--outDir', appDistDir], buildEnv)); log('initial build completed'); const watchBuild = runNodeScript([viteBin, 'build', '--watch', '--clearScreen', 'false', '--outDir', appDistDir], buildEnv, { stdio: ['inherit', 'pipe', 'pipe'], }); const previewServer = runNodeScript([serveScript], serveEnv); backgroundChildren.push(watchBuild, previewServer); attachWatchBuildLogging(watchBuild); log('preview server and build watcher are running'); watchBuild.once('exit', (code, signal) => { if (shuttingDown) { return; } logError(`build watcher stopped: code=${code ?? 'null'} signal=${signal ?? 'none'}`); shutdown(code && code > 0 ? code : 1); }); previewServer.once('exit', (code, signal) => { if (shuttingDown) { return; } logError(`preview server stopped: code=${code ?? 'null'} signal=${signal ?? 'none'}`); shutdown(code && code > 0 ? code : 1); }); } catch (error) { logError('failed to prepare preview build'); console.error(error instanceof Error ? error.message : String(error)); process.exit(1); }