Files
ai-code-app/scripts/preview-test-app-watch.mjs

186 lines
4.8 KiB
JavaScript

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);
}