import {spawn} from 'node:child_process'; import {existsSync, readFileSync} from 'node:fs'; import path from 'node:path'; import {fileURLToPath} from 'node:url'; const repoRoot = fileURLToPath(new URL('../', import.meta.url)); const serverRoot = fileURLToPath(new URL('../server-node/', import.meta.url)); const viteCliPath = fileURLToPath(new URL('./vite-cli.mjs', import.meta.url)); const envExamplePath = fileURLToPath(new URL('../.env.example', import.meta.url)); const envLocalPath = fileURLToPath(new URL('../.env.local', import.meta.url)); const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'; function parseEnvContents(contents) { return contents .split(/\r?\n/u) .reduce((envMap, rawLine) => { const line = rawLine.trim(); if (!line || line.startsWith('#')) { return envMap; } const separatorIndex = line.indexOf('='); if (separatorIndex < 0) { return envMap; } const key = line.slice(0, separatorIndex).trim(); let value = line.slice(separatorIndex + 1).trim(); if ( (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) ) { value = value.slice(1, -1); } envMap[key] = value; return envMap; }, {}); } function readEnvFile(filePath) { if (!existsSync(filePath)) { return {}; } return parseEnvContents(readFileSync(filePath, 'utf8')); } function resolveServerTarget(serverAddr) { const trimmed = serverAddr.trim(); if (!trimmed) { return 'http://127.0.0.1:8081'; } if (/^https?:\/\//u.test(trimmed)) { try { const url = new URL(trimmed); if (url.hostname === '0.0.0.0') { url.hostname = '127.0.0.1'; } return url.toString().replace(/\/$/u, ''); } catch { return trimmed.replace(/\/$/u, ''); } } if (trimmed.startsWith(':')) { return `http://127.0.0.1${trimmed}`; } if (trimmed.startsWith('0.0.0.0:')) { return `http://127.0.0.1:${trimmed.slice('0.0.0.0:'.length)}`; } return `http://${trimmed}`; } const mergedEnv = { ...readEnvFile(envExamplePath), ...readEnvFile(envLocalPath), ...process.env, }; mergedEnv.PROJECT_ROOT = mergedEnv.PROJECT_ROOT || repoRoot; mergedEnv.NODE_SERVER_ADDR = mergedEnv.NODE_SERVER_ADDR || ':8081'; mergedEnv.NODE_SERVER_TARGET = mergedEnv.NODE_SERVER_TARGET || resolveServerTarget(mergedEnv.NODE_SERVER_ADDR); mergedEnv.SQLITE_PATH = mergedEnv.SQLITE_PATH || path.join(repoRoot, 'server-node', 'data', 'genarrative.sqlite'); console.log(`[dev:node] PROJECT_ROOT=${mergedEnv.PROJECT_ROOT}`); console.log(`[dev:node] NODE_SERVER_ADDR=${mergedEnv.NODE_SERVER_ADDR}`); console.log(`[dev:node] NODE_SERVER_TARGET=${mergedEnv.NODE_SERVER_TARGET}`); console.log(`[dev:node] SQLITE_PATH=${mergedEnv.SQLITE_PATH}`); const children = new Set(); let shuttingDown = false; let pendingExitCode = 0; function stopChild(child) { if (!child || child.exitCode !== null) { return; } child.kill('SIGTERM'); setTimeout(() => { if (child.exitCode === null) { child.kill('SIGKILL'); } }, 2000).unref(); } function stopAllChildren() { for (const child of children) { stopChild(child); } } function finalizeExit(code = 0) { pendingExitCode = code; if (children.size === 0) { process.exit(pendingExitCode); } } function requestShutdown(code = 0) { if (!shuttingDown) { shuttingDown = true; pendingExitCode = code; stopAllChildren(); } finalizeExit(pendingExitCode); } function registerChild(name, child, siblingProvider) { children.add(child); child.on('error', (error) => { console.error(`[dev:node] ${name} failed to start`, error); requestShutdown(1); }); child.on('exit', (code, signal) => { children.delete(child); if (!shuttingDown) { const resolvedExitCode = code ?? 1; const signalSuffix = signal ? ` (${signal})` : ''; console.error( `[dev:node] ${name} exited with code ${resolvedExitCode}${signalSuffix}`, ); const sibling = siblingProvider(); if (sibling) { stopChild(sibling); } requestShutdown(resolvedExitCode); return; } finalizeExit(pendingExitCode); }); } const serverProcess = spawn(npmCommand, ['run', 'dev'], { cwd: serverRoot, env: mergedEnv, shell: process.platform === 'win32', stdio: 'inherit', }); const viteProcess = spawn( process.execPath, [viteCliPath, '--port=3000', '--host=0.0.0.0'], { cwd: repoRoot, env: mergedEnv, stdio: 'inherit', }, ); registerChild('node server', serverProcess, () => viteProcess); registerChild('vite dev server', viteProcess, () => serverProcess); process.on('SIGINT', () => { console.log('[dev:node] received SIGINT, shutting down...'); requestShutdown(0); }); process.on('SIGTERM', () => { console.log('[dev:node] received SIGTERM, shutting down...'); requestShutdown(0); });