// Фаза A: структурный (DOM) аудит. Для каждого preview-маршрута снимает // нормализованное дерево элементов нашей страницы и соответствующего refs-html // файла, сравнивает по структурному пути (без IGNORE) и пишет отчёт о // расхождениях (added / removed / tagMismatch / textMismatch). // Запуск: pnpm refs-dom-audit (опции: --route --ref --no-serve --ours ) import { chromium } from "playwright"; import { spawn } from "node:child_process"; import { readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs"; import { pathToFileURL } from "node:url"; import { resolve, join } from "node:path"; const args = (() => { const a = {}; for (let i = 0; i < process.argv.length; i++) { const v = process.argv[i]; if (v === "--no-serve") a.noServe = true; else if (v === "--ours") a.ours = process.argv[++i]; else if (v === "--ref") a.ref = process.argv[++i]; else if (v === "--route") a.route = process.argv[++i]; } return a; })(); const WEB = process.cwd(); const PORT = 5179; const REFS_DIR = resolve(WEB, "../refs-html"); // Динамические сегменты: явный список значений + сопоставление id → файл рефа. const DYNAMIC = { "tools/[id]": { ids: ["linear-gradient-png", "remove-background-png"], refFor: (id) => id === "linear-gradient-png" ? "gradient.html" : id === "remove-background-png" ? "background-remover.html" : null, }, }; // Маршруты без релевантного рефа (refs-html/index.html — зеркало demo из Next, не реф). const EXCLUDE = new Set(["/preview"]); const refNameFor = (route) => { if (route === "/preview") return "index.html"; const last = route.split("/").filter(Boolean).pop(); return `${last}.html`; }; function discoverRoutes() { const base = resolve(WEB, "src/routes/preview"); const out = []; const walk = (dir) => { let ents; try { ents = readdirSync(dir, { withFileTypes: true }); } catch { return; } for (const e of ents) { const p = join(dir, e.name); if (e.isDirectory()) walk(p); else if (e.name === "+page.svelte") { const rel = p.slice(base.length).replace(/\\/g, "/"); const parts = rel.split("/").filter(Boolean); parts.pop(); out.push(parts.length ? `/preview/${parts.join("/")}` : "/preview"); } } }; walk(base); return out; } function refsList() { try { return readdirSync(REFS_DIR).filter((f) => f.endsWith(".html")); } catch { return []; } } function refExists(name) { return refsList().includes(name); } function buildTargets() { if (args.route) { const ref = args.ref || refNameFor(args.route); return [{ route: args.route, ref }]; } const targets = []; const seen = new Set(); const add = (route, ref) => { if (!ref || seen.has(route)) return; if (!refExists(ref)) { console.warn(`[dom-audit] ref not found, skipped: ${route} -> ${ref}`); return; } seen.add(route); targets.push({ route, ref }); }; for (const route of discoverRoutes()) { if (route.includes("[")) continue; if (EXCLUDE.has(route)) { console.log(`[dom-audit] excluded (no relevant ref): ${route}`); continue; } add(route, refNameFor(route)); } for (const [seg, cfg] of Object.entries(DYNAMIC)) { for (const id of cfg.ids) { add(`/preview/${seg.replace("[id]", id)}`, cfg.refFor(id)); } } const routed = new Set(targets.map((t) => t.ref)); for (const f of refsList()) { if (!routed.has(f)) console.warn(`[dom-audit] ref без маршрута: ${f}`); } return targets; } const waitFor = async (url, ms = 60000) => { const t = Date.now(); while (Date.now() - t < ms) { try { const r = await fetch(url); if (r.ok) return; if (r.status >= 400) { throw new Error( `route ${url} returned HTTP ${r.status} (server is up, page failed to render)`, ); } } catch (e) { if (e instanceof Error && e.message.includes("returned HTTP")) throw e; } await new Promise((r) => setTimeout(r, 500)); } throw new Error("dev server not up: " + url); }; // Нормализованное дерево элементов: тег, отсортированные классы, собственный // текст (без SVG-иконок), дети. Пропускаем служебное; SVG не спускаемся внутрь. const snapshot = (page) => page.evaluate(() => { const walk = (el) => { const tag = el.tagName.toLowerCase(); if (["script", "style", "noscript", "template"].includes(tag)) return null; // Пропускаем не отображаемые узлы (Next-боилерплейт: