// A tiny stand-in for the Wagtail CMS, reachable over HTTP so both // `next build` and the standalone server (separate child processes) can // query it. Serves the canned GraphQL responses from fixtures.ts, a 1x1 PNG // for image URLs, and a state endpoint the revalidation test uses to // "publish a change" in the CMS. import http from "node:http"; import type { AddressInfo } from "node:net"; import { makeFixtures, type Fixtures, type MockState } from "./fixtures"; // 1x1 transparent PNG const TINY_PNG = Buffer.from( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==", "base64" ); export type MockCms = { url: string; state: MockState; graphqlRequests: number; close(): Promise; }; function readBody(req: http.IncomingMessage): Promise { return new Promise((resolve, reject) => { const chunks: Buffer[] = []; req.on("data", (c) => chunks.push(c)); req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8"))); req.on("error", reject); }); } export async function startMockCms(): Promise { const state: MockState = { newsHeadlineVersion: "v1" }; let fixtures: Fixtures | null = null; // built once we know our own URL const mock: MockCms = { url: "", state, graphqlRequests: 0, close: () => new Promise((resolve, reject) => server.close((err) => (err ? reject(err) : resolve())) ), }; const server = http.createServer(async (req, res) => { try { if (req.method === "GET" && req.url?.startsWith("/media/")) { res.writeHead(200, { "content-type": "image/png", "content-length": TINY_PNG.length, }); res.end(TINY_PNG); return; } if (req.method === "POST" && req.url === "/__mock/state") { Object.assign(state, JSON.parse(await readBody(req))); res.writeHead(200, { "content-type": "application/json" }); res.end("{}"); return; } if (req.url?.startsWith("/api/graphql")) { mock.graphqlRequests += 1; // urql sends both POST (JSON body) and GET (URL params) requests let body: { query?: string; operationName?: string; variables?: Record; }; if (req.method === "GET") { const params = new URL(req.url, mock.url).searchParams; body = { query: params.get("query") ?? undefined, operationName: params.get("operationName") ?? undefined, variables: JSON.parse(params.get("variables") ?? "{}"), }; } else { body = JSON.parse(await readBody(req)); } const name = body.operationName ?? /(?:query|mutation)\s+(\w+)/.exec(body.query ?? "")?.[1]; const resolve = name ? fixtures?.[name] : undefined; res.writeHead(200, { "content-type": "application/json" }); if (!resolve) { // Loud gap detection: build-time queries throw on GraphQL errors, // so a missing fixture fails `next build` with the name visible. res.end( JSON.stringify({ errors: [{ message: `mock-cms: no fixture for operation ${name}` }], }) ); return; } res.end(JSON.stringify({ data: resolve(body.variables ?? {}, state) })); return; } console.error(`mock-cms: unhandled ${req.method} ${req.url}`); res.writeHead(404); res.end(); } catch (err) { res.writeHead(500, { "content-type": "application/json" }); res.end(JSON.stringify({ errors: [{ message: String(err) }] })); } }); await new Promise((resolve) => server.listen(0, "127.0.0.1", () => resolve()) ); const { port } = server.address() as AddressInfo; mock.url = `http://127.0.0.1:${port}`; fixtures = makeFixtures(mock.url); return mock; }