// Builds the app against the mock CMS and serves the standalone output, // reproducing the Dockerfile's layout (server.js + .next/static + public). import { spawn, type ChildProcess } from "node:child_process"; import { cpSync, existsSync, rmSync } from "node:fs"; import http from "node:http"; import type { AddressInfo } from "node:net"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import type { TestProject } from "vitest/node"; import { startMockCms } from "./mock-cms"; const WEB = fileURLToPath(new URL("../..", import.meta.url)); declare module "vitest" { interface ProvidedContext { appUrl: string; mockUrl: string; } } async function getFreePort(): Promise { return new Promise((resolve, reject) => { const srv = http.createServer(); srv.listen(0, "127.0.0.1", () => { const { port } = srv.address() as AddressInfo; srv.close((err) => (err ? reject(err) : resolve(port))); }); }); } async function waitForOk(url: string, timeoutMs: number): Promise { const deadline = Date.now() + timeoutMs; let lastError: unknown = null; while (Date.now() < deadline) { try { const res = await fetch(url); if (res.ok) return; lastError = new Error(`GET ${url} -> ${res.status}`); } catch (err) { lastError = err; } await new Promise((r) => setTimeout(r, 250)); } throw new Error(`timed out waiting for ${url}: ${lastError}`); } export default async function setup(project: TestProject) { const mock = await startMockCms(); const appPort = await getFreePort(); const appUrl = `http://127.0.0.1:${appPort}`; const env = { ...process.env, // process env beats web/.env, steering the build away from cms.neuf.no WAGTAIL_BASE_URL: mock.url, // production builds refuse local-IP image sources without this override SMOKE_ALLOW_LOCAL_IMAGES: "1", REVALIDATE_WEBHOOK_SECRET: "smoke-secret", URL: appUrl, NEXT_TELEMETRY_DISABLED: "1", }; rmSync(join(WEB, ".next", "standalone"), { recursive: true, force: true }); // async spawn: the mock CMS lives in this process, so the event loop must // stay free to answer the build's GraphQL requests await new Promise((resolve, reject) => { const build = spawn("node", ["node_modules/next/dist/bin/next", "build"], { cwd: WEB, env, stdio: "inherit", }); build.on("error", reject); build.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`next build exited with code ${code}`)) ); }); if (mock.graphqlRequests === 0) { throw new Error( "next build made no GraphQL requests to the mock CMS — is WAGTAIL_BASE_URL being overridden?" ); } // mirror the Dockerfile: standalone lacks static assets and public/ const standalone = join(WEB, ".next", "standalone"); if (!existsSync(join(standalone, "server.js"))) { throw new Error(`no server.js in ${standalone} — standalone layout changed?`); } cpSync(join(WEB, ".next", "static"), join(standalone, ".next", "static"), { recursive: true, }); cpSync(join(WEB, "public"), join(standalone, "public"), { recursive: true }); const app: ChildProcess = spawn("node", ["server.js"], { cwd: standalone, env: { ...env, PORT: String(appPort), HOSTNAME: "127.0.0.1" }, stdio: "inherit", }); await waitForOk(`${appUrl}/`, 60_000); project.provide("appUrl", appUrl); project.provide("mockUrl", mock.url); return async () => { app.kill(); await mock.close(); }; }