// THE CITED FIXTURE SITE, BUILT FOR REAL. // // The report-site suite (playwright.report.config.ts) runs against a real // static export of a cited site, served like a deployed one (serve.mjs), so // what it checks is what ships: the routes Next emits, the files it copies, // the requests the built pages make. The same out/ is held to the cited // out/ allowlist audit (lib/builtExport.ts) by audit.spec.ts. // // THE STAGE. `next build` copies the public/ BESIDE ITS PROJECT into out/, and // export/public (and export/out) are the checkout's own — in the primary // checkout, the operator's real site build. So the build never runs in // export/: it runs in a stage project at the repo root (`.e2e-report-site/`, // gitignored) holding a copy of export/app and export's config files, a link to // export's node_modules, and its own public/. The stage sits at the same depth // as export/, so every relative path in the copies (globals.css's // `../../common/…`, tsconfig's `../tsconfig.base.json`, next.config's // workspace root) resolves exactly as it does there. // // THE PUBLIC DIR is what compose's reports stage would write for the site: // - the fixture report site (export/fixtures/report-site/public): the view // JSON R4's builder made, the stills, the post's shot, the evidence clips; // - the citation downloads, the report's exports (report.html, report.pdf, // report.md, evidence-pack.zip) and the cited site's contract (site.json, // corpus.json, llms.txt, robots.txt, sitemap.xml, _headers), written by // compose's and the export step's own functions in a child process // (contract.ts); // - export/public's checked-in assets (git-tracked files only — the // checkout's composed corpus is never read). // // Every server-side read points into the stage too (TRANSCRIPTS_DIR, // SITES_DIR, SETTINGS_FILE, EXPORT_*): the build never sees a real corpus. import { execFileSync } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { builtBundleProblem } from "yt-dlp-transcript-common/lib/builtExport"; const HERE = path.dirname(fileURLToPath(import.meta.url)); const EXPORT_DIR = path.resolve(HERE, ".."); const REPO = path.resolve(EXPORT_DIR, ".."); export const REPORT_SITE_ID = "reportsite"; export const REPORT_SITE_STAGE = path.join(REPO, ".e2e-report-site"); export const REPORT_SITE_OUT = path.join(REPORT_SITE_STAGE, "out"); export const REPORT_SITE_SITES_DIR = path.join(HERE, "fixtures", "sites"); const FIXTURE_PUBLIC = path.join(EXPORT_DIR, "fixtures", "report-site", "public"); // export's files the stage project copies, beside app/ less its tests (the // build type-checks what it holds, and a test's relative imports reach outside // app/). The changelog page reads CHANGELOG.md from the project's cwd. const PROJECT_FILES = ["next.config.ts", "tsconfig.json", "postcss.config.mjs", "package.json", "CHANGELOG.md"]; // The environment every stage step runs with. export function reportSiteEnv(base: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv { const env: NodeJS.ProcessEnv = { ...base, NODE_ENV: "production", SITE_ID: REPORT_SITE_ID, SITES_DIR: REPORT_SITE_SITES_DIR, SETTINGS_FILE: path.join(REPORT_SITE_STAGE, "settings.json"), TRANSCRIPTS_DIR: path.join(REPORT_SITE_STAGE, "transcripts"), EXPORT_PUBLIC_DIR: path.join(REPORT_SITE_STAGE, "public"), EXPORT_INDEX_DIR: path.join(REPORT_SITE_STAGE, ".export-index"), EXPORT_BUILDS_DIR: path.join(REPORT_SITE_STAGE, ".export-builds"), NEXT_TELEMETRY_DISABLED: "1", }; delete env.INSTANCE_MODE; return env; } function copyApp(): void { fs.cpSync(path.join(EXPORT_DIR, "app"), path.join(REPORT_SITE_STAGE, "app"), { recursive: true, filter: (src) => !/\.test\.tsx?$/.test(src), }); for (const f of PROJECT_FILES) { fs.copyFileSync(path.join(EXPORT_DIR, f), path.join(REPORT_SITE_STAGE, f)); } fs.symlinkSync(path.join(EXPORT_DIR, "node_modules"), path.join(REPORT_SITE_STAGE, "node_modules"), "dir"); } function stagePublic(): void { const pub = path.join(REPORT_SITE_STAGE, "public"); const tracked = execFileSync("git", ["ls-files", "-z", "--", "public"], { cwd: EXPORT_DIR, encoding: "utf8" }) .split("\0") .filter(Boolean); for (const rel of tracked) { const dest = path.join(REPORT_SITE_STAGE, rel); fs.mkdirSync(path.dirname(dest), { recursive: true }); fs.copyFileSync(path.join(EXPORT_DIR, rel), dest); } fs.cpSync(FIXTURE_PUBLIC, pub, { recursive: true }); } function run(command: string, args: string[], cwd: string): void { execFileSync(command, args, { cwd, env: reportSiteEnv(), stdio: "inherit" }); } /** * Stage, compose and build the cited fixture site; returns its out/. * `reuse` keeps a stage that already holds a build (E2E_REPORT_SITE_REUSE=1, * for iterating on the specs alone — it does not see a change to the app). */ export function stageReportSite(opts: { reuse?: boolean } = {}): string { if (opts.reuse && fs.existsSync(path.join(REPORT_SITE_OUT, "corpus.json"))) return REPORT_SITE_OUT; fs.rmSync(REPORT_SITE_STAGE, { recursive: true, force: true }); fs.mkdirSync(path.join(REPORT_SITE_STAGE, "transcripts"), { recursive: true }); fs.writeFileSync(path.join(REPORT_SITE_STAGE, "settings.json"), "{}\n"); copyApp(); stagePublic(); const bin = path.join(EXPORT_DIR, "node_modules", ".bin"); run(path.join(bin, "tsx"), [path.join(HERE, "contract.ts")], EXPORT_DIR); run(path.join(bin, "next"), ["build"], REPORT_SITE_STAGE); return REPORT_SITE_OUT; } // The audit every deploy path asks of a bundle (builtBundleProblem: identity, // then the cited allowlist and Pages' limits), of the staged build. export function reportSiteAuditProblem(outDir = REPORT_SITE_OUT): string | null { return builtBundleProblem(outDir, REPORT_SITE_ID); } // `tsx e2e-report/stage.ts`: stage and build the site, then audit it. if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { const out = stageReportSite({ reuse: process.argv.includes("--reuse") }); const problem = reportSiteAuditProblem(out); console.log(problem ? `[report-site] AUDIT REFUSES: ${problem}` : `[report-site] ${out} passes the cited audit`); process.exitCode = problem ? 1 : 0; }