import { execSync } from "node:child_process"; import { cpSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import path from "node:path"; // Global setup for the REAL two-origin test. Unlike the route-mocked e2e-hub // suite, this stands up two genuine static origins so the browser's real // same-origin / CORS enforcement and the prod service worker are exercised: // // Origin B (.2origin/originB) — a tiny hand-authored single-site archive: the // federation JSON contract (site.json + summaries + transcripts + subs) // plus a serve.json that sends Access-Control-Allow-Origin: * so a // cross-origin hub can read it. No Next build needed — the hub only ever // fetches Origin B's JSON, never its HTML. // Origin A (.2origin/hubA) — a real INSTANCE_MODE=hub production build (so its // service worker registers), with hub-sites.json emptied so only Origin B // is in play. // // Both are served by `serve` in the config's webServer array. const STAGE = path.resolve(process.cwd(), ".2origin"); const ORIGIN_B = path.join(STAGE, "originB"); const HUB_A = path.join(STAGE, "hubA"); const GEN = "2026-01-01T00:00:00.000Z"; function writeJson(rel: string, value: unknown) { const p = path.join(ORIGIN_B, rel); mkdirSync(path.dirname(p), { recursive: true }); writeFileSync(p, JSON.stringify(value)); } function buildOriginB() { rmSync(ORIGIN_B, { recursive: true, force: true }); writeJson("site.json", { contract: 1, siteId: "originb", siteTitle: "Origin B Archive", siteDescription: "A real federated archive served over CORS.", headerTitle: "Origin B", homeTagline: "", accent: "#c026d3", siteUrl: "", pwa: false, socialLinks: [], groups: [{ id: "gb", name: "Origin B", selectedByDefault: true }], defaultGroupId: "gb", channels: [{ slug: "chan-b", name: "Bravo Channel", count: 2, groupId: "gb" }], generatedAt: GEN, summariesVersion: 3, }); writeJson("summaries/manifest.json", { version: 3, totalCount: 2, pageSize: 1000, pageCount: 1, generatedAt: GEN, channels: [{ name: "Bravo Channel", count: 2, slug: "chan-b", groupId: "gb" }], groups: [{ id: "gb", name: "Origin B", selectedByDefault: true }], defaultGroupId: "gb", siteId: "originb", }); const sum = (id: string, title: string) => ({ slug: `chan-b/${id}`, id, channelSlug: "chan-b", title, uploadDate: "20260101", date: "2026-01-01", duration: "5:00", channel: "Bravo Channel", isLivestream: false, ageRestricted: false, isDeleted: false, isUnlisted: false, platform: "youtube", webpageUrl: `https://example.com/${id}`, }); writeJson("summaries/page-0000.json", [ sum("v1", "The Hydrogen Atom"), sum("v2", "Helium and Beyond"), ]); writeJson("transcripts/chan-b/manifest.json", { version: 1, channelSlug: "chan-b", pageCount: 1, maxPageBytes: 8388608, generatedAt: GEN, slugToPage: { v1: 0, v2: 0 }, }); const det = (id: string, title: string, text: string) => ({ slug: `chan-b/${id}`, id, channelSlug: "chan-b", title, uploadDate: "20260101", duration: 300, channel: "Bravo Channel", description: "", tags: [], isLivestream: false, ageRestricted: false, platform: "youtube", webpageUrl: `https://example.com/${id}`, cues: [{ start: 0, end: 5, text }], }); writeJson("transcripts/chan-b/page-0000.json", [ det("v1", "The Hydrogen Atom", "the hydrogen atom has one proton"), det("v2", "Helium and Beyond", "helium is a noble gas"), ]); writeJson("subs/manifest.json", { version: 4, channels: [], totalCount: 0, liveChatTotalCount: 0, generatedAt: GEN, }); // CORS on all JSON so a cross-origin hub can read this origin. writeJson("serve.json", { headers: [ { source: "**/*.json", headers: [{ key: "Access-Control-Allow-Origin", value: "*" }], }, ], }); } function buildHub() { // A real hub production build (SW registers only in prod). Skip the rebuild // when a stage already exists unless E2E_TWO_ORIGIN_REBUILD=1 — the build is the // slow part and the hub bundle rarely changes between local runs. if (process.env.E2E_TWO_ORIGIN_REBUILD === "1") { rmSync(HUB_A, { recursive: true, force: true }); } try { // Cheap existence probe: if hubA/sw.js is missing, (re)build. // eslint-disable-next-line @typescript-eslint/no-require-imports require("node:fs").accessSync(path.join(HUB_A, "sw.js")); return; } catch { /* needs build */ } execSync("pnpm run build:hub", { cwd: process.cwd(), stdio: "inherit" }); rmSync(HUB_A, { recursive: true, force: true }); cpSync(path.join(process.cwd(), "out"), HUB_A, { recursive: true }); // Only Origin B should be in play — drop the built-in prod pool (unreachable // in the test) so the shelf isn't cluttered with failing origins. writeFileSync(path.join(HUB_A, "hub-sites.json"), "[]"); } // Stage both origins. Invoked from playwright.2origin.config.ts at module load // (NOT as Playwright globalSetup) because the served dirs must exist before the // webServer array starts, and Playwright launches webServers before globalSetup. export function stageTwoOrigins() { mkdirSync(STAGE, { recursive: true }); buildOriginB(); buildHub(); }