#!/usr/bin/env node /** * Minimal server for e2e runs: serves the built SPA (apps/web/dist) with * SPA fallback, proxies /api/* to the api process, and proxies the /collab * WebSocket to the collab process — mirroring what nginx + Caddy do in * production, without a dev server that may die under CI memory pressure. * Node builtins only. * * PORT=5173 API_TARGET=http://127.0.0.1:3001 COLLAB_TARGET=http://127.0.0.1:3002 \ * node scripts/e2e-static-server.mjs */ import { readFile } from 'node:fs/promises'; import { createServer, request as httpRequest } from 'node:http'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const DIST = path.join(path.dirname(fileURLToPath(import.meta.url)), '../apps/web/dist'); const PORT = Number(process.env.PORT ?? 5173); const API_TARGET = new URL(process.env.API_TARGET ?? 'http://127.0.0.1:3001'); const COLLAB_TARGET = new URL(process.env.COLLAB_TARGET ?? 'http://127.0.0.1:3002'); const MIME = { '.html': 'text/html; charset=utf-8', '.js': 'application/javascript', '.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', '.ico': 'image/x-icon', '.woff2': 'font/woff2', '.webmanifest': 'application/manifest+json', }; function proxyApi(req, res) { const upstream = httpRequest( { hostname: API_TARGET.hostname, port: API_TARGET.port, path: req.url, method: req.method, headers: { ...req.headers, host: `${API_TARGET.hostname}:${API_TARGET.port}` }, }, (upstreamRes) => { res.writeHead(upstreamRes.statusCode ?? 502, upstreamRes.headers); upstreamRes.pipe(res); }, ); upstream.on('error', () => { res.writeHead(502).end('api unreachable'); }); req.pipe(upstream); } async function serveStatic(req, res) { const urlPath = new URL(req.url, 'http://x').pathname; // Path traversal guard: resolve inside dist or fall back to the shell. const filePath = path.normalize(path.join(DIST, urlPath)); const target = filePath.startsWith(DIST) && path.extname(filePath) ? filePath : path.join(DIST, 'index.html'); try { const body = await readFile(target); res.writeHead(200, { 'Content-Type': MIME[path.extname(target)] ?? 'application/octet-stream', }); res.end(body); } catch { res.writeHead(404).end('not found'); } } const server = createServer((req, res) => { if (req.url.startsWith('/api/')) proxyApi(req, res); else void serveStatic(req, res); }); // Proxy the collab WebSocket (Caddy forwards /collab without stripping the // prefix in production; mirror that here so the editor connects the same way). server.on('upgrade', (req, socket, head) => { if (!req.url.startsWith('/collab')) { socket.destroy(); return; } // A WebSocket that is dropped abruptly (e.g. a test toggling the browser // offline, or a context closing) emits 'error' on these sockets. Without a // listener that becomes an uncaught exception that would crash this server — // so always handle it and just tear the pair down. socket.on('error', () => socket.destroy()); const upstream = httpRequest({ hostname: COLLAB_TARGET.hostname, port: COLLAB_TARGET.port, path: req.url, method: req.method, headers: { ...req.headers, host: `${COLLAB_TARGET.hostname}:${COLLAB_TARGET.port}` }, }); upstream.on('upgrade', (upstreamRes, upstreamSocket, upstreamHead) => { upstreamSocket.on('error', () => socket.destroy()); const statusLine = `HTTP/1.1 ${upstreamRes.statusCode} ${upstreamRes.statusMessage}\r\n`; const headerLines = Object.entries(upstreamRes.headers) .map(([key, value]) => `${key}: ${value}`) .join('\r\n'); socket.write(`${statusLine}${headerLines}\r\n\r\n`); if (upstreamHead?.length) socket.write(upstreamHead); upstreamSocket.pipe(socket); socket.pipe(upstreamSocket); }); upstream.on('error', () => socket.destroy()); if (head?.length) upstream.write(head); upstream.end(); }); // Last-resort guard: this is a throwaway test server, so a stray socket error // must never take it down mid-run and fail an unrelated later test pack. process.on('uncaughtException', (error) => { console.error('e2e static server: ignored uncaught error:', error?.message ?? error); }); server.listen(PORT, () => console.log( `e2e static server on :${PORT} → api ${API_TARGET.href} collab ${COLLAB_TARGET.href}`, ), );