114 lines
3.5 KiB
TypeScript
114 lines
3.5 KiB
TypeScript
// 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<number> {
|
|
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<void> {
|
|
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<void>((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();
|
|
};
|
|
}
|