186 lines
4.8 KiB
JavaScript
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);
|
|
}
|