import { createHash, randomUUID } from 'node:crypto'; import { mkdir, readFile, unlink, writeFile } from 'node:fs/promises'; import { spawn } from 'node:child_process'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { build, createServer } from 'vite'; const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const backendHealthUrl = 'http://127.0.0.1:4173/api/v1/health'; const backendEntry = resolve(projectRoot, 'dist-server/server.mjs'); const serverConfig = resolve(projectRoot, 'vite.server.config.ts'); const clientConfig = resolve(projectRoot, 'vite.client.config.ts'); const tscCli = resolve(projectRoot, 'node_modules/typescript/bin/tsc'); const runtimeStateFile = resolve(projectRoot, 'node_modules/.assessor-dev-runtime.json'); const developmentInstanceId = randomUUID(); let backendProcess; let clientServer; let serverBuildWatcher; let restartChain = Promise.resolve(); let shuttingDown = false; const delay = milliseconds => new Promise(resolveDelay => setTimeout(resolveDelay, milliseconds)); async function backendHealth() { try { const response = await fetch(backendHealthUrl, { method: 'GET' }); if (!response.ok) return undefined; const health = await response.json(); return health?.status === 'ok' && health?.application === 'ASSESSOR' ? health : undefined; } catch { return undefined; } } async function readRuntimeState() { try { return JSON.parse(await readFile(runtimeStateFile, 'utf8')); } catch { return undefined; } } async function writeRuntimeState(releaseId, processId) { await mkdir(dirname(runtimeStateFile), { recursive: true }); await writeFile(runtimeStateFile, JSON.stringify({ developmentInstanceId, releaseId, processId }), 'utf8'); } async function removeOwnedRuntimeState() { const state = await readRuntimeState(); if (state?.developmentInstanceId !== developmentInstanceId) return; try { await unlink(runtimeStateFile); } catch (error) { if (error?.code !== 'ENOENT') throw error; } } function runNode(args) { return new Promise((resolveRun, rejectRun) => { const child = spawn(process.execPath, args, { cwd: projectRoot, stdio: 'inherit' }); child.once('error', rejectRun); child.once('exit', (code, signal) => { if (code === 0) resolveRun(); else rejectRun(new Error('Command failed (' + (signal ?? code) + '): node ' + args.join(' '))); }); }); } async function releaseIdForBackend() { return createHash('sha256').update(await readFile(backendEntry)).digest('hex'); } async function waitForBackend(releaseId) { for (let attempt = 0; attempt < 80; attempt += 1) { const health = await backendHealth(); if (health?.releaseId === releaseId && health?.developmentInstanceId === developmentInstanceId) return; if (backendProcess && backendProcess.exitCode !== null) { throw new Error('Local backend exited before becoming healthy (code ' + backendProcess.exitCode + ').'); } await delay(250); } throw new Error('Local backend did not report the expected development release at ' + backendHealthUrl + '.'); } async function waitForBackendToStop() { for (let attempt = 0; attempt < 40; attempt += 1) { if (!await backendHealth()) return; await delay(100); } throw new Error('The previous local backend did not release port 4173 in time.'); } async function reclaimManagedBackend() { const health = await backendHealth(); if (!health) return; const state = await readRuntimeState(); const managed = health.developmentInstanceId && state?.developmentInstanceId === health.developmentInstanceId && state?.releaseId === health.releaseId && Number.isSafeInteger(state.processId) && state.processId > 0; if (!managed) { throw new Error('Port 4173 is occupied by an unmanaged or incompatible backend. Stop that one process once; subsequent development restarts are managed automatically.'); } console.log('[assessor] Reclaiming the previously managed development backend...'); try { process.kill(state.processId, 'SIGTERM'); } catch (error) { if (error?.code !== 'ESRCH') throw error; } await waitForBackendToStop(); } async function stopBackend() { const child = backendProcess; backendProcess = undefined; if (!child || child.exitCode !== null) return; const exited = new Promise(resolveExit => child.once('exit', resolveExit)); child.kill('SIGTERM'); await Promise.race([exited, delay(5_000)]); if (child.exitCode === null) { child.kill('SIGKILL'); await exited; } } async function startBackend(releaseId) { backendProcess = spawn(process.execPath, ['--env-file-if-exists=.env.postgres', backendEntry], { cwd: projectRoot, env: { ...process.env, ASSESSOR_HOST: '127.0.0.1', ASSESSOR_PORT: '4173', ASSESSOR_RELEASE_ID: releaseId, ASSESSOR_DEVELOPMENT_INSTANCE_ID: developmentInstanceId }, stdio: 'inherit' }); const child = backendProcess; child.once('error', error => { console.error('[assessor] Backend start failed:', error); void shutdown(1); }); child.once('exit', code => { if (backendProcess === child && !shuttingDown) { console.error('[assessor] Backend stopped unexpectedly (code ' + code + ').'); void shutdown(code ?? 1); } }); await writeRuntimeState(releaseId, child.pid); await waitForBackend(releaseId); } async function activateBuiltBackend() { const releaseId = await releaseIdForBackend(); const hadBackend = Boolean(await backendHealth()); await stopBackend(); if (hadBackend) await waitForBackendToStop(); await startBackend(releaseId); console.log('[assessor] Backend ready: ' + backendHealthUrl + ' ยท release ' + releaseId.slice(0, 12)); } async function startServerBuildWatcher() { return new Promise(async (resolveInitial, rejectInitial) => { let initialSettled = false; try { serverBuildWatcher = await build({ configFile: serverConfig, build: { watch: {} } }); serverBuildWatcher.on('event', event => { if (event.code === 'ERROR') { console.error('[assessor] Backend rebuild failed; the last healthy backend remains active.', event.error); if (!initialSettled) { initialSettled = true; rejectInitial(event.error); } return; } if (event.code !== 'END') return; restartChain = restartChain.then(activateBuiltBackend); restartChain.then(() => { if (!initialSettled) { initialSettled = true; resolveInitial(); } }).catch(error => { console.error('[assessor] Built backend could not be activated:', error); if (!initialSettled) { initialSettled = true; rejectInitial(error); } else void shutdown(1); }); }); } catch (error) { rejectInitial(error); } }); } async function shutdown(exitCode = 0) { if (shuttingDown) return; shuttingDown = true; await clientServer?.close(); await serverBuildWatcher?.close(); await stopBackend(); await removeOwnedRuntimeState(); process.exitCode = exitCode; } async function start() { await reclaimManagedBackend(); console.log('[assessor] Type-checking and watching the local backend...'); await runNode([tscCli, '-p', 'tsconfig.server.json', '--noEmit']); await startServerBuildWatcher(); clientServer = await createServer({ configFile: clientConfig, server: { host: '127.0.0.1', port: 5173, strictPort: true } }); await clientServer.listen(); clientServer.printUrls(); } process.once('SIGINT', () => void shutdown()); process.once('SIGTERM', () => void shutdown()); start().catch(error => { console.error('[assessor] Local development stack failed:', error); void shutdown(1); });