// Headless screenshot of the skin harness, so the Win7 look can be reviewed // visually during development rather than inferred from a clean build log. // // bun scripts/shot.ts [--dark] [--width 1200] [--height 1400] [--out path.png] // bun scripts/shot.ts --url http://localhost:4200 (any page, not just the harness) // // Firefox is used because it ships on this machine and has a built-in // --screenshot mode; no Chromium/puppeteer download is needed. Two constraints // it imposes: // - a DEDICATED PROFILE is mandatory. A running Firefox holds a lock on the // default profile and the headless run aborts with "Firefox is already // running, but is not responding". // - it renders with Gecko, NOT the WebKitGTK engine the app actually uses. // Good enough for skin fidelity (layout, colour, gradients, borders); // useless for engine-specific behaviour or native window transparency. // Real confirmation is still `bun run start`. // // The harness links the REAL hashed stylesheet from ui/dist (produced by the // normal `bun run build:ui`), so there is no second build pipeline to keep in // sync. // // The composed page is written INTO that same dist folder, and this is not // incidental: Firefox's security.fileuri.strict_origin_policy stops a file:// // document from loading a stylesheet that lives in a different directory. Host // the page elsewhere and it renders completely unstyled — which looks exactly // like "the skin is broken" rather than "the CSS never loaded". Keeping them // co-located also lets media/ fonts resolve. import { existsSync, mkdirSync, readdirSync, rmSync } from "node:fs"; import path from "node:path"; const ROOT = path.join(import.meta.dir, ".."); const DIST = path.join(ROOT, "ui", "dist", "ui", "browser"); const HARNESS = path.join(ROOT, "ui", "dev", "harness.html"); const WORK = path.join(ROOT, ".shot"); function arg(name: string): string | undefined { const i = process.argv.indexOf(`--${name}`); return i >= 0 ? process.argv[i + 1] : undefined; } const flag = (name: string) => process.argv.includes(`--${name}`); function fail(message: string): never { console.error(`[shot] ERROR: ${message}`); process.exit(1); } // ---- resolve the built stylesheet ---- if (!existsSync(DIST)) fail(`no build output at ${DIST} — run: bun run build:ui`); const styles = readdirSync(DIST).find((f) => f.startsWith("styles-") && f.endsWith(".css")); if (!styles) fail(`no styles-*.css in ${DIST} — run: bun run build:ui`); // ---- compose the page ---- mkdirSync(WORK, { recursive: true }); const profile = path.join(WORK, "profile"); mkdirSync(profile, { recursive: true }); const url = arg("url"); let target: string; if (url) { target = url; } else { const dark = flag("dark"); let html = await Bun.file(HARNESS).text(); // replaceAll, not replace: the harness documents its own {{STYLES}} token in // a header comment that precedes the , so a first-match replace // substitutes the comment and silently leaves the stylesheet unlinked. html = html.replaceAll("{{STYLES}}", styles); // relative: page sits next to it // The preload bakes these attributes in the real app; do it by hand here. if (dark) html = html.replace('