import { createServer, type Server } from 'node:http'; import { readFile, stat } from 'node:fs/promises'; import { extname, join, resolve, sep } from 'node:path'; // Zero-dependency static file server for the mocked-backend CI driver. // // It replaces `frontend/scripts/e2e-server.js` (express-based) on purpose: // that script resolves `express`/`compression` from `frontend/node_modules`, // which the CI jobs never install (they only run `pnpm install` inside // `plugins/` and restore the prebuilt bundle), so the driver crashed with // ERR_MODULE_NOT_FOUND and timed out waiting for localhost:3000. Serving the // bundle from here keeps the suite runnable with only `plugins/` // dependencies — the documented local workflow — and identical in CI. // // NOTE on provenance: this file is duplicated in // `plugins/apps/composable-test-suite/ci/static-server.ts`. Keep the two in // sync (same as the mock harness in `run-ci.ts`). const MIME_TYPES: Record = { '.css': 'text/css; charset=utf-8', '.gif': 'image/gif', '.html': 'text/html; charset=utf-8', '.ico': 'image/x-icon', '.jpeg': 'image/jpeg', '.jpg': 'image/jpeg', '.js': 'application/javascript; charset=utf-8', '.json': 'application/json; charset=utf-8', '.map': 'application/json; charset=utf-8', '.mjs': 'application/javascript; charset=utf-8', '.otf': 'font/otf', '.png': 'image/png', '.svg': 'image/svg+xml', '.ttf': 'font/ttf', '.txt': 'text/plain; charset=utf-8', '.wasm': 'application/wasm', '.webmanifest': 'application/manifest+json', '.webp': 'image/webp', '.woff': 'font/woff', '.woff2': 'font/woff2', '.xml': 'application/xml; charset=utf-8', }; const INDEX = 'index.html'; export interface StaticServer { /** Base URL the server listens on (e.g. `http://localhost:3000`). */ url: string; /** Stop accepting connections; safe to call more than once. */ close: () => Promise; } /** * Serve `root` over HTTP on `port` (`0` picks a free port, reported in * `url`). Directory requests fall back to `index.html`; the app uses hash * routing, so no other fallback is needed. */ export function startStaticServer( root: string, port: number, ): Promise { const docRoot = resolve(root); const server: Server = createServer(async (req, res) => { try { if (req.method !== 'GET' && req.method !== 'HEAD') { res.writeHead(405, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end('Method Not Allowed'); return; } const rawPath = (req.url ?? '/').split(/[?#]/, 1)[0] ?? '/'; let pathname: string; try { pathname = decodeURIComponent(rawPath); } catch { res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end('Bad Request'); return; } const resolved = resolve(docRoot, `.${sep}${pathname}`); if (resolved !== docRoot && !resolved.startsWith(docRoot + sep)) { res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end('Forbidden'); return; } let filePath = resolved; const info = await stat(filePath).catch((error: unknown) => { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null; throw error; }); if (info === null) { res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end('Not Found'); return; } if (info.isDirectory()) { filePath = join(filePath, INDEX); } const body = await readFile(filePath).catch((error: unknown) => { if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null; throw error; }); if (body === null) { res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end('Not Found'); return; } const contentType = MIME_TYPES[extname(filePath).toLowerCase()] ?? 'application/octet-stream'; res.writeHead(200, { 'Content-Type': contentType, 'Content-Length': body.length, }); res.end(req.method === 'GET' ? body : undefined); } catch { if (!res.headersSent) { res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' }); } res.end('Internal Server Error'); } }); return new Promise((fulfill, reject) => { server.once('error', reject); server.listen(port, '0.0.0.0', () => { server.off('error', reject); const address = server.address(); const actualPort = typeof address === 'object' && address !== null ? address.port : port; fulfill({ url: `http://localhost:${actualPort}`, close: () => new Promise((done, fail) => { if (!server.listening) { done(); return; } server.close((error) => (error ? fail(error) : done())); }), }); }); }); }