import { spawn, ChildProcess, execSync } from 'child_process'; import { resolve, dirname } from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const ROOT_DIR = resolve(__dirname, '..'); const SERVER_DIR = resolve(ROOT_DIR, 'server'); const CLIENT_DIR = resolve(ROOT_DIR, 'client'); export interface ServerProcesses { backend: ChildProcess | null; frontend: ChildProcess | null; } let processes: ServerProcesses = { backend: null, frontend: null, }; function waitForServer(url: string, timeout = 30000): Promise { const start = Date.now(); // Allow self-signed certificates for HTTPS const originalTlsReject = process.env.NODE_TLS_REJECT_UNAUTHORIZED; if (url.startsWith('https://')) { process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; } return new Promise((resolve, reject) => { const cleanup = () => { if (originalTlsReject !== undefined) { process.env.NODE_TLS_REJECT_UNAUTHORIZED = originalTlsReject; } else { delete process.env.NODE_TLS_REJECT_UNAUTHORIZED; } }; const check = async () => { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 2000); const response = await fetch(url, { signal: controller.signal }); clearTimeout(timeoutId); if (response.ok) { // Add small delay to ensure server is fully ready await new Promise((r) => setTimeout(r, 500)); cleanup(); resolve(); return; } } catch { // Server not ready yet } if (Date.now() - start > timeout) { cleanup(); reject(new Error(`Server at ${url} did not start within ${timeout}ms`)); return; } setTimeout(check, 500); }; check(); }); } export async function startBackend(port = 3000): Promise { console.log('Starting backend server...'); // Use a test database to avoid polluting the main one const testDbPath = resolve(SERVER_DIR, 'test-e2e.db'); // Remove old test database try { execSync(`rm -f ${testDbPath}`); } catch { // Ignore if doesn't exist } const backend = spawn('clj', ['-M:run'], { cwd: SERVER_DIR, env: { ...process.env, SPICEFLOW_PORT: String(port), SPICEFLOW_DB: testDbPath, }, stdio: ['pipe', 'pipe', 'pipe'], }); backend.stdout?.on('data', (data) => { console.log(`[backend] ${data.toString().trim()}`); }); backend.stderr?.on('data', (data) => { console.error(`[backend] ${data.toString().trim()}`); }); processes.backend = backend; await waitForServer(`http://localhost:${port}/api/health`); console.log('Backend server ready'); return backend; } export async function startFrontend(port = 5173, backendPort = 3000): Promise { console.log('Starting frontend server...'); const frontend = spawn('npm', ['run', 'dev', '--', '--port', String(port)], { cwd: CLIENT_DIR, env: { ...process.env, VITE_BACKEND_PORT: String(backendPort), }, stdio: ['pipe', 'pipe', 'pipe'], }); frontend.stdout?.on('data', (data) => { console.log(`[frontend] ${data.toString().trim()}`); }); frontend.stderr?.on('data', (data) => { console.error(`[frontend] ${data.toString().trim()}`); }); processes.frontend = frontend; await waitForServer(`https://localhost:${port}`); console.log('Frontend server ready'); return frontend; } export async function startServers(backendPort = 3000, frontendPort = 5173): Promise { await startBackend(backendPort); await startFrontend(frontendPort, backendPort); return processes; } export function stopServers(): void { console.log('Stopping servers...'); if (processes.frontend) { processes.frontend.kill('SIGTERM'); processes.frontend = null; } if (processes.backend) { processes.backend.kill('SIGTERM'); processes.backend = null; } console.log('Servers stopped'); } export function getProcesses(): ServerProcesses { return processes; }