Initial commit

This commit is contained in:
Samraaj Bath
2026-06-29 15:11:48 -07:00
commit 66dfdcc58d
404 changed files with 45970 additions and 0 deletions
+324
View File
@@ -0,0 +1,324 @@
import { createServer, type Server } from "node:http";
import { readFile } from "node:fs/promises";
import { existsSync, rmSync, cpSync, readFileSync, statSync, readdirSync } from "node:fs";
import { join, extname, normalize } from "node:path";
import { spawnSync } from "node:child_process";
import { chromium } from "playwright";
import { collectPage, type PageSnapshot } from "../capture/walker.js";
import { ensureDir, writeJSONCompact } from "../util/fsx.js";
const ESBUILD_SHIM =
"globalThis.__name = globalThis.__name || ((fn) => fn);" +
"globalThis.__defProp = globalThis.__defProp || Object.defineProperty;";
const VIEWPORT_HEIGHTS: Record<number, number> = { 375: 812, 768: 1024, 1280: 800, 1920: 1080 };
function viewportHeight(w: number): number { return VIEWPORT_HEIGHTS[w] ?? Math.round(w * 0.66); }
export type BuildResult = {
ok: boolean;
outDir: string | null;
stderr: string;
durationMs: number;
};
/** Build the generated app inside the shared harness (deps preinstalled). */
export function buildApp(appDir: string, harnessDir: string): BuildResult {
const t0 = Date.now();
const isVite = existsSync(join(appDir, "vite.config.ts")) || existsSync(join(appDir, "index.html"));
if (isVite) {
for (const entry of readdirSync(harnessDir, { withFileTypes: true })) {
if (entry.name === "node_modules" || entry.name === "package.json" || entry.name === "package-lock.json") continue;
rmSync(join(harnessDir, entry.name), { recursive: true, force: true });
}
for (const entry of readdirSync(appDir, { withFileTypes: true })) {
if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "package-lock.json" || entry.name === "package.json") continue;
cpSync(join(appDir, entry.name), join(harnessDir, entry.name), { recursive: true });
}
const res = spawnSync("./node_modules/.bin/vite", ["build"], {
cwd: harnessDir,
encoding: "utf8",
env: { ...process.env },
maxBuffer: 64 * 1024 * 1024,
timeout: 240000,
});
const outDir = join(harnessDir, "dist");
const ok = res.status === 0 && existsSync(join(outDir, "index.html"));
return { ok, outDir: ok ? outDir : null, stderr: (res.stderr || "") + (res.stdout || ""), durationMs: Date.now() - t0 };
}
for (const sub of ["src", "public", "next.config.mjs", "postcss.config.mjs", "tsconfig.json", "next-env.d.ts", "out", ".next/cache/webpack"]) {
const p = join(harnessDir, sub);
if (sub === ".next/cache/webpack") continue; // keep webpack cache for speed
if (existsSync(p)) rmSync(p, { recursive: true, force: true });
}
cpSync(join(appDir, "src"), join(harnessDir, "src"), { recursive: true });
if (existsSync(join(appDir, "public"))) cpSync(join(appDir, "public"), join(harnessDir, "public"), { recursive: true });
for (const f of ["next.config.mjs", "postcss.config.mjs", "tsconfig.json", "next-env.d.ts"]) {
if (existsSync(join(appDir, f))) cpSync(join(appDir, f), join(harnessDir, f));
}
const res = spawnSync("./node_modules/.bin/next", ["build"], {
cwd: harnessDir,
encoding: "utf8",
env: { ...process.env, NEXT_TELEMETRY_DISABLED: "1" },
maxBuffer: 64 * 1024 * 1024,
timeout: 240000,
});
const outDir = join(harnessDir, "out");
const ok = res.status === 0 && existsSync(join(outDir, "index.html"));
return { ok, outDir: ok ? outDir : null, stderr: (res.stderr || "") + (res.stdout || ""), durationMs: Date.now() - t0 };
}
const CONTENT_TYPES: Record<string, string> = {
".html": "text/html; charset=utf-8", ".js": "text/javascript", ".mjs": "text/javascript",
".css": "text/css", ".json": "application/json", ".png": "image/png", ".jpg": "image/jpeg",
".jpeg": "image/jpeg", ".webp": "image/webp", ".avif": "image/avif", ".gif": "image/gif",
".svg": "image/svg+xml", ".ico": "image/x-icon", ".woff2": "font/woff2", ".woff": "font/woff",
".ttf": "font/ttf", ".otf": "font/otf", ".mp4": "video/mp4", ".webm": "video/webm", ".txt": "text/plain",
};
// Bound concurrent file reads: a heavy clone requests hundreds of assets at once,
// which exhausts file descriptors (EMFILE) and made existing assets fail to serve
// (apple: 9/544). A small semaphore + retry keeps every real asset a clean 200.
let activeReads = 0;
const readQueue: Array<() => void> = [];
const MAX_READS = 48;
async function readLimited(p: string): Promise<Buffer> {
if (activeReads >= MAX_READS) await new Promise<void>((r) => readQueue.push(r));
activeReads++;
try {
for (let i = 0; ; i++) {
try { return await readFile(p); }
catch (e) {
const code = (e as NodeJS.ErrnoException).code;
if (code === "ENOENT" || i >= 4) throw e;
await new Promise((r) => setTimeout(r, 40 * (i + 1))); // EMFILE/EAGAIN: back off
}
}
} finally {
activeReads--;
readQueue.shift()?.();
}
}
export function serveStatic(rootDir: string): Promise<{ url: string; close: () => Promise<void> }> {
const server: Server = createServer(async (req, res) => {
try {
let urlPath = decodeURIComponent((req.url || "/").split("?")[0]!);
if (urlPath === "/") urlPath = "/index.html";
let filePath = join(rootDir, normalize(urlPath).replace(/^(\.\.[/\\])+/, ""));
// Prefer the `.html` export, then `index.html`. A multi-route export can have a
// route that is BOTH a page and a parent of child routes (e.g. /generators is a
// listing AND /generators/x exists), producing `generators.html` next to a
// `generators/` dir — the directory must not shadow the page (reading it as a
// file throws EISDIR → 500).
if (existsSync(filePath + ".html")) {
filePath += ".html";
} else if (existsSync(filePath) && statSync(filePath).isDirectory()) {
const idx = join(filePath, "index.html");
if (existsSync(idx)) filePath = idx;
}
if (!existsSync(filePath) || statSync(filePath).isDirectory()) {
const fallback = join(rootDir, "404.html");
if (existsSync(fallback)) { res.writeHead(404, { "content-type": "text/html" }); res.end(await readLimited(fallback)); return; }
res.writeHead(404); res.end("not found"); return;
}
const data = await readLimited(filePath);
res.writeHead(200, { "content-type": CONTENT_TYPES[extname(filePath).toLowerCase()] ?? "application/octet-stream" });
res.end(data);
} catch {
res.writeHead(500); res.end("error");
}
});
return new Promise((resolve) => {
server.listen(0, "127.0.0.1", () => {
const addr = server.address();
const port = typeof addr === "object" && addr ? addr.port : 0;
resolve({
url: `http://127.0.0.1:${port}`,
close: () => new Promise<void>((r) => server.close(() => r())),
});
});
});
}
export type RenderResult = {
snapshots: Record<number, PageSnapshot>;
runtimeErrors: string[];
httpStatus: number;
failedResources: string[];
};
async function mapLimit<T, R>(items: T[], limit: number, fn: (item: T, i: number) => Promise<R>): Promise<R[]> {
const out: R[] = new Array(items.length);
let next = 0;
const worker = async (): Promise<void> => {
for (let i = next++; i < items.length; i = next++) out[i] = await fn(items[i]!, i);
};
await Promise.all(Array.from({ length: Math.max(1, Math.min(limit, items.length || 1)) }, worker));
return out;
}
/** Render the served static export at each viewport with the same walker used
* for capture, dumping DOM/computed/screenshots into the run's rendered/ dir. */
export async function renderApp(opts: {
url: string;
viewports: number[];
renderedDir: string;
concurrency?: number;
}): Promise<RenderResult> {
const browser = await chromium.launch({ headless: true, args: ["--disable-dev-shm-usage"] });
const snapshots: Record<number, PageSnapshot> = {};
try {
const results = await mapLimit(opts.viewports, opts.concurrency ?? 2, async (vw) => {
const vh = viewportHeight(vw);
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: vw, height: vh }, deviceScaleFactor: 1 });
const runtimeErrors: string[] = [];
const failedResources = new Set<string>();
let httpStatus = 0;
try {
const page = await ctx.newPage();
await page.addInitScript(ESBUILD_SHIM);
page.on("pageerror", (e) => runtimeErrors.push(String(e)));
page.on("response", (r) => { if (r.status() >= 400) failedResources.add(`${r.status()} ${r.url()}`); });
page.on("requestfailed", (r) => failedResources.add(`failed ${r.url()}`));
const resp = await page.goto(opts.url, { waitUntil: "networkidle", timeout: 45000 });
if (resp) httpStatus = resp.status();
try { await page.evaluate(() => (document as Document).fonts?.ready); } catch { /* ignore */ }
// Scroll to settle any lazy effects, then back to top.
await page.evaluate(async () => {
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
const max = document.documentElement.scrollHeight;
for (let y = 0; y < max; y += 800) { window.scrollTo(0, y); await sleep(30); }
window.scrollTo(0, 0); await sleep(120);
});
// Stage 5: the shipped clone REPLAYS motion on load (CSS @keyframes / WAAPI), but
// the gates grade the settled base. Cancel all running animations before the walk +
// screenshot so each element falls back to its emitted base CSS — i.e. exactly the
// source-captured values the static clone always reproduced. This keeps gates 06 +
// perceptual measuring the proven static frame (no motion regression); the motion
// gate separately drives an un-cancelled page to verify the animations actually run.
await page.evaluate(() => {
const w = window as unknown as { __dittoMotionStopped?: boolean; __dittoMotionStop?: () => void };
// Set the stopped flag FIRST so a not-yet-hydrated DittoMotion skips applying motion
// on mount (closes the hydration race that could leave reveal content hidden).
try { w.__dittoMotionStopped = true; } catch { /* ignore */ }
// Restore rotating-text to the captured word, reveal all reveals, stop timers/WAAPI.
try { w.__dittoMotionStop?.(); } catch { /* ignore */ }
// Cancel any remaining running animations (CSS @keyframes / WAAPI) so each element
// shows its emitted base CSS = the source-captured settled frame the gates expect.
try { for (const a of (document.getAnimations ? document.getAnimations() : [])) { try { a.cancel(); } catch { /* ignore */ } } } catch { /* ignore */ }
});
const snapshot = await page.evaluate(collectPage);
writeJSONCompact(join(opts.renderedDir, "dom", `dom-${vw}.json`), snapshot);
try {
await page.screenshot({ path: join(opts.renderedDir, "screenshots", `${vw}.png`), fullPage: true, timeout: 90_000, animations: "disabled" });
} catch { /* ignore */ }
return { viewport: vw, snapshot, runtimeErrors, failedResources: [...failedResources], httpStatus };
} finally {
await ctx.close();
}
});
const runtimeErrors = results.flatMap((r) => r.runtimeErrors);
const failedResources = new Set(results.flatMap((r) => r.failedResources));
for (const r of results) snapshots[r.viewport] = r.snapshot;
const non200 = results.find((r) => r.httpStatus && r.httpStatus !== 200);
const httpStatus = non200?.httpStatus ?? results.find((r) => r.httpStatus)?.httpStatus ?? 0;
ensureDir(join(opts.renderedDir, "computed"));
return { snapshots, runtimeErrors, httpStatus, failedResources: [...failedResources] };
} finally {
await browser.close();
}
}
/** Render the served clone at widths the grader never captured (between and beyond the
* captured band edges) and return a snapshot per width. Used by the responsive gate to
* measure behaviour where baked-px output stairsteps or off-centres — the blind spot that
* let non-fluid output pass every gate. Deliberately light: no screenshots, just one DOM
* walk per width, with motion cancelled exactly as renderApp does so we grade the settled
* frame. Never throws — a probe that fails to load is simply omitted. */
export async function measureProbeWidths(opts: { url: string; widths: number[] }): Promise<Record<number, PageSnapshot>> {
const out: Record<number, PageSnapshot> = {};
let browser;
try {
browser = await chromium.launch({ headless: true, args: ["--disable-dev-shm-usage"] });
} catch { return out; }
try {
for (const w of opts.widths) {
try {
const ctx = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: w, height: viewportHeight(w) }, deviceScaleFactor: 1 });
const page = await ctx.newPage();
await page.addInitScript(ESBUILD_SHIM);
await page.goto(opts.url, { waitUntil: "networkidle", timeout: 45000 });
try { await page.evaluate(() => (document as Document).fonts?.ready); } catch { /* ignore */ }
await page.evaluate(() => {
const win = window as unknown as { __dittoMotionStopped?: boolean; __dittoMotionStop?: () => void };
try { win.__dittoMotionStopped = true; } catch { /* ignore */ }
try { win.__dittoMotionStop?.(); } catch { /* ignore */ }
try { for (const a of (document.getAnimations ? document.getAnimations() : [])) { try { a.cancel(); } catch { /* ignore */ } } } catch { /* ignore */ }
});
out[w] = await page.evaluate(collectPage);
await ctx.close();
} catch { /* skip this width */ }
}
} finally {
await browser.close();
}
return out;
}
/** Generated asset references that still point to a remote origin (rubric Gate 2
* forbids these unless explicitly external-allowed). Scans rendered DOM attrs +
* computed background images. */
export function findRemoteRefs(snapshots: Record<number, PageSnapshot>): string[] {
const remote = new Set<string>();
const isRemote = (u: string): boolean =>
/^https?:\/\//.test(u) && !u.includes("127.0.0.1") && !u.includes("localhost");
for (const snap of Object.values(snapshots)) {
const walk = (node: PageSnapshot["root"]): void => {
for (const a of ["src", "poster"]) {
const v = node.attrs[a];
if (v && isRemote(v)) remote.add(v);
}
const srcset = node.attrs.srcset;
if (srcset) for (const part of srcset.split(",")) { const u = part.trim().split(/\s+/)[0]; if (u && isRemote(u)) remote.add(u); }
const bg = node.computed.backgroundImage;
if (bg) { const m = bg.match(/url\(['"]?([^'")]+)['"]?\)/g); if (m) for (const mm of m) { const u = mm.replace(/url\(['"]?|['"]?\)/g, ""); if (isRemote(u)) remote.add(u); } }
for (const c of node.children) if ((c as { text?: string }).text === undefined) walk(c as PageSnapshot["root"]);
};
walk(snap.root);
}
return [...remote];
}
/** Index a rendered snapshot's nodes by their data-cid for source↔generated alignment. */
export type GenNode = {
cid: string;
tag: string;
attrs: Record<string, string>;
computed: Record<string, string>;
bbox: { x: number; y: number; width: number; height: number };
visible: boolean;
text: string; // direct text content
};
export function indexByCid(snapshot: PageSnapshot): Map<string, GenNode> {
const map = new Map<string, GenNode>();
const walk = (node: PageSnapshot["root"]): void => {
const cid = node.attrs["data-cid"];
if (cid !== undefined) {
let directText = "";
for (const c of node.children) {
if ((c as { text?: string }).text !== undefined) directText += (c as { text: string }).text;
}
map.set(cid, {
cid, tag: node.tag, attrs: node.attrs, computed: node.computed,
bbox: node.bbox, visible: node.visible, text: directText,
});
}
for (const c of node.children) {
if ((c as { text?: string }).text === undefined) walk(c as PageSnapshot["root"]);
}
};
walk(snapshot.root);
return map;
}