stateofpixel 1.11.0 → 1.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { createReadStream } from "node:fs";
|
|
|
8
8
|
import { createServer } from "node:http";
|
|
9
9
|
|
|
10
10
|
//#region package.json
|
|
11
|
-
var version = "1.11.
|
|
11
|
+
var version = "1.11.1";
|
|
12
12
|
|
|
13
13
|
//#endregion
|
|
14
14
|
//#region src/compare.ts
|
|
@@ -685,7 +685,7 @@ async function withStorybook(staticDir, run) {
|
|
|
685
685
|
throw new Error("stateofpixel storybook needs Playwright. Run npm install -D playwright, then npx playwright install chromium.");
|
|
686
686
|
});
|
|
687
687
|
const server = await serveStatic(staticDir);
|
|
688
|
-
const browser = await chromium.launch();
|
|
688
|
+
const browser = await chromium.launch({ args: ["--disable-lcd-text"] });
|
|
689
689
|
try {
|
|
690
690
|
return await run({
|
|
691
691
|
browser,
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["packageJson.version"],"sources":["../package.json","../src/compare.ts","../src/report/template.ts","../src/report/write-report.ts","../src/commands/compare.ts","../src/commands/finalize.ts","../src/commands/storybook.ts","../src/index.ts"],"sourcesContent":["","import { copyFile, mkdir, readFile, rename, rm } from \"node:fs/promises\";\nimport { availableParallelism } from \"node:os\";\nimport path from \"node:path\";\nimport type { DiffEngine, DiffOptions } from \"./diff/engine\";\nimport { mapConcurrent } from \"./map-concurrent\";\nimport { readPngSize, sha256 } from \"./png\";\nimport { collectSnapshots } from \"./snapshots\";\n\nexport type SnapshotStatus = \"unchanged\" | \"changed\" | \"added\" | \"removed\";\n\nexport type ImageRef = {\n file: string;\n width: number;\n height: number;\n};\n\nexport type SnapshotResult = {\n name: string;\n status: SnapshotStatus;\n image?: ImageRef;\n baselineImage?: ImageRef;\n diffImage?: ImageRef;\n diffPixels?: number;\n diffRatio?: number;\n};\n\ntype CompareDirectoriesInput = {\n dir: string;\n baselineDir: string;\n outDir: string;\n engine: DiffEngine;\n options: DiffOptions;\n};\n\nexport async function compareDirectories({\n dir,\n baselineDir,\n outDir,\n engine,\n options,\n}: CompareDirectoriesInput): Promise<SnapshotResult[]> {\n const [snapshots, baselines] = await Promise.all([\n collectSnapshots(dir),\n collectSnapshots(baselineDir),\n ]);\n const imagesDir = path.join(outDir, \"images\");\n const workDir = path.join(outDir, \".work\");\n await mkdir(imagesDir, { recursive: true });\n await mkdir(workDir, { recursive: true });\n\n const names = [...new Set([...snapshots.keys(), ...baselines.keys()])].sort(\n (a, b) => (a < b ? -1 : a > b ? 1 : 0),\n );\n\n const results = await mapConcurrent(\n names,\n availableParallelism(),\n async (name, index): Promise<SnapshotResult> => {\n const filePath = snapshots.get(name);\n const baselinePath = baselines.get(name);\n\n if (!filePath && baselinePath) {\n return {\n name,\n status: \"removed\",\n baselineImage: await storeImage(baselinePath, imagesDir),\n };\n }\n if (!filePath) {\n throw new Error(`Snapshot ${name} has no file`);\n }\n\n const image = await storeImage(filePath, imagesDir);\n if (!baselinePath) {\n return { name, status: \"added\", image };\n }\n\n const baselineImage = await storeImage(baselinePath, imagesDir);\n if (image.file === baselineImage.file) {\n return { name, status: \"unchanged\", image, baselineImage };\n }\n\n const workPath = path.join(workDir, `${index}.png`);\n const { diffPixels, diffRatio } = await engine.diff(\n baselinePath,\n filePath,\n workPath,\n options,\n );\n if (diffPixels === 0) {\n await rm(workPath, { force: true });\n return { name, status: \"unchanged\", image, baselineImage };\n }\n\n return {\n name,\n status: \"changed\",\n image,\n baselineImage,\n diffImage: await storeImage(workPath, imagesDir, { move: true }),\n diffPixels,\n diffRatio,\n };\n },\n );\n\n await rm(workDir, { recursive: true, force: true });\n return results;\n}\n\nasync function storeImage(\n filePath: string,\n imagesDir: string,\n { move = false } = {},\n): Promise<ImageRef> {\n const bytes = await readFile(filePath);\n const file = `images/${sha256(bytes)}.png`;\n const target = path.join(imagesDir, path.basename(file));\n if (move) {\n await rename(filePath, target);\n } else {\n await copyFile(filePath, target);\n }\n return { file, ...readPngSize(bytes) };\n}\n","import type { SnapshotResult } from \"../compare\";\nimport type { ReportMeta } from \"./write-report\";\n\nexport function renderReport(\n snapshots: SnapshotResult[],\n meta: ReportMeta,\n): string {\n const data = JSON.stringify({ meta, snapshots }).replace(/</g, \"\\\\u003c\");\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>stateofpixel report</title>\n<style>${STYLES}</style>\n</head>\n<body>\n<header class=\"header\">\n <div class=\"title\"><strong>stateofpixel</strong> <span class=\"muted mono\" id=\"dirs\"></span></div>\n <div class=\"counts\" id=\"counts\"></div>\n</header>\n<div class=\"layout\">\n <aside class=\"sidebar\">\n <input class=\"filter\" id=\"filter\" type=\"search\" placeholder=\"Filter snapshots\" aria-label=\"Filter snapshots\">\n <div id=\"list\" role=\"listbox\" aria-label=\"Snapshots\"></div>\n </aside>\n <main class=\"viewer\" id=\"viewer\"></main>\n</div>\n<script id=\"data\" type=\"application/json\">${data}</script>\n<script>${SCRIPT}</script>\n</body>\n</html>\n`;\n}\n\nconst STYLES = `\n:root {\n color-scheme: light dark;\n --bg: #fafafa; --surface: #ffffff; --surface-2: #f2f2f2; --hover: #ebebeb; --border: #ebebeb;\n --text: #171717; --muted: #666666; --accent: #171717; --accent-fg: #ffffff; --link: #0068d6;\n --unchanged: #666666; --unchanged-bg: #f2f2f2; --changed: #a35200; --changed-bg: #fff6e6;\n --added: #0068d6; --added-bg: #f0f7ff; --removed: #7820bc; --removed-bg: #f9f0ff;\n --canvas: #f2f2f2; --checker-a: #ffffff; --checker-b: #e6e6e6;\n --sans: \"IBM Plex Sans Variable\", \"IBM Plex Sans\", ui-sans-serif, system-ui, sans-serif;\n --mono: \"Lilex Variable\", \"Lilex\", ui-monospace, \"SF Mono\", monospace;\n}\n@media (prefers-color-scheme: dark) {\n :root {\n --bg: #000000; --surface: #0a0a0a; --surface-2: #1a1a1a; --hover: #1f1f1f; --border: #2e2e2e;\n --text: #ededed; --muted: #a1a1a1; --accent: #ededed; --accent-fg: #0a0a0a; --link: #52a8ff;\n --unchanged: #a1a1a1; --unchanged-bg: #1a1a1a; --changed: #f2a20d; --changed-bg: #291800;\n --added: #52a8ff; --added-bg: #0f1c2e; --removed: #bf7af0; --removed-bg: #231528;\n --canvas: #000000; --checker-a: #1a1a1a; --checker-b: #292929;\n }\n}\n* { box-sizing: border-box; }\nhtml, body { height: 100%; margin: 0; }\nbody { display: flex; flex-direction: column; background: var(--bg); color: var(--text); font: 400 13px/20px var(--sans); -webkit-font-smoothing: antialiased; }\n:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; }\n.muted { color: var(--muted); }\n.mono { font-family: var(--mono); }\n.num { font-variant-numeric: tabular-nums; }\n.header { display: flex; flex-wrap: wrap; gap: 8px 16px; align-items: center; justify-content: space-between; padding: 12px 16px; background: var(--surface); border-bottom: 1px solid var(--border); }\n.title strong { font-size: 14px; font-weight: 600; }\n.counts { display: flex; flex-wrap: wrap; gap: 6px; }\n.pill { display: inline-flex; align-items: center; gap: 4px; height: 20px; padding: 0 6px; border-radius: 5px; font-size: 11px; font-weight: 500; font-variant-numeric: tabular-nums; white-space: nowrap; }\n.pill[data-status=unchanged] { color: var(--unchanged); background: var(--unchanged-bg); }\n.pill[data-status=changed] { color: var(--changed); background: var(--changed-bg); }\n.pill[data-status=added] { color: var(--added); background: var(--added-bg); }\n.pill[data-status=removed] { color: var(--removed); background: var(--removed-bg); }\n.layout { flex: 1; display: flex; min-height: 0; }\n.sidebar { width: 300px; flex-shrink: 0; display: flex; flex-direction: column; background: var(--surface); border-right: 1px solid var(--border); }\n.filter { margin: 8px; height: 28px; padding: 0 10px; border: 0; border-radius: 8px; background: var(--surface); color: var(--text); font: inherit; box-shadow: inset 0 0 0 1px #8f8f8f; }\n#list { flex: 1; overflow-y: auto; padding: 0 8px 8px; }\n.group { display: flex; align-items: center; gap: 6px; width: 100%; height: 28px; padding: 0 8px; border: 0; background: none; color: var(--muted); font: 500 12px/16px var(--sans); cursor: pointer; text-align: left; }\n.row { display: flex; align-items: center; gap: 8px; width: 100%; height: 32px; padding: 0 8px; border: 0; border-radius: 7px; background: none; color: var(--text); font: 13px/20px var(--mono); text-align: left; cursor: pointer; position: relative; }\n.row:hover { background: var(--hover); }\n.row[aria-selected=true] { background: var(--hover); }\n.row[aria-selected=true]::before { content: \"\"; position: absolute; left: 0; top: 6px; bottom: 6px; width: 2px; border-radius: 2px; background: var(--link); }\n.row .name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.row .ratio { color: var(--muted); font: 12px/16px var(--sans); font-variant-numeric: tabular-nums; }\n.viewer { flex: 1; min-width: 0; display: flex; flex-direction: column; background: var(--surface); }\n.viewer-title { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; min-height: 48px; padding: 8px 16px; border-bottom: 1px solid var(--border); }\n.viewer-title .name { font: 500 14px/20px var(--mono); }\n.viewer-title .dims { margin-left: auto; }\n.toolbar { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 8px; padding: 6px 16px; border-bottom: 1px solid var(--border); }\n.segmented { display: inline-flex; padding: 2px; border-radius: 11px; background: var(--surface-2); }\n.segmented button { height: 28px; padding: 0 10px; border: 1px solid transparent; border-radius: 7px; background: none; color: var(--muted); font: 500 13px/20px var(--sans); cursor: pointer; }\n.segmented button[aria-pressed=true] { background: var(--surface); color: var(--text); border-color: var(--border); }\n.segmented button:disabled { opacity: .45; cursor: default; }\nkbd { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; margin-left: 6px; padding: 0 4px; border: 1px solid var(--border); border-radius: 5px; background: var(--surface); color: var(--muted); font: 500 11px/16px var(--mono); }\n.stage { flex: 1; overflow: auto; padding: 16px; background: var(--canvas); }\n.pair { display: flex; gap: 16px; align-items: flex-start; }\n.pair > figure { flex: 1; min-width: 0; }\nfigure { margin: 0; }\nfigcaption { height: 24px; color: var(--muted); font-size: 12px; }\n.frame { position: relative; background-color: var(--checker-a); background-image: conic-gradient(var(--checker-b) 25%, transparent 0 50%, var(--checker-b) 0 75%, transparent 0); background-size: 16px 16px; }\n.frame img { position: absolute; top: 0; left: 0; display: block; }\n.zoom-actual .frame img { image-rendering: pixelated; }\n.frame[data-flip=baseline] { outline: 1px solid var(--link); }\n.diff-layer { opacity: .7; }\n.slider { display: block; width: 100%; margin: 12px 0 0; accent-color: var(--link); }\n.handle { position: absolute; top: 0; bottom: 0; width: 2px; margin-left: -1px; background: var(--link); pointer-events: none; }\n.empty { padding: 24px; color: var(--muted); font-size: 24px; line-height: 34px; }\n.empty strong { color: var(--text); font-weight: 600; }\n@media (max-width: 767px) {\n .layout { flex-direction: column; }\n .sidebar { width: auto; max-height: 40vh; border-right: 0; border-bottom: 1px solid var(--border); }\n .pair { flex-direction: column; }\n .pair > figure { width: 100%; }\n}\n`;\n\nconst SCRIPT = `\nconst { meta, snapshots } = JSON.parse(document.getElementById(\"data\").textContent);\nconst GROUPS = [\"changed\", \"added\", \"removed\", \"unchanged\"];\nconst MODES = [[\"side\", \"Side by side\"], [\"diff\", \"Diff\"], [\"slider\", \"Slider\"], [\"flip\", \"Flip\"]];\nconst store = {\n get(key, fallback) { try { return localStorage.getItem(\"stateofpixel:\" + key) || fallback; } catch { return fallback; } },\n set(key, value) { try { localStorage.setItem(\"stateofpixel:\" + key, value); } catch {} },\n};\nconst state = {\n filter: \"\",\n collapsed: new Set([\"unchanged\"]),\n selected: null,\n mode: store.get(\"mode\", \"side\"),\n zoom: store.get(\"zoom\", \"fit\"),\n flip: \"new\",\n slider: 50,\n};\n\nfunction el(tag, props, children) {\n const node = document.createElement(tag);\n Object.assign(node, props || {});\n for (const child of children || []) node.append(child);\n return node;\n}\n\nfunction pill(status, label) {\n const node = el(\"span\", { className: \"pill\", textContent: label });\n node.dataset.status = status;\n return node;\n}\n\nfunction percent(ratio) {\n return (ratio * 100).toFixed(ratio < 0.001 ? 3 : 2) + \"%\";\n}\n\nfunction visibleGroups() {\n const query = state.filter.toLowerCase();\n return GROUPS.map((status) => ({\n status,\n items: snapshots.filter((s) => s.status === status && s.name.toLowerCase().includes(query)),\n })).filter((group) => group.items.length > 0);\n}\n\nfunction navigable() {\n return visibleGroups().flatMap((group) => (state.collapsed.has(group.status) ? [] : group.items));\n}\n\nfunction renderHeader() {\n document.getElementById(\"dirs\").textContent = meta.dir + \" vs \" + meta.baselineDir;\n const counts = document.getElementById(\"counts\");\n counts.replaceChildren(\n ...GROUPS.map((status) => {\n const count = snapshots.filter((s) => s.status === status).length;\n return pill(status, count.toLocaleString() + \" \" + status);\n }),\n );\n}\n\nfunction renderList() {\n const list = document.getElementById(\"list\");\n list.replaceChildren(\n ...visibleGroups().flatMap((group) => {\n const collapsed = state.collapsed.has(group.status);\n const header = el(\"button\", {\n className: \"group\",\n textContent: (collapsed ? \"+ \" : \"- \") + group.status[0].toUpperCase() + group.status.slice(1) + \" (\" + group.items.length.toLocaleString() + \")\",\n onclick: () => {\n if (collapsed) state.collapsed.delete(group.status);\n else state.collapsed.add(group.status);\n renderList();\n },\n });\n header.setAttribute(\"aria-expanded\", String(!collapsed));\n if (collapsed) return [header];\n return [\n header,\n ...group.items.map((snapshot) => {\n const row = el(\"button\", { className: \"row\", title: snapshot.name, onclick: () => select(snapshot) }, [\n el(\"span\", { className: \"name\", textContent: snapshot.name }),\n ]);\n row.setAttribute(\"role\", \"option\");\n row.setAttribute(\"aria-selected\", String(snapshot === state.selected));\n if (snapshot.diffRatio !== undefined) row.append(el(\"span\", { className: \"ratio\", textContent: percent(snapshot.diffRatio) }));\n return row;\n }),\n ];\n }),\n );\n list.querySelector(\"[aria-selected=true]\")?.scrollIntoView({ block: \"nearest\" });\n}\n\nfunction frame(images, width, height) {\n const node = el(\"div\", { className: \"frame\" });\n if (state.zoom === \"fit\") {\n node.style.width = \"100%\";\n node.style.maxWidth = width + \"px\";\n node.style.aspectRatio = width + \" / \" + height;\n } else {\n node.style.width = width + \"px\";\n node.style.height = height + \"px\";\n }\n for (const { image, className, style } of images) {\n const img = el(\"img\", { src: image.file, alt: \"\", className: className || \"\" });\n img.style.width = (image.width / width) * 100 + \"%\";\n Object.assign(img.style, style || {});\n node.append(img);\n }\n return node;\n}\n\nfunction figure(caption, image) {\n return el(\"figure\", {}, [el(\"figcaption\", { textContent: caption }), frame([{ image }], image.width, image.height)]);\n}\n\nfunction renderStage(snapshot) {\n const { image, baselineImage, diffImage } = snapshot;\n if (!image || !baselineImage) {\n const only = image || baselineImage;\n return [figure(image ? \"New\" : \"Baseline\", only)];\n }\n const width = Math.max(image.width, baselineImage.width);\n const height = Math.max(image.height, baselineImage.height);\n if (state.mode === \"side\") {\n return [el(\"div\", { className: \"pair\" }, [figure(\"Baseline\", baselineImage), figure(\"New\", image)])];\n }\n if (state.mode === \"diff\") {\n const layers = [{ image }];\n if (diffImage) layers.push({ image: diffImage, className: \"diff-layer\" });\n return [el(\"figure\", {}, [el(\"figcaption\", { textContent: \"New with diff overlay\" }), frame(layers, width, height)])];\n }\n if (state.mode === \"slider\") {\n const clipFor = (value) => \"inset(0 \" + (100 - value) + \"% 0 0)\";\n const stageNode = frame([{ image }, { image: baselineImage, style: { clipPath: clipFor(state.slider) } }], width, height);\n const handle = el(\"div\", { className: \"handle\" });\n handle.style.left = state.slider + \"%\";\n stageNode.append(handle);\n const slider = el(\"input\", {\n className: \"slider\",\n type: \"range\",\n min: 0,\n max: 100,\n value: state.slider,\n oninput: (event) => {\n state.slider = Number(event.target.value);\n stageNode.querySelectorAll(\"img\")[1].style.clipPath = clipFor(state.slider);\n handle.style.left = state.slider + \"%\";\n },\n });\n slider.setAttribute(\"aria-label\", \"Baseline to new\");\n const node = el(\"figure\", {}, [el(\"figcaption\", { textContent: \"Baseline on the left of the handle, new on the right\" }), stageNode, slider]);\n node.style[state.zoom === \"fit\" ? \"maxWidth\" : \"width\"] = width + \"px\";\n return [node];\n }\n const showing = state.flip === \"baseline\" ? baselineImage : image;\n const node = frame([{ image: showing }], width, height);\n node.dataset.flip = state.flip;\n return [el(\"figure\", {}, [el(\"figcaption\", { textContent: \"Showing \" + state.flip + \". Press space to flip.\" }), node])];\n}\n\nfunction renderViewer() {\n const viewer = document.getElementById(\"viewer\");\n const snapshot = state.selected;\n if (!snapshot) {\n const empty = el(\"p\", { className: \"empty\" }, [el(\"strong\", { textContent: \"No visual changes.\" }), \" Every snapshot matches its baseline.\"]);\n viewer.replaceChildren(empty);\n return;\n }\n const comparable = Boolean(snapshot.image && snapshot.baselineImage);\n const title = el(\"div\", { className: \"viewer-title\" }, [\n el(\"span\", { className: \"name\", textContent: snapshot.name }),\n pill(snapshot.status, snapshot.status),\n ]);\n if (snapshot.diffPixels !== undefined) {\n title.append(el(\"span\", { className: \"muted num\", textContent: percent(snapshot.diffRatio) + \" diff, \" + snapshot.diffPixels.toLocaleString() + \" px\" }));\n }\n const dims = [snapshot.baselineImage, snapshot.image].filter(Boolean).map((i) => i.width + \"x\" + i.height);\n title.append(el(\"span\", { className: \"dims muted num\", textContent: [...new Set(dims)].join(\" to \") }));\n\n const modes = el(\"div\", { className: \"segmented\" }, MODES.map(([mode, label], index) => {\n const button = el(\"button\", { disabled: !comparable, onclick: () => setMode(mode) }, [label, el(\"kbd\", { textContent: String(index + 1) })]);\n button.setAttribute(\"aria-pressed\", String(comparable && state.mode === mode));\n return button;\n }));\n const zoom = el(\"div\", { className: \"segmented\" }, [[\"fit\", \"Fit\", \"f\"], [\"actual\", \"100%\", \"0\"]].map(([value, label, key]) => {\n const button = el(\"button\", { onclick: () => setZoom(value) }, [label, el(\"kbd\", { textContent: key })]);\n button.setAttribute(\"aria-pressed\", String(state.zoom === value));\n return button;\n }));\n const stage = el(\"div\", { className: \"stage zoom-\" + state.zoom }, renderStage(snapshot));\n viewer.replaceChildren(title, el(\"div\", { className: \"toolbar\" }, [modes, zoom]), stage);\n}\n\nfunction select(snapshot) {\n state.selected = snapshot;\n state.flip = \"new\";\n renderList();\n renderViewer();\n}\n\nfunction setMode(mode) {\n state.mode = mode;\n store.set(\"mode\", mode);\n renderViewer();\n}\n\nfunction setZoom(zoom) {\n state.zoom = zoom;\n store.set(\"zoom\", zoom);\n renderViewer();\n}\n\nfunction move(step) {\n const items = navigable();\n if (items.length === 0) return;\n const index = items.indexOf(state.selected);\n const next = index === -1 ? 0 : Math.min(items.length - 1, Math.max(0, index + step));\n select(items[next]);\n}\n\nconst filter = document.getElementById(\"filter\");\nfilter.addEventListener(\"input\", () => {\n state.filter = filter.value;\n renderList();\n});\n\ndocument.addEventListener(\"keydown\", (event) => {\n if (event.target === filter) {\n if (event.key === \"Escape\") filter.blur();\n return;\n }\n if (event.metaKey || event.ctrlKey || event.altKey) return;\n const modeIndex = [\"1\", \"2\", \"3\", \"4\"].indexOf(event.key);\n if (event.key === \"j\") move(1);\n else if (event.key === \"k\") move(-1);\n else if (modeIndex !== -1) setMode(MODES[modeIndex][0]);\n else if (event.key === \"f\") setZoom(\"fit\");\n else if (event.key === \"0\") setZoom(\"actual\");\n else if (event.key === \"/\") { event.preventDefault(); filter.focus(); }\n else if (event.key === \" \" && state.mode === \"flip\") {\n event.preventDefault();\n state.flip = state.flip === \"new\" ? \"baseline\" : \"new\";\n renderViewer();\n }\n});\n\nrenderHeader();\nconst first = navigable().find((s) => s.status !== \"unchanged\");\nif (first) select(first);\nelse { renderList(); renderViewer(); }\n`;\n","import { mkdir, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { SnapshotResult } from \"../compare\";\nimport { renderReport } from \"./template\";\n\nexport type ReportMeta = {\n dir: string;\n baselineDir: string;\n engine: string;\n createdAt: string;\n};\n\nexport async function writeReport(\n outDir: string,\n results: SnapshotResult[],\n meta: ReportMeta,\n): Promise<string> {\n await mkdir(outDir, { recursive: true });\n const reportPath = path.join(outDir, \"index.html\");\n await writeFile(reportPath, renderReport(results, meta));\n return reportPath;\n}\n","import { stat } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { InvalidArgumentError } from \"commander\";\nimport { compareDirectories, type SnapshotStatus } from \"../compare\";\nimport { createDiffEngine } from \"../diff/engine\";\nimport { formatCounts } from \"../format\";\nimport { writeReport } from \"../report/write-report\";\n\nexport type CompareCommandOptions = {\n out: string;\n threshold: number;\n includeAa: boolean;\n};\n\nexport async function compareCommand(\n dir: string,\n baselineDir: string,\n options: CompareCommandOptions,\n): Promise<void> {\n await assertDirectory(dir);\n await assertDirectory(baselineDir);\n\n const startedAt = performance.now();\n const engine = await createDiffEngine();\n const results = await compareDirectories({\n dir,\n baselineDir,\n outDir: options.out,\n engine,\n options: { threshold: options.threshold, includeAA: options.includeAa },\n });\n const reportPath = await writeReport(options.out, results, {\n dir,\n baselineDir,\n engine: engine.name,\n createdAt: new Date().toISOString(),\n });\n\n const counts = countByStatus(results.map((result) => result.status));\n const seconds = ((performance.now() - startedAt) / 1000).toFixed(1);\n console.log(`stateofpixel compare ${dir} vs ${baselineDir}`);\n console.log(formatCounts(results.length, counts));\n console.log(` compared with ${engine.name} in ${seconds} s`);\n console.log(` report: ${path.relative(process.cwd(), reportPath)}`);\n}\n\nexport function parseThreshold(value: string): number {\n const threshold = Number(value);\n if (!Number.isFinite(threshold) || threshold < 0 || threshold > 1) {\n throw new InvalidArgumentError(\"Must be a number from 0 to 1.\");\n }\n return threshold;\n}\n\nasync function assertDirectory(dir: string): Promise<void> {\n const stats = await stat(dir).catch(() => undefined);\n if (!stats?.isDirectory()) {\n throw new Error(`Not a directory: ${dir}`);\n }\n}\n\nfunction countByStatus(\n statuses: SnapshotStatus[],\n): Record<SnapshotStatus, number> {\n const counts = { unchanged: 0, changed: 0, added: 0, removed: 0 };\n for (const status of statuses) {\n counts[status]++;\n }\n return counts;\n}\n","import {\n ApiError,\n createApiClient,\n isRateLimited,\n isServerError,\n} from \"../api\";\nimport {\n defaultNonce,\n ForkPullRequestError,\n previousNonces,\n readCiInfo,\n readGitInfo,\n resolveApiUrl,\n resolveToken,\n} from \"../ci-env\";\nimport { formatCounts } from \"../format\";\nimport { ENV } from \"../reference\";\nimport { waitForFinalize } from \"../upload\";\n\nexport type FinalizeCommandOptions = {\n buildName?: string;\n nonce?: string;\n baselineBranch?: string;\n skipIfEmpty: boolean;\n strict: boolean;\n};\n\nexport async function finalizeCommand(\n options: FinalizeCommandOptions,\n): Promise<void> {\n const env = process.env;\n const apiUrl = resolveApiUrl(env);\n const buildName = options.buildName ?? env[ENV.buildName] ?? \"default\";\n const nonce = options.nonce ?? env[ENV.nonce] ?? defaultNonce(env);\n if (nonce === null) {\n throw new Error(\"Set --nonce to the nonce the shards used.\");\n }\n const git = options.skipIfEmpty\n ? await readGitInfo(\n env,\n process.cwd(),\n options.baselineBranch ?? env[ENV.baselineBranch],\n )\n : undefined;\n\n let build: { buildId: string; buildNumber: number; url: string };\n let final: Awaited<ReturnType<typeof waitForFinalize>>;\n try {\n const api = createApiClient({\n baseUrl: apiUrl,\n token: await resolveToken(env),\n refreshToken: env[ENV.token] ? undefined : () => resolveToken(env),\n });\n build = await api.request(\"POST\", \"/builds/finalize\", {\n buildName,\n nonce,\n ...((options.nonce ?? env[ENV.nonce])\n ? {}\n : { previousNonces: previousNonces(env) }),\n skipIfEmpty: options.skipIfEmpty,\n git,\n ci: readCiInfo(env),\n });\n final = await waitForFinalize(api, build.buildId, true);\n } catch (error) {\n if (error instanceof ForkPullRequestError && !options.strict) {\n console.warn(\n `stateofpixel: skipped, ${error.message} Use --strict to fail instead.`,\n );\n return;\n }\n if (isRateLimited(error) && !options.strict) {\n console.warn(\n `stateofpixel: skipped, ${error.message} Use --strict to fail instead.`,\n );\n return;\n }\n if (isServerError(error) && !options.strict) {\n console.warn(\n `stateofpixel: skipped, the service at ${apiUrl} is not reachable (${error instanceof Error ? error.message : error}). Use --strict to fail instead.`,\n );\n return;\n }\n if (error instanceof ApiError) {\n throw new Error(`${error.message} (${error.code}) from ${apiUrl}`);\n }\n throw error;\n }\n\n if (final === null) {\n console.log(`stateofpixel build #${build.buildNumber} finalizing`);\n } else {\n const { counts, shards } = final;\n console.log(\n `stateofpixel build #${build.buildNumber} finalized, ${shards.done} ${shards.done === 1 ? \"shard\" : \"shards\"}`,\n );\n console.log(\n formatCounts(\n counts.unchanged + counts.changed + counts.added + counts.failed,\n counts,\n ),\n );\n }\n console.log(` review: ${build.url}`);\n}\n","import { createReadStream } from \"node:fs\";\nimport {\n mkdir,\n mkdtemp,\n readFile,\n rm,\n stat,\n writeFile,\n} from \"node:fs/promises\";\nimport { createServer, type Server } from \"node:http\";\nimport type { AddressInfo } from \"node:net\";\nimport { tmpdir } from \"node:os\";\nimport path from \"node:path\";\nimport { InvalidArgumentError } from \"commander\";\nimport type { Browser, BrowserContext } from \"playwright\";\nimport { mapConcurrent } from \"../map-concurrent\";\nimport { ENV } from \"../reference\";\nimport { metadataFile, type Shard } from \"../upload\";\nimport { parseShard, type UploadCommandOptions, uploadCommand } from \"./upload\";\n\nconst CAPTURE_CONCURRENCY = 4;\nconst STORY_TIMEOUT_MS = 15_000;\nconst PARAMETERS_TIMEOUT_MS = 60_000;\nconst VIEWPORT_HEIGHT = 720;\n\nconst CONTENT_TYPES: Record<string, string> = {\n \".html\": \"text/html\",\n \".js\": \"text/javascript\",\n \".mjs\": \"text/javascript\",\n \".css\": \"text/css\",\n \".json\": \"application/json\",\n \".svg\": \"image/svg+xml\",\n \".png\": \"image/png\",\n \".jpg\": \"image/jpeg\",\n \".gif\": \"image/gif\",\n \".webp\": \"image/webp\",\n \".woff\": \"font/woff\",\n \".woff2\": \"font/woff2\",\n \".ttf\": \"font/ttf\",\n};\n\nexport type StorybookCommandOptions = UploadCommandOptions & {\n viewports: number[];\n include?: string;\n exclude?: string;\n tag?: string;\n globals?: string;\n chromatic: boolean;\n waitForSelector: string;\n delay: number;\n deviceScaleFactor: number;\n};\n\nexport type ChromaticParameters = {\n disable?: boolean;\n disableSnapshot?: boolean;\n delay?: number;\n viewports?: number[];\n modes?: Record<string, Record<string, unknown>>;\n};\n\nexport type Story = {\n id: string;\n title: string;\n name: string;\n importPath?: string;\n tags?: string[];\n chromatic?: ChromaticParameters;\n namedViewports?: Record<string, number>;\n};\n\nexport type Capture = {\n story: Story;\n name: string;\n width: number;\n globals?: string;\n delay: number;\n};\n\ntype Storybook = { browser: Browser; baseUrl: string };\n\ntype PreviewStore = {\n cacheAllCSFFiles(): Promise<void>;\n extract(): Record<\n string,\n { id: string; tags?: string[]; parameters?: Record<string, unknown> }\n >;\n};\n\ntype StorybookWindow = {\n __STORYBOOK_PREVIEW__?: {\n storyStoreValue?: PreviewStore;\n storyStore?: PreviewStore;\n };\n};\n\nexport async function storybookCommand(\n staticDir: string,\n options: StorybookCommandOptions,\n): Promise<void> {\n const shard = options.shard ?? parseShard(process.env[ENV.shard] ?? \"1/1\");\n if (shard.total === null) {\n throw new Error(\n \"storybook does not split stories with --shard auto. Use --shard i/n, like --shard 2/4.\",\n );\n }\n const indexedStories = await readStories(staticDir);\n const outDir = await mkdtemp(path.join(tmpdir(), \"stateofpixel-storybook-\"));\n try {\n const failed = await withStorybook(staticDir, async (storybook) => {\n const stories = shardStories(\n filterStories(\n options.chromatic\n ? (await readChromaticParameters(storybook, indexedStories)).filter(\n isCapturedByChromatic,\n )\n : indexedStories,\n options.include,\n options.exclude,\n options.tag,\n ),\n shard,\n );\n if (stories.length === 0) {\n throw new Error(`No stories found in ${staticDir}/index.json.`);\n }\n const captures = planCaptures(stories, options);\n const notRendered = await captureStories(\n storybook,\n outDir,\n captures,\n options.waitForSelector,\n options.deviceScaleFactor,\n );\n console.log(\n `stateofpixel captured ${captures.length - notRendered.length} screenshots from ${stories.length} stories`,\n );\n return notRendered;\n });\n if (failed.length > 0) {\n throw new Error(`Stories that did not render:\\n ${failed.join(\"\\n \")}`);\n }\n await uploadCommand(outDir, { ...options, shard });\n } finally {\n await rm(outDir, { recursive: true, force: true });\n }\n}\n\nasync function readStories(staticDir: string): Promise<Story[]> {\n const text = await readFile(path.join(staticDir, \"index.json\"), \"utf8\").catch(\n () => {\n throw new Error(\n `No index.json in ${staticDir}. Run storybook build first, Storybook 7 or newer.`,\n );\n },\n );\n const index = JSON.parse(text) as {\n entries?: Record<string, Story & { type?: string }>;\n };\n return Object.values(index.entries ?? {})\n .filter((entry) => entry.type === \"story\")\n .map(({ id, title, name, importPath, tags }) => ({\n id,\n title,\n name,\n importPath,\n tags,\n }));\n}\n\nexport function filterStories(\n stories: Story[],\n include: string | undefined,\n exclude: string | undefined,\n tag?: string,\n): Story[] {\n const includes = include === undefined ? null : globToRegExp(include);\n const excludes = exclude === undefined ? null : globToRegExp(exclude);\n return stories.filter((story) => {\n const name = `${story.title}/${story.name}`;\n return (\n (includes === null || includes.test(name)) &&\n (excludes === null || !excludes.test(name)) &&\n (tag === undefined || (story.tags ?? []).includes(tag))\n );\n });\n}\n\nexport function shardStories(stories: Story[], shard: Shard): Story[] {\n const { index, total } = shard;\n if (index === null || total === null) {\n return stories;\n }\n return stories.filter((_, position) => position % total === index - 1);\n}\n\nfunction globToRegExp(glob: string): RegExp {\n const source = glob\n .split(\"**\")\n .map((part) =>\n part\n .split(\"*\")\n .map((text) => text.replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\"))\n .join(\"[^/]*\"),\n )\n .join(\".*\");\n return new RegExp(`^${source}$`);\n}\n\nexport function storySnapshotName(story: Story, width: number): string {\n return `${story.title}/${story.name} [chromium ${width}]`;\n}\n\nexport function storyUrl(\n baseUrl: string,\n story: Story,\n globals: string | undefined,\n): string {\n const url = `${baseUrl}/iframe.html?id=${encodeURIComponent(story.id)}&viewMode=story`;\n return globals === undefined\n ? url\n : `${url}&globals=${encodeURIComponent(globals)}`;\n}\n\nexport function isCapturedByChromatic(story: Story): boolean {\n return (\n story.chromatic?.disable !== true &&\n story.chromatic?.disableSnapshot !== true\n );\n}\n\nexport function planCaptures(\n stories: Story[],\n options: Pick<\n StorybookCommandOptions,\n \"viewports\" | \"globals\" | \"chromatic\" | \"delay\"\n >,\n): Capture[] {\n const defaultWidth = options.viewports[0] as number;\n return stories.flatMap((story) => {\n const chromatic = options.chromatic ? story.chromatic : undefined;\n const delay = chromatic?.delay ?? options.delay;\n const modes = Object.entries(chromatic?.modes ?? {}).filter(\n ([, mode]) => mode.disable !== true,\n );\n if (modes.length > 0) {\n return modes.map(([modeName, { viewport, ...modeGlobals }]) => {\n const label = `${story.title}/${story.name}: mode \"${modeName}\"`;\n return {\n story,\n name: `${story.title}/${story.name} [chromium ${modeName}]`,\n width:\n viewport === undefined\n ? defaultWidth\n : modeWidth(label, story, viewport),\n globals: mergeGlobals(label, options.globals, modeGlobals),\n delay,\n };\n });\n }\n return (chromatic?.viewports ?? options.viewports).map((width) => ({\n story,\n name: storySnapshotName(story, width),\n width,\n globals: options.globals,\n delay,\n }));\n });\n}\n\nfunction modeWidth(label: string, story: Story, viewport: unknown): number {\n const width =\n typeof viewport === \"number\"\n ? viewport\n : typeof viewport === \"string\"\n ? story.namedViewports?.[viewport]\n : undefined;\n if (width === undefined || !Number.isInteger(width) || width < 1) {\n throw new Error(\n `${label} has viewport ${JSON.stringify(viewport)}, which is not a width in pixels.`,\n );\n }\n return width;\n}\n\nfunction mergeGlobals(\n label: string,\n flagGlobals: string | undefined,\n modeGlobals: Record<string, unknown>,\n): string | undefined {\n const globals = new Map(\n (flagGlobals?.split(\";\") ?? []).map((pair) => {\n const [key = \"\", value = \"\"] = pair.split(\":\");\n return [key, value];\n }),\n );\n for (const [key, value] of Object.entries(modeGlobals)) {\n if (key === \"disable\") {\n continue;\n }\n if (typeof value === \"boolean\") {\n globals.set(key, `!${value}`);\n } else if (typeof value === \"string\" || typeof value === \"number\") {\n globals.set(key, String(value));\n } else {\n throw new Error(\n `${label} sets ${key} to a value the CLI cannot pass to Storybook.`,\n );\n }\n }\n return globals.size === 0\n ? undefined\n : [...globals].map(([key, value]) => `${key}:${value}`).join(\";\");\n}\n\nasync function withStorybook<T>(\n staticDir: string,\n run: (storybook: Storybook) => Promise<T>,\n): Promise<T> {\n const { chromium } = await import(\"playwright\").catch(() => {\n throw new Error(\n \"stateofpixel storybook needs Playwright. Run npm install -D playwright, then npx playwright install chromium.\",\n );\n });\n const server = await serveStatic(staticDir);\n const browser = await chromium.launch();\n try {\n return await run({\n browser,\n baseUrl: `http://127.0.0.1:${(server.address() as AddressInfo).port}`,\n });\n } finally {\n await browser.close();\n server.close();\n }\n}\n\nasync function readChromaticParameters(\n storybook: Storybook,\n stories: Story[],\n): Promise<Story[]> {\n const page = await storybook.browser.newPage();\n try {\n await page.goto(`${storybook.baseUrl}/iframe.html`, {\n timeout: PARAMETERS_TIMEOUT_MS,\n });\n await page.waitForFunction(\n () => {\n const preview = (globalThis as StorybookWindow).__STORYBOOK_PREVIEW__;\n try {\n const store = preview?.storyStoreValue ?? preview?.storyStore;\n return typeof store?.extract === \"function\";\n } catch {\n return false;\n }\n },\n undefined,\n { timeout: PARAMETERS_TIMEOUT_MS },\n );\n const text = await page.evaluate(async () => {\n const preview = (globalThis as StorybookWindow).__STORYBOOK_PREVIEW__;\n const store = (preview?.storyStoreValue ??\n preview?.storyStore) as PreviewStore;\n await store.cacheAllCSFFiles();\n return JSON.stringify(\n Object.values(store.extract()).map(({ id, tags, parameters }) => {\n const viewport = parameters?.viewport as\n | {\n viewports?: Record<string, { styles?: { width?: string } }>;\n options?: Record<string, { styles?: { width?: string } }>;\n }\n | undefined;\n const namedViewports: Record<string, number> = {};\n for (const [name, value] of Object.entries(\n viewport?.viewports ?? viewport?.options ?? {},\n )) {\n const width = /^(\\d+)px$/.exec(value?.styles?.width ?? \"\")?.[1];\n if (width !== undefined) {\n namedViewports[name] = Number(width);\n }\n }\n return { id, tags, chromatic: parameters?.chromatic, namedViewports };\n }),\n );\n });\n const parameters = new Map(\n (JSON.parse(text) as Story[]).map((entry) => [entry.id, entry]),\n );\n return stories.map((story) => {\n const entry = parameters.get(story.id);\n return entry === undefined\n ? story\n : {\n ...story,\n tags: entry.tags,\n chromatic: entry.chromatic,\n namedViewports: entry.namedViewports,\n };\n });\n } catch (error) {\n throw new Error(\n `Could not read story parameters from ${storybook.baseUrl}/iframe.html: ${error instanceof Error ? error.message : error}`,\n );\n } finally {\n await page.close();\n }\n}\n\nasync function captureStories(\n storybook: Storybook,\n outDir: string,\n captures: Capture[],\n waitForSelector: string,\n deviceScaleFactor: number,\n): Promise<string[]> {\n const failed: string[] = [];\n await mapConcurrent(\n captures,\n CAPTURE_CONCURRENCY,\n async ({ story, name, width, globals, delay }) => {\n const file = path.join(outDir, `${name}.png`);\n let context: BrowserContext | undefined;\n try {\n context = await storybook.browser.newContext({\n viewport: { width, height: VIEWPORT_HEIGHT },\n deviceScaleFactor,\n reducedMotion: \"reduce\",\n });\n const page = await context.newPage();\n await page.goto(storyUrl(storybook.baseUrl, story, globals), {\n timeout: STORY_TIMEOUT_MS,\n });\n await page.waitForSelector(waitForSelector, {\n timeout: STORY_TIMEOUT_MS,\n });\n await page.evaluate(\"document.fonts.ready\");\n if (delay > 0) {\n await page.waitForTimeout(delay);\n }\n await mkdir(path.dirname(file), { recursive: true });\n await page.screenshot({\n path: file,\n fullPage: true,\n animations: \"disabled\",\n caret: \"hide\",\n });\n await writeFile(\n metadataFile(file),\n JSON.stringify({\n browser: \"chromium\",\n viewport: width,\n storyId: story.id,\n importPath: story.importPath,\n }),\n );\n } catch (error) {\n const reason = (\n error instanceof Error ? error.message : String(error)\n ).split(\"\\n\")[0];\n failed.push(`${name}: ${reason}`);\n } finally {\n await context?.close();\n }\n },\n );\n return failed;\n}\n\nasync function serveStatic(root: string): Promise<Server> {\n const base = path.resolve(root);\n const server = createServer((request, response) => {\n const pathname = decodeURIComponent(\n new URL(request.url ?? \"/\", \"http://localhost\").pathname,\n );\n const file = path.join(\n base,\n pathname.endsWith(\"/\") ? `${pathname}index.html` : pathname,\n );\n if (!file.startsWith(`${base}${path.sep}`)) {\n response.writeHead(403).end();\n return;\n }\n stat(file).then(\n (stats) => {\n if (!stats.isFile()) {\n response.writeHead(404).end();\n return;\n }\n response.writeHead(200, {\n \"Content-Type\":\n CONTENT_TYPES[path.extname(file).toLowerCase()] ??\n \"application/octet-stream\",\n \"Cache-Control\": \"max-age=3600\",\n });\n createReadStream(file).pipe(response);\n },\n () => response.writeHead(404).end(),\n );\n });\n await new Promise<void>((resolve) => server.listen(0, \"127.0.0.1\", resolve));\n return server;\n}\n\nexport function parseViewports(value: string): number[] {\n const widths = value.split(\",\").map((width) => Number(width.trim()));\n if (\n widths.length === 0 ||\n widths.some((width) => !Number.isInteger(width) || width < 1)\n ) {\n throw new InvalidArgumentError(\"Must look like 375,1280.\");\n }\n return widths;\n}\n\nexport function parseGlobals(value: string): string {\n if (!/^[\\w-]+:[^;:]+(;[\\w-]+:[^;:]+)*$/.test(value)) {\n throw new InvalidArgumentError(\"Must look like brand:new;theme:dark.\");\n }\n return value;\n}\n\nexport function parseDelay(value: string): number {\n const delay = Number(value);\n if (!Number.isInteger(delay) || delay < 0) {\n throw new InvalidArgumentError(\"Must be a whole number of milliseconds.\");\n }\n return delay;\n}\n\nexport function parseDeviceScaleFactor(value: string): number {\n const scale = Number(value);\n if (!Number.isFinite(scale) || scale <= 0) {\n throw new InvalidArgumentError(\"Must be a number above 0, like 2.\");\n }\n return scale;\n}\n","#!/usr/bin/env node\nimport { Command, Option } from \"commander\";\nimport packageJson from \"../package.json\" with { type: \"json\" };\nimport { compareCommand, parseThreshold } from \"./commands/compare\";\nimport { finalizeCommand } from \"./commands/finalize\";\nimport {\n parseDelay,\n parseDeviceScaleFactor,\n parseGlobals,\n parseViewports,\n storybookCommand,\n} from \"./commands/storybook\";\nimport { parseShard, uploadCommand } from \"./commands/upload\";\nimport { COMMANDS, type CommandSpec } from \"./reference\";\n\nconst PARSERS: Record<string, (value: string) => unknown> = {\n \"--threshold <number>\": parseThreshold,\n \"--shard <i/n>\": parseShard,\n \"--viewports <widths>\": parseViewports,\n \"--delay <ms>\": parseDelay,\n \"--globals <globals>\": parseGlobals,\n \"--device-scale-factor <n>\": parseDeviceScaleFactor,\n};\n\nconst ACTIONS: Record<CommandSpec[\"name\"], Parameters<Command[\"action\"]>[0]> = {\n compare: compareCommand,\n upload: uploadCommand,\n storybook: storybookCommand,\n finalize: finalizeCommand,\n};\n\nconst program = new Command()\n .name(\"stateofpixel\")\n .description(\"Catch UI regressions before they merge\")\n .version(packageJson.version);\n\nfor (const spec of COMMANDS) {\n const command = program.command(spec.name).description(spec.description);\n for (const argument of spec.arguments) {\n command.argument(argument.name, argument.description);\n }\n for (const option of spec.options) {\n const commanderOption = new Option(option.flags, option.description);\n const parse = PARSERS[option.flags];\n if (parse !== undefined) {\n commanderOption.argParser(parse);\n }\n if (option.default !== undefined) {\n commanderOption.default(option.default);\n }\n command.addOption(commanderOption);\n }\n command.action(ACTIONS[spec.name]);\n}\n\ntry {\n await program.parseAsync();\n} catch (error) {\n console.error(\n `stateofpixel: ${error instanceof Error ? error.message : error}`,\n );\n process.exitCode = 1;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["packageJson.version"],"sources":["../package.json","../src/compare.ts","../src/report/template.ts","../src/report/write-report.ts","../src/commands/compare.ts","../src/commands/finalize.ts","../src/commands/storybook.ts","../src/index.ts"],"sourcesContent":["","import { copyFile, mkdir, readFile, rename, rm } from \"node:fs/promises\";\nimport { availableParallelism } from \"node:os\";\nimport path from \"node:path\";\nimport type { DiffEngine, DiffOptions } from \"./diff/engine\";\nimport { mapConcurrent } from \"./map-concurrent\";\nimport { readPngSize, sha256 } from \"./png\";\nimport { collectSnapshots } from \"./snapshots\";\n\nexport type SnapshotStatus = \"unchanged\" | \"changed\" | \"added\" | \"removed\";\n\nexport type ImageRef = {\n file: string;\n width: number;\n height: number;\n};\n\nexport type SnapshotResult = {\n name: string;\n status: SnapshotStatus;\n image?: ImageRef;\n baselineImage?: ImageRef;\n diffImage?: ImageRef;\n diffPixels?: number;\n diffRatio?: number;\n};\n\ntype CompareDirectoriesInput = {\n dir: string;\n baselineDir: string;\n outDir: string;\n engine: DiffEngine;\n options: DiffOptions;\n};\n\nexport async function compareDirectories({\n dir,\n baselineDir,\n outDir,\n engine,\n options,\n}: CompareDirectoriesInput): Promise<SnapshotResult[]> {\n const [snapshots, baselines] = await Promise.all([\n collectSnapshots(dir),\n collectSnapshots(baselineDir),\n ]);\n const imagesDir = path.join(outDir, \"images\");\n const workDir = path.join(outDir, \".work\");\n await mkdir(imagesDir, { recursive: true });\n await mkdir(workDir, { recursive: true });\n\n const names = [...new Set([...snapshots.keys(), ...baselines.keys()])].sort(\n (a, b) => (a < b ? -1 : a > b ? 1 : 0),\n );\n\n const results = await mapConcurrent(\n names,\n availableParallelism(),\n async (name, index): Promise<SnapshotResult> => {\n const filePath = snapshots.get(name);\n const baselinePath = baselines.get(name);\n\n if (!filePath && baselinePath) {\n return {\n name,\n status: \"removed\",\n baselineImage: await storeImage(baselinePath, imagesDir),\n };\n }\n if (!filePath) {\n throw new Error(`Snapshot ${name} has no file`);\n }\n\n const image = await storeImage(filePath, imagesDir);\n if (!baselinePath) {\n return { name, status: \"added\", image };\n }\n\n const baselineImage = await storeImage(baselinePath, imagesDir);\n if (image.file === baselineImage.file) {\n return { name, status: \"unchanged\", image, baselineImage };\n }\n\n const workPath = path.join(workDir, `${index}.png`);\n const { diffPixels, diffRatio } = await engine.diff(\n baselinePath,\n filePath,\n workPath,\n options,\n );\n if (diffPixels === 0) {\n await rm(workPath, { force: true });\n return { name, status: \"unchanged\", image, baselineImage };\n }\n\n return {\n name,\n status: \"changed\",\n image,\n baselineImage,\n diffImage: await storeImage(workPath, imagesDir, { move: true }),\n diffPixels,\n diffRatio,\n };\n },\n );\n\n await rm(workDir, { recursive: true, force: true });\n return results;\n}\n\nasync function storeImage(\n filePath: string,\n imagesDir: string,\n { move = false } = {},\n): Promise<ImageRef> {\n const bytes = await readFile(filePath);\n const file = `images/${sha256(bytes)}.png`;\n const target = path.join(imagesDir, path.basename(file));\n if (move) {\n await rename(filePath, target);\n } else {\n await copyFile(filePath, target);\n }\n return { file, ...readPngSize(bytes) };\n}\n","import type { SnapshotResult } from \"../compare\";\nimport type { ReportMeta } from \"./write-report\";\n\nexport function renderReport(\n snapshots: SnapshotResult[],\n meta: ReportMeta,\n): string {\n const data = JSON.stringify({ meta, snapshots }).replace(/</g, \"\\\\u003c\");\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>stateofpixel report</title>\n<style>${STYLES}</style>\n</head>\n<body>\n<header class=\"header\">\n <div class=\"title\"><strong>stateofpixel</strong> <span class=\"muted mono\" id=\"dirs\"></span></div>\n <div class=\"counts\" id=\"counts\"></div>\n</header>\n<div class=\"layout\">\n <aside class=\"sidebar\">\n <input class=\"filter\" id=\"filter\" type=\"search\" placeholder=\"Filter snapshots\" aria-label=\"Filter snapshots\">\n <div id=\"list\" role=\"listbox\" aria-label=\"Snapshots\"></div>\n </aside>\n <main class=\"viewer\" id=\"viewer\"></main>\n</div>\n<script id=\"data\" type=\"application/json\">${data}</script>\n<script>${SCRIPT}</script>\n</body>\n</html>\n`;\n}\n\nconst STYLES = `\n:root {\n color-scheme: light dark;\n --bg: #fafafa; --surface: #ffffff; --surface-2: #f2f2f2; --hover: #ebebeb; --border: #ebebeb;\n --text: #171717; --muted: #666666; --accent: #171717; --accent-fg: #ffffff; --link: #0068d6;\n --unchanged: #666666; --unchanged-bg: #f2f2f2; --changed: #a35200; --changed-bg: #fff6e6;\n --added: #0068d6; --added-bg: #f0f7ff; --removed: #7820bc; --removed-bg: #f9f0ff;\n --canvas: #f2f2f2; --checker-a: #ffffff; --checker-b: #e6e6e6;\n --sans: \"IBM Plex Sans Variable\", \"IBM Plex Sans\", ui-sans-serif, system-ui, sans-serif;\n --mono: \"Lilex Variable\", \"Lilex\", ui-monospace, \"SF Mono\", monospace;\n}\n@media (prefers-color-scheme: dark) {\n :root {\n --bg: #000000; --surface: #0a0a0a; --surface-2: #1a1a1a; --hover: #1f1f1f; --border: #2e2e2e;\n --text: #ededed; --muted: #a1a1a1; --accent: #ededed; --accent-fg: #0a0a0a; --link: #52a8ff;\n --unchanged: #a1a1a1; --unchanged-bg: #1a1a1a; --changed: #f2a20d; --changed-bg: #291800;\n --added: #52a8ff; --added-bg: #0f1c2e; --removed: #bf7af0; --removed-bg: #231528;\n --canvas: #000000; --checker-a: #1a1a1a; --checker-b: #292929;\n }\n}\n* { box-sizing: border-box; }\nhtml, body { height: 100%; margin: 0; }\nbody { display: flex; flex-direction: column; background: var(--bg); color: var(--text); font: 400 13px/20px var(--sans); -webkit-font-smoothing: antialiased; }\n:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; }\n.muted { color: var(--muted); }\n.mono { font-family: var(--mono); }\n.num { font-variant-numeric: tabular-nums; }\n.header { display: flex; flex-wrap: wrap; gap: 8px 16px; align-items: center; justify-content: space-between; padding: 12px 16px; background: var(--surface); border-bottom: 1px solid var(--border); }\n.title strong { font-size: 14px; font-weight: 600; }\n.counts { display: flex; flex-wrap: wrap; gap: 6px; }\n.pill { display: inline-flex; align-items: center; gap: 4px; height: 20px; padding: 0 6px; border-radius: 5px; font-size: 11px; font-weight: 500; font-variant-numeric: tabular-nums; white-space: nowrap; }\n.pill[data-status=unchanged] { color: var(--unchanged); background: var(--unchanged-bg); }\n.pill[data-status=changed] { color: var(--changed); background: var(--changed-bg); }\n.pill[data-status=added] { color: var(--added); background: var(--added-bg); }\n.pill[data-status=removed] { color: var(--removed); background: var(--removed-bg); }\n.layout { flex: 1; display: flex; min-height: 0; }\n.sidebar { width: 300px; flex-shrink: 0; display: flex; flex-direction: column; background: var(--surface); border-right: 1px solid var(--border); }\n.filter { margin: 8px; height: 28px; padding: 0 10px; border: 0; border-radius: 8px; background: var(--surface); color: var(--text); font: inherit; box-shadow: inset 0 0 0 1px #8f8f8f; }\n#list { flex: 1; overflow-y: auto; padding: 0 8px 8px; }\n.group { display: flex; align-items: center; gap: 6px; width: 100%; height: 28px; padding: 0 8px; border: 0; background: none; color: var(--muted); font: 500 12px/16px var(--sans); cursor: pointer; text-align: left; }\n.row { display: flex; align-items: center; gap: 8px; width: 100%; height: 32px; padding: 0 8px; border: 0; border-radius: 7px; background: none; color: var(--text); font: 13px/20px var(--mono); text-align: left; cursor: pointer; position: relative; }\n.row:hover { background: var(--hover); }\n.row[aria-selected=true] { background: var(--hover); }\n.row[aria-selected=true]::before { content: \"\"; position: absolute; left: 0; top: 6px; bottom: 6px; width: 2px; border-radius: 2px; background: var(--link); }\n.row .name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.row .ratio { color: var(--muted); font: 12px/16px var(--sans); font-variant-numeric: tabular-nums; }\n.viewer { flex: 1; min-width: 0; display: flex; flex-direction: column; background: var(--surface); }\n.viewer-title { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; min-height: 48px; padding: 8px 16px; border-bottom: 1px solid var(--border); }\n.viewer-title .name { font: 500 14px/20px var(--mono); }\n.viewer-title .dims { margin-left: auto; }\n.toolbar { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 8px; padding: 6px 16px; border-bottom: 1px solid var(--border); }\n.segmented { display: inline-flex; padding: 2px; border-radius: 11px; background: var(--surface-2); }\n.segmented button { height: 28px; padding: 0 10px; border: 1px solid transparent; border-radius: 7px; background: none; color: var(--muted); font: 500 13px/20px var(--sans); cursor: pointer; }\n.segmented button[aria-pressed=true] { background: var(--surface); color: var(--text); border-color: var(--border); }\n.segmented button:disabled { opacity: .45; cursor: default; }\nkbd { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; margin-left: 6px; padding: 0 4px; border: 1px solid var(--border); border-radius: 5px; background: var(--surface); color: var(--muted); font: 500 11px/16px var(--mono); }\n.stage { flex: 1; overflow: auto; padding: 16px; background: var(--canvas); }\n.pair { display: flex; gap: 16px; align-items: flex-start; }\n.pair > figure { flex: 1; min-width: 0; }\nfigure { margin: 0; }\nfigcaption { height: 24px; color: var(--muted); font-size: 12px; }\n.frame { position: relative; background-color: var(--checker-a); background-image: conic-gradient(var(--checker-b) 25%, transparent 0 50%, var(--checker-b) 0 75%, transparent 0); background-size: 16px 16px; }\n.frame img { position: absolute; top: 0; left: 0; display: block; }\n.zoom-actual .frame img { image-rendering: pixelated; }\n.frame[data-flip=baseline] { outline: 1px solid var(--link); }\n.diff-layer { opacity: .7; }\n.slider { display: block; width: 100%; margin: 12px 0 0; accent-color: var(--link); }\n.handle { position: absolute; top: 0; bottom: 0; width: 2px; margin-left: -1px; background: var(--link); pointer-events: none; }\n.empty { padding: 24px; color: var(--muted); font-size: 24px; line-height: 34px; }\n.empty strong { color: var(--text); font-weight: 600; }\n@media (max-width: 767px) {\n .layout { flex-direction: column; }\n .sidebar { width: auto; max-height: 40vh; border-right: 0; border-bottom: 1px solid var(--border); }\n .pair { flex-direction: column; }\n .pair > figure { width: 100%; }\n}\n`;\n\nconst SCRIPT = `\nconst { meta, snapshots } = JSON.parse(document.getElementById(\"data\").textContent);\nconst GROUPS = [\"changed\", \"added\", \"removed\", \"unchanged\"];\nconst MODES = [[\"side\", \"Side by side\"], [\"diff\", \"Diff\"], [\"slider\", \"Slider\"], [\"flip\", \"Flip\"]];\nconst store = {\n get(key, fallback) { try { return localStorage.getItem(\"stateofpixel:\" + key) || fallback; } catch { return fallback; } },\n set(key, value) { try { localStorage.setItem(\"stateofpixel:\" + key, value); } catch {} },\n};\nconst state = {\n filter: \"\",\n collapsed: new Set([\"unchanged\"]),\n selected: null,\n mode: store.get(\"mode\", \"side\"),\n zoom: store.get(\"zoom\", \"fit\"),\n flip: \"new\",\n slider: 50,\n};\n\nfunction el(tag, props, children) {\n const node = document.createElement(tag);\n Object.assign(node, props || {});\n for (const child of children || []) node.append(child);\n return node;\n}\n\nfunction pill(status, label) {\n const node = el(\"span\", { className: \"pill\", textContent: label });\n node.dataset.status = status;\n return node;\n}\n\nfunction percent(ratio) {\n return (ratio * 100).toFixed(ratio < 0.001 ? 3 : 2) + \"%\";\n}\n\nfunction visibleGroups() {\n const query = state.filter.toLowerCase();\n return GROUPS.map((status) => ({\n status,\n items: snapshots.filter((s) => s.status === status && s.name.toLowerCase().includes(query)),\n })).filter((group) => group.items.length > 0);\n}\n\nfunction navigable() {\n return visibleGroups().flatMap((group) => (state.collapsed.has(group.status) ? [] : group.items));\n}\n\nfunction renderHeader() {\n document.getElementById(\"dirs\").textContent = meta.dir + \" vs \" + meta.baselineDir;\n const counts = document.getElementById(\"counts\");\n counts.replaceChildren(\n ...GROUPS.map((status) => {\n const count = snapshots.filter((s) => s.status === status).length;\n return pill(status, count.toLocaleString() + \" \" + status);\n }),\n );\n}\n\nfunction renderList() {\n const list = document.getElementById(\"list\");\n list.replaceChildren(\n ...visibleGroups().flatMap((group) => {\n const collapsed = state.collapsed.has(group.status);\n const header = el(\"button\", {\n className: \"group\",\n textContent: (collapsed ? \"+ \" : \"- \") + group.status[0].toUpperCase() + group.status.slice(1) + \" (\" + group.items.length.toLocaleString() + \")\",\n onclick: () => {\n if (collapsed) state.collapsed.delete(group.status);\n else state.collapsed.add(group.status);\n renderList();\n },\n });\n header.setAttribute(\"aria-expanded\", String(!collapsed));\n if (collapsed) return [header];\n return [\n header,\n ...group.items.map((snapshot) => {\n const row = el(\"button\", { className: \"row\", title: snapshot.name, onclick: () => select(snapshot) }, [\n el(\"span\", { className: \"name\", textContent: snapshot.name }),\n ]);\n row.setAttribute(\"role\", \"option\");\n row.setAttribute(\"aria-selected\", String(snapshot === state.selected));\n if (snapshot.diffRatio !== undefined) row.append(el(\"span\", { className: \"ratio\", textContent: percent(snapshot.diffRatio) }));\n return row;\n }),\n ];\n }),\n );\n list.querySelector(\"[aria-selected=true]\")?.scrollIntoView({ block: \"nearest\" });\n}\n\nfunction frame(images, width, height) {\n const node = el(\"div\", { className: \"frame\" });\n if (state.zoom === \"fit\") {\n node.style.width = \"100%\";\n node.style.maxWidth = width + \"px\";\n node.style.aspectRatio = width + \" / \" + height;\n } else {\n node.style.width = width + \"px\";\n node.style.height = height + \"px\";\n }\n for (const { image, className, style } of images) {\n const img = el(\"img\", { src: image.file, alt: \"\", className: className || \"\" });\n img.style.width = (image.width / width) * 100 + \"%\";\n Object.assign(img.style, style || {});\n node.append(img);\n }\n return node;\n}\n\nfunction figure(caption, image) {\n return el(\"figure\", {}, [el(\"figcaption\", { textContent: caption }), frame([{ image }], image.width, image.height)]);\n}\n\nfunction renderStage(snapshot) {\n const { image, baselineImage, diffImage } = snapshot;\n if (!image || !baselineImage) {\n const only = image || baselineImage;\n return [figure(image ? \"New\" : \"Baseline\", only)];\n }\n const width = Math.max(image.width, baselineImage.width);\n const height = Math.max(image.height, baselineImage.height);\n if (state.mode === \"side\") {\n return [el(\"div\", { className: \"pair\" }, [figure(\"Baseline\", baselineImage), figure(\"New\", image)])];\n }\n if (state.mode === \"diff\") {\n const layers = [{ image }];\n if (diffImage) layers.push({ image: diffImage, className: \"diff-layer\" });\n return [el(\"figure\", {}, [el(\"figcaption\", { textContent: \"New with diff overlay\" }), frame(layers, width, height)])];\n }\n if (state.mode === \"slider\") {\n const clipFor = (value) => \"inset(0 \" + (100 - value) + \"% 0 0)\";\n const stageNode = frame([{ image }, { image: baselineImage, style: { clipPath: clipFor(state.slider) } }], width, height);\n const handle = el(\"div\", { className: \"handle\" });\n handle.style.left = state.slider + \"%\";\n stageNode.append(handle);\n const slider = el(\"input\", {\n className: \"slider\",\n type: \"range\",\n min: 0,\n max: 100,\n value: state.slider,\n oninput: (event) => {\n state.slider = Number(event.target.value);\n stageNode.querySelectorAll(\"img\")[1].style.clipPath = clipFor(state.slider);\n handle.style.left = state.slider + \"%\";\n },\n });\n slider.setAttribute(\"aria-label\", \"Baseline to new\");\n const node = el(\"figure\", {}, [el(\"figcaption\", { textContent: \"Baseline on the left of the handle, new on the right\" }), stageNode, slider]);\n node.style[state.zoom === \"fit\" ? \"maxWidth\" : \"width\"] = width + \"px\";\n return [node];\n }\n const showing = state.flip === \"baseline\" ? baselineImage : image;\n const node = frame([{ image: showing }], width, height);\n node.dataset.flip = state.flip;\n return [el(\"figure\", {}, [el(\"figcaption\", { textContent: \"Showing \" + state.flip + \". Press space to flip.\" }), node])];\n}\n\nfunction renderViewer() {\n const viewer = document.getElementById(\"viewer\");\n const snapshot = state.selected;\n if (!snapshot) {\n const empty = el(\"p\", { className: \"empty\" }, [el(\"strong\", { textContent: \"No visual changes.\" }), \" Every snapshot matches its baseline.\"]);\n viewer.replaceChildren(empty);\n return;\n }\n const comparable = Boolean(snapshot.image && snapshot.baselineImage);\n const title = el(\"div\", { className: \"viewer-title\" }, [\n el(\"span\", { className: \"name\", textContent: snapshot.name }),\n pill(snapshot.status, snapshot.status),\n ]);\n if (snapshot.diffPixels !== undefined) {\n title.append(el(\"span\", { className: \"muted num\", textContent: percent(snapshot.diffRatio) + \" diff, \" + snapshot.diffPixels.toLocaleString() + \" px\" }));\n }\n const dims = [snapshot.baselineImage, snapshot.image].filter(Boolean).map((i) => i.width + \"x\" + i.height);\n title.append(el(\"span\", { className: \"dims muted num\", textContent: [...new Set(dims)].join(\" to \") }));\n\n const modes = el(\"div\", { className: \"segmented\" }, MODES.map(([mode, label], index) => {\n const button = el(\"button\", { disabled: !comparable, onclick: () => setMode(mode) }, [label, el(\"kbd\", { textContent: String(index + 1) })]);\n button.setAttribute(\"aria-pressed\", String(comparable && state.mode === mode));\n return button;\n }));\n const zoom = el(\"div\", { className: \"segmented\" }, [[\"fit\", \"Fit\", \"f\"], [\"actual\", \"100%\", \"0\"]].map(([value, label, key]) => {\n const button = el(\"button\", { onclick: () => setZoom(value) }, [label, el(\"kbd\", { textContent: key })]);\n button.setAttribute(\"aria-pressed\", String(state.zoom === value));\n return button;\n }));\n const stage = el(\"div\", { className: \"stage zoom-\" + state.zoom }, renderStage(snapshot));\n viewer.replaceChildren(title, el(\"div\", { className: \"toolbar\" }, [modes, zoom]), stage);\n}\n\nfunction select(snapshot) {\n state.selected = snapshot;\n state.flip = \"new\";\n renderList();\n renderViewer();\n}\n\nfunction setMode(mode) {\n state.mode = mode;\n store.set(\"mode\", mode);\n renderViewer();\n}\n\nfunction setZoom(zoom) {\n state.zoom = zoom;\n store.set(\"zoom\", zoom);\n renderViewer();\n}\n\nfunction move(step) {\n const items = navigable();\n if (items.length === 0) return;\n const index = items.indexOf(state.selected);\n const next = index === -1 ? 0 : Math.min(items.length - 1, Math.max(0, index + step));\n select(items[next]);\n}\n\nconst filter = document.getElementById(\"filter\");\nfilter.addEventListener(\"input\", () => {\n state.filter = filter.value;\n renderList();\n});\n\ndocument.addEventListener(\"keydown\", (event) => {\n if (event.target === filter) {\n if (event.key === \"Escape\") filter.blur();\n return;\n }\n if (event.metaKey || event.ctrlKey || event.altKey) return;\n const modeIndex = [\"1\", \"2\", \"3\", \"4\"].indexOf(event.key);\n if (event.key === \"j\") move(1);\n else if (event.key === \"k\") move(-1);\n else if (modeIndex !== -1) setMode(MODES[modeIndex][0]);\n else if (event.key === \"f\") setZoom(\"fit\");\n else if (event.key === \"0\") setZoom(\"actual\");\n else if (event.key === \"/\") { event.preventDefault(); filter.focus(); }\n else if (event.key === \" \" && state.mode === \"flip\") {\n event.preventDefault();\n state.flip = state.flip === \"new\" ? \"baseline\" : \"new\";\n renderViewer();\n }\n});\n\nrenderHeader();\nconst first = navigable().find((s) => s.status !== \"unchanged\");\nif (first) select(first);\nelse { renderList(); renderViewer(); }\n`;\n","import { mkdir, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { SnapshotResult } from \"../compare\";\nimport { renderReport } from \"./template\";\n\nexport type ReportMeta = {\n dir: string;\n baselineDir: string;\n engine: string;\n createdAt: string;\n};\n\nexport async function writeReport(\n outDir: string,\n results: SnapshotResult[],\n meta: ReportMeta,\n): Promise<string> {\n await mkdir(outDir, { recursive: true });\n const reportPath = path.join(outDir, \"index.html\");\n await writeFile(reportPath, renderReport(results, meta));\n return reportPath;\n}\n","import { stat } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { InvalidArgumentError } from \"commander\";\nimport { compareDirectories, type SnapshotStatus } from \"../compare\";\nimport { createDiffEngine } from \"../diff/engine\";\nimport { formatCounts } from \"../format\";\nimport { writeReport } from \"../report/write-report\";\n\nexport type CompareCommandOptions = {\n out: string;\n threshold: number;\n includeAa: boolean;\n};\n\nexport async function compareCommand(\n dir: string,\n baselineDir: string,\n options: CompareCommandOptions,\n): Promise<void> {\n await assertDirectory(dir);\n await assertDirectory(baselineDir);\n\n const startedAt = performance.now();\n const engine = await createDiffEngine();\n const results = await compareDirectories({\n dir,\n baselineDir,\n outDir: options.out,\n engine,\n options: { threshold: options.threshold, includeAA: options.includeAa },\n });\n const reportPath = await writeReport(options.out, results, {\n dir,\n baselineDir,\n engine: engine.name,\n createdAt: new Date().toISOString(),\n });\n\n const counts = countByStatus(results.map((result) => result.status));\n const seconds = ((performance.now() - startedAt) / 1000).toFixed(1);\n console.log(`stateofpixel compare ${dir} vs ${baselineDir}`);\n console.log(formatCounts(results.length, counts));\n console.log(` compared with ${engine.name} in ${seconds} s`);\n console.log(` report: ${path.relative(process.cwd(), reportPath)}`);\n}\n\nexport function parseThreshold(value: string): number {\n const threshold = Number(value);\n if (!Number.isFinite(threshold) || threshold < 0 || threshold > 1) {\n throw new InvalidArgumentError(\"Must be a number from 0 to 1.\");\n }\n return threshold;\n}\n\nasync function assertDirectory(dir: string): Promise<void> {\n const stats = await stat(dir).catch(() => undefined);\n if (!stats?.isDirectory()) {\n throw new Error(`Not a directory: ${dir}`);\n }\n}\n\nfunction countByStatus(\n statuses: SnapshotStatus[],\n): Record<SnapshotStatus, number> {\n const counts = { unchanged: 0, changed: 0, added: 0, removed: 0 };\n for (const status of statuses) {\n counts[status]++;\n }\n return counts;\n}\n","import {\n ApiError,\n createApiClient,\n isRateLimited,\n isServerError,\n} from \"../api\";\nimport {\n defaultNonce,\n ForkPullRequestError,\n previousNonces,\n readCiInfo,\n readGitInfo,\n resolveApiUrl,\n resolveToken,\n} from \"../ci-env\";\nimport { formatCounts } from \"../format\";\nimport { ENV } from \"../reference\";\nimport { waitForFinalize } from \"../upload\";\n\nexport type FinalizeCommandOptions = {\n buildName?: string;\n nonce?: string;\n baselineBranch?: string;\n skipIfEmpty: boolean;\n strict: boolean;\n};\n\nexport async function finalizeCommand(\n options: FinalizeCommandOptions,\n): Promise<void> {\n const env = process.env;\n const apiUrl = resolveApiUrl(env);\n const buildName = options.buildName ?? env[ENV.buildName] ?? \"default\";\n const nonce = options.nonce ?? env[ENV.nonce] ?? defaultNonce(env);\n if (nonce === null) {\n throw new Error(\"Set --nonce to the nonce the shards used.\");\n }\n const git = options.skipIfEmpty\n ? await readGitInfo(\n env,\n process.cwd(),\n options.baselineBranch ?? env[ENV.baselineBranch],\n )\n : undefined;\n\n let build: { buildId: string; buildNumber: number; url: string };\n let final: Awaited<ReturnType<typeof waitForFinalize>>;\n try {\n const api = createApiClient({\n baseUrl: apiUrl,\n token: await resolveToken(env),\n refreshToken: env[ENV.token] ? undefined : () => resolveToken(env),\n });\n build = await api.request(\"POST\", \"/builds/finalize\", {\n buildName,\n nonce,\n ...((options.nonce ?? env[ENV.nonce])\n ? {}\n : { previousNonces: previousNonces(env) }),\n skipIfEmpty: options.skipIfEmpty,\n git,\n ci: readCiInfo(env),\n });\n final = await waitForFinalize(api, build.buildId, true);\n } catch (error) {\n if (error instanceof ForkPullRequestError && !options.strict) {\n console.warn(\n `stateofpixel: skipped, ${error.message} Use --strict to fail instead.`,\n );\n return;\n }\n if (isRateLimited(error) && !options.strict) {\n console.warn(\n `stateofpixel: skipped, ${error.message} Use --strict to fail instead.`,\n );\n return;\n }\n if (isServerError(error) && !options.strict) {\n console.warn(\n `stateofpixel: skipped, the service at ${apiUrl} is not reachable (${error instanceof Error ? error.message : error}). Use --strict to fail instead.`,\n );\n return;\n }\n if (error instanceof ApiError) {\n throw new Error(`${error.message} (${error.code}) from ${apiUrl}`);\n }\n throw error;\n }\n\n if (final === null) {\n console.log(`stateofpixel build #${build.buildNumber} finalizing`);\n } else {\n const { counts, shards } = final;\n console.log(\n `stateofpixel build #${build.buildNumber} finalized, ${shards.done} ${shards.done === 1 ? \"shard\" : \"shards\"}`,\n );\n console.log(\n formatCounts(\n counts.unchanged + counts.changed + counts.added + counts.failed,\n counts,\n ),\n );\n }\n console.log(` review: ${build.url}`);\n}\n","import { createReadStream } from \"node:fs\";\nimport {\n mkdir,\n mkdtemp,\n readFile,\n rm,\n stat,\n writeFile,\n} from \"node:fs/promises\";\nimport { createServer, type Server } from \"node:http\";\nimport type { AddressInfo } from \"node:net\";\nimport { tmpdir } from \"node:os\";\nimport path from \"node:path\";\nimport { InvalidArgumentError } from \"commander\";\nimport type { Browser, BrowserContext } from \"playwright\";\nimport { mapConcurrent } from \"../map-concurrent\";\nimport { ENV } from \"../reference\";\nimport { metadataFile, type Shard } from \"../upload\";\nimport { parseShard, type UploadCommandOptions, uploadCommand } from \"./upload\";\n\nconst CAPTURE_CONCURRENCY = 4;\nconst STORY_TIMEOUT_MS = 15_000;\nconst PARAMETERS_TIMEOUT_MS = 60_000;\nconst VIEWPORT_HEIGHT = 720;\n\nconst CONTENT_TYPES: Record<string, string> = {\n \".html\": \"text/html\",\n \".js\": \"text/javascript\",\n \".mjs\": \"text/javascript\",\n \".css\": \"text/css\",\n \".json\": \"application/json\",\n \".svg\": \"image/svg+xml\",\n \".png\": \"image/png\",\n \".jpg\": \"image/jpeg\",\n \".gif\": \"image/gif\",\n \".webp\": \"image/webp\",\n \".woff\": \"font/woff\",\n \".woff2\": \"font/woff2\",\n \".ttf\": \"font/ttf\",\n};\n\nexport type StorybookCommandOptions = UploadCommandOptions & {\n viewports: number[];\n include?: string;\n exclude?: string;\n tag?: string;\n globals?: string;\n chromatic: boolean;\n waitForSelector: string;\n delay: number;\n deviceScaleFactor: number;\n};\n\nexport type ChromaticParameters = {\n disable?: boolean;\n disableSnapshot?: boolean;\n delay?: number;\n viewports?: number[];\n modes?: Record<string, Record<string, unknown>>;\n};\n\nexport type Story = {\n id: string;\n title: string;\n name: string;\n importPath?: string;\n tags?: string[];\n chromatic?: ChromaticParameters;\n namedViewports?: Record<string, number>;\n};\n\nexport type Capture = {\n story: Story;\n name: string;\n width: number;\n globals?: string;\n delay: number;\n};\n\ntype Storybook = { browser: Browser; baseUrl: string };\n\ntype PreviewStore = {\n cacheAllCSFFiles(): Promise<void>;\n extract(): Record<\n string,\n { id: string; tags?: string[]; parameters?: Record<string, unknown> }\n >;\n};\n\ntype StorybookWindow = {\n __STORYBOOK_PREVIEW__?: {\n storyStoreValue?: PreviewStore;\n storyStore?: PreviewStore;\n };\n};\n\nexport async function storybookCommand(\n staticDir: string,\n options: StorybookCommandOptions,\n): Promise<void> {\n const shard = options.shard ?? parseShard(process.env[ENV.shard] ?? \"1/1\");\n if (shard.total === null) {\n throw new Error(\n \"storybook does not split stories with --shard auto. Use --shard i/n, like --shard 2/4.\",\n );\n }\n const indexedStories = await readStories(staticDir);\n const outDir = await mkdtemp(path.join(tmpdir(), \"stateofpixel-storybook-\"));\n try {\n const failed = await withStorybook(staticDir, async (storybook) => {\n const stories = shardStories(\n filterStories(\n options.chromatic\n ? (await readChromaticParameters(storybook, indexedStories)).filter(\n isCapturedByChromatic,\n )\n : indexedStories,\n options.include,\n options.exclude,\n options.tag,\n ),\n shard,\n );\n if (stories.length === 0) {\n throw new Error(`No stories found in ${staticDir}/index.json.`);\n }\n const captures = planCaptures(stories, options);\n const notRendered = await captureStories(\n storybook,\n outDir,\n captures,\n options.waitForSelector,\n options.deviceScaleFactor,\n );\n console.log(\n `stateofpixel captured ${captures.length - notRendered.length} screenshots from ${stories.length} stories`,\n );\n return notRendered;\n });\n if (failed.length > 0) {\n throw new Error(`Stories that did not render:\\n ${failed.join(\"\\n \")}`);\n }\n await uploadCommand(outDir, { ...options, shard });\n } finally {\n await rm(outDir, { recursive: true, force: true });\n }\n}\n\nasync function readStories(staticDir: string): Promise<Story[]> {\n const text = await readFile(path.join(staticDir, \"index.json\"), \"utf8\").catch(\n () => {\n throw new Error(\n `No index.json in ${staticDir}. Run storybook build first, Storybook 7 or newer.`,\n );\n },\n );\n const index = JSON.parse(text) as {\n entries?: Record<string, Story & { type?: string }>;\n };\n return Object.values(index.entries ?? {})\n .filter((entry) => entry.type === \"story\")\n .map(({ id, title, name, importPath, tags }) => ({\n id,\n title,\n name,\n importPath,\n tags,\n }));\n}\n\nexport function filterStories(\n stories: Story[],\n include: string | undefined,\n exclude: string | undefined,\n tag?: string,\n): Story[] {\n const includes = include === undefined ? null : globToRegExp(include);\n const excludes = exclude === undefined ? null : globToRegExp(exclude);\n return stories.filter((story) => {\n const name = `${story.title}/${story.name}`;\n return (\n (includes === null || includes.test(name)) &&\n (excludes === null || !excludes.test(name)) &&\n (tag === undefined || (story.tags ?? []).includes(tag))\n );\n });\n}\n\nexport function shardStories(stories: Story[], shard: Shard): Story[] {\n const { index, total } = shard;\n if (index === null || total === null) {\n return stories;\n }\n return stories.filter((_, position) => position % total === index - 1);\n}\n\nfunction globToRegExp(glob: string): RegExp {\n const source = glob\n .split(\"**\")\n .map((part) =>\n part\n .split(\"*\")\n .map((text) => text.replace(/[.+?^${}()|[\\]\\\\]/g, \"\\\\$&\"))\n .join(\"[^/]*\"),\n )\n .join(\".*\");\n return new RegExp(`^${source}$`);\n}\n\nexport function storySnapshotName(story: Story, width: number): string {\n return `${story.title}/${story.name} [chromium ${width}]`;\n}\n\nexport function storyUrl(\n baseUrl: string,\n story: Story,\n globals: string | undefined,\n): string {\n const url = `${baseUrl}/iframe.html?id=${encodeURIComponent(story.id)}&viewMode=story`;\n return globals === undefined\n ? url\n : `${url}&globals=${encodeURIComponent(globals)}`;\n}\n\nexport function isCapturedByChromatic(story: Story): boolean {\n return (\n story.chromatic?.disable !== true &&\n story.chromatic?.disableSnapshot !== true\n );\n}\n\nexport function planCaptures(\n stories: Story[],\n options: Pick<\n StorybookCommandOptions,\n \"viewports\" | \"globals\" | \"chromatic\" | \"delay\"\n >,\n): Capture[] {\n const defaultWidth = options.viewports[0] as number;\n return stories.flatMap((story) => {\n const chromatic = options.chromatic ? story.chromatic : undefined;\n const delay = chromatic?.delay ?? options.delay;\n const modes = Object.entries(chromatic?.modes ?? {}).filter(\n ([, mode]) => mode.disable !== true,\n );\n if (modes.length > 0) {\n return modes.map(([modeName, { viewport, ...modeGlobals }]) => {\n const label = `${story.title}/${story.name}: mode \"${modeName}\"`;\n return {\n story,\n name: `${story.title}/${story.name} [chromium ${modeName}]`,\n width:\n viewport === undefined\n ? defaultWidth\n : modeWidth(label, story, viewport),\n globals: mergeGlobals(label, options.globals, modeGlobals),\n delay,\n };\n });\n }\n return (chromatic?.viewports ?? options.viewports).map((width) => ({\n story,\n name: storySnapshotName(story, width),\n width,\n globals: options.globals,\n delay,\n }));\n });\n}\n\nfunction modeWidth(label: string, story: Story, viewport: unknown): number {\n const width =\n typeof viewport === \"number\"\n ? viewport\n : typeof viewport === \"string\"\n ? story.namedViewports?.[viewport]\n : undefined;\n if (width === undefined || !Number.isInteger(width) || width < 1) {\n throw new Error(\n `${label} has viewport ${JSON.stringify(viewport)}, which is not a width in pixels.`,\n );\n }\n return width;\n}\n\nfunction mergeGlobals(\n label: string,\n flagGlobals: string | undefined,\n modeGlobals: Record<string, unknown>,\n): string | undefined {\n const globals = new Map(\n (flagGlobals?.split(\";\") ?? []).map((pair) => {\n const [key = \"\", value = \"\"] = pair.split(\":\");\n return [key, value];\n }),\n );\n for (const [key, value] of Object.entries(modeGlobals)) {\n if (key === \"disable\") {\n continue;\n }\n if (typeof value === \"boolean\") {\n globals.set(key, `!${value}`);\n } else if (typeof value === \"string\" || typeof value === \"number\") {\n globals.set(key, String(value));\n } else {\n throw new Error(\n `${label} sets ${key} to a value the CLI cannot pass to Storybook.`,\n );\n }\n }\n return globals.size === 0\n ? undefined\n : [...globals].map(([key, value]) => `${key}:${value}`).join(\";\");\n}\n\nasync function withStorybook<T>(\n staticDir: string,\n run: (storybook: Storybook) => Promise<T>,\n): Promise<T> {\n const { chromium } = await import(\"playwright\").catch(() => {\n throw new Error(\n \"stateofpixel storybook needs Playwright. Run npm install -D playwright, then npx playwright install chromium.\",\n );\n });\n const server = await serveStatic(staticDir);\n const browser = await chromium.launch({ args: [\"--disable-lcd-text\"] });\n try {\n return await run({\n browser,\n baseUrl: `http://127.0.0.1:${(server.address() as AddressInfo).port}`,\n });\n } finally {\n await browser.close();\n server.close();\n }\n}\n\nasync function readChromaticParameters(\n storybook: Storybook,\n stories: Story[],\n): Promise<Story[]> {\n const page = await storybook.browser.newPage();\n try {\n await page.goto(`${storybook.baseUrl}/iframe.html`, {\n timeout: PARAMETERS_TIMEOUT_MS,\n });\n await page.waitForFunction(\n () => {\n const preview = (globalThis as StorybookWindow).__STORYBOOK_PREVIEW__;\n try {\n const store = preview?.storyStoreValue ?? preview?.storyStore;\n return typeof store?.extract === \"function\";\n } catch {\n return false;\n }\n },\n undefined,\n { timeout: PARAMETERS_TIMEOUT_MS },\n );\n const text = await page.evaluate(async () => {\n const preview = (globalThis as StorybookWindow).__STORYBOOK_PREVIEW__;\n const store = (preview?.storyStoreValue ??\n preview?.storyStore) as PreviewStore;\n await store.cacheAllCSFFiles();\n return JSON.stringify(\n Object.values(store.extract()).map(({ id, tags, parameters }) => {\n const viewport = parameters?.viewport as\n | {\n viewports?: Record<string, { styles?: { width?: string } }>;\n options?: Record<string, { styles?: { width?: string } }>;\n }\n | undefined;\n const namedViewports: Record<string, number> = {};\n for (const [name, value] of Object.entries(\n viewport?.viewports ?? viewport?.options ?? {},\n )) {\n const width = /^(\\d+)px$/.exec(value?.styles?.width ?? \"\")?.[1];\n if (width !== undefined) {\n namedViewports[name] = Number(width);\n }\n }\n return { id, tags, chromatic: parameters?.chromatic, namedViewports };\n }),\n );\n });\n const parameters = new Map(\n (JSON.parse(text) as Story[]).map((entry) => [entry.id, entry]),\n );\n return stories.map((story) => {\n const entry = parameters.get(story.id);\n return entry === undefined\n ? story\n : {\n ...story,\n tags: entry.tags,\n chromatic: entry.chromatic,\n namedViewports: entry.namedViewports,\n };\n });\n } catch (error) {\n throw new Error(\n `Could not read story parameters from ${storybook.baseUrl}/iframe.html: ${error instanceof Error ? error.message : error}`,\n );\n } finally {\n await page.close();\n }\n}\n\nasync function captureStories(\n storybook: Storybook,\n outDir: string,\n captures: Capture[],\n waitForSelector: string,\n deviceScaleFactor: number,\n): Promise<string[]> {\n const failed: string[] = [];\n await mapConcurrent(\n captures,\n CAPTURE_CONCURRENCY,\n async ({ story, name, width, globals, delay }) => {\n const file = path.join(outDir, `${name}.png`);\n let context: BrowserContext | undefined;\n try {\n context = await storybook.browser.newContext({\n viewport: { width, height: VIEWPORT_HEIGHT },\n deviceScaleFactor,\n reducedMotion: \"reduce\",\n });\n const page = await context.newPage();\n await page.goto(storyUrl(storybook.baseUrl, story, globals), {\n timeout: STORY_TIMEOUT_MS,\n });\n await page.waitForSelector(waitForSelector, {\n timeout: STORY_TIMEOUT_MS,\n });\n await page.evaluate(\"document.fonts.ready\");\n if (delay > 0) {\n await page.waitForTimeout(delay);\n }\n await mkdir(path.dirname(file), { recursive: true });\n await page.screenshot({\n path: file,\n fullPage: true,\n animations: \"disabled\",\n caret: \"hide\",\n });\n await writeFile(\n metadataFile(file),\n JSON.stringify({\n browser: \"chromium\",\n viewport: width,\n storyId: story.id,\n importPath: story.importPath,\n }),\n );\n } catch (error) {\n const reason = (\n error instanceof Error ? error.message : String(error)\n ).split(\"\\n\")[0];\n failed.push(`${name}: ${reason}`);\n } finally {\n await context?.close();\n }\n },\n );\n return failed;\n}\n\nasync function serveStatic(root: string): Promise<Server> {\n const base = path.resolve(root);\n const server = createServer((request, response) => {\n const pathname = decodeURIComponent(\n new URL(request.url ?? \"/\", \"http://localhost\").pathname,\n );\n const file = path.join(\n base,\n pathname.endsWith(\"/\") ? `${pathname}index.html` : pathname,\n );\n if (!file.startsWith(`${base}${path.sep}`)) {\n response.writeHead(403).end();\n return;\n }\n stat(file).then(\n (stats) => {\n if (!stats.isFile()) {\n response.writeHead(404).end();\n return;\n }\n response.writeHead(200, {\n \"Content-Type\":\n CONTENT_TYPES[path.extname(file).toLowerCase()] ??\n \"application/octet-stream\",\n \"Cache-Control\": \"max-age=3600\",\n });\n createReadStream(file).pipe(response);\n },\n () => response.writeHead(404).end(),\n );\n });\n await new Promise<void>((resolve) => server.listen(0, \"127.0.0.1\", resolve));\n return server;\n}\n\nexport function parseViewports(value: string): number[] {\n const widths = value.split(\",\").map((width) => Number(width.trim()));\n if (\n widths.length === 0 ||\n widths.some((width) => !Number.isInteger(width) || width < 1)\n ) {\n throw new InvalidArgumentError(\"Must look like 375,1280.\");\n }\n return widths;\n}\n\nexport function parseGlobals(value: string): string {\n if (!/^[\\w-]+:[^;:]+(;[\\w-]+:[^;:]+)*$/.test(value)) {\n throw new InvalidArgumentError(\"Must look like brand:new;theme:dark.\");\n }\n return value;\n}\n\nexport function parseDelay(value: string): number {\n const delay = Number(value);\n if (!Number.isInteger(delay) || delay < 0) {\n throw new InvalidArgumentError(\"Must be a whole number of milliseconds.\");\n }\n return delay;\n}\n\nexport function parseDeviceScaleFactor(value: string): number {\n const scale = Number(value);\n if (!Number.isFinite(scale) || scale <= 0) {\n throw new InvalidArgumentError(\"Must be a number above 0, like 2.\");\n }\n return scale;\n}\n","#!/usr/bin/env node\nimport { Command, Option } from \"commander\";\nimport packageJson from \"../package.json\" with { type: \"json\" };\nimport { compareCommand, parseThreshold } from \"./commands/compare\";\nimport { finalizeCommand } from \"./commands/finalize\";\nimport {\n parseDelay,\n parseDeviceScaleFactor,\n parseGlobals,\n parseViewports,\n storybookCommand,\n} from \"./commands/storybook\";\nimport { parseShard, uploadCommand } from \"./commands/upload\";\nimport { COMMANDS, type CommandSpec } from \"./reference\";\n\nconst PARSERS: Record<string, (value: string) => unknown> = {\n \"--threshold <number>\": parseThreshold,\n \"--shard <i/n>\": parseShard,\n \"--viewports <widths>\": parseViewports,\n \"--delay <ms>\": parseDelay,\n \"--globals <globals>\": parseGlobals,\n \"--device-scale-factor <n>\": parseDeviceScaleFactor,\n};\n\nconst ACTIONS: Record<CommandSpec[\"name\"], Parameters<Command[\"action\"]>[0]> = {\n compare: compareCommand,\n upload: uploadCommand,\n storybook: storybookCommand,\n finalize: finalizeCommand,\n};\n\nconst program = new Command()\n .name(\"stateofpixel\")\n .description(\"Catch UI regressions before they merge\")\n .version(packageJson.version);\n\nfor (const spec of COMMANDS) {\n const command = program.command(spec.name).description(spec.description);\n for (const argument of spec.arguments) {\n command.argument(argument.name, argument.description);\n }\n for (const option of spec.options) {\n const commanderOption = new Option(option.flags, option.description);\n const parse = PARSERS[option.flags];\n if (parse !== undefined) {\n commanderOption.argParser(parse);\n }\n if (option.default !== undefined) {\n commanderOption.default(option.default);\n }\n command.addOption(commanderOption);\n }\n command.action(ACTIONS[spec.name]);\n}\n\ntry {\n await program.parseAsync();\n} catch (error) {\n console.error(\n `stateofpixel: ${error instanceof Error ? error.message : error}`,\n );\n process.exitCode = 1;\n}\n"],"mappings":"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"}
|