stateofpixel 1.7.0 → 1.9.0
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/README.md
CHANGED
|
@@ -15,7 +15,7 @@ The Free plan needs no card. Paid plans are priced by storage only. See [pricing
|
|
|
15
15
|
|
|
16
16
|
## GitHub Actions
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Sign in on [stateofpixel.com](https://stateofpixel.com), connect GitHub and install the GitHub App, then add a step after the one that writes your screenshots:
|
|
19
19
|
|
|
20
20
|
```yaml
|
|
21
21
|
on:
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { C as mapConcurrent, S as sha256, _ as isServerError, a as ForkPullRequestError, b as collectSnapshots, c as readCiInfo, d as
|
|
2
|
+
import { C as mapConcurrent, S as sha256, _ as isServerError, a as ForkPullRequestError, b as collectSnapshots, c as readCiInfo, d as resolveToken, f as COMMANDS, g as isRateLimited, h as createApiClient, i as waitForFinalize, l as readGitInfo, m as ApiError, n as uploadCommand, o as defaultNonce, p as ENV, r as metadataFile, s as previousNonces, t as parseShard, u as resolveApiUrl, v as formatCounts, x as readPngSize, y as createDiffEngine } from "./upload-CER-OH3Q.mjs";
|
|
3
3
|
import { Command, InvalidArgumentError, Option } from "commander";
|
|
4
4
|
import { copyFile, mkdir, mkdtemp, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
@@ -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
|
+
var version = "1.9.0";
|
|
12
12
|
|
|
13
13
|
//#endregion
|
|
14
14
|
//#region src/compare.ts
|
|
@@ -503,6 +503,7 @@ function countByStatus(statuses) {
|
|
|
503
503
|
//#region src/commands/finalize.ts
|
|
504
504
|
async function finalizeCommand(options) {
|
|
505
505
|
const env = process.env;
|
|
506
|
+
const apiUrl = resolveApiUrl(env);
|
|
506
507
|
const buildName = options.buildName ?? env[ENV.buildName] ?? "default";
|
|
507
508
|
const nonce = options.nonce ?? env[ENV.nonce] ?? defaultNonce(env);
|
|
508
509
|
if (nonce === null) throw new Error("Set --nonce to the nonce the shards used.");
|
|
@@ -511,7 +512,7 @@ async function finalizeCommand(options) {
|
|
|
511
512
|
let final;
|
|
512
513
|
try {
|
|
513
514
|
const api = createApiClient({
|
|
514
|
-
baseUrl:
|
|
515
|
+
baseUrl: apiUrl,
|
|
515
516
|
token: await resolveToken(env),
|
|
516
517
|
refreshToken: env[ENV.token] ? void 0 : () => resolveToken(env)
|
|
517
518
|
});
|
|
@@ -534,10 +535,10 @@ async function finalizeCommand(options) {
|
|
|
534
535
|
return;
|
|
535
536
|
}
|
|
536
537
|
if (isServerError(error) && !options.strict) {
|
|
537
|
-
console.warn(`stateofpixel: skipped, the service is not reachable (${error instanceof Error ? error.message : error}). Use --strict to fail instead.`);
|
|
538
|
+
console.warn(`stateofpixel: skipped, the service at ${apiUrl} is not reachable (${error instanceof Error ? error.message : error}). Use --strict to fail instead.`);
|
|
538
539
|
return;
|
|
539
540
|
}
|
|
540
|
-
if (error instanceof ApiError) throw new Error(`${error.message} (${error.code})`);
|
|
541
|
+
if (error instanceof ApiError) throw new Error(`${error.message} (${error.code}) from ${apiUrl}`);
|
|
541
542
|
throw error;
|
|
542
543
|
}
|
|
543
544
|
if (final === null) console.log(`stateofpixel build #${build.buildNumber} finalizing`);
|
|
@@ -572,7 +573,7 @@ const CONTENT_TYPES = {
|
|
|
572
573
|
async function storybookCommand(staticDir, options) {
|
|
573
574
|
const shard = options.shard ?? parseShard(process.env[ENV.shard] ?? "1/1");
|
|
574
575
|
if (shard.total === null) throw new Error("storybook does not split stories with --shard auto. Use --shard i/n, like --shard 2/4.");
|
|
575
|
-
const stories = shardStories(filterStories(await readStories(staticDir), options.include, options.exclude), shard);
|
|
576
|
+
const stories = shardStories(filterStories(await readStories(staticDir), options.include, options.exclude, options.tag), shard);
|
|
576
577
|
if (stories.length === 0) throw new Error(`No stories found in ${staticDir}/index.json.`);
|
|
577
578
|
const outDir = await mkdtemp(path.join(tmpdir(), "stateofpixel-storybook-"));
|
|
578
579
|
try {
|
|
@@ -594,19 +595,20 @@ async function readStories(staticDir) {
|
|
|
594
595
|
throw new Error(`No index.json in ${staticDir}. Run storybook build first, Storybook 7 or newer.`);
|
|
595
596
|
});
|
|
596
597
|
const index = JSON.parse(text);
|
|
597
|
-
return Object.values(index.entries ?? {}).filter((entry) => entry.type === "story").map(({ id, title, name, importPath }) => ({
|
|
598
|
+
return Object.values(index.entries ?? {}).filter((entry) => entry.type === "story").map(({ id, title, name, importPath, tags }) => ({
|
|
598
599
|
id,
|
|
599
600
|
title,
|
|
600
601
|
name,
|
|
601
|
-
importPath
|
|
602
|
+
importPath,
|
|
603
|
+
tags
|
|
602
604
|
}));
|
|
603
605
|
}
|
|
604
|
-
function filterStories(stories, include, exclude) {
|
|
606
|
+
function filterStories(stories, include, exclude, tag) {
|
|
605
607
|
const includes = include === void 0 ? null : globToRegExp(include);
|
|
606
608
|
const excludes = exclude === void 0 ? null : globToRegExp(exclude);
|
|
607
609
|
return stories.filter((story) => {
|
|
608
610
|
const name = `${story.title}/${story.name}`;
|
|
609
|
-
return (includes === null || includes.test(name)) && (excludes === null || !excludes.test(name));
|
|
611
|
+
return (includes === null || includes.test(name)) && (excludes === null || !excludes.test(name)) && (tag === void 0 || (story.tags ?? []).includes(tag));
|
|
610
612
|
});
|
|
611
613
|
}
|
|
612
614
|
function shardStories(stories, shard) {
|
|
@@ -621,6 +623,10 @@ function globToRegExp(glob) {
|
|
|
621
623
|
function storySnapshotName(story, width) {
|
|
622
624
|
return `${story.title}/${story.name} [chromium ${width}]`;
|
|
623
625
|
}
|
|
626
|
+
function storyUrl(baseUrl, story, globals) {
|
|
627
|
+
const url = `${baseUrl}/iframe.html?id=${encodeURIComponent(story.id)}&viewMode=story`;
|
|
628
|
+
return globals === void 0 ? url : `${url}&globals=${encodeURIComponent(globals)}`;
|
|
629
|
+
}
|
|
624
630
|
async function captureStories(staticDir, outDir, stories, options) {
|
|
625
631
|
const { chromium } = await import("playwright").catch(() => {
|
|
626
632
|
throw new Error("stateofpixel storybook needs Playwright. Run npm install -D playwright, then npx playwright install chromium.");
|
|
@@ -644,7 +650,7 @@ async function captureStories(staticDir, outDir, stories, options) {
|
|
|
644
650
|
const file = path.join(outDir, `${name}.png`);
|
|
645
651
|
const page = await context.newPage();
|
|
646
652
|
try {
|
|
647
|
-
await page.goto(
|
|
653
|
+
await page.goto(storyUrl(baseUrl, story, options.globals), { timeout: STORY_TIMEOUT_MS });
|
|
648
654
|
await page.waitForSelector(options.waitForSelector, { timeout: STORY_TIMEOUT_MS });
|
|
649
655
|
await page.evaluate("document.fonts.ready");
|
|
650
656
|
if (options.delay > 0) await page.waitForTimeout(options.delay);
|
|
@@ -705,6 +711,10 @@ function parseViewports(value) {
|
|
|
705
711
|
if (widths.length === 0 || widths.some((width) => !Number.isInteger(width) || width < 1)) throw new InvalidArgumentError("Must look like 375,1280.");
|
|
706
712
|
return widths;
|
|
707
713
|
}
|
|
714
|
+
function parseGlobals(value) {
|
|
715
|
+
if (!/^[\w-]+:[^;:]+(;[\w-]+:[^;:]+)*$/.test(value)) throw new InvalidArgumentError("Must look like brand:new;theme:dark.");
|
|
716
|
+
return value;
|
|
717
|
+
}
|
|
708
718
|
function parseDelay(value) {
|
|
709
719
|
const delay = Number(value);
|
|
710
720
|
if (!Number.isInteger(delay) || delay < 0) throw new InvalidArgumentError("Must be a whole number of milliseconds.");
|
|
@@ -717,7 +727,8 @@ const PARSERS = {
|
|
|
717
727
|
"--threshold <number>": parseThreshold,
|
|
718
728
|
"--shard <i/n>": parseShard,
|
|
719
729
|
"--viewports <widths>": parseViewports,
|
|
720
|
-
"--delay <ms>": parseDelay
|
|
730
|
+
"--delay <ms>": parseDelay,
|
|
731
|
+
"--globals <globals>": parseGlobals
|
|
721
732
|
};
|
|
722
733
|
const ACTIONS = {
|
|
723
734
|
compare: compareCommand,
|
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 DEFAULT_API_URL,\n isRateLimited,\n isServerError,\n} from \"../api\";\nimport {\n defaultNonce,\n ForkPullRequestError,\n previousNonces,\n readCiInfo,\n readGitInfo,\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 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: env.STATEOFPIXEL_API_URL ?? DEFAULT_API_URL,\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 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})`);\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 { 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 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 waitForSelector: string;\n delay: number;\n};\n\nexport type Story = {\n id: string;\n title: string;\n name: string;\n importPath?: string;\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 stories = shardStories(\n filterStories(\n await readStories(staticDir),\n options.include,\n options.exclude,\n ),\n shard,\n );\n if (stories.length === 0) {\n throw new Error(`No stories found in ${staticDir}/index.json.`);\n }\n const outDir = await mkdtemp(path.join(tmpdir(), \"stateofpixel-storybook-\"));\n try {\n const failed = await captureStories(staticDir, outDir, stories, options);\n if (failed.length > 0) {\n throw new Error(`Stories that did not render: ${failed.join(\", \")}`);\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 }) => ({\n id,\n title,\n name,\n importPath,\n }));\n}\n\nexport function filterStories(\n stories: Story[],\n include: string | undefined,\n exclude: string | undefined,\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 );\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\nasync function captureStories(\n staticDir: string,\n outDir: string,\n stories: Story[],\n options: StorybookCommandOptions,\n): Promise<string[]> {\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 baseUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;\n const browser = await chromium.launch();\n const failed: string[] = [];\n try {\n for (const width of options.viewports) {\n const context = await browser.newContext({\n viewport: { width, height: VIEWPORT_HEIGHT },\n deviceScaleFactor: 1,\n reducedMotion: \"reduce\",\n });\n await mapConcurrent(stories, CAPTURE_CONCURRENCY, async (story) => {\n const name = storySnapshotName(story, width);\n const file = path.join(outDir, `${name}.png`);\n const page = await context.newPage();\n try {\n await page.goto(\n `${baseUrl}/iframe.html?id=${encodeURIComponent(story.id)}&viewMode=story`,\n { timeout: STORY_TIMEOUT_MS },\n );\n await page.waitForSelector(options.waitForSelector, {\n timeout: STORY_TIMEOUT_MS,\n });\n await page.evaluate(\"document.fonts.ready\");\n if (options.delay > 0) {\n await page.waitForTimeout(options.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 {\n failed.push(name);\n } finally {\n await page.close();\n }\n });\n await context.close();\n }\n } finally {\n await browser.close();\n server.close();\n }\n console.log(\n `stateofpixel captured ${stories.length * options.viewports.length - failed.length} screenshots from ${stories.length} stories`,\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 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","#!/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 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};\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":";;;;;;;;;;;;;;ACkCA,eAAsB,mBAAmB,EACvC,KACA,aACA,QACA,QACA,WACqD;CACrD,MAAM,CAAC,WAAW,aAAa,MAAM,QAAQ,IAAI,CAC/C,iBAAiB,GAAG,GACpB,iBAAiB,WAAW,CAC9B,CAAC;CACD,MAAM,YAAY,KAAK,KAAK,QAAQ,QAAQ;CAC5C,MAAM,UAAU,KAAK,KAAK,QAAQ,OAAO;CACzC,MAAM,MAAM,WAAW,EAAE,WAAW,KAAK,CAAC;CAC1C,MAAM,MAAM,SAAS,EAAE,WAAW,KAAK,CAAC;CAExC,MAAM,QAAQ,CAAC,mBAAG,IAAI,IAAI,CAAC,GAAG,UAAU,KAAK,GAAG,GAAG,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,MACpE,GAAG,MAAO,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,CACtC;CAEA,MAAM,UAAU,MAAM,cACpB,OACA,qBAAqB,GACrB,OAAO,MAAM,UAAmC;EAC9C,MAAM,WAAW,UAAU,IAAI,IAAI;EACnC,MAAM,eAAe,UAAU,IAAI,IAAI;EAEvC,IAAI,CAAC,YAAY,cACf,OAAO;GACL;GACA,QAAQ;GACR,eAAe,MAAM,WAAW,cAAc,SAAS;EACzD;EAEF,IAAI,CAAC,UACH,MAAM,IAAI,MAAM,YAAY,KAAK,aAAa;EAGhD,MAAM,QAAQ,MAAM,WAAW,UAAU,SAAS;EAClD,IAAI,CAAC,cACH,OAAO;GAAE;GAAM,QAAQ;GAAS;EAAM;EAGxC,MAAM,gBAAgB,MAAM,WAAW,cAAc,SAAS;EAC9D,IAAI,MAAM,SAAS,cAAc,MAC/B,OAAO;GAAE;GAAM,QAAQ;GAAa;GAAO;EAAc;EAG3D,MAAM,WAAW,KAAK,KAAK,SAAS,GAAG,MAAM,KAAK;EAClD,MAAM,EAAE,YAAY,cAAc,MAAM,OAAO,KAC7C,cACA,UACA,UACA,OACF;EACA,IAAI,eAAe,GAAG;GACpB,MAAM,GAAG,UAAU,EAAE,OAAO,KAAK,CAAC;GAClC,OAAO;IAAE;IAAM,QAAQ;IAAa;IAAO;GAAc;EAC3D;EAEA,OAAO;GACL;GACA,QAAQ;GACR;GACA;GACA,WAAW,MAAM,WAAW,UAAU,WAAW,EAAE,MAAM,KAAK,CAAC;GAC/D;GACA;EACF;CACF,CACF;CAEA,MAAM,GAAG,SAAS;EAAE,WAAW;EAAM,OAAO;CAAK,CAAC;CAClD,OAAO;AACT;AAEA,eAAe,WACb,UACA,WACA,EAAE,OAAO,UAAU,CAAC,GACD;CACnB,MAAM,QAAQ,MAAM,SAAS,QAAQ;CACrC,MAAM,OAAO,UAAU,OAAO,KAAK,EAAE;CACrC,MAAM,SAAS,KAAK,KAAK,WAAW,KAAK,SAAS,IAAI,CAAC;CACvD,IAAI,MACF,MAAM,OAAO,UAAU,MAAM;MAE7B,MAAM,SAAS,UAAU,MAAM;CAEjC,OAAO;EAAE;EAAM,GAAG,YAAY,KAAK;CAAE;AACvC;;;;ACzHA,SAAgB,aACd,WACA,MACQ;CACR,MAAM,OAAO,KAAK,UAAU;EAAE;EAAM;CAAU,CAAC,CAAC,CAAC,QAAQ,MAAM,SAAS;CAExE,OAAO;;;;;;SAMA,OAAO;;;;;;;;;;;;;;4CAc4B,KAAK;UACvC,OAAO;;;;AAIjB;AAEA,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8Ef,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACtGf,eAAsB,YACpB,QACA,SACA,MACiB;CACjB,MAAM,MAAM,QAAQ,EAAE,WAAW,KAAK,CAAC;CACvC,MAAM,aAAa,KAAK,KAAK,QAAQ,YAAY;CACjD,MAAM,UAAU,YAAY,aAAa,SAAS,IAAI,CAAC;CACvD,OAAO;AACT;;;;ACPA,eAAsB,eACpB,KACA,aACA,SACe;CACf,MAAM,gBAAgB,GAAG;CACzB,MAAM,gBAAgB,WAAW;CAEjC,MAAM,YAAY,YAAY,IAAI;CAClC,MAAM,SAAS,MAAM,iBAAiB;CACtC,MAAM,UAAU,MAAM,mBAAmB;EACvC;EACA;EACA,QAAQ,QAAQ;EAChB;EACA,SAAS;GAAE,WAAW,QAAQ;GAAW,WAAW,QAAQ;EAAU;CACxE,CAAC;CACD,MAAM,aAAa,MAAM,YAAY,QAAQ,KAAK,SAAS;EACzD;EACA;EACA,QAAQ,OAAO;EACf,4BAAW,IAAI,KAAK,EAAC,CAAC,YAAY;CACpC,CAAC;CAED,MAAM,SAAS,cAAc,QAAQ,KAAK,WAAW,OAAO,MAAM,CAAC;CACnE,MAAM,YAAY,YAAY,IAAI,IAAI,aAAa,IAAI,CAAE,QAAQ,CAAC;CAClE,QAAQ,IAAI,0BAA0B,IAAI,MAAM,aAAa;CAC7D,QAAQ,IAAI,aAAa,QAAQ,QAAQ,MAAM,CAAC;CAChD,QAAQ,IAAI,mBAAmB,OAAO,KAAK,MAAM,QAAQ,GAAG;CAC5D,QAAQ,IAAI,aAAa,KAAK,SAAS,QAAQ,IAAI,GAAG,UAAU,GAAG;AACrE;AAEA,SAAgB,eAAe,OAAuB;CACpD,MAAM,YAAY,OAAO,KAAK;CAC9B,IAAI,CAAC,OAAO,SAAS,SAAS,KAAK,YAAY,KAAK,YAAY,GAC9D,MAAM,IAAI,qBAAqB,+BAA+B;CAEhE,OAAO;AACT;AAEA,eAAe,gBAAgB,KAA4B;CAEzD,IAAI,EAAC,MADe,KAAK,GAAG,CAAC,CAAC,YAAY,MAAS,EACzC,EAAE,YAAY,GACtB,MAAM,IAAI,MAAM,oBAAoB,KAAK;AAE7C;AAEA,SAAS,cACP,UACgC;CAChC,MAAM,SAAS;EAAE,WAAW;EAAG,SAAS;EAAG,OAAO;EAAG,SAAS;CAAE;CAChE,KAAK,MAAM,UAAU,UACnB,OAAO,OAAO;CAEhB,OAAO;AACT;;;;AC1CA,eAAsB,gBACpB,SACe;CACf,MAAM,MAAM,QAAQ;CACpB,MAAM,YAAY,QAAQ,aAAa,IAAI,IAAI,cAAc;CAC7D,MAAM,QAAQ,QAAQ,SAAS,IAAI,IAAI,UAAU,aAAa,GAAG;CACjE,IAAI,UAAU,MACZ,MAAM,IAAI,MAAM,2CAA2C;CAE7D,MAAM,MAAM,QAAQ,cAChB,MAAM,YACJ,KACA,QAAQ,IAAI,GACZ,QAAQ,kBAAkB,IAAI,IAAI,eACpC,IACA;CAEJ,IAAI;CACJ,IAAI;CACJ,IAAI;EACF,MAAM,MAAM,gBAAgB;GAC1B,SAAS,IAAI;GACb,OAAO,MAAM,aAAa,GAAG;GAC7B,cAAc,IAAI,IAAI,SAAS,eAAkB,aAAa,GAAG;EACnE,CAAC;EACD,QAAQ,MAAM,IAAI,QAAQ,QAAQ,oBAAoB;GACpD;GACA;GACA,GAAK,QAAQ,SAAS,IAAI,IAAI,SAC1B,CAAC,IACD,EAAE,gBAAgB,eAAe,GAAG,EAAE;GAC1C,aAAa,QAAQ;GACrB;GACA,IAAI,WAAW,GAAG;EACpB,CAAC;EACD,QAAQ,MAAM,gBAAgB,KAAK,MAAM,SAAS,IAAI;CACxD,SAAS,OAAO;EACd,IAAI,iBAAiB,wBAAwB,CAAC,QAAQ,QAAQ;GAC5D,QAAQ,KACN,0BAA0B,MAAM,QAAQ,+BAC1C;GACA;EACF;EACA,IAAI,cAAc,KAAK,KAAK,CAAC,QAAQ,QAAQ;GAC3C,QAAQ,KACN,0BAA0B,MAAM,QAAQ,+BAC1C;GACA;EACF;EACA,IAAI,cAAc,KAAK,KAAK,CAAC,QAAQ,QAAQ;GAC3C,QAAQ,KACN,wDAAwD,iBAAiB,QAAQ,MAAM,UAAU,MAAM,iCACzG;GACA;EACF;EACA,IAAI,iBAAiB,UACnB,MAAM,IAAI,MAAM,GAAG,MAAM,QAAQ,IAAI,MAAM,KAAK,EAAE;EAEpD,MAAM;CACR;CAEA,IAAI,UAAU,MACZ,QAAQ,IAAI,wBAAwB,MAAM,YAAY,aAAa;MAC9D;EACL,MAAM,EAAE,QAAQ,WAAW;EAC3B,QAAQ,IACN,wBAAwB,MAAM,YAAY,eAAe,OAAO,KAAK,GAAG,OAAO,SAAS,IAAI,UAAU,UACxG;EACA,QAAQ,IACN,aACE,OAAO,YAAY,OAAO,UAAU,OAAO,QAAQ,OAAO,QAC1D,MACF,CACF;CACF;CACA,QAAQ,IAAI,aAAa,MAAM,KAAK;AACtC;;;;ACpFA,MAAM,sBAAsB;AAC5B,MAAM,mBAAmB;AACzB,MAAM,kBAAkB;AAExB,MAAM,gBAAwC;CAC5C,SAAS;CACT,OAAO;CACP,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,QAAQ;CACR,SAAS;CACT,SAAS;CACT,UAAU;CACV,QAAQ;AACV;AAiBA,eAAsB,iBACpB,WACA,SACe;CACf,MAAM,QAAQ,QAAQ,SAAS,WAAW,QAAQ,IAAI,IAAI,UAAU,KAAK;CACzE,IAAI,MAAM,UAAU,MAClB,MAAM,IAAI,MACR,wFACF;CAEF,MAAM,UAAU,aACd,cACE,MAAM,YAAY,SAAS,GAC3B,QAAQ,SACR,QAAQ,OACV,GACA,KACF;CACA,IAAI,QAAQ,WAAW,GACrB,MAAM,IAAI,MAAM,uBAAuB,UAAU,aAAa;CAEhE,MAAM,SAAS,MAAM,QAAQ,KAAK,KAAK,OAAO,GAAG,yBAAyB,CAAC;CAC3E,IAAI;EACF,MAAM,SAAS,MAAM,eAAe,WAAW,QAAQ,SAAS,OAAO;EACvE,IAAI,OAAO,SAAS,GAClB,MAAM,IAAI,MAAM,gCAAgC,OAAO,KAAK,IAAI,GAAG;EAErE,MAAM,cAAc,QAAQ;GAAE,GAAG;GAAS;EAAM,CAAC;CACnD,UAAU;EACR,MAAM,GAAG,QAAQ;GAAE,WAAW;GAAM,OAAO;EAAK,CAAC;CACnD;AACF;AAEA,eAAe,YAAY,WAAqC;CAC9D,MAAM,OAAO,MAAM,SAAS,KAAK,KAAK,WAAW,YAAY,GAAG,MAAM,CAAC,CAAC,YAChE;EACJ,MAAM,IAAI,MACR,oBAAoB,UAAU,mDAChC;CACF,CACF;CACA,MAAM,QAAQ,KAAK,MAAM,IAAI;CAG7B,OAAO,OAAO,OAAO,MAAM,WAAW,CAAC,CAAC,CAAC,CACtC,QAAQ,UAAU,MAAM,SAAS,OAAO,CAAC,CACzC,KAAK,EAAE,IAAI,OAAO,MAAM,kBAAkB;EACzC;EACA;EACA;EACA;CACF,EAAE;AACN;AAEA,SAAgB,cACd,SACA,SACA,SACS;CACT,MAAM,WAAW,YAAY,SAAY,OAAO,aAAa,OAAO;CACpE,MAAM,WAAW,YAAY,SAAY,OAAO,aAAa,OAAO;CACpE,OAAO,QAAQ,QAAQ,UAAU;EAC/B,MAAM,OAAO,GAAG,MAAM,MAAM,GAAG,MAAM;EACrC,QACG,aAAa,QAAQ,SAAS,KAAK,IAAI,OACvC,aAAa,QAAQ,CAAC,SAAS,KAAK,IAAI;CAE7C,CAAC;AACH;AAEA,SAAgB,aAAa,SAAkB,OAAuB;CACpE,MAAM,EAAE,OAAO,UAAU;CACzB,IAAI,UAAU,QAAQ,UAAU,MAC9B,OAAO;CAET,OAAO,QAAQ,QAAQ,GAAG,aAAa,WAAW,UAAU,QAAQ,CAAC;AACvE;AAEA,SAAS,aAAa,MAAsB;CAC1C,MAAM,SAAS,KACZ,MAAM,IAAI,CAAC,CACX,KAAK,SACJ,KACG,MAAM,GAAG,CAAC,CACV,KAAK,SAAS,KAAK,QAAQ,sBAAsB,MAAM,CAAC,CAAC,CACzD,KAAK,OAAO,CACjB,CAAC,CACA,KAAK,IAAI;CACZ,OAAO,IAAI,OAAO,IAAI,OAAO,EAAE;AACjC;AAEA,SAAgB,kBAAkB,OAAc,OAAuB;CACrE,OAAO,GAAG,MAAM,MAAM,GAAG,MAAM,KAAK,aAAa,MAAM;AACzD;AAEA,eAAe,eACb,WACA,QACA,SACA,SACmB;CACnB,MAAM,EAAE,aAAa,MAAM,OAAO,aAAa,CAAC,YAAY;EAC1D,MAAM,IAAI,MACR,+GACF;CACF,CAAC;CACD,MAAM,SAAS,MAAM,YAAY,SAAS;CAC1C,MAAM,UAAU,oBAAqB,OAAO,QAAQ,CAAC,CAAiB;CACtE,MAAM,UAAU,MAAM,SAAS,OAAO;CACtC,MAAM,SAAmB,CAAC;CAC1B,IAAI;EACF,KAAK,MAAM,SAAS,QAAQ,WAAW;GACrC,MAAM,UAAU,MAAM,QAAQ,WAAW;IACvC,UAAU;KAAE;KAAO,QAAQ;IAAgB;IAC3C,mBAAmB;IACnB,eAAe;GACjB,CAAC;GACD,MAAM,cAAc,SAAS,qBAAqB,OAAO,UAAU;IACjE,MAAM,OAAO,kBAAkB,OAAO,KAAK;IAC3C,MAAM,OAAO,KAAK,KAAK,QAAQ,GAAG,KAAK,KAAK;IAC5C,MAAM,OAAO,MAAM,QAAQ,QAAQ;IACnC,IAAI;KACF,MAAM,KAAK,KACT,GAAG,QAAQ,kBAAkB,mBAAmB,MAAM,EAAE,EAAE,kBAC1D,EAAE,SAAS,iBAAiB,CAC9B;KACA,MAAM,KAAK,gBAAgB,QAAQ,iBAAiB,EAClD,SAAS,iBACX,CAAC;KACD,MAAM,KAAK,SAAS,sBAAsB;KAC1C,IAAI,QAAQ,QAAQ,GAClB,MAAM,KAAK,eAAe,QAAQ,KAAK;KAEzC,MAAM,MAAM,KAAK,QAAQ,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;KACnD,MAAM,KAAK,WAAW;MACpB,MAAM;MACN,UAAU;MACV,YAAY;MACZ,OAAO;KACT,CAAC;KACD,MAAM,UACJ,aAAa,IAAI,GACjB,KAAK,UAAU;MACb,SAAS;MACT,UAAU;MACV,SAAS,MAAM;MACf,YAAY,MAAM;KACpB,CAAC,CACH;IACF,QAAQ;KACN,OAAO,KAAK,IAAI;IAClB,UAAU;KACR,MAAM,KAAK,MAAM;IACnB;GACF,CAAC;GACD,MAAM,QAAQ,MAAM;EACtB;CACF,UAAU;EACR,MAAM,QAAQ,MAAM;EACpB,OAAO,MAAM;CACf;CACA,QAAQ,IACN,0BAA0B,QAAQ,SAAS,QAAQ,UAAU,SAAS,OAAO,OAAO,oBAAoB,QAAQ,OAAO,SACzH;CACA,OAAO;AACT;AAEA,eAAe,YAAY,MAA+B;CACxD,MAAM,OAAO,KAAK,QAAQ,IAAI;CAC9B,MAAM,SAAS,cAAc,SAAS,aAAa;EACjD,MAAM,WAAW,mBACf,IAAI,IAAI,QAAQ,OAAO,KAAK,kBAAkB,CAAC,CAAC,QAClD;EACA,MAAM,OAAO,KAAK,KAChB,MACA,SAAS,SAAS,GAAG,IAAI,GAAG,SAAS,cAAc,QACrD;EACA,IAAI,CAAC,KAAK,WAAW,GAAG,OAAO,KAAK,KAAK,GAAG;GAC1C,SAAS,UAAU,GAAG,CAAC,CAAC,IAAI;GAC5B;EACF;EACA,KAAK,IAAI,CAAC,CAAC,MACR,UAAU;GACT,IAAI,CAAC,MAAM,OAAO,GAAG;IACnB,SAAS,UAAU,GAAG,CAAC,CAAC,IAAI;IAC5B;GACF;GACA,SAAS,UAAU,KAAK;IACtB,gBACE,cAAc,KAAK,QAAQ,IAAI,CAAC,CAAC,YAAY,MAC7C;IACF,iBAAiB;GACnB,CAAC;GACD,iBAAiB,IAAI,CAAC,CAAC,KAAK,QAAQ;EACtC,SACM,SAAS,UAAU,GAAG,CAAC,CAAC,IAAI,CACpC;CACF,CAAC;CACD,MAAM,IAAI,SAAe,YAAY,OAAO,OAAO,GAAG,aAAa,OAAO,CAAC;CAC3E,OAAO;AACT;AAEA,SAAgB,eAAe,OAAyB;CACtD,MAAM,SAAS,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,UAAU,OAAO,MAAM,KAAK,CAAC,CAAC;CACnE,IACE,OAAO,WAAW,KAClB,OAAO,MAAM,UAAU,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,CAAC,GAE5D,MAAM,IAAI,qBAAqB,0BAA0B;CAE3D,OAAO;AACT;AAEA,SAAgB,WAAW,OAAuB;CAChD,MAAM,QAAQ,OAAO,KAAK;CAC1B,IAAI,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,GACtC,MAAM,IAAI,qBAAqB,yCAAyC;CAE1E,OAAO;AACT;;;;ACpQA,MAAM,UAAsD;CAC1D,wBAAwB;CACxB,iBAAiB;CACjB,wBAAwB;CACxB,gBAAgB;AAClB;AAEA,MAAM,UAAyE;CAC7E,SAAS;CACT,QAAQ;CACR,WAAW;CACX,UAAU;AACZ;AAEA,MAAM,UAAU,IAAI,QAAQ,CAAC,CAC1B,KAAK,cAAc,CAAC,CACpB,YAAY,wCAAwC,CAAC,CACrD,QAAQA,OAAmB;AAE9B,KAAK,MAAM,QAAQ,UAAU;CAC3B,MAAM,UAAU,QAAQ,QAAQ,KAAK,IAAI,CAAC,CAAC,YAAY,KAAK,WAAW;CACvE,KAAK,MAAM,YAAY,KAAK,WAC1B,QAAQ,SAAS,SAAS,MAAM,SAAS,WAAW;CAEtD,KAAK,MAAM,UAAU,KAAK,SAAS;EACjC,MAAM,kBAAkB,IAAI,OAAO,OAAO,OAAO,OAAO,WAAW;EACnE,MAAM,QAAQ,QAAQ,OAAO;EAC7B,IAAI,UAAU,QACZ,gBAAgB,UAAU,KAAK;EAEjC,IAAI,OAAO,YAAY,QACrB,gBAAgB,QAAQ,OAAO,OAAO;EAExC,QAAQ,UAAU,eAAe;CACnC;CACA,QAAQ,OAAO,QAAQ,KAAK,KAAK;AACnC;AAEA,IAAI;CACF,MAAM,QAAQ,WAAW;AAC3B,SAAS,OAAO;CACd,QAAQ,MACN,iBAAiB,iBAAiB,QAAQ,MAAM,UAAU,OAC5D;CACA,QAAQ,WAAW;AACrB"}
|
|
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 { 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 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 waitForSelector: string;\n delay: number;\n};\n\nexport type Story = {\n id: string;\n title: string;\n name: string;\n importPath?: string;\n tags?: string[];\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 stories = shardStories(\n filterStories(\n await readStories(staticDir),\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 outDir = await mkdtemp(path.join(tmpdir(), \"stateofpixel-storybook-\"));\n try {\n const failed = await captureStories(staticDir, outDir, stories, options);\n if (failed.length > 0) {\n throw new Error(`Stories that did not render: ${failed.join(\", \")}`);\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\nasync function captureStories(\n staticDir: string,\n outDir: string,\n stories: Story[],\n options: StorybookCommandOptions,\n): Promise<string[]> {\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 baseUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;\n const browser = await chromium.launch();\n const failed: string[] = [];\n try {\n for (const width of options.viewports) {\n const context = await browser.newContext({\n viewport: { width, height: VIEWPORT_HEIGHT },\n deviceScaleFactor: 1,\n reducedMotion: \"reduce\",\n });\n await mapConcurrent(stories, CAPTURE_CONCURRENCY, async (story) => {\n const name = storySnapshotName(story, width);\n const file = path.join(outDir, `${name}.png`);\n const page = await context.newPage();\n try {\n await page.goto(storyUrl(baseUrl, story, options.globals), {\n timeout: STORY_TIMEOUT_MS,\n });\n await page.waitForSelector(options.waitForSelector, {\n timeout: STORY_TIMEOUT_MS,\n });\n await page.evaluate(\"document.fonts.ready\");\n if (options.delay > 0) {\n await page.waitForTimeout(options.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 {\n failed.push(name);\n } finally {\n await page.close();\n }\n });\n await context.close();\n }\n } finally {\n await browser.close();\n server.close();\n }\n console.log(\n `stateofpixel captured ${stories.length * options.viewports.length - failed.length} screenshots from ${stories.length} stories`,\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","#!/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 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};\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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"}
|
package/dist/playwright.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as uploadCommand, p as ENV, r as metadataFile } from "./upload-CER-OH3Q.mjs";
|
|
2
2
|
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { test } from "@playwright/test";
|
|
@@ -258,7 +258,8 @@ const ENV = {
|
|
|
258
258
|
nonce: "STATEOFPIXEL_NONCE",
|
|
259
259
|
baselineBranch: "STATEOFPIXEL_BASELINE_BRANCH",
|
|
260
260
|
token: "STATEOFPIXEL_TOKEN",
|
|
261
|
-
dir: "STATEOFPIXEL_DIR"
|
|
261
|
+
dir: "STATEOFPIXEL_DIR",
|
|
262
|
+
apiUrl: "STATEOFPIXEL_API_URL"
|
|
262
263
|
};
|
|
263
264
|
const ENVIRONMENT = [
|
|
264
265
|
{
|
|
@@ -269,6 +270,10 @@ const ENVIRONMENT = [
|
|
|
269
270
|
name: ENV.dir,
|
|
270
271
|
description: "folder that snapshot() writes to and the Playwright reporter uploads, stateofpixel-screenshots by default"
|
|
271
272
|
},
|
|
273
|
+
{
|
|
274
|
+
name: ENV.apiUrl,
|
|
275
|
+
description: `API of a self-hosted server, ${DEFAULT_API_URL} by default`
|
|
276
|
+
},
|
|
272
277
|
{
|
|
273
278
|
name: "CI",
|
|
274
279
|
description: "the Playwright reporter uploads only when it is set"
|
|
@@ -346,6 +351,14 @@ const COMMANDS = [
|
|
|
346
351
|
flags: "--exclude <glob>",
|
|
347
352
|
description: "skip stories whose title/name match"
|
|
348
353
|
},
|
|
354
|
+
{
|
|
355
|
+
flags: "--tag <tag>",
|
|
356
|
+
description: "only stories with this tag"
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
flags: "--globals <globals>",
|
|
360
|
+
description: "Storybook globals for every story, like brand:new"
|
|
361
|
+
},
|
|
349
362
|
{
|
|
350
363
|
flags: "--wait-for-selector <selector>",
|
|
351
364
|
description: "wait for this before each screenshot",
|
|
@@ -439,7 +452,7 @@ async function readGitInfo(env, cwd, baselineBranchOverride) {
|
|
|
439
452
|
commitMessage: await git(cwd, "log", "-1", "--format=%s", commit) ?? event?.head_commit?.message?.split("\n")[0] ?? "",
|
|
440
453
|
branch,
|
|
441
454
|
baselineBranch,
|
|
442
|
-
prNumber: pullRequest?.number ??
|
|
455
|
+
prNumber: pullRequest?.number ?? circlePullRequestNumber(env),
|
|
443
456
|
mergeBase: await git(cwd, "merge-base", commit, `origin/${baselineBranch}`),
|
|
444
457
|
ancestors: await ancestors(cwd, commit)
|
|
445
458
|
};
|
|
@@ -449,17 +462,24 @@ function readCiInfo(env) {
|
|
|
449
462
|
provider: "github-actions",
|
|
450
463
|
runUrl: env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY && env.GITHUB_RUN_ID ? `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}` : void 0
|
|
451
464
|
};
|
|
465
|
+
if (env.CIRCLECI === "true") return {
|
|
466
|
+
provider: "circleci",
|
|
467
|
+
runUrl: env.CIRCLE_BUILD_URL || void 0
|
|
468
|
+
};
|
|
452
469
|
return env.CI ? { provider: "unknown" } : {};
|
|
453
470
|
}
|
|
454
471
|
function defaultNonce(env) {
|
|
455
472
|
if (env.GITHUB_RUN_ID) return `${env.GITHUB_RUN_ID}-${env.GITHUB_RUN_ATTEMPT ?? "1"}`;
|
|
456
|
-
return null;
|
|
473
|
+
return env.CIRCLE_WORKFLOW_ID || null;
|
|
457
474
|
}
|
|
458
475
|
function previousNonces(env) {
|
|
459
476
|
const attempt = Number(env.GITHUB_RUN_ATTEMPT ?? "1");
|
|
460
477
|
if (!env.GITHUB_RUN_ID || !Number.isInteger(attempt)) return [];
|
|
461
478
|
return Array.from({ length: Math.min(Math.max(attempt - 1, 0), 10) }, (_, index) => `${env.GITHUB_RUN_ID}-${attempt - 1 - index}`);
|
|
462
479
|
}
|
|
480
|
+
function resolveApiUrl(env) {
|
|
481
|
+
return (env[ENV.apiUrl] || "https://stateofpixel.com/api/v1").replace(/\/+$/, "");
|
|
482
|
+
}
|
|
463
483
|
async function resolveToken(env, fetchImpl = fetch, retryDelayMs = 1e3) {
|
|
464
484
|
const token = env[ENV.token];
|
|
465
485
|
if (token) return token;
|
|
@@ -478,6 +498,10 @@ async function resolveToken(env, fetchImpl = fetch, retryDelayMs = 1e3) {
|
|
|
478
498
|
return value;
|
|
479
499
|
}, 3, retryDelayMs);
|
|
480
500
|
}
|
|
501
|
+
function circlePullRequestNumber(env) {
|
|
502
|
+
const match = env.CIRCLE_PULL_REQUEST?.match(/\/pull\/(\d+)$/);
|
|
503
|
+
return match ? Number(match[1]) : null;
|
|
504
|
+
}
|
|
481
505
|
function isForkPullRequest(event) {
|
|
482
506
|
const headRepo = event?.pull_request?.head.repo?.full_name;
|
|
483
507
|
return headRepo !== void 0 && headRepo !== event?.repository?.full_name;
|
|
@@ -704,6 +728,7 @@ function summarizeUploadErrors(errors) {
|
|
|
704
728
|
//#region src/commands/upload.ts
|
|
705
729
|
async function uploadCommand(dir, options) {
|
|
706
730
|
const env = process.env;
|
|
731
|
+
const apiUrl = resolveApiUrl(env);
|
|
707
732
|
if (!(await stat(dir).catch(() => void 0))?.isDirectory()) throw new Error(`Not a directory: ${dir}`);
|
|
708
733
|
const shard = options.shard ?? parseShard(env[ENV.shard] ?? "1/1");
|
|
709
734
|
const buildName = options.buildName ?? env[ENV.buildName] ?? "default";
|
|
@@ -725,7 +750,7 @@ async function uploadCommand(dir, options) {
|
|
|
725
750
|
dir,
|
|
726
751
|
workDir,
|
|
727
752
|
api: createApiClient({
|
|
728
|
-
baseUrl:
|
|
753
|
+
baseUrl: apiUrl,
|
|
729
754
|
token: await resolveToken(env),
|
|
730
755
|
refreshToken: env[ENV.token] ? void 0 : () => resolveToken(env)
|
|
731
756
|
}),
|
|
@@ -749,10 +774,10 @@ async function uploadCommand(dir, options) {
|
|
|
749
774
|
return;
|
|
750
775
|
}
|
|
751
776
|
if (isServerError(error) && !options.strict) {
|
|
752
|
-
console.warn(`stateofpixel: skipped, the service is not reachable (${error instanceof Error ? error.message : error}). Use --strict to fail instead.`);
|
|
777
|
+
console.warn(`stateofpixel: skipped, the service at ${apiUrl} is not reachable (${error instanceof Error ? error.message : error}). Use --strict to fail instead.`);
|
|
753
778
|
return;
|
|
754
779
|
}
|
|
755
|
-
if (error instanceof ApiError) throw new Error(`${error.message} (${error.code})`);
|
|
780
|
+
if (error instanceof ApiError) throw new Error(`${error.message} (${error.code}) from ${apiUrl}`);
|
|
756
781
|
throw error;
|
|
757
782
|
} finally {
|
|
758
783
|
await rm(workDir, {
|
|
@@ -802,5 +827,5 @@ function parseShard(value) {
|
|
|
802
827
|
}
|
|
803
828
|
|
|
804
829
|
//#endregion
|
|
805
|
-
export { mapConcurrent as C, sha256 as S, isServerError as _, ForkPullRequestError as a, collectSnapshots as b, readCiInfo as c,
|
|
806
|
-
//# sourceMappingURL=upload-
|
|
830
|
+
export { mapConcurrent as C, sha256 as S, isServerError as _, ForkPullRequestError as a, collectSnapshots as b, readCiInfo as c, resolveToken as d, COMMANDS as f, isRateLimited as g, createApiClient as h, waitForFinalize as i, readGitInfo as l, ApiError as m, uploadCommand as n, defaultNonce as o, ENV as p, metadataFile as r, previousNonces as s, parseShard as t, resolveApiUrl as u, formatCounts as v, readPngSize as x, createDiffEngine as y };
|
|
831
|
+
//# sourceMappingURL=upload-CER-OH3Q.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload-CER-OH3Q.mjs","names":[],"sources":["../src/map-concurrent.ts","../src/png.ts","../src/snapshots.ts","../src/diff/odiff.ts","../src/diff/pixelmatch.ts","../src/diff/engine.ts","../src/format.ts","../src/api.ts","../src/reference.ts","../src/ci-env.ts","../src/upload.ts","../src/commands/upload.ts"],"sourcesContent":["export async function mapConcurrent<Item, Result>(\n items: Item[],\n concurrency: number,\n fn: (item: Item, index: number) => Promise<Result>,\n): Promise<Result[]> {\n const results = new Array<Result>(items.length);\n let nextIndex = 0;\n\n async function worker() {\n while (nextIndex < items.length) {\n const index = nextIndex++;\n results[index] = await fn(items[index] as Item, index);\n }\n }\n\n await Promise.all(\n Array.from({ length: Math.min(concurrency, items.length) }, worker),\n );\n return results;\n}\n","import { createHash } from \"node:crypto\";\n\nconst PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);\n\nexport function sha256(bytes: Buffer): string {\n return createHash(\"sha256\").update(bytes).digest(\"hex\");\n}\n\nexport function readPngSize(bytes: Buffer): { width: number; height: number } {\n if (bytes.length < 24 || !bytes.subarray(0, 8).equals(PNG_SIGNATURE)) {\n throw new Error(\"Not a PNG file\");\n }\n return { width: bytes.readUInt32BE(16), height: bytes.readUInt32BE(20) };\n}\n","import { readdir } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nexport async function collectSnapshots(\n dir: string,\n): Promise<Map<string, string>> {\n const entries = await readdir(dir, { recursive: true, withFileTypes: true });\n const snapshots = new Map<string, string>();\n\n for (const entry of entries) {\n if (!entry.isFile() || !entry.name.toLowerCase().endsWith(\".png\")) {\n continue;\n }\n const filePath = path.join(entry.parentPath, entry.name);\n snapshots.set(snapshotName(dir, filePath), filePath);\n }\n\n return new Map(\n [...snapshots].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),\n );\n}\n\nfunction snapshotName(dir: string, filePath: string): string {\n const relativePath = path.relative(dir, filePath).split(path.sep).join(\"/\");\n return relativePath.slice(0, -\".png\".length);\n}\n","import { open } from \"node:fs/promises\";\nimport { createRequire } from \"node:module\";\nimport { readPngSize } from \"../png\";\nimport type { DiffEngine } from \"./engine\";\n\nexport async function createOdiffEngine(): Promise<DiffEngine> {\n const require = createRequire(import.meta.url);\n const { findBinary } = require(\"odiff-bin/binary.js\") as {\n findBinary(): string;\n };\n findBinary();\n const { compare } = await import(\"odiff-bin\");\n\n return {\n name: \"odiff\",\n async diff(basePath, newPath, diffPath, options) {\n const result = await compare(basePath, newPath, diffPath, {\n threshold: options.threshold,\n antialiasing: !options.includeAA,\n outputDiffMask: true,\n });\n\n if (result.match) {\n return { diffPixels: 0, diffRatio: 0 };\n }\n if (result.reason === \"pixel-diff\") {\n const [base, next] = await Promise.all([\n readPngSizeFromFile(basePath),\n readPngSizeFromFile(newPath),\n ]);\n const pixels =\n Math.max(base.width, next.width) * Math.max(base.height, next.height);\n return {\n diffPixels: result.diffCount,\n diffRatio: result.diffCount / pixels,\n };\n }\n throw new Error(`odiff could not compare ${newPath}: ${result.reason}`);\n },\n };\n}\n\nasync function readPngSizeFromFile(\n filePath: string,\n): Promise<{ width: number; height: number }> {\n const file = await open(filePath);\n try {\n const { buffer } = await file.read(Buffer.alloc(24), 0, 24, 0);\n return readPngSize(buffer);\n } finally {\n await file.close();\n }\n}\n","import { readFile, writeFile } from \"node:fs/promises\";\nimport pixelmatch from \"pixelmatch\";\nimport { PNG } from \"pngjs\";\nimport type { DiffEngine } from \"./engine\";\n\nexport function createPixelmatchEngine(): DiffEngine {\n return {\n name: \"pixelmatch\",\n async diff(basePath, newPath, diffPath, options) {\n const [base, next] = await Promise.all([\n readPng(basePath),\n readPng(newPath),\n ]);\n const width = Math.max(base.width, next.width);\n const height = Math.max(base.height, next.height);\n const diff = new PNG({ width, height });\n\n const diffPixels = pixelmatch(\n padTo(base, width, height),\n padTo(next, width, height),\n diff.data,\n width,\n height,\n {\n threshold: options.threshold,\n includeAA: options.includeAA,\n diffMask: true,\n },\n );\n\n if (diffPixels > 0) {\n await writeFile(diffPath, PNG.sync.write(diff));\n }\n return { diffPixels, diffRatio: diffPixels / (width * height) };\n },\n };\n}\n\nasync function readPng(filePath: string): Promise<PNG> {\n return PNG.sync.read(await readFile(filePath));\n}\n\nfunction padTo(image: PNG, width: number, height: number): Uint8Array {\n if (image.width === width && image.height === height) {\n return image.data;\n }\n const padded = new Uint8Array(width * height * 4);\n const rowBytes = image.width * 4;\n for (let y = 0; y < image.height; y++) {\n padded.set(\n image.data.subarray(y * rowBytes, (y + 1) * rowBytes),\n y * width * 4,\n );\n }\n return padded;\n}\n","import { createOdiffEngine } from \"./odiff\";\nimport { createPixelmatchEngine } from \"./pixelmatch\";\n\nexport type DiffOptions = {\n threshold: number;\n includeAA: boolean;\n};\n\nexport type DiffResult = {\n diffPixels: number;\n diffRatio: number;\n};\n\nexport type DiffEngine = {\n name: \"odiff\" | \"pixelmatch\";\n diff(\n basePath: string,\n newPath: string,\n diffPath: string,\n options: DiffOptions,\n ): Promise<DiffResult>;\n};\n\nexport async function createDiffEngine(): Promise<DiffEngine> {\n try {\n return await createOdiffEngine();\n } catch {\n return createPixelmatchEngine();\n }\n}\n","import type { BuildCounts } from \"./api\";\n\nexport function formatCount(count: number): string {\n return count.toLocaleString(\"en-US\");\n}\n\nexport function formatCounts(\n total: number,\n counts: Pick<BuildCounts, \"unchanged\" | \"changed\" | \"added\" | \"removed\"> &\n Partial<Pick<BuildCounts, \"failed\">>,\n): string {\n const failed = counts.failed ?? 0;\n return ` ${formatCount(total)} snapshots ${formatCount(counts.unchanged)} unchanged ${formatCount(counts.changed)} changed ${formatCount(counts.added)} added ${formatCount(counts.removed)} removed${failed > 0 ? ` ${formatCount(failed)} failed` : \"\"}`;\n}\n","export const DEFAULT_API_URL = \"https://stateofpixel.com/api/v1\";\n\nexport class ApiError extends Error {\n constructor(\n readonly status: number,\n readonly code: string,\n message: string,\n ) {\n super(message);\n }\n}\n\nexport type SnapshotLookup = {\n name: string;\n status: \"unchanged\" | \"changed\" | \"added\";\n baselineUrl?: string;\n uploadUrl?: string;\n};\n\nexport type CreateBuildResponse = {\n buildId: string;\n buildNumber: number;\n shardIndex: number;\n url: string;\n diff: { threshold: number; includeAA: boolean };\n baseline: { buildNumber: number; commit: string } | null;\n snapshots: SnapshotLookup[];\n warnings: string[];\n};\n\nexport type BuildCounts = {\n unchanged: number;\n changed: number;\n added: number;\n removed: number;\n failed: number;\n pending: number;\n approved: number;\n rejected: number;\n};\n\nexport type BuildResponse = {\n buildId: string;\n buildNumber: number;\n url: string;\n status: \"pending\" | \"finalized\" | \"expired\" | \"error\";\n conclusion: \"no_changes\" | \"changes\" | \"approved\" | \"rejected\" | null;\n counts: BuildCounts;\n shards: { done: number; total: number | null };\n};\n\ntype ApiClientOptions = {\n baseUrl: string;\n token: string;\n refreshToken?: () => Promise<string>;\n fetch?: typeof fetch;\n retries?: number;\n retryDelayMs?: number;\n transferTimeoutMs?: number;\n};\n\nexport type ApiClient = ReturnType<typeof createApiClient>;\n\nexport function createApiClient({\n baseUrl,\n token: initialToken,\n refreshToken,\n fetch = globalThis.fetch,\n retries = 3,\n retryDelayMs = 1000,\n transferTimeoutMs = 30_000,\n}: ApiClientOptions) {\n let token = initialToken;\n function withRetries<Result>(fn: () => Promise<Result>): Promise<Result> {\n return retryServerErrors(fn, retries, retryDelayMs);\n }\n\n async function request<Result>(\n method: \"GET\" | \"POST\",\n path: string,\n body?: unknown,\n ): Promise<Result> {\n try {\n return await send<Result>(method, path, body);\n } catch (error) {\n if (\n refreshToken === undefined ||\n !(error instanceof ApiError && error.status === 401)\n ) {\n throw error;\n }\n token = await refreshToken();\n return send<Result>(method, path, body);\n }\n }\n\n function send<Result>(\n method: \"GET\" | \"POST\",\n path: string,\n body?: unknown,\n ): Promise<Result> {\n return withRetries(async () => {\n const response = await fetch(`${baseUrl}${path}`, {\n method,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n },\n ...(body === undefined ? {} : { body: JSON.stringify(body) }),\n });\n if (!response.ok) {\n throw await toApiError(response);\n }\n return (await response.json()) as Result;\n });\n }\n\n function upload(uploadUrl: string, bytes: Buffer): Promise<string> {\n return withRetries(async () => {\n const response = await fetch(uploadUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"image/png\" },\n body: new Uint8Array(bytes),\n signal: AbortSignal.timeout(transferTimeoutMs),\n });\n if (!response.ok) {\n throw await toApiError(response);\n }\n const { storageId } = (await response.json()) as { storageId: string };\n return storageId;\n });\n }\n\n function download(url: string): Promise<Buffer> {\n return withRetries(async () => {\n const response = await fetch(url, {\n signal: AbortSignal.timeout(transferTimeoutMs),\n });\n if (!response.ok) {\n throw await toApiError(response);\n }\n return Buffer.from(await response.arrayBuffer());\n });\n }\n\n return { request, upload, download };\n}\n\nexport async function retryServerErrors<Result>(\n fn: () => Promise<Result>,\n retries = 3,\n retryDelayMs = 1000,\n): Promise<Result> {\n for (let attempt = 0; ; attempt++) {\n try {\n return await fn();\n } catch (error) {\n if (attempt >= retries || !isServerError(error)) {\n throw error;\n }\n await sleep(retryDelayMs * 2 ** attempt);\n }\n }\n}\n\nasync function toApiError(response: Response): Promise<ApiError> {\n const body = (await response.json().catch(() => null)) as {\n error?: { code?: string; message?: string };\n } | null;\n return new ApiError(\n response.status,\n body?.error?.code ?? \"http_error\",\n body?.error?.message ?? `HTTP ${response.status}`,\n );\n}\n\nexport function isServerError(error: unknown): boolean {\n return (\n (error instanceof ApiError && error.status >= 500) ||\n error instanceof TypeError ||\n (error instanceof Error && error.name === \"TimeoutError\")\n );\n}\n\nexport function isRateLimited(error: unknown): error is ApiError {\n return error instanceof ApiError && error.status === 429;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","import { DEFAULT_API_URL } from \"./api\";\n\nexport const ENV = {\n buildName: \"STATEOFPIXEL_BUILD_NAME\",\n shard: \"STATEOFPIXEL_SHARD\",\n nonce: \"STATEOFPIXEL_NONCE\",\n baselineBranch: \"STATEOFPIXEL_BASELINE_BRANCH\",\n token: \"STATEOFPIXEL_TOKEN\",\n dir: \"STATEOFPIXEL_DIR\",\n apiUrl: \"STATEOFPIXEL_API_URL\",\n} as const;\n\nexport const ENVIRONMENT: { name: string; description: string }[] = [\n {\n name: ENV.token,\n description: \"project token, for CI other than GitHub Actions\",\n },\n {\n name: ENV.dir,\n description:\n \"folder that snapshot() writes to and the Playwright reporter uploads, stateofpixel-screenshots by default\",\n },\n {\n name: ENV.apiUrl,\n description: `API of a self-hosted server, ${DEFAULT_API_URL} by default`,\n },\n {\n name: \"CI\",\n description: \"the Playwright reporter uploads only when it is set\",\n },\n];\n\nexport type OptionSpec = {\n flags: string;\n description: string;\n default?: string | number | boolean | number[];\n env?: string;\n};\n\nexport type CommandSpec = {\n name: \"compare\" | \"upload\" | \"storybook\" | \"finalize\";\n description: string;\n arguments: { name: string; description: string }[];\n options: OptionSpec[];\n};\n\nconst buildOptions = (dryRun: string): OptionSpec[] => [\n {\n flags: \"--build-name <name>\",\n description: \"separate builds of one project, like storybook\",\n env: ENV.buildName,\n },\n {\n flags: \"--shard <i/n>\",\n description: \"this shard and the shard count, or auto with a finalize step\",\n env: ENV.shard,\n },\n {\n flags: \"--nonce <id>\",\n description: \"shared by every shard of one build\",\n env: ENV.nonce,\n },\n {\n flags: \"--baseline-branch <branch>\",\n description: \"branch to compare against\",\n env: ENV.baselineBranch,\n },\n {\n flags: \"--subset\",\n description: \"only some snapshots ran, do not mark others removed\",\n default: false,\n },\n {\n flags: \"--threshold <number>\",\n description:\n \"color difference threshold, 0 to 1, overrides project settings\",\n },\n {\n flags: \"--strict\",\n description:\n \"fail instead of skipping on an outage, a rate limit or a fork\",\n default: false,\n },\n { flags: \"--dry-run\", description: dryRun, default: false },\n];\n\nexport const COMMANDS: CommandSpec[] = [\n {\n name: \"upload\",\n description:\n \"Upload a folder of screenshots and compare it with the baseline\",\n arguments: [{ name: \"<dir>\", description: \"folder with screenshots\" }],\n options: buildOptions(\"hash and print the plan, upload nothing\"),\n },\n {\n name: \"storybook\",\n description: \"Capture every story of a built Storybook, then upload\",\n arguments: [\n { name: \"<static-dir>\", description: \"the output of storybook build\" },\n ],\n options: [\n {\n flags: \"--viewports <widths>\",\n description: \"comma separated viewport widths\",\n default: [1280],\n },\n {\n flags: \"--include <glob>\",\n description: \"only stories whose title/name match\",\n },\n {\n flags: \"--exclude <glob>\",\n description: \"skip stories whose title/name match\",\n },\n {\n flags: \"--tag <tag>\",\n description: \"only stories with this tag\",\n },\n {\n flags: \"--globals <globals>\",\n description: \"Storybook globals for every story, like brand:new\",\n },\n {\n flags: \"--wait-for-selector <selector>\",\n description: \"wait for this before each screenshot\",\n default: \"#storybook-root > *\",\n },\n {\n flags: \"--delay <ms>\",\n description: \"wait this long before each screenshot\",\n default: 0,\n },\n ...buildOptions(\"capture and print the plan, upload nothing\"),\n ],\n },\n {\n name: \"finalize\",\n description: \"Finish a build whose shards ran with --shard auto\",\n arguments: [],\n options: [\n {\n flags: \"--build-name <name>\",\n description: \"the build name the shards used\",\n env: ENV.buildName,\n },\n {\n flags: \"--nonce <id>\",\n description: \"the nonce the shards used\",\n env: ENV.nonce,\n },\n {\n flags: \"--baseline-branch <branch>\",\n description: \"branch to compare against, for --skip-if-empty\",\n env: ENV.baselineBranch,\n },\n {\n flags: \"--skip-if-empty\",\n description: \"create a build with no changes when no shard ran\",\n default: false,\n },\n {\n flags: \"--strict\",\n description:\n \"fail instead of skipping on an outage, a rate limit or a fork\",\n default: false,\n },\n ],\n },\n {\n name: \"compare\",\n description: \"Compare a folder of screenshots against a baseline folder\",\n arguments: [\n { name: \"<dir>\", description: \"folder with new screenshots\" },\n {\n name: \"<baseline-dir>\",\n description: \"folder with baseline screenshots\",\n },\n ],\n options: [\n {\n flags: \"--out <dir>\",\n description: \"report folder\",\n default: \"stateofpixel-report\",\n },\n {\n flags: \"--threshold <number>\",\n description: \"color difference threshold, 0 to 1\",\n default: 0.1,\n },\n {\n flags: \"--include-aa\",\n description: \"count anti-aliased pixels as changes\",\n default: false,\n },\n ],\n },\n];\n","import { execFile } from \"node:child_process\";\nimport { readFile } from \"node:fs/promises\";\nimport { promisify } from \"node:util\";\nimport { ApiError, DEFAULT_API_URL, retryServerErrors } from \"./api\";\nimport { ENV } from \"./reference\";\n\nconst execFileAsync = promisify(execFile);\nconst MAX_ANCESTORS = 100;\nconst OIDC_AUDIENCE = \"stateofpixel\";\n\nexport type Env = Record<string, string | undefined>;\n\nexport type GitInfo = {\n commit: string;\n commitMessage: string;\n branch: string;\n baselineBranch: string;\n prNumber: number | null;\n mergeBase: string | null;\n ancestors: string[];\n};\n\nexport type CiInfo = { provider?: string; runUrl?: string };\n\ntype GithubEvent = {\n pull_request?: {\n number: number;\n head: { sha: string; ref: string; repo?: { full_name: string } | null };\n base: { ref: string };\n };\n head_commit?: { message: string } | null;\n repository?: { default_branch?: string; full_name?: string };\n};\n\nexport class ForkPullRequestError extends Error {}\n\nexport async function readGitInfo(\n env: Env,\n cwd: string,\n baselineBranchOverride?: string,\n): Promise<GitInfo> {\n const event = await readGithubEvent(env);\n const pullRequest = event?.pull_request;\n const commit =\n pullRequest?.head.sha ||\n env.GITHUB_SHA ||\n (await git(cwd, \"rev-parse\", \"HEAD\"));\n if (commit === null) {\n throw new Error(\"Could not find the commit. Run inside a git checkout.\");\n }\n const branch =\n pullRequest?.head.ref ||\n env.GITHUB_HEAD_REF ||\n env.GITHUB_REF_NAME ||\n (await git(cwd, \"rev-parse\", \"--abbrev-ref\", \"HEAD\")) ||\n \"HEAD\";\n const baselineBranch =\n baselineBranchOverride ||\n pullRequest?.base.ref ||\n event?.repository?.default_branch ||\n (await defaultBranch(cwd)) ||\n \"main\";\n\n return {\n commit,\n commitMessage:\n (await git(cwd, \"log\", \"-1\", \"--format=%s\", commit)) ??\n event?.head_commit?.message?.split(\"\\n\")[0] ??\n \"\",\n branch,\n baselineBranch,\n prNumber: pullRequest?.number ?? circlePullRequestNumber(env),\n mergeBase: await git(cwd, \"merge-base\", commit, `origin/${baselineBranch}`),\n ancestors: await ancestors(cwd, commit),\n };\n}\n\nexport function readCiInfo(env: Env): CiInfo {\n if (env.GITHUB_ACTIONS === \"true\") {\n return {\n provider: \"github-actions\",\n runUrl:\n env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY && env.GITHUB_RUN_ID\n ? `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}`\n : undefined,\n };\n }\n if (env.CIRCLECI === \"true\") {\n return { provider: \"circleci\", runUrl: env.CIRCLE_BUILD_URL || undefined };\n }\n return env.CI ? { provider: \"unknown\" } : {};\n}\n\nexport function defaultNonce(env: Env): string | null {\n if (env.GITHUB_RUN_ID) {\n return `${env.GITHUB_RUN_ID}-${env.GITHUB_RUN_ATTEMPT ?? \"1\"}`;\n }\n return env.CIRCLE_WORKFLOW_ID || null;\n}\n\nexport function previousNonces(env: Env): string[] {\n const attempt = Number(env.GITHUB_RUN_ATTEMPT ?? \"1\");\n if (!env.GITHUB_RUN_ID || !Number.isInteger(attempt)) {\n return [];\n }\n return Array.from(\n { length: Math.min(Math.max(attempt - 1, 0), 10) },\n (_, index) => `${env.GITHUB_RUN_ID}-${attempt - 1 - index}`,\n );\n}\n\nexport function resolveApiUrl(env: Env): string {\n return (env[ENV.apiUrl] || DEFAULT_API_URL).replace(/\\/+$/, \"\");\n}\n\nexport async function resolveToken(\n env: Env,\n fetchImpl: typeof fetch = fetch,\n retryDelayMs = 1000,\n): Promise<string> {\n const token = env[ENV.token];\n if (token) {\n return token;\n }\n const requestUrl = env.ACTIONS_ID_TOKEN_REQUEST_URL;\n const requestToken = env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;\n if (!requestUrl || !requestToken) {\n if (isForkPullRequest(await readGithubEvent(env))) {\n throw new ForkPullRequestError(\n \"GitHub Actions gives no OIDC token to pull requests from forks.\",\n );\n }\n throw new Error(\n \"No token. On GitHub Actions add `permissions: id-token: write`, elsewhere set STATEOFPIXEL_TOKEN.\",\n );\n }\n const url = new URL(requestUrl);\n url.searchParams.set(\"audience\", OIDC_AUDIENCE);\n return retryServerErrors(\n async () => {\n const response = await fetchImpl(url, {\n headers: { Authorization: `Bearer ${requestToken}` },\n });\n if (!response.ok) {\n throw new ApiError(\n response.status,\n \"oidc_failed\",\n `GitHub Actions OIDC token request failed with HTTP ${response.status}.`,\n );\n }\n const { value } = (await response.json()) as { value: string };\n return value;\n },\n 3,\n retryDelayMs,\n );\n}\n\nfunction circlePullRequestNumber(env: Env): number | null {\n const match = env.CIRCLE_PULL_REQUEST?.match(/\\/pull\\/(\\d+)$/);\n return match ? Number(match[1]) : null;\n}\n\nfunction isForkPullRequest(event: GithubEvent | null): boolean {\n const headRepo = event?.pull_request?.head.repo?.full_name;\n return headRepo !== undefined && headRepo !== event?.repository?.full_name;\n}\n\nasync function readGithubEvent(env: Env): Promise<GithubEvent | null> {\n if (!env.GITHUB_EVENT_PATH) {\n return null;\n }\n try {\n return JSON.parse(await readFile(env.GITHUB_EVENT_PATH, \"utf8\"));\n } catch {\n return null;\n }\n}\n\nasync function ancestors(cwd: string, commit: string): Promise<string[]> {\n for (const start of [commit, \"HEAD\"]) {\n const output = await git(\n cwd,\n \"rev-list\",\n `--max-count=${MAX_ANCESTORS + 2}`,\n start,\n );\n if (output !== null) {\n return output\n .split(\"\\n\")\n .filter((sha) => sha !== \"\" && sha !== commit)\n .slice(start === \"HEAD\" ? 1 : 0)\n .slice(0, MAX_ANCESTORS);\n }\n }\n return [];\n}\n\nasync function defaultBranch(cwd: string): Promise<string | null> {\n const ref = await git(\n cwd,\n \"symbolic-ref\",\n \"--short\",\n \"refs/remotes/origin/HEAD\",\n );\n return ref?.replace(/^origin\\//, \"\") ?? null;\n}\n\nasync function git(cwd: string, ...args: string[]): Promise<string | null> {\n try {\n const { stdout } = await execFileAsync(\"git\", args, { cwd });\n return stdout.trim();\n } catch {\n return null;\n }\n}\n","import { readFile, writeFile } from \"node:fs/promises\";\nimport { availableParallelism } from \"node:os\";\nimport path from \"node:path\";\nimport {\n type ApiClient,\n ApiError,\n type BuildResponse,\n type CreateBuildResponse,\n type SnapshotLookup,\n} from \"./api\";\nimport type { CiInfo, GitInfo } from \"./ci-env\";\nimport type { DiffEngine } from \"./diff/engine\";\nimport { mapConcurrent } from \"./map-concurrent\";\nimport { readPngSize, sha256 } from \"./png\";\nimport { collectSnapshots } from \"./snapshots\";\n\nconst UPLOAD_CONCURRENCY = 8;\nconst FINALIZE_POLLS = 10;\nconst FINALIZE_POLL_MS = 1000;\n\nexport type LocalSnapshot = {\n name: string;\n file: string;\n hash: string;\n bytes: number;\n width: number;\n height: number;\n metadata?: Record<string, unknown>;\n};\n\ntype Upload = {\n hash: string;\n storageId: string;\n kind: \"screenshot\" | \"diff\";\n width: number;\n height: number;\n};\n\nexport type ShardResult = {\n name: string;\n hash: string;\n status: \"unchanged\" | \"changed\" | \"added\" | \"failed\";\n diffHash?: string;\n diffRatio?: number;\n diffPixels?: number;\n metadata?: Record<string, unknown>;\n};\n\nexport type Shard = { index: number | null; total: number | null };\n\nexport type UploadInput = {\n dir: string;\n workDir: string;\n api: ApiClient;\n engine: DiffEngine;\n buildName: string;\n nonce: string;\n previousNonces?: string[];\n shard: Shard;\n subset: boolean;\n threshold?: number;\n git: GitInfo;\n ci: CiInfo;\n};\n\nexport type UploadOutput = {\n build: CreateBuildResponse;\n results: ShardResult[];\n uploadedImages: number;\n uploadedBytes: number;\n failedUploads: string[];\n uploadErrors: string[];\n compareErrors: string[];\n final: BuildResponse | null;\n};\n\nexport async function readSnapshots(dir: string): Promise<LocalSnapshot[]> {\n const files = await collectSnapshots(dir);\n return mapConcurrent(\n [...files],\n availableParallelism(),\n async ([name, file]) => {\n const bytes = await readFile(file);\n const metadata = await readMetadata(file);\n return {\n name,\n file,\n hash: sha256(bytes),\n bytes: bytes.length,\n ...readPngSize(bytes),\n ...(metadata === undefined ? {} : { metadata }),\n };\n },\n );\n}\n\nexport function metadataFile(pngFile: string): string {\n return `${pngFile.slice(0, -\".png\".length)}.meta.json`;\n}\n\nasync function readMetadata(\n pngFile: string,\n): Promise<Record<string, unknown> | undefined> {\n const text = await readFile(metadataFile(pngFile), \"utf8\").catch(\n () => undefined,\n );\n if (text === undefined) {\n return undefined;\n }\n try {\n return JSON.parse(text);\n } catch (error) {\n throw new Error(\n `${metadataFile(pngFile)} is not valid JSON: ${describeError(error)}`,\n );\n }\n}\n\nexport async function uploadDirectory(\n input: UploadInput,\n): Promise<UploadOutput> {\n const snapshots = await readSnapshots(input.dir);\n const build = await input.api.request<CreateBuildResponse>(\n \"POST\",\n \"/builds\",\n {\n buildName: input.buildName,\n nonce: input.nonce,\n ...(input.previousNonces?.length\n ? { previousNonces: input.previousNonces }\n : {}),\n shard: input.shard,\n subset: input.subset,\n git: input.git,\n ci: input.ci,\n snapshots: snapshots.map(({ name, hash, bytes, width, height }) => ({\n name,\n hash,\n bytes,\n width,\n height,\n })),\n },\n );\n const lookups = new Map(\n build.snapshots.map((lookup) => [lookup.name, lookup]),\n );\n\n const failedUploads: string[] = [];\n const uploadErrors: string[] = [];\n const compareErrors: string[] = [];\n const uploads: Upload[] = [];\n let uploadedBytes = 0;\n const pendingUploads = uniqueByHash(\n snapshots.filter((snapshot) => lookups.get(snapshot.name)?.uploadUrl),\n );\n await mapConcurrent(pendingUploads, UPLOAD_CONCURRENCY, async (snapshot) => {\n const uploadUrl = lookups.get(snapshot.name)?.uploadUrl as string;\n try {\n const bytes = await readFile(snapshot.file);\n uploads.push({\n hash: snapshot.hash,\n storageId: await input.api.upload(uploadUrl, bytes),\n kind: \"screenshot\",\n width: snapshot.width,\n height: snapshot.height,\n });\n uploadedBytes += bytes.length;\n } catch (error) {\n failedUploads.push(snapshot.hash);\n uploadErrors.push(describeError(error));\n }\n });\n\n const failedScreenshots = new Set(failedUploads);\n const options = {\n threshold: input.threshold ?? build.diff.threshold,\n includeAA: build.diff.includeAA,\n };\n const diffs = new Map<\n string,\n { file: string; hash: string; width: number; height: number }\n >();\n const results = await mapConcurrent(\n snapshots,\n availableParallelism(),\n async (snapshot, index): Promise<ShardResult> => {\n const lookup = lookups.get(snapshot.name) as SnapshotLookup;\n const base = {\n name: snapshot.name,\n hash: snapshot.hash,\n ...(snapshot.metadata === undefined\n ? {}\n : { metadata: snapshot.metadata }),\n };\n if (failedScreenshots.has(snapshot.hash)) {\n return { ...base, status: \"failed\" };\n }\n if (lookup.status !== \"changed\") {\n return { ...base, status: lookup.status };\n }\n if (lookup.baselineUrl === undefined) {\n return { ...base, status: \"changed\" };\n }\n try {\n const baselineFile = path.join(input.workDir, `baseline-${index}.png`);\n const diffFile = path.join(input.workDir, `diff-${index}.png`);\n await writeFile(\n baselineFile,\n await input.api.download(lookup.baselineUrl),\n );\n const { diffPixels, diffRatio } = await input.engine.diff(\n baselineFile,\n snapshot.file,\n diffFile,\n options,\n );\n if (diffPixels === 0) {\n return { ...base, status: \"unchanged\" };\n }\n const diffBytes = await readFile(diffFile);\n const diffHash = sha256(diffBytes);\n diffs.set(diffHash, {\n file: diffFile,\n hash: diffHash,\n ...readPngSize(diffBytes),\n });\n return { ...base, status: \"changed\", diffHash, diffRatio, diffPixels };\n } catch (error) {\n compareErrors.push(\n `could not compare ${snapshot.name}: ${describeError(error)}`,\n );\n return { ...base, status: \"failed\" };\n }\n },\n );\n\n if (diffs.size > 0) {\n const { uploads: diffTargets } = await input.api.request<{\n uploads: { hash: string; uploadUrl: string }[];\n }>(\"POST\", `/builds/${build.buildId}/upload-urls`, {\n hashes: [...diffs.keys()],\n });\n await mapConcurrent(\n diffTargets,\n UPLOAD_CONCURRENCY,\n async ({ hash, uploadUrl }) => {\n const diff = diffs.get(hash);\n if (diff === undefined) {\n return;\n }\n try {\n const bytes = await readFile(diff.file);\n uploads.push({\n hash,\n storageId: await input.api.upload(uploadUrl, bytes),\n kind: \"diff\",\n width: diff.width,\n height: diff.height,\n });\n uploadedBytes += bytes.length;\n } catch (error) {\n failedUploads.push(hash);\n uploadErrors.push(describeError(error));\n }\n },\n );\n }\n\n const { rejectedUploads } = await input.api.request<{\n rejectedUploads: string[];\n }>(\"POST\", `/builds/${build.buildId}/shards/${build.shardIndex}/complete`, {\n uploads,\n results,\n ...(uploadErrors.length > 0\n ? { errors: [summarizeUploadErrors(uploadErrors)] }\n : {}),\n });\n failedUploads.push(...rejectedUploads);\n uploadErrors.push(\n ...rejectedUploads.map(\n () => \"stateofpixel rejected the image: its size, dimensions or hash\",\n ),\n );\n\n return {\n build,\n results,\n uploadedImages: uploads.length,\n uploadedBytes,\n failedUploads,\n uploadErrors,\n compareErrors,\n final: await waitForFinalize(input.api, build.buildId, false),\n };\n}\n\nexport async function waitForFinalize(\n api: ApiClient,\n buildId: string,\n finalizeRequested: boolean,\n): Promise<BuildResponse | null> {\n for (let poll = 0; poll < FINALIZE_POLLS; poll++) {\n const build = await api.request<BuildResponse>(\"GET\", `/builds/${buildId}`);\n const lastShardDone =\n build.shards.total !== null && build.shards.done >= build.shards.total;\n if (build.status !== \"pending\" || !(finalizeRequested || lastShardDone)) {\n return build.status === \"finalized\" ? build : null;\n }\n await new Promise((resolve) => setTimeout(resolve, FINALIZE_POLL_MS));\n }\n return null;\n}\n\nfunction uniqueByHash(snapshots: LocalSnapshot[]): LocalSnapshot[] {\n return [\n ...new Map(snapshots.map((snapshot) => [snapshot.hash, snapshot])).values(),\n ];\n}\n\nfunction describeError(error: unknown): string {\n if (error instanceof ApiError) {\n return `HTTP ${error.status}, ${error.message}`;\n }\n return error instanceof Error ? error.message : String(error);\n}\n\nexport function summarizeUploadErrors(errors: string[]): string {\n const counts = new Map<string, number>();\n for (const error of errors) {\n counts.set(error, (counts.get(error) ?? 0) + 1);\n }\n return [...counts]\n .map(([error, count]) => ` ${count} x ${error}`)\n .join(\"\\n\");\n}\n","import { mkdtemp, rm, stat } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport path from \"node:path\";\nimport { InvalidArgumentError } from \"commander\";\nimport {\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 { createDiffEngine } from \"../diff/engine\";\nimport { formatCount, formatCounts } from \"../format\";\nimport { ENV } from \"../reference\";\nimport {\n readSnapshots,\n type Shard,\n summarizeUploadErrors,\n type UploadOutput,\n uploadDirectory,\n} from \"../upload\";\n\nexport type UploadCommandOptions = {\n buildName?: string;\n shard?: Shard;\n nonce?: string;\n baselineBranch?: string;\n subset: boolean;\n threshold?: number;\n strict: boolean;\n dryRun: boolean;\n};\n\nexport async function uploadCommand(\n dir: string,\n options: UploadCommandOptions,\n): Promise<void> {\n const env = process.env;\n const apiUrl = resolveApiUrl(env);\n const stats = await stat(dir).catch(() => undefined);\n if (!stats?.isDirectory()) {\n throw new Error(`Not a directory: ${dir}`);\n }\n const shard = options.shard ?? parseShard(env[ENV.shard] ?? \"1/1\");\n const buildName = options.buildName ?? env[ENV.buildName] ?? \"default\";\n const nonce = options.nonce ?? env[ENV.nonce] ?? defaultNonce(env);\n if (nonce === null && shard.total !== 1) {\n throw new Error(\"Set --nonce so every shard joins the same build.\");\n }\n const git = await readGitInfo(\n env,\n process.cwd(),\n options.baselineBranch ?? env[ENV.baselineBranch],\n );\n\n if (options.dryRun) {\n const snapshots = await readSnapshots(dir);\n console.log(\n `stateofpixel dry run ${git.branch} vs ${git.baselineBranch}`,\n );\n console.log(` ${formatCount(snapshots.length)} snapshots in ${dir}`);\n console.log(\n ` build ${buildName}, shard ${shard.total === null ? \"auto\" : `${shard.index}/${shard.total}`}, commit ${git.commit.slice(0, 7)}, ${git.ancestors.length} ancestors`,\n );\n return;\n }\n\n const startedAt = performance.now();\n const workDir = await mkdtemp(path.join(tmpdir(), \"stateofpixel-upload-\"));\n let output: UploadOutput;\n try {\n output = await uploadDirectory({\n dir,\n workDir,\n api: createApiClient({\n baseUrl: apiUrl,\n token: await resolveToken(env),\n refreshToken: env[ENV.token] ? undefined : () => resolveToken(env),\n }),\n engine: await createDiffEngine(),\n buildName,\n nonce: nonce ?? `local-${Date.now()}`,\n previousNonces:\n (options.nonce ?? env[ENV.nonce]) ? [] : previousNonces(env),\n shard,\n subset: options.subset,\n threshold: options.threshold,\n git,\n ci: readCiInfo(env),\n });\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 } finally {\n await rm(workDir, { recursive: true, force: true });\n }\n\n printSummary(output, git.branch, git.baselineBranch, startedAt);\n if (output.failedUploads.length > 0) {\n throw new Error(\n `${output.failedUploads.length} uploads failed:\\n${summarizeUploadErrors(output.uploadErrors)}`,\n );\n }\n}\n\nfunction printSummary(\n output: UploadOutput,\n branch: string,\n baselineBranch: string,\n startedAt: number,\n) {\n const { build, results, final } = output;\n const counts = final?.counts ?? countResults(results);\n const baseline =\n build.baseline === null ? \"no baseline\" : `#${build.baseline.buildNumber}`;\n const seconds = ((performance.now() - startedAt) / 1000).toFixed(1);\n const megabytes = (output.uploadedBytes / 1024 / 1024).toFixed(1);\n console.log(\n `stateofpixel build #${build.buildNumber} ${branch} vs ${baselineBranch} (${baseline})`,\n );\n console.log(formatCounts(results.length, counts));\n console.log(\n ` uploaded ${formatCount(output.uploadedImages)} images (${megabytes} MB) in ${seconds} s`,\n );\n for (const warning of [...build.warnings, ...output.compareErrors]) {\n console.log(` warning: ${warning}`);\n }\n console.log(` review: ${build.url}`);\n}\n\nfunction countResults(results: UploadOutput[\"results\"]) {\n const counts = { unchanged: 0, changed: 0, added: 0, removed: 0, failed: 0 };\n for (const result of results) {\n counts[result.status]++;\n }\n return counts;\n}\n\nexport function parseShard(value: string): Shard {\n if (value === \"auto\") {\n return { index: null, total: null };\n }\n const match = /^(\\d+)\\/(\\d+)$/.exec(value);\n const index = Number(match?.[1]);\n const total = Number(match?.[2]);\n if (!match || index < 1 || total < 1 || index > total) {\n throw new InvalidArgumentError(\"Must look like 1/4, or auto.\");\n }\n return { index, total };\n}\n"],"mappings":"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AW,CAAC;GAAE,MAAM;GAAS,aAAa;EAA0B,CAAC;EACrE,SAAS,aAAa,yCAAyC;CACjE;CACA;EACE,MAAM;EACN,aAAa;EACb,WAAW,CACT;GAAE,MAAM;GAAgB,aAAa;EAAgC,CACvE;EACA,SAAS;GACP;IACE,OAAO;IACP,aAAa;IACb,SAAS,CAAC,IAAI;GAChB;GACA;IACE,OAAO;IACP,aAAa;GACf;GACA;IACE,OAAO;IACP,aAAa;GACf;GACA;IACE,OAAO;IACP,aAAa;GACf;GACA;IACE,OAAO;IACP,aAAa;GACf;GACA;IACE,OAAO;IACP,aAAa;IACb,SAAS;GACX;GACA;IACE,OAAO;IACP,aAAa;IACb,SAAS;GACX;GACA,GAAG,aAAa,4CAA4C;EAC9D;CACF;CACA;EACE,MAAM;EACN,aAAa;EACb,WAAW,CAAC;EACZ,SAAS;GACP;IACE,OAAO;IACP,aAAa;IACb,KAAK,IAAI;GACX;GACA;IACE,OAAO;IACP,aAAa;IACb,KAAK,IAAI;GACX;GACA;IACE,OAAO;IACP,aAAa;IACb,KAAK,IAAI;GACX;GACA;IACE,OAAO;IACP,aAAa;IACb,SAAS;GACX;GACA;IACE,OAAO;IACP,aACE;IACF,SAAS;GACX;EACF;CACF;CACA;EACE,MAAM;EACN,aAAa;EACb,WAAW,CACT;GAAE,MAAM;GAAS,aAAa;EAA8B,GAC5D;GACE,MAAM;GACN,aAAa;EACf,CACF;EACA,SAAS;GACP;IACE,OAAO;IACP,aAAa;IACb,SAAS;GACX;GACA;IACE,OAAO;IACP,aAAa;IACb,SAAS;GACX;GACA;IACE,OAAO;IACP,aAAa;IACb,SAAS;GACX;EACF;CACF;AACF;;;;AC9LA,MAAM,gBAAgB,UAAU,QAAQ;AACxC,MAAM,gBAAgB;AACtB,MAAM,gBAAgB;AA0BtB,IAAa,uBAAb,cAA0C,MAAM,CAAC;AAEjD,eAAsB,YACpB,KACA,KACA,wBACkB;CAClB,MAAM,QAAQ,MAAM,gBAAgB,GAAG;CACvC,MAAM,cAAc,OAAO;CAC3B,MAAM,SACJ,aAAa,KAAK,OAClB,IAAI,cACH,MAAM,IAAI,K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"}
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"upload-DnhAYXJx.mjs","names":[],"sources":["../src/map-concurrent.ts","../src/png.ts","../src/snapshots.ts","../src/diff/odiff.ts","../src/diff/pixelmatch.ts","../src/diff/engine.ts","../src/format.ts","../src/api.ts","../src/reference.ts","../src/ci-env.ts","../src/upload.ts","../src/commands/upload.ts"],"sourcesContent":["export async function mapConcurrent<Item, Result>(\n items: Item[],\n concurrency: number,\n fn: (item: Item, index: number) => Promise<Result>,\n): Promise<Result[]> {\n const results = new Array<Result>(items.length);\n let nextIndex = 0;\n\n async function worker() {\n while (nextIndex < items.length) {\n const index = nextIndex++;\n results[index] = await fn(items[index] as Item, index);\n }\n }\n\n await Promise.all(\n Array.from({ length: Math.min(concurrency, items.length) }, worker),\n );\n return results;\n}\n","import { createHash } from \"node:crypto\";\n\nconst PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);\n\nexport function sha256(bytes: Buffer): string {\n return createHash(\"sha256\").update(bytes).digest(\"hex\");\n}\n\nexport function readPngSize(bytes: Buffer): { width: number; height: number } {\n if (bytes.length < 24 || !bytes.subarray(0, 8).equals(PNG_SIGNATURE)) {\n throw new Error(\"Not a PNG file\");\n }\n return { width: bytes.readUInt32BE(16), height: bytes.readUInt32BE(20) };\n}\n","import { readdir } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nexport async function collectSnapshots(\n dir: string,\n): Promise<Map<string, string>> {\n const entries = await readdir(dir, { recursive: true, withFileTypes: true });\n const snapshots = new Map<string, string>();\n\n for (const entry of entries) {\n if (!entry.isFile() || !entry.name.toLowerCase().endsWith(\".png\")) {\n continue;\n }\n const filePath = path.join(entry.parentPath, entry.name);\n snapshots.set(snapshotName(dir, filePath), filePath);\n }\n\n return new Map(\n [...snapshots].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),\n );\n}\n\nfunction snapshotName(dir: string, filePath: string): string {\n const relativePath = path.relative(dir, filePath).split(path.sep).join(\"/\");\n return relativePath.slice(0, -\".png\".length);\n}\n","import { open } from \"node:fs/promises\";\nimport { createRequire } from \"node:module\";\nimport { readPngSize } from \"../png\";\nimport type { DiffEngine } from \"./engine\";\n\nexport async function createOdiffEngine(): Promise<DiffEngine> {\n const require = createRequire(import.meta.url);\n const { findBinary } = require(\"odiff-bin/binary.js\") as {\n findBinary(): string;\n };\n findBinary();\n const { compare } = await import(\"odiff-bin\");\n\n return {\n name: \"odiff\",\n async diff(basePath, newPath, diffPath, options) {\n const result = await compare(basePath, newPath, diffPath, {\n threshold: options.threshold,\n antialiasing: !options.includeAA,\n outputDiffMask: true,\n });\n\n if (result.match) {\n return { diffPixels: 0, diffRatio: 0 };\n }\n if (result.reason === \"pixel-diff\") {\n const [base, next] = await Promise.all([\n readPngSizeFromFile(basePath),\n readPngSizeFromFile(newPath),\n ]);\n const pixels =\n Math.max(base.width, next.width) * Math.max(base.height, next.height);\n return {\n diffPixels: result.diffCount,\n diffRatio: result.diffCount / pixels,\n };\n }\n throw new Error(`odiff could not compare ${newPath}: ${result.reason}`);\n },\n };\n}\n\nasync function readPngSizeFromFile(\n filePath: string,\n): Promise<{ width: number; height: number }> {\n const file = await open(filePath);\n try {\n const { buffer } = await file.read(Buffer.alloc(24), 0, 24, 0);\n return readPngSize(buffer);\n } finally {\n await file.close();\n }\n}\n","import { readFile, writeFile } from \"node:fs/promises\";\nimport pixelmatch from \"pixelmatch\";\nimport { PNG } from \"pngjs\";\nimport type { DiffEngine } from \"./engine\";\n\nexport function createPixelmatchEngine(): DiffEngine {\n return {\n name: \"pixelmatch\",\n async diff(basePath, newPath, diffPath, options) {\n const [base, next] = await Promise.all([\n readPng(basePath),\n readPng(newPath),\n ]);\n const width = Math.max(base.width, next.width);\n const height = Math.max(base.height, next.height);\n const diff = new PNG({ width, height });\n\n const diffPixels = pixelmatch(\n padTo(base, width, height),\n padTo(next, width, height),\n diff.data,\n width,\n height,\n {\n threshold: options.threshold,\n includeAA: options.includeAA,\n diffMask: true,\n },\n );\n\n if (diffPixels > 0) {\n await writeFile(diffPath, PNG.sync.write(diff));\n }\n return { diffPixels, diffRatio: diffPixels / (width * height) };\n },\n };\n}\n\nasync function readPng(filePath: string): Promise<PNG> {\n return PNG.sync.read(await readFile(filePath));\n}\n\nfunction padTo(image: PNG, width: number, height: number): Uint8Array {\n if (image.width === width && image.height === height) {\n return image.data;\n }\n const padded = new Uint8Array(width * height * 4);\n const rowBytes = image.width * 4;\n for (let y = 0; y < image.height; y++) {\n padded.set(\n image.data.subarray(y * rowBytes, (y + 1) * rowBytes),\n y * width * 4,\n );\n }\n return padded;\n}\n","import { createOdiffEngine } from \"./odiff\";\nimport { createPixelmatchEngine } from \"./pixelmatch\";\n\nexport type DiffOptions = {\n threshold: number;\n includeAA: boolean;\n};\n\nexport type DiffResult = {\n diffPixels: number;\n diffRatio: number;\n};\n\nexport type DiffEngine = {\n name: \"odiff\" | \"pixelmatch\";\n diff(\n basePath: string,\n newPath: string,\n diffPath: string,\n options: DiffOptions,\n ): Promise<DiffResult>;\n};\n\nexport async function createDiffEngine(): Promise<DiffEngine> {\n try {\n return await createOdiffEngine();\n } catch {\n return createPixelmatchEngine();\n }\n}\n","import type { BuildCounts } from \"./api\";\n\nexport function formatCount(count: number): string {\n return count.toLocaleString(\"en-US\");\n}\n\nexport function formatCounts(\n total: number,\n counts: Pick<BuildCounts, \"unchanged\" | \"changed\" | \"added\" | \"removed\"> &\n Partial<Pick<BuildCounts, \"failed\">>,\n): string {\n const failed = counts.failed ?? 0;\n return ` ${formatCount(total)} snapshots ${formatCount(counts.unchanged)} unchanged ${formatCount(counts.changed)} changed ${formatCount(counts.added)} added ${formatCount(counts.removed)} removed${failed > 0 ? ` ${formatCount(failed)} failed` : \"\"}`;\n}\n","export const DEFAULT_API_URL = \"https://stateofpixel.com/api/v1\";\n\nexport class ApiError extends Error {\n constructor(\n readonly status: number,\n readonly code: string,\n message: string,\n ) {\n super(message);\n }\n}\n\nexport type SnapshotLookup = {\n name: string;\n status: \"unchanged\" | \"changed\" | \"added\";\n baselineUrl?: string;\n uploadUrl?: string;\n};\n\nexport type CreateBuildResponse = {\n buildId: string;\n buildNumber: number;\n shardIndex: number;\n url: string;\n diff: { threshold: number; includeAA: boolean };\n baseline: { buildNumber: number; commit: string } | null;\n snapshots: SnapshotLookup[];\n warnings: string[];\n};\n\nexport type BuildCounts = {\n unchanged: number;\n changed: number;\n added: number;\n removed: number;\n failed: number;\n pending: number;\n approved: number;\n rejected: number;\n};\n\nexport type BuildResponse = {\n buildId: string;\n buildNumber: number;\n url: string;\n status: \"pending\" | \"finalized\" | \"expired\" | \"error\";\n conclusion: \"no_changes\" | \"changes\" | \"approved\" | \"rejected\" | null;\n counts: BuildCounts;\n shards: { done: number; total: number | null };\n};\n\ntype ApiClientOptions = {\n baseUrl: string;\n token: string;\n refreshToken?: () => Promise<string>;\n fetch?: typeof fetch;\n retries?: number;\n retryDelayMs?: number;\n transferTimeoutMs?: number;\n};\n\nexport type ApiClient = ReturnType<typeof createApiClient>;\n\nexport function createApiClient({\n baseUrl,\n token: initialToken,\n refreshToken,\n fetch = globalThis.fetch,\n retries = 3,\n retryDelayMs = 1000,\n transferTimeoutMs = 30_000,\n}: ApiClientOptions) {\n let token = initialToken;\n function withRetries<Result>(fn: () => Promise<Result>): Promise<Result> {\n return retryServerErrors(fn, retries, retryDelayMs);\n }\n\n async function request<Result>(\n method: \"GET\" | \"POST\",\n path: string,\n body?: unknown,\n ): Promise<Result> {\n try {\n return await send<Result>(method, path, body);\n } catch (error) {\n if (\n refreshToken === undefined ||\n !(error instanceof ApiError && error.status === 401)\n ) {\n throw error;\n }\n token = await refreshToken();\n return send<Result>(method, path, body);\n }\n }\n\n function send<Result>(\n method: \"GET\" | \"POST\",\n path: string,\n body?: unknown,\n ): Promise<Result> {\n return withRetries(async () => {\n const response = await fetch(`${baseUrl}${path}`, {\n method,\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n },\n ...(body === undefined ? {} : { body: JSON.stringify(body) }),\n });\n if (!response.ok) {\n throw await toApiError(response);\n }\n return (await response.json()) as Result;\n });\n }\n\n function upload(uploadUrl: string, bytes: Buffer): Promise<string> {\n return withRetries(async () => {\n const response = await fetch(uploadUrl, {\n method: \"POST\",\n headers: { \"Content-Type\": \"image/png\" },\n body: new Uint8Array(bytes),\n signal: AbortSignal.timeout(transferTimeoutMs),\n });\n if (!response.ok) {\n throw await toApiError(response);\n }\n const { storageId } = (await response.json()) as { storageId: string };\n return storageId;\n });\n }\n\n function download(url: string): Promise<Buffer> {\n return withRetries(async () => {\n const response = await fetch(url, {\n signal: AbortSignal.timeout(transferTimeoutMs),\n });\n if (!response.ok) {\n throw await toApiError(response);\n }\n return Buffer.from(await response.arrayBuffer());\n });\n }\n\n return { request, upload, download };\n}\n\nexport async function retryServerErrors<Result>(\n fn: () => Promise<Result>,\n retries = 3,\n retryDelayMs = 1000,\n): Promise<Result> {\n for (let attempt = 0; ; attempt++) {\n try {\n return await fn();\n } catch (error) {\n if (attempt >= retries || !isServerError(error)) {\n throw error;\n }\n await sleep(retryDelayMs * 2 ** attempt);\n }\n }\n}\n\nasync function toApiError(response: Response): Promise<ApiError> {\n const body = (await response.json().catch(() => null)) as {\n error?: { code?: string; message?: string };\n } | null;\n return new ApiError(\n response.status,\n body?.error?.code ?? \"http_error\",\n body?.error?.message ?? `HTTP ${response.status}`,\n );\n}\n\nexport function isServerError(error: unknown): boolean {\n return (\n (error instanceof ApiError && error.status >= 500) ||\n error instanceof TypeError ||\n (error instanceof Error && error.name === \"TimeoutError\")\n );\n}\n\nexport function isRateLimited(error: unknown): error is ApiError {\n return error instanceof ApiError && error.status === 429;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","export const ENV = {\n buildName: \"STATEOFPIXEL_BUILD_NAME\",\n shard: \"STATEOFPIXEL_SHARD\",\n nonce: \"STATEOFPIXEL_NONCE\",\n baselineBranch: \"STATEOFPIXEL_BASELINE_BRANCH\",\n token: \"STATEOFPIXEL_TOKEN\",\n dir: \"STATEOFPIXEL_DIR\",\n} as const;\n\nexport const ENVIRONMENT: { name: string; description: string }[] = [\n {\n name: ENV.token,\n description: \"project token, for CI other than GitHub Actions\",\n },\n {\n name: ENV.dir,\n description:\n \"folder that snapshot() writes to and the Playwright reporter uploads, stateofpixel-screenshots by default\",\n },\n {\n name: \"CI\",\n description: \"the Playwright reporter uploads only when it is set\",\n },\n];\n\nexport type OptionSpec = {\n flags: string;\n description: string;\n default?: string | number | boolean | number[];\n env?: string;\n};\n\nexport type CommandSpec = {\n name: \"compare\" | \"upload\" | \"storybook\" | \"finalize\";\n description: string;\n arguments: { name: string; description: string }[];\n options: OptionSpec[];\n};\n\nconst buildOptions = (dryRun: string): OptionSpec[] => [\n {\n flags: \"--build-name <name>\",\n description: \"separate builds of one project, like storybook\",\n env: ENV.buildName,\n },\n {\n flags: \"--shard <i/n>\",\n description: \"this shard and the shard count, or auto with a finalize step\",\n env: ENV.shard,\n },\n {\n flags: \"--nonce <id>\",\n description: \"shared by every shard of one build\",\n env: ENV.nonce,\n },\n {\n flags: \"--baseline-branch <branch>\",\n description: \"branch to compare against\",\n env: ENV.baselineBranch,\n },\n {\n flags: \"--subset\",\n description: \"only some snapshots ran, do not mark others removed\",\n default: false,\n },\n {\n flags: \"--threshold <number>\",\n description:\n \"color difference threshold, 0 to 1, overrides project settings\",\n },\n {\n flags: \"--strict\",\n description:\n \"fail instead of skipping on an outage, a rate limit or a fork\",\n default: false,\n },\n { flags: \"--dry-run\", description: dryRun, default: false },\n];\n\nexport const COMMANDS: CommandSpec[] = [\n {\n name: \"upload\",\n description:\n \"Upload a folder of screenshots and compare it with the baseline\",\n arguments: [{ name: \"<dir>\", description: \"folder with screenshots\" }],\n options: buildOptions(\"hash and print the plan, upload nothing\"),\n },\n {\n name: \"storybook\",\n description: \"Capture every story of a built Storybook, then upload\",\n arguments: [\n { name: \"<static-dir>\", description: \"the output of storybook build\" },\n ],\n options: [\n {\n flags: \"--viewports <widths>\",\n description: \"comma separated viewport widths\",\n default: [1280],\n },\n {\n flags: \"--include <glob>\",\n description: \"only stories whose title/name match\",\n },\n {\n flags: \"--exclude <glob>\",\n description: \"skip stories whose title/name match\",\n },\n {\n flags: \"--wait-for-selector <selector>\",\n description: \"wait for this before each screenshot\",\n default: \"#storybook-root > *\",\n },\n {\n flags: \"--delay <ms>\",\n description: \"wait this long before each screenshot\",\n default: 0,\n },\n ...buildOptions(\"capture and print the plan, upload nothing\"),\n ],\n },\n {\n name: \"finalize\",\n description: \"Finish a build whose shards ran with --shard auto\",\n arguments: [],\n options: [\n {\n flags: \"--build-name <name>\",\n description: \"the build name the shards used\",\n env: ENV.buildName,\n },\n {\n flags: \"--nonce <id>\",\n description: \"the nonce the shards used\",\n env: ENV.nonce,\n },\n {\n flags: \"--baseline-branch <branch>\",\n description: \"branch to compare against, for --skip-if-empty\",\n env: ENV.baselineBranch,\n },\n {\n flags: \"--skip-if-empty\",\n description: \"create a build with no changes when no shard ran\",\n default: false,\n },\n {\n flags: \"--strict\",\n description:\n \"fail instead of skipping on an outage, a rate limit or a fork\",\n default: false,\n },\n ],\n },\n {\n name: \"compare\",\n description: \"Compare a folder of screenshots against a baseline folder\",\n arguments: [\n { name: \"<dir>\", description: \"folder with new screenshots\" },\n {\n name: \"<baseline-dir>\",\n description: \"folder with baseline screenshots\",\n },\n ],\n options: [\n {\n flags: \"--out <dir>\",\n description: \"report folder\",\n default: \"stateofpixel-report\",\n },\n {\n flags: \"--threshold <number>\",\n description: \"color difference threshold, 0 to 1\",\n default: 0.1,\n },\n {\n flags: \"--include-aa\",\n description: \"count anti-aliased pixels as changes\",\n default: false,\n },\n ],\n },\n];\n","import { execFile } from \"node:child_process\";\nimport { readFile } from \"node:fs/promises\";\nimport { promisify } from \"node:util\";\nimport { ApiError, retryServerErrors } from \"./api\";\nimport { ENV } from \"./reference\";\n\nconst execFileAsync = promisify(execFile);\nconst MAX_ANCESTORS = 100;\nconst OIDC_AUDIENCE = \"stateofpixel\";\n\nexport type Env = Record<string, string | undefined>;\n\nexport type GitInfo = {\n commit: string;\n commitMessage: string;\n branch: string;\n baselineBranch: string;\n prNumber: number | null;\n mergeBase: string | null;\n ancestors: string[];\n};\n\nexport type CiInfo = { provider?: string; runUrl?: string };\n\ntype GithubEvent = {\n pull_request?: {\n number: number;\n head: { sha: string; ref: string; repo?: { full_name: string } | null };\n base: { ref: string };\n };\n head_commit?: { message: string } | null;\n repository?: { default_branch?: string; full_name?: string };\n};\n\nexport class ForkPullRequestError extends Error {}\n\nexport async function readGitInfo(\n env: Env,\n cwd: string,\n baselineBranchOverride?: string,\n): Promise<GitInfo> {\n const event = await readGithubEvent(env);\n const pullRequest = event?.pull_request;\n const commit =\n pullRequest?.head.sha ||\n env.GITHUB_SHA ||\n (await git(cwd, \"rev-parse\", \"HEAD\"));\n if (commit === null) {\n throw new Error(\"Could not find the commit. Run inside a git checkout.\");\n }\n const branch =\n pullRequest?.head.ref ||\n env.GITHUB_HEAD_REF ||\n env.GITHUB_REF_NAME ||\n (await git(cwd, \"rev-parse\", \"--abbrev-ref\", \"HEAD\")) ||\n \"HEAD\";\n const baselineBranch =\n baselineBranchOverride ||\n pullRequest?.base.ref ||\n event?.repository?.default_branch ||\n (await defaultBranch(cwd)) ||\n \"main\";\n\n return {\n commit,\n commitMessage:\n (await git(cwd, \"log\", \"-1\", \"--format=%s\", commit)) ??\n event?.head_commit?.message?.split(\"\\n\")[0] ??\n \"\",\n branch,\n baselineBranch,\n prNumber: pullRequest?.number ?? null,\n mergeBase: await git(cwd, \"merge-base\", commit, `origin/${baselineBranch}`),\n ancestors: await ancestors(cwd, commit),\n };\n}\n\nexport function readCiInfo(env: Env): CiInfo {\n if (env.GITHUB_ACTIONS === \"true\") {\n return {\n provider: \"github-actions\",\n runUrl:\n env.GITHUB_SERVER_URL && env.GITHUB_REPOSITORY && env.GITHUB_RUN_ID\n ? `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}`\n : undefined,\n };\n }\n return env.CI ? { provider: \"unknown\" } : {};\n}\n\nexport function defaultNonce(env: Env): string | null {\n if (env.GITHUB_RUN_ID) {\n return `${env.GITHUB_RUN_ID}-${env.GITHUB_RUN_ATTEMPT ?? \"1\"}`;\n }\n return null;\n}\n\nexport function previousNonces(env: Env): string[] {\n const attempt = Number(env.GITHUB_RUN_ATTEMPT ?? \"1\");\n if (!env.GITHUB_RUN_ID || !Number.isInteger(attempt)) {\n return [];\n }\n return Array.from(\n { length: Math.min(Math.max(attempt - 1, 0), 10) },\n (_, index) => `${env.GITHUB_RUN_ID}-${attempt - 1 - index}`,\n );\n}\n\nexport async function resolveToken(\n env: Env,\n fetchImpl: typeof fetch = fetch,\n retryDelayMs = 1000,\n): Promise<string> {\n const token = env[ENV.token];\n if (token) {\n return token;\n }\n const requestUrl = env.ACTIONS_ID_TOKEN_REQUEST_URL;\n const requestToken = env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;\n if (!requestUrl || !requestToken) {\n if (isForkPullRequest(await readGithubEvent(env))) {\n throw new ForkPullRequestError(\n \"GitHub Actions gives no OIDC token to pull requests from forks.\",\n );\n }\n throw new Error(\n \"No token. On GitHub Actions add `permissions: id-token: write`, elsewhere set STATEOFPIXEL_TOKEN.\",\n );\n }\n const url = new URL(requestUrl);\n url.searchParams.set(\"audience\", OIDC_AUDIENCE);\n return retryServerErrors(\n async () => {\n const response = await fetchImpl(url, {\n headers: { Authorization: `Bearer ${requestToken}` },\n });\n if (!response.ok) {\n throw new ApiError(\n response.status,\n \"oidc_failed\",\n `GitHub Actions OIDC token request failed with HTTP ${response.status}.`,\n );\n }\n const { value } = (await response.json()) as { value: string };\n return value;\n },\n 3,\n retryDelayMs,\n );\n}\n\nfunction isForkPullRequest(event: GithubEvent | null): boolean {\n const headRepo = event?.pull_request?.head.repo?.full_name;\n return headRepo !== undefined && headRepo !== event?.repository?.full_name;\n}\n\nasync function readGithubEvent(env: Env): Promise<GithubEvent | null> {\n if (!env.GITHUB_EVENT_PATH) {\n return null;\n }\n try {\n return JSON.parse(await readFile(env.GITHUB_EVENT_PATH, \"utf8\"));\n } catch {\n return null;\n }\n}\n\nasync function ancestors(cwd: string, commit: string): Promise<string[]> {\n for (const start of [commit, \"HEAD\"]) {\n const output = await git(\n cwd,\n \"rev-list\",\n `--max-count=${MAX_ANCESTORS + 2}`,\n start,\n );\n if (output !== null) {\n return output\n .split(\"\\n\")\n .filter((sha) => sha !== \"\" && sha !== commit)\n .slice(start === \"HEAD\" ? 1 : 0)\n .slice(0, MAX_ANCESTORS);\n }\n }\n return [];\n}\n\nasync function defaultBranch(cwd: string): Promise<string | null> {\n const ref = await git(\n cwd,\n \"symbolic-ref\",\n \"--short\",\n \"refs/remotes/origin/HEAD\",\n );\n return ref?.replace(/^origin\\//, \"\") ?? null;\n}\n\nasync function git(cwd: string, ...args: string[]): Promise<string | null> {\n try {\n const { stdout } = await execFileAsync(\"git\", args, { cwd });\n return stdout.trim();\n } catch {\n return null;\n }\n}\n","import { readFile, writeFile } from \"node:fs/promises\";\nimport { availableParallelism } from \"node:os\";\nimport path from \"node:path\";\nimport {\n type ApiClient,\n ApiError,\n type BuildResponse,\n type CreateBuildResponse,\n type SnapshotLookup,\n} from \"./api\";\nimport type { CiInfo, GitInfo } from \"./ci-env\";\nimport type { DiffEngine } from \"./diff/engine\";\nimport { mapConcurrent } from \"./map-concurrent\";\nimport { readPngSize, sha256 } from \"./png\";\nimport { collectSnapshots } from \"./snapshots\";\n\nconst UPLOAD_CONCURRENCY = 8;\nconst FINALIZE_POLLS = 10;\nconst FINALIZE_POLL_MS = 1000;\n\nexport type LocalSnapshot = {\n name: string;\n file: string;\n hash: string;\n bytes: number;\n width: number;\n height: number;\n metadata?: Record<string, unknown>;\n};\n\ntype Upload = {\n hash: string;\n storageId: string;\n kind: \"screenshot\" | \"diff\";\n width: number;\n height: number;\n};\n\nexport type ShardResult = {\n name: string;\n hash: string;\n status: \"unchanged\" | \"changed\" | \"added\" | \"failed\";\n diffHash?: string;\n diffRatio?: number;\n diffPixels?: number;\n metadata?: Record<string, unknown>;\n};\n\nexport type Shard = { index: number | null; total: number | null };\n\nexport type UploadInput = {\n dir: string;\n workDir: string;\n api: ApiClient;\n engine: DiffEngine;\n buildName: string;\n nonce: string;\n previousNonces?: string[];\n shard: Shard;\n subset: boolean;\n threshold?: number;\n git: GitInfo;\n ci: CiInfo;\n};\n\nexport type UploadOutput = {\n build: CreateBuildResponse;\n results: ShardResult[];\n uploadedImages: number;\n uploadedBytes: number;\n failedUploads: string[];\n uploadErrors: string[];\n compareErrors: string[];\n final: BuildResponse | null;\n};\n\nexport async function readSnapshots(dir: string): Promise<LocalSnapshot[]> {\n const files = await collectSnapshots(dir);\n return mapConcurrent(\n [...files],\n availableParallelism(),\n async ([name, file]) => {\n const bytes = await readFile(file);\n const metadata = await readMetadata(file);\n return {\n name,\n file,\n hash: sha256(bytes),\n bytes: bytes.length,\n ...readPngSize(bytes),\n ...(metadata === undefined ? {} : { metadata }),\n };\n },\n );\n}\n\nexport function metadataFile(pngFile: string): string {\n return `${pngFile.slice(0, -\".png\".length)}.meta.json`;\n}\n\nasync function readMetadata(\n pngFile: string,\n): Promise<Record<string, unknown> | undefined> {\n const text = await readFile(metadataFile(pngFile), \"utf8\").catch(\n () => undefined,\n );\n if (text === undefined) {\n return undefined;\n }\n try {\n return JSON.parse(text);\n } catch (error) {\n throw new Error(\n `${metadataFile(pngFile)} is not valid JSON: ${describeError(error)}`,\n );\n }\n}\n\nexport async function uploadDirectory(\n input: UploadInput,\n): Promise<UploadOutput> {\n const snapshots = await readSnapshots(input.dir);\n const build = await input.api.request<CreateBuildResponse>(\n \"POST\",\n \"/builds\",\n {\n buildName: input.buildName,\n nonce: input.nonce,\n ...(input.previousNonces?.length\n ? { previousNonces: input.previousNonces }\n : {}),\n shard: input.shard,\n subset: input.subset,\n git: input.git,\n ci: input.ci,\n snapshots: snapshots.map(({ name, hash, bytes, width, height }) => ({\n name,\n hash,\n bytes,\n width,\n height,\n })),\n },\n );\n const lookups = new Map(\n build.snapshots.map((lookup) => [lookup.name, lookup]),\n );\n\n const failedUploads: string[] = [];\n const uploadErrors: string[] = [];\n const compareErrors: string[] = [];\n const uploads: Upload[] = [];\n let uploadedBytes = 0;\n const pendingUploads = uniqueByHash(\n snapshots.filter((snapshot) => lookups.get(snapshot.name)?.uploadUrl),\n );\n await mapConcurrent(pendingUploads, UPLOAD_CONCURRENCY, async (snapshot) => {\n const uploadUrl = lookups.get(snapshot.name)?.uploadUrl as string;\n try {\n const bytes = await readFile(snapshot.file);\n uploads.push({\n hash: snapshot.hash,\n storageId: await input.api.upload(uploadUrl, bytes),\n kind: \"screenshot\",\n width: snapshot.width,\n height: snapshot.height,\n });\n uploadedBytes += bytes.length;\n } catch (error) {\n failedUploads.push(snapshot.hash);\n uploadErrors.push(describeError(error));\n }\n });\n\n const failedScreenshots = new Set(failedUploads);\n const options = {\n threshold: input.threshold ?? build.diff.threshold,\n includeAA: build.diff.includeAA,\n };\n const diffs = new Map<\n string,\n { file: string; hash: string; width: number; height: number }\n >();\n const results = await mapConcurrent(\n snapshots,\n availableParallelism(),\n async (snapshot, index): Promise<ShardResult> => {\n const lookup = lookups.get(snapshot.name) as SnapshotLookup;\n const base = {\n name: snapshot.name,\n hash: snapshot.hash,\n ...(snapshot.metadata === undefined\n ? {}\n : { metadata: snapshot.metadata }),\n };\n if (failedScreenshots.has(snapshot.hash)) {\n return { ...base, status: \"failed\" };\n }\n if (lookup.status !== \"changed\") {\n return { ...base, status: lookup.status };\n }\n if (lookup.baselineUrl === undefined) {\n return { ...base, status: \"changed\" };\n }\n try {\n const baselineFile = path.join(input.workDir, `baseline-${index}.png`);\n const diffFile = path.join(input.workDir, `diff-${index}.png`);\n await writeFile(\n baselineFile,\n await input.api.download(lookup.baselineUrl),\n );\n const { diffPixels, diffRatio } = await input.engine.diff(\n baselineFile,\n snapshot.file,\n diffFile,\n options,\n );\n if (diffPixels === 0) {\n return { ...base, status: \"unchanged\" };\n }\n const diffBytes = await readFile(diffFile);\n const diffHash = sha256(diffBytes);\n diffs.set(diffHash, {\n file: diffFile,\n hash: diffHash,\n ...readPngSize(diffBytes),\n });\n return { ...base, status: \"changed\", diffHash, diffRatio, diffPixels };\n } catch (error) {\n compareErrors.push(\n `could not compare ${snapshot.name}: ${describeError(error)}`,\n );\n return { ...base, status: \"failed\" };\n }\n },\n );\n\n if (diffs.size > 0) {\n const { uploads: diffTargets } = await input.api.request<{\n uploads: { hash: string; uploadUrl: string }[];\n }>(\"POST\", `/builds/${build.buildId}/upload-urls`, {\n hashes: [...diffs.keys()],\n });\n await mapConcurrent(\n diffTargets,\n UPLOAD_CONCURRENCY,\n async ({ hash, uploadUrl }) => {\n const diff = diffs.get(hash);\n if (diff === undefined) {\n return;\n }\n try {\n const bytes = await readFile(diff.file);\n uploads.push({\n hash,\n storageId: await input.api.upload(uploadUrl, bytes),\n kind: \"diff\",\n width: diff.width,\n height: diff.height,\n });\n uploadedBytes += bytes.length;\n } catch (error) {\n failedUploads.push(hash);\n uploadErrors.push(describeError(error));\n }\n },\n );\n }\n\n const { rejectedUploads } = await input.api.request<{\n rejectedUploads: string[];\n }>(\"POST\", `/builds/${build.buildId}/shards/${build.shardIndex}/complete`, {\n uploads,\n results,\n ...(uploadErrors.length > 0\n ? { errors: [summarizeUploadErrors(uploadErrors)] }\n : {}),\n });\n failedUploads.push(...rejectedUploads);\n uploadErrors.push(\n ...rejectedUploads.map(\n () => \"stateofpixel rejected the image: its size, dimensions or hash\",\n ),\n );\n\n return {\n build,\n results,\n uploadedImages: uploads.length,\n uploadedBytes,\n failedUploads,\n uploadErrors,\n compareErrors,\n final: await waitForFinalize(input.api, build.buildId, false),\n };\n}\n\nexport async function waitForFinalize(\n api: ApiClient,\n buildId: string,\n finalizeRequested: boolean,\n): Promise<BuildResponse | null> {\n for (let poll = 0; poll < FINALIZE_POLLS; poll++) {\n const build = await api.request<BuildResponse>(\"GET\", `/builds/${buildId}`);\n const lastShardDone =\n build.shards.total !== null && build.shards.done >= build.shards.total;\n if (build.status !== \"pending\" || !(finalizeRequested || lastShardDone)) {\n return build.status === \"finalized\" ? build : null;\n }\n await new Promise((resolve) => setTimeout(resolve, FINALIZE_POLL_MS));\n }\n return null;\n}\n\nfunction uniqueByHash(snapshots: LocalSnapshot[]): LocalSnapshot[] {\n return [\n ...new Map(snapshots.map((snapshot) => [snapshot.hash, snapshot])).values(),\n ];\n}\n\nfunction describeError(error: unknown): string {\n if (error instanceof ApiError) {\n return `HTTP ${error.status}, ${error.message}`;\n }\n return error instanceof Error ? error.message : String(error);\n}\n\nexport function summarizeUploadErrors(errors: string[]): string {\n const counts = new Map<string, number>();\n for (const error of errors) {\n counts.set(error, (counts.get(error) ?? 0) + 1);\n }\n return [...counts]\n .map(([error, count]) => ` ${count} x ${error}`)\n .join(\"\\n\");\n}\n","import { mkdtemp, rm, stat } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport path from \"node:path\";\nimport { InvalidArgumentError } from \"commander\";\nimport {\n ApiError,\n createApiClient,\n DEFAULT_API_URL,\n isRateLimited,\n isServerError,\n} from \"../api\";\nimport {\n defaultNonce,\n ForkPullRequestError,\n previousNonces,\n readCiInfo,\n readGitInfo,\n resolveToken,\n} from \"../ci-env\";\nimport { createDiffEngine } from \"../diff/engine\";\nimport { formatCount, formatCounts } from \"../format\";\nimport { ENV } from \"../reference\";\nimport {\n readSnapshots,\n type Shard,\n summarizeUploadErrors,\n type UploadOutput,\n uploadDirectory,\n} from \"../upload\";\n\nexport type UploadCommandOptions = {\n buildName?: string;\n shard?: Shard;\n nonce?: string;\n baselineBranch?: string;\n subset: boolean;\n threshold?: number;\n strict: boolean;\n dryRun: boolean;\n};\n\nexport async function uploadCommand(\n dir: string,\n options: UploadCommandOptions,\n): Promise<void> {\n const env = process.env;\n const stats = await stat(dir).catch(() => undefined);\n if (!stats?.isDirectory()) {\n throw new Error(`Not a directory: ${dir}`);\n }\n const shard = options.shard ?? parseShard(env[ENV.shard] ?? \"1/1\");\n const buildName = options.buildName ?? env[ENV.buildName] ?? \"default\";\n const nonce = options.nonce ?? env[ENV.nonce] ?? defaultNonce(env);\n if (nonce === null && shard.total !== 1) {\n throw new Error(\"Set --nonce so every shard joins the same build.\");\n }\n const git = await readGitInfo(\n env,\n process.cwd(),\n options.baselineBranch ?? env[ENV.baselineBranch],\n );\n\n if (options.dryRun) {\n const snapshots = await readSnapshots(dir);\n console.log(\n `stateofpixel dry run ${git.branch} vs ${git.baselineBranch}`,\n );\n console.log(` ${formatCount(snapshots.length)} snapshots in ${dir}`);\n console.log(\n ` build ${buildName}, shard ${shard.total === null ? \"auto\" : `${shard.index}/${shard.total}`}, commit ${git.commit.slice(0, 7)}, ${git.ancestors.length} ancestors`,\n );\n return;\n }\n\n const startedAt = performance.now();\n const workDir = await mkdtemp(path.join(tmpdir(), \"stateofpixel-upload-\"));\n let output: UploadOutput;\n try {\n output = await uploadDirectory({\n dir,\n workDir,\n api: createApiClient({\n baseUrl: env.STATEOFPIXEL_API_URL ?? DEFAULT_API_URL,\n token: await resolveToken(env),\n refreshToken: env[ENV.token] ? undefined : () => resolveToken(env),\n }),\n engine: await createDiffEngine(),\n buildName,\n nonce: nonce ?? `local-${Date.now()}`,\n previousNonces:\n (options.nonce ?? env[ENV.nonce]) ? [] : previousNonces(env),\n shard,\n subset: options.subset,\n threshold: options.threshold,\n git,\n ci: readCiInfo(env),\n });\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 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})`);\n }\n throw error;\n } finally {\n await rm(workDir, { recursive: true, force: true });\n }\n\n printSummary(output, git.branch, git.baselineBranch, startedAt);\n if (output.failedUploads.length > 0) {\n throw new Error(\n `${output.failedUploads.length} uploads failed:\\n${summarizeUploadErrors(output.uploadErrors)}`,\n );\n }\n}\n\nfunction printSummary(\n output: UploadOutput,\n branch: string,\n baselineBranch: string,\n startedAt: number,\n) {\n const { build, results, final } = output;\n const counts = final?.counts ?? countResults(results);\n const baseline =\n build.baseline === null ? \"no baseline\" : `#${build.baseline.buildNumber}`;\n const seconds = ((performance.now() - startedAt) / 1000).toFixed(1);\n const megabytes = (output.uploadedBytes / 1024 / 1024).toFixed(1);\n console.log(\n `stateofpixel build #${build.buildNumber} ${branch} vs ${baselineBranch} (${baseline})`,\n );\n console.log(formatCounts(results.length, counts));\n console.log(\n ` uploaded ${formatCount(output.uploadedImages)} images (${megabytes} MB) in ${seconds} s`,\n );\n for (const warning of [...build.warnings, ...output.compareErrors]) {\n console.log(` warning: ${warning}`);\n }\n console.log(` review: ${build.url}`);\n}\n\nfunction countResults(results: UploadOutput[\"results\"]) {\n const counts = { unchanged: 0, changed: 0, added: 0, removed: 0, failed: 0 };\n for (const result of results) {\n counts[result.status]++;\n }\n return counts;\n}\n\nexport function parseShard(value: string): Shard {\n if (value === \"auto\") {\n return { index: null, total: null };\n }\n const match = /^(\\d+)\\/(\\d+)$/.exec(value);\n const index = Number(match?.[1]);\n const total = Number(match?.[2]);\n if (!match || index < 1 || total < 1 || index > total) {\n throw new InvalidArgumentError(\"Must look like 1/4, or auto.\");\n }\n return { index, total };\n}\n"],"mappings":";;;;;;;;;;;;AAAA,eAAsB,cACpB,OACA,aACA,IACmB;CACnB,MAAM,UAAU,IAAI,MAAc,MAAM,MAAM;CAC9C,IAAI,YAAY;CAEhB,eAAe,SAAS;EACtB,OAAO,YAAY,MAAM,QAAQ;GAC/B,MAAM,QAAQ;GACd,QAAQ,SAAS,MAAM,GAAG,MAAM,QAAgB,KAAK;EACvD;CACF;CAEA,MAAM,QAAQ,IACZ,MAAM,KAAK,EAAE,QAAQ,KAAK,IAAI,aAAa,MAAM,MAAM,EAAE,GAAG,MAAM,CACpE;CACA,OAAO;AACT;;;;ACjBA,MAAM,gBAAgB,OAAO,KAAK;CAAC;CAAK;CAAI;CAAI;CAAI;CAAI;CAAI;CAAI;AAAE,CAAC;AAEnE,SAAgB,OAAO,OAAuB;CAC5C,OAAO,WAAW,QAAQ,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO,KAAK;AACxD;AAEA,SAAgB,YAAY,OAAkD;CAC5E,IAAI,MAAM,SAAS,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,CAAC,CAAC,OAAO,aAAa,GACjE,MAAM,IAAI,MAAM,gBAAgB;CAElC,OAAO;EAAE,OAAO,MAAM,aAAa,EAAE;EAAG,QAAQ,MAAM,aAAa,EAAE;CAAE;AACzE;;;;ACVA,eAAsB,iBACpB,KAC8B;CAC9B,MAAM,UAAU,MAAM,QAAQ,KAAK;EAAE,WAAW;EAAM,eAAe;CAAK,CAAC;CAC3E,MAAM,4BAAY,IAAI,IAAoB;CAE1C,KAAK,MAAM,SAAS,SAAS;EAC3B,IAAI,CAAC,MAAM,OAAO,KAAK,CAAC,MAAM,KAAK,YAAY,CAAC,CAAC,SAAS,MAAM,GAC9D;EAEF,MAAM,WAAW,KAAK,KAAK,MAAM,YAAY,MAAM,IAAI;EACvD,UAAU,IAAI,aAAa,KAAK,QAAQ,GAAG,QAAQ;CACrD;CAEA,OAAO,IAAI,IACT,CAAC,GAAG,SAAS,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAQ,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI,CAAE,CAChE;AACF;AAEA,SAAS,aAAa,KAAa,UAA0B;CAE3D,OADqB,KAAK,SAAS,KAAK,QAAQ,CAAC,CAAC,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,GACrD,CAAC,CAAC,MAAM,GAAG,EAAc;AAC7C;;;;ACpBA,eAAsB,oBAAyC;CAE7D,MAAM,EAAE,eADQ,cAAc,YAAY,GACb,CAAC,CAAC,qBAAqB;CAGpD,WAAW;CACX,MAAM,EAAE,YAAY,MAAM,OAAO;CAEjC,OAAO;EACL,MAAM;EACN,MAAM,KAAK,UAAU,SAAS,UAAU,SAAS;GAC/C,MAAM,SAAS,MAAM,QAAQ,UAAU,SAAS,UAAU;IACxD,WAAW,QAAQ;IACnB,cAAc,CAAC,QAAQ;IACvB,gBAAgB;GAClB,CAAC;GAED,IAAI,OAAO,OACT,OAAO;IAAE,YAAY;IAAG,WAAW;GAAE;GAEvC,IAAI,OAAO,WAAW,cAAc;IAClC,MAAM,CAAC,MAAM,QAAQ,MAAM,QAAQ,IAAI,CACrC,oBAAoB,QAAQ,GAC5B,oBAAoB,OAAO,CAC7B,CAAC;IACD,MAAM,SACJ,KAAK,IAAI,KAAK,OAAO,KAAK,KAAK,IAAI,KAAK,IAAI,KAAK,QAAQ,KAAK,MAAM;IACtE,OAAO;KACL,YAAY,OAAO;KACnB,WAAW,OAAO,YAAY;IAChC;GACF;GACA,MAAM,IAAI,MAAM,2BAA2B,QAAQ,IAAI,OAAO,QAAQ;EACxE;CACF;AACF;AAEA,eAAe,oBACb,UAC4C;CAC5C,MAAM,OAAO,MAAM,KAAK,QAAQ;CAChC,IAAI;EACF,MAAM,EAAE,WAAW,MAAM,KAAK,KAAK,OAAO,MAAM,EAAE,GAAG,GAAG,IAAI,CAAC;EAC7D,OAAO,YAAY,MAAM;CAC3B,UAAU;EACR,MAAM,KAAK,MAAM;CACnB;AACF;;;;AC/CA,SAAgB,yBAAqC;CACnD,OAAO;EACL,MAAM;EACN,MAAM,KAAK,UAAU,SAAS,UAAU,SAAS;GAC/C,MAAM,CAAC,MAAM,QAAQ,MAAM,QAAQ,IAAI,CACrC,QAAQ,QAAQ,GAChB,QAAQ,OAAO,CACjB,CAAC;GACD,MAAM,QAAQ,KAAK,IAAI,KAAK,OAAO,KAAK,KAAK;GAC7C,MAAM,SAAS,KAAK,IAAI,KAAK,QAAQ,KAAK,MAAM;GAChD,MAAM,OAAO,IAAI,IAAI;IAAE;IAAO;GAAO,CAAC;GAEtC,MAAM,aAAa,WACjB,MAAM,MAAM,OAAO,MAAM,GACzB,MAAM,MAAM,OAAO,MAAM,GACzB,KAAK,MACL,OACA,QACA;IACE,WAAW,QAAQ;IACnB,WAAW,QAAQ;IACnB,UAAU;GACZ,CACF;GAEA,IAAI,aAAa,GACf,MAAM,UAAU,UAAU,IAAI,KAAK,MAAM,IAAI,CAAC;GAEhD,OAAO;IAAE;IAAY,WAAW,cAAc,QAAQ;GAAQ;EAChE;CACF;AACF;AAEA,eAAe,QAAQ,UAAgC;CACrD,OAAO,IAAI,KAAK,KAAK,MAAM,SAAS,QAAQ,CAAC;AAC/C;AAEA,SAAS,MAAM,OAAY,OAAe,QAA4B;CACpE,IAAI,MAAM,UAAU,SAAS,MAAM,WAAW,QAC5C,OAAO,MAAM;CAEf,MAAM,SAAS,IAAI,WAAW,QAAQ,SAAS,CAAC;CAChD,MAAM,WAAW,MAAM,QAAQ;CAC/B,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAChC,OAAO,IACL,MAAM,KAAK,SAAS,IAAI,WAAW,IAAI,KAAK,QAAQ,GACpD,IAAI,QAAQ,CACd;CAEF,OAAO;AACT;;;;AChCA,eAAsB,mBAAwC;CAC5D,IAAI;EACF,OAAO,MAAM,kBAAkB;CACjC,QAAQ;EACN,OAAO,uBAAuB;CAChC;AACF;;;;AC3BA,SAAgB,YAAY,OAAuB;CACjD,OAAO,MAAM,eAAe,OAAO;AACrC;AAEA,SAAgB,aACd,OACA,QAEQ;CACR,MAAM,SAAS,OAAO,UAAU;CAChC,OAAO,KAAK,YAAY,KAAK,EAAE,cAAc,YAAY,OAAO,SAAS,EAAE,cAAc,YAAY,OAAO,OAAO,EAAE,YAAY,YAAY,OAAO,KAAK,EAAE,UAAU,YAAY,OAAO,OAAO,EAAE,UAAU,SAAS,IAAI,KAAK,YAAY,MAAM,EAAE,WAAW;AAC9P;;;;ACbA,MAAa,kBAAkB;AAE/B,IAAa,WAAb,cAA8B,MAAM;CAEvB;CACA;CAFX,YACE,AAAS,QACT,AAAS,MACT,SACA;EACA,MAAM,OAAO;EAJJ;EACA;CAIX;AACF;AAqDA,SAAgB,gBAAgB,EAC9B,SACA,OAAO,cACP,cACA,QAAQ,WAAW,OACnB,UAAU,GACV,eAAe,KACf,oBAAoB,OACD;CACnB,IAAI,QAAQ;CACZ,SAAS,YAAoB,IAA4C;EACvE,OAAO,kBAAkB,IAAI,SAAS,YAAY;CACpD;CAEA,eAAe,QACb,QACA,MACA,MACiB;EACjB,IAAI;GACF,OAAO,MAAM,KAAa,QAAQ,MAAM,IAAI;EAC9C,SAAS,OAAO;GACd,IACE,iBAAiB,UACjB,EAAE,iBAAiB,YAAY,MAAM,WAAW,MAEhD,MAAM;GAER,QAAQ,MAAM,aAAa;GAC3B,OAAO,KAAa,QAAQ,MAAM,IAAI;EACxC;CACF;CAEA,SAAS,KACP,QACA,MACA,MACiB;EACjB,OAAO,YAAY,YAAY;GAC7B,MAAM,WAAW,MAAM,MAAM,GAAG,UAAU,QAAQ;IAChD;IACA,SAAS;KACP,eAAe,UAAU;KACzB,gBAAgB;IAClB;IACA,GAAI,SAAS,SAAY,CAAC,IAAI,EAAE,MAAM,KAAK,UAAU,IAAI,EAAE;GAC7D,CAAC;GACD,IAAI,CAAC,SAAS,IACZ,MAAM,MAAM,WAAW,QAAQ;GAEjC,OAAQ,MAAM,SAAS,KAAK;EAC9B,CAAC;CACH;CAEA,SAAS,OAAO,WAAmB,OAAgC;EACjE,OAAO,YAAY,YAAY;GAC7B,MAAM,WAAW,MAAM,MAAM,WAAW;IACtC,QAAQ;IACR,SAAS,EAAE,gBAAgB,YAAY;IACvC,MAAM,IAAI,WAAW,KAAK;IAC1B,QAAQ,YAAY,QAAQ,iBAAiB;GAC/C,CAAC;GACD,IAAI,CAAC,SAAS,IACZ,MAAM,MAAM,WAAW,QAAQ;GAEjC,MAAM,EAAE,cAAe,MAAM,SAAS,KAAK;GAC3C,OAAO;EACT,CAAC;CACH;CAEA,SAAS,SAAS,KAA8B;EAC9C,OAAO,YAAY,YAAY;GAC7B,MAAM,WAAW,MAAM,MAAM,KAAK,EAChC,QAAQ,YAAY,QAAQ,iBAAiB,EAC/C,CAAC;GACD,IAAI,CAAC,SAAS,IACZ,MAAM,MAAM,WAAW,QAAQ;GAEjC,OAAO,OAAO,KAAK,MAAM,SAAS,YAAY,CAAC;EACjD,CAAC;CACH;CAEA,OAAO;EAAE;EAAS;EAAQ;CAAS;AACrC;AAEA,eAAsB,kBACpB,IACA,UAAU,GACV,eAAe,KACE;CACjB,KAAK,IAAI,UAAU,IAAK,WACtB,IAAI;EACF,OAAO,MAAM,GAAG;CAClB,SAAS,OAAO;EACd,IAAI,WAAW,WAAW,CAAC,cAAc,KAAK,GAC5C,MAAM;EAER,MAAM,MAAM,eAAe,KAAK,OAAO;CACzC;AAEJ;AAEA,eAAe,WAAW,UAAuC;CAC/D,MAAM,OAAQ,MAAM,SAAS,KAAK,CAAC,CAAC,YAAY,IAAI;CAGpD,OAAO,IAAI,SACT,SAAS,QACT,MAAM,OAAO,QAAQ,cACrB,MAAM,OAAO,WAAW,QAAQ,SAAS,QAC3C;AACF;AAEA,SAAgB,cAAc,OAAyB;CACrD,OACG,iBAAiB,YAAY,MAAM,UAAU,OAC9C,iBAAiB,aAChB,iBAAiB,SAAS,MAAM,SAAS;AAE9C;AAEA,SAAgB,cAAc,OAAmC;CAC/D,OAAO,iBAAiB,YAAY,MAAM,WAAW;AACvD;AAEA,SAAS,MAAM,IAA2B;CACxC,OAAO,IAAI,SAAS,YAAY,WAAW,SAAS,EAAE,CAAC;AACzD;;;;AC9LA,MAAa,MAAM;CACjB,WAAW;CACX,OAAO;CACP,OAAO;CACP,gBAAgB;CAChB,OAAO;CACP,KAAK;AACP;AAEA,MAAa,cAAuD;CAClE;EACE,MAAM,IAAI;EACV,aAAa;CACf;CACA;EACE,MAAM,IAAI;EACV,aACE;CACJ;CACA;EACE,MAAM;EACN,aAAa;CACf;AACF;AAgBA,MAAM,gBAAgB,WAAiC;CACrD;EACE,OAAO;EACP,aAAa;EACb,KAAK,IAAI;CACX;CACA;EACE,OAAO;EACP,aAAa;EACb,KAAK,IAAI;CACX;CACA;EACE,OAAO;EACP,aAAa;EACb,KAAK,IAAI;CACX;CACA;EACE,OAAO;EACP,aAAa;EACb,KAAK,IAAI;CACX;CACA;EACE,OAAO;EACP,aAAa;EACb,SAAS;CACX;CACA;EACE,OAAO;EACP,aACE;CACJ;CACA;EACE,OAAO;EACP,aACE;EACF,SAAS;CACX;CACA;EAAE,OAAO;EAAa,aAAa;EAAQ,SAAS;CAAM;AAC5D;AAEA,MAAa,WAA0B;CACrC;EACE,MAAM;EACN,aACE;EACF,WAAW,CAAC;GAAE,MAAM;GAAS,aAAa;EAA0B,CAAC;EACrE,SAAS,aAAa,yCAAyC;CACjE;CACA;EACE,MAAM;EACN,aAAa;EACb,WAAW,CACT;GAAE,MAAM;GAAgB,aAAa;EAAgC,CACvE;EACA,SAAS;GACP;IACE,OAAO;IACP,aAAa;IACb,SAAS,CAAC,IAAI;GAChB;GACA;IACE,OAAO;IACP,aAAa;GACf;GACA;IACE,OAAO;IACP,aAAa;GACf;GACA;IACE,OAAO;IACP,aAAa;IACb,SAAS;GACX;GACA;IACE,OAAO;IACP,aAAa;IACb,SAAS;GACX;GACA,GAAG,aAAa,4CAA4C;EAC9D;CACF;CACA;EACE,MAAM;EACN,aAAa;EACb,WAAW,CAAC;EACZ,SAAS;GACP;IACE,OAAO;IACP,aAAa;IACb,KAAK,IAAI;GACX;GACA;IACE,OAAO;IACP,aAAa;IACb,KAAK,IAAI;GACX;GACA;IACE,OAAO;IACP,aAAa;IACb,KAAK,IAAI;GACX;GACA;IACE,OAAO;IACP,aAAa;IACb,SAAS;GACX;GACA;IACE,OAAO;IACP,aACE;IACF,SAAS;GACX;EACF;CACF;CACA;EACE,MAAM;EACN,aAAa;EACb,WAAW,CACT;GAAE,MAAM;GAAS,aAAa;EAA8B,GAC5D;GACE,MAAM;GACN,aAAa;EACf,CACF;EACA,SAAS;GACP;IACE,OAAO;IACP,aAAa;IACb,SAAS;GACX;GACA;IACE,OAAO;IACP,aAAa;IACb,SAAS;GACX;GACA;IACE,OAAO;IACP,aAAa;IACb,SAAS;GACX;EACF;CACF;AACF;;;;AC/KA,MAAM,gBAAgB,UAAU,QAAQ;AACxC,MAAM,gBAAgB;AACtB,MAAM,gBAAgB;AA0BtB,IAAa,uBAAb,cAA0C,MAAM,CAAC;AAEjD,eAAsB,YACpB,KACA,KACA,wBACkB;CAClB,MAAM,QAAQ,MAAM,gBAAgB,GAAG;CACvC,MAAM,cAAc,OAAO;CAC3B,MAAM,SACJ,aAAa,KAAK,OAClB,IAAI,cACH,MAAM,IAAI,KAAK,aAAa,MAAM;CACrC,IAAI,WAAW,MACb,MAAM,IAAI,MAAM,uDAAuD;CAEzE,MAAM,SACJ,aAAa,KAAK,OAClB,IAAI,mBACJ,IAAI,mBACH,MAAM,IAAI,KAAK,aAAa,gBAAgB,MAAM,KACnD;CACF,MAAM,iBACJ,0BACA,aAAa,KAAK,OAClB,OAAO,YAAY,kBAClB,MAAM,cAAc,GAAG,KACxB;CAEF,OAAO;EACL;EACA,eACG,MAAM,IAAI,KAAK,OAAO,MAAM,eAAe,MAAM,KAClD,OAAO,aAAa,SAAS,MAAM,IAAI,CAAC,CAAC,MACzC;EACF;EACA;EACA,UAAU,aAAa,UAAU;EACjC,WAAW,MAAM,IAAI,KAAK,cAAc,QAAQ,UAAU,gBAAgB;EAC1E,WAAW,MAAM,UAAU,KAAK,MAAM;CACxC;AACF;AAEA,SAAgB,WAAW,KAAkB;CAC3C,IAAI,IAAI,mBAAmB,QACzB,OAAO;EACL,UAAU;EACV,QACE,IAAI,qBAAqB,IAAI,qBAAqB,IAAI,gBAClD,GAAG,IAAI,kBAAkB,GAAG,IAAI,kBAAkB,gBAAgB,IAAI,kBACtE;CACR;CAEF,OAAO,IAAI,KAAK,EAAE,UAAU,UAAU,IAAI,CAAC;AAC7C;AAEA,SAAgB,aAAa,KAAyB;CACpD,IAAI,IAAI,eACN,OAAO,GAAG,IAAI,cAAc,GAAG,IAAI,sBAAsB;CAE3D,OAAO;AACT;AAEA,SAAgB,eAAe,KAAoB;CACjD,MAAM,UAAU,OAAO,IAAI,sBAAsB,GAAG;CACpD,IAAI,CAAC,IAAI,iBAAiB,CAAC,OAAO,UAAU,OAAO,GACjD,OAAO,CAAC;CAEV,OAAO,MAAM,KACX,EAAE,QAAQ,KAAK,IAAI,KAAK,IAAI,UAAU,GAAG,CAAC,GAAG,EAAE,EAAE,IAChD,GAAG,UAAU,GAAG,IAAI,cAAc,GAAG,UAAU,IAAI,OACtD;AACF;AAEA,eAAsB,aACpB,KACA,YAA0B,OAC1B,eAAe,KACE;CACjB,MAAM,QAAQ,IAAI,IAAI;CACtB,IAAI,OACF,OAAO;CAET,MAAM,aAAa,IAAI;CACvB,MAAM,eAAe,IAAI;CACzB,IAAI,CAAC,cAAc,CAAC,cAAc;EAChC,IAAI,kBAAkB,MAAM,gBAAgB,GAAG,CAAC,GAC9C,MAAM,IAAI,qBACR,iEACF;EAEF,MAAM,IAAI,MACR,mGACF;CACF;CACA,MAAM,MAAM,IAAI,IAAI,UAAU;CAC9B,IAAI,aAAa,IAAI,YAAY,aAAa;CAC9C,OAAO,kBACL,YAAY;EACV,MAAM,WAAW,MAAM,UAAU,KAAK,EACpC,SAAS,EAAE,eAAe,UAAU,eAAe,EACrD,CAAC;EACD,IAAI,CAAC,SAAS,IACZ,MAAM,IAAI,SACR,SAAS,QACT,eACA,sDAAsD,SAAS,OAAO,EACxE;EAEF,MAAM,EAAE,UAAW,MAAM,SAAS,KAAK;EACvC,OAAO;CACT,GACA,GACA,YACF;AACF;AAEA,SAAS,kBAAkB,OAAoC;CAC7D,MAAM,WAAW,OAAO,cAAc,KAAK,MAAM;CACjD,OAAO,aAAa,UAAa,aAAa,OAAO,YAAY;AACnE;AAEA,eAAe,gBAAgB,KAAuC;CACpE,IAAI,CAAC,IAAI,mBACP,OAAO;CAET,IAAI;EACF,OAAO,KAAK,MAAM,MAAM,SAAS,IAAI,mBAAmB,MAAM,CAAC;CACjE,QAAQ;EACN,OAAO;CACT;AACF;AAEA,eAAe,UAAU,KAAa,QAAmC;CACvE,KAAK,MAAM,SAAS,CAAC,QAAQ,MAAM,GAAG;EACpC,MAAM,SAAS,MAAM,IACnB,KACA,YACA,mBACA,KACF;EACA,IAAI,WAAW,MACb,OAAO,OACJ,MAAM,IAAI,CAAC,CACX,QAAQ,QAAQ,QAAQ,MAAM,QAAQ,MAAM,CAAC,CAC7C,MAAM,UAAU,SAAS,IAAI,CAAC,CAAC,CAC/B,MAAM,GAAG,aAAa;CAE7B;CACA,OAAO,CAAC;AACV;AAEA,eAAe,cAAc,KAAqC;CAOhE,QAAO,MANW,IAChB,KACA,gBACA,WACA,0BACF,EACU,EAAE,QAAQ,aAAa,EAAE,KAAK;AAC1C;AAEA,eAAe,IAAI,KAAa,GAAG,MAAwC;CACzE,IAAI;EACF,MAAM,EAAE,WAAW,MAAM,cAAc,OAAO,MAAM,EAAE,IAAI,CAAC;EAC3D,OAAO,OAAO,KAAK;CACrB,QAAQ;EACN,OAAO;CACT;AACF;;;;AC3LA,MAAM,qBAAqB;AAC3B,MAAM,iBAAiB;AACvB,MAAM,mBAAmB;AA0DzB,eAAsB,cAAc,KAAuC;CACzE,MAAM,QAAQ,MAAM,iBAAiB,GAAG;CACxC,OAAO,cACL,CAAC,GAAG,KAAK,GACT,qBAAqB,GACrB,OAAO,CAAC,MAAM,UAAU;EACtB,MAAM,QAAQ,MAAM,SAAS,IAAI;EACjC,MAAM,WAAW,MAAM,aAAa,IAAI;EACxC,OAAO;GACL;GACA;GACA,MAAM,OAAO,KAAK;GAClB,OAAO,MAAM;GACb,GAAG,YAAY,KAAK;GACpB,GAAI,aAAa,SAAY,CAAC,IAAI,EAAE,SAAS;EAC/C;CACF,CACF;AACF;AAEA,SAAgB,aAAa,SAAyB;CACpD,OAAO,GAAG,QAAQ,MAAM,GAAG,EAAc,EAAE;AAC7C;AAEA,eAAe,aACb,SAC8C;CAC9C,MAAM,OAAO,MAAM,SAAS,aAAa,OAAO,GAAG,MAAM,CAAC,CAAC,YACnD,MACR;CACA,IAAI,SAAS,QACX;CAEF,IAAI;EACF,OAAO,KAAK,MAAM,IAAI;CACxB,SAAS,OAAO;EACd,MAAM,IAAI,MACR,GAAG,aAAa,OAAO,EAAE,sBAAsB,cAAc,KAAK,GACpE;CACF;AACF;AAEA,eAAsB,gBACpB,OACuB;CACvB,MAAM,YAAY,MAAM,cAAc,MAAM,GAAG;CAC/C,MAAM,QAAQ,MAAM,MAAM,IAAI,QAC5B,QACA,WACA;EACE,WAAW,MAAM;EACjB,OAAO,MAAM;EACb,GAAI,MAAM,gBAAgB,SACtB,EAAE,gBAAgB,MAAM,eAAe,IACvC,CAAC;EACL,OAAO,MAAM;EACb,QAAQ,MAAM;EACd,KAAK,MAAM;EACX,IAAI,MAAM;EACV,WAAW,UAAU,KAAK,EAAE,MAAM,MAAM,OAAO,OAAO,cAAc;GAClE;GACA;GACA;GACA;GACA;EACF,EAAE;CACJ,CACF;CACA,MAAM,UAAU,IAAI,IAClB,MAAM,UAAU,KAAK,WAAW,CAAC,OAAO,MAAM,MAAM,CAAC,CACvD;CAEA,MAAM,gBAA0B,CAAC;CACjC,MAAM,eAAyB,CAAC;CAChC,MAAM,gBAA0B,CAAC;CACjC,MAAM,UAAoB,CAAC;CAC3B,IAAI,gBAAgB;CACpB,MAAM,iBAAiB,aACrB,UAAU,QAAQ,aAAa,QAAQ,IAAI,SAAS,IAAI,CAAC,EAAE,SAAS,CACtE;CACA,MAAM,cAAc,gBAAgB,oBAAoB,OAAO,aAAa;EAC1E,MAAM,YAAY,QAAQ,IAAI,SAAS,IAAI,CAAC,EAAE;EAC9C,IAAI;GACF,MAAM,QAAQ,MAAM,SAAS,SAAS,IAAI;GAC1C,QAAQ,KAAK;IACX,MAAM,SAAS;IACf,WAAW,MAAM,MAAM,IAAI,OAAO,WAAW,KAAK;IAClD,MAAM;IACN,OAAO,SAAS;IAChB,QAAQ,SAAS;GACnB,CAAC;GACD,iBAAiB,MAAM;EACzB,SAAS,OAAO;GACd,cAAc,KAAK,SAAS,IAAI;GAChC,aAAa,KAAK,cAAc,KAAK,CAAC;EACxC;CACF,CAAC;CAED,MAAM,oBAAoB,IAAI,IAAI,aAAa;CAC/C,MAAM,UAAU;EACd,WAAW,MAAM,aAAa,MAAM,KAAK;EACzC,WAAW,MAAM,KAAK;CACxB;CACA,MAAM,wBAAQ,IAAI,IAGhB;CACF,MAAM,UAAU,MAAM,cACpB,WACA,qBAAqB,GACrB,OAAO,UAAU,UAAgC;EAC/C,MAAM,SAAS,QAAQ,IAAI,SAAS,IAAI;EACxC,MAAM,OAAO;GACX,MAAM,SAAS;GACf,MAAM,SAAS;GACf,GAAI,SAAS,aAAa,SACtB,CAAC,IACD,EAAE,UAAU,SAAS,SAAS;EACpC;EACA,IAAI,kBAAkB,IAAI,SAAS,IAAI,GACrC,OAAO;GAAE,GAAG;GAAM,QAAQ;EAAS;EAErC,IAAI,OAAO,WAAW,WACpB,OAAO;GAAE,GAAG;GAAM,QAAQ,OAAO;EAAO;EAE1C,IAAI,OAAO,gBAAgB,QACzB,OAAO;GAAE,GAAG;GAAM,QAAQ;EAAU;EAEtC,IAAI;GACF,MAAM,eAAe,KAAK,KAAK,MAAM,SAAS,YAAY,MAAM,KAAK;GACrE,MAAM,WAAW,KAAK,KAAK,MAAM,SAAS,QAAQ,MAAM,KAAK;GAC7D,MAAM,UACJ,cACA,MAAM,MAAM,IAAI,SAAS,OAAO,WAAW,CAC7C;GACA,MAAM,EAAE,YAAY,cAAc,MAAM,MAAM,OAAO,KACnD,cACA,SAAS,MACT,UACA,OACF;GACA,IAAI,eAAe,GACjB,OAAO;IAAE,GAAG;IAAM,QAAQ;GAAY;GAExC,MAAM,YAAY,MAAM,SAAS,QAAQ;GACzC,MAAM,WAAW,OAAO,SAAS;GACjC,MAAM,IAAI,UAAU;IAClB,MAAM;IACN,MAAM;IACN,GAAG,YAAY,SAAS;GAC1B,CAAC;GACD,OAAO;IAAE,GAAG;IAAM,QAAQ;IAAW;IAAU;IAAW;GAAW;EACvE,SAAS,OAAO;GACd,cAAc,KACZ,qBAAqB,SAAS,KAAK,IAAI,cAAc,KAAK,GAC5D;GACA,OAAO;IAAE,GAAG;IAAM,QAAQ;GAAS;EACrC;CACF,CACF;CAEA,IAAI,MAAM,OAAO,GAAG;EAClB,MAAM,EAAE,SAAS,gBAAgB,MAAM,MAAM,IAAI,QAE9C,QAAQ,WAAW,MAAM,QAAQ,eAAe,EACjD,QAAQ,CAAC,GAAG,MAAM,KAAK,CAAC,EAC1B,CAAC;EACD,MAAM,cACJ,aACA,oBACA,OAAO,EAAE,MAAM,gBAAgB;GAC7B,MAAM,OAAO,MAAM,IAAI,IAAI;GAC3B,IAAI,SAAS,QACX;GAEF,IAAI;IACF,MAAM,QAAQ,MAAM,SAAS,KAAK,IAAI;IACtC,QAAQ,KAAK;KACX;KACA,WAAW,MAAM,MAAM,IAAI,OAAO,WAAW,KAAK;KAClD,MAAM;KACN,OAAO,KAAK;KACZ,QAAQ,KAAK;IACf,CAAC;IACD,iBAAiB,MAAM;GACzB,SAAS,OAAO;IACd,cAAc,KAAK,IAAI;IACvB,aAAa,KAAK,cAAc,KAAK,CAAC;GACxC;EACF,CACF;CACF;CAEA,MAAM,EAAE,oBAAoB,MAAM,MAAM,IAAI,QAEzC,QAAQ,WAAW,MAAM,QAAQ,UAAU,MAAM,WAAW,YAAY;EACzE;EACA;EACA,GAAI,aAAa,SAAS,IACtB,EAAE,QAAQ,CAAC,sBAAsB,YAAY,CAAC,EAAE,IAChD,CAAC;CACP,CAAC;CACD,cAAc,KAAK,GAAG,eAAe;CACrC,aAAa,KACX,GAAG,gBAAgB,UACX,+DACR,CACF;CAEA,OAAO;EACL;EACA;EACA,gBAAgB,QAAQ;EACxB;EACA;EACA;EACA;EACA,OAAO,MAAM,gBAAgB,MAAM,KAAK,MAAM,SAAS,KAAK;CAC9D;AACF;AAEA,eAAsB,gBACpB,KACA,SACA,mBAC+B;CAC/B,KAAK,IAAI,OAAO,GAAG,OAAO,gBAAgB,QAAQ;EAChD,MAAM,QAAQ,MAAM,IAAI,QAAuB,OAAO,WAAW,SAAS;EAC1E,MAAM,gBACJ,MAAM,OAAO,UAAU,QAAQ,MAAM,OAAO,QAAQ,MAAM,OAAO;EACnE,IAAI,MAAM,WAAW,aAAa,EAAE,qBAAqB,gBACvD,OAAO,MAAM,WAAW,cAAc,QAAQ;EAEhD,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,gBAAgB,CAAC;CACtE;CACA,OAAO;AACT;AAEA,SAAS,aAAa,WAA6C;CACjE,OAAO,CACL,GAAG,IAAI,IAAI,UAAU,KAAK,aAAa,CAAC,SAAS,MAAM,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAC5E;AACF;AAEA,SAAS,cAAc,OAAwB;CAC7C,IAAI,iBAAiB,UACnB,OAAO,QAAQ,MAAM,OAAO,IAAI,MAAM;CAExC,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAC9D;AAEA,SAAgB,sBAAsB,QAA0B;CAC9D,MAAM,yBAAS,IAAI,IAAoB;CACvC,KAAK,MAAM,SAAS,QAClB,OAAO,IAAI,QAAQ,OAAO,IAAI,KAAK,KAAK,KAAK,CAAC;CAEhD,OAAO,CAAC,GAAG,MAAM,CAAC,CACf,KAAK,CAAC,OAAO,WAAW,KAAK,MAAM,KAAK,OAAO,CAAC,CAChD,KAAK,IAAI;AACd;;;;ACtSA,eAAsB,cACpB,KACA,SACe;CACf,MAAM,MAAM,QAAQ;CAEpB,IAAI,EAAC,MADe,KAAK,GAAG,CAAC,CAAC,YAAY,MAAS,EACzC,EAAE,YAAY,GACtB,MAAM,IAAI,MAAM,oBAAoB,KAAK;CAE3C,MAAM,QAAQ,QAAQ,SAAS,WAAW,IAAI,IAAI,UAAU,KAAK;CACjE,MAAM,YAAY,QAAQ,aAAa,IAAI,IAAI,cAAc;CAC7D,MAAM,QAAQ,QAAQ,SAAS,IAAI,IAAI,UAAU,aAAa,GAAG;CACjE,IAAI,UAAU,QAAQ,MAAM,UAAU,GACpC,MAAM,IAAI,MAAM,kDAAkD;CAEpE,MAAM,MAAM,MAAM,YAChB,KACA,QAAQ,IAAI,GACZ,QAAQ,kBAAkB,IAAI,IAAI,eACpC;CAEA,IAAI,QAAQ,QAAQ;EAClB,MAAM,YAAY,MAAM,cAAc,GAAG;EACzC,QAAQ,IACN,0BAA0B,IAAI,OAAO,MAAM,IAAI,gBACjD;EACA,QAAQ,IAAI,KAAK,YAAY,UAAU,MAAM,EAAE,gBAAgB,KAAK;EACpE,QAAQ,IACN,WAAW,UAAU,UAAU,MAAM,UAAU,OAAO,SAAS,GAAG,MAAM,MAAM,GAAG,MAAM,QAAQ,WAAW,IAAI,OAAO,MAAM,GAAG,CAAC,EAAE,IAAI,IAAI,UAAU,OAAO,WAC5J;EACA;CACF;CAEA,MAAM,YAAY,YAAY,IAAI;CAClC,MAAM,UAAU,MAAM,QAAQ,KAAK,KAAK,OAAO,GAAG,sBAAsB,CAAC;CACzE,IAAI;CACJ,IAAI;EACF,SAAS,MAAM,gBAAgB;GAC7B;GACA;GACA,KAAK,gBAAgB;IACnB,SAAS,IAAI;IACb,OAAO,MAAM,aAAa,GAAG;IAC7B,cAAc,IAAI,IAAI,SAAS,eAAkB,aAAa,GAAG;GACnE,CAAC;GACD,QAAQ,MAAM,iBAAiB;GAC/B;GACA,OAAO,SAAS,SAAS,KAAK,IAAI;GAClC,gBACG,QAAQ,SAAS,IAAI,IAAI,SAAU,CAAC,IAAI,eAAe,GAAG;GAC7D;GACA,QAAQ,QAAQ;GAChB,WAAW,QAAQ;GACnB;GACA,IAAI,WAAW,GAAG;EACpB,CAAC;CACH,SAAS,OAAO;EACd,IAAI,iBAAiB,wBAAwB,CAAC,QAAQ,QAAQ;GAC5D,QAAQ,KACN,0BAA0B,MAAM,QAAQ,+BAC1C;GACA;EACF;EACA,IAAI,cAAc,KAAK,KAAK,CAAC,QAAQ,QAAQ;GAC3C,QAAQ,KACN,0BAA0B,MAAM,QAAQ,+BAC1C;GACA;EACF;EACA,IAAI,cAAc,KAAK,KAAK,CAAC,QAAQ,QAAQ;GAC3C,QAAQ,KACN,wDAAwD,iBAAiB,QAAQ,MAAM,UAAU,MAAM,iCACzG;GACA;EACF;EACA,IAAI,iBAAiB,UACnB,MAAM,IAAI,MAAM,GAAG,MAAM,QAAQ,IAAI,MAAM,KAAK,EAAE;EAEpD,MAAM;CACR,UAAU;EACR,MAAM,GAAG,SAAS;GAAE,WAAW;GAAM,OAAO;EAAK,CAAC;CACpD;CAEA,aAAa,QAAQ,IAAI,QAAQ,IAAI,gBAAgB,SAAS;CAC9D,IAAI,OAAO,cAAc,SAAS,GAChC,MAAM,IAAI,MACR,GAAG,OAAO,cAAc,OAAO,oBAAoB,sBAAsB,OAAO,YAAY,GAC9F;AAEJ;AAEA,SAAS,aACP,QACA,QACA,gBACA,WACA;CACA,MAAM,EAAE,OAAO,SAAS,UAAU;CAClC,MAAM,SAAS,OAAO,UAAU,aAAa,OAAO;CACpD,MAAM,WACJ,MAAM,aAAa,OAAO,gBAAgB,IAAI,MAAM,SAAS;CAC/D,MAAM,YAAY,YAAY,IAAI,IAAI,aAAa,IAAI,CAAE,QAAQ,CAAC;CAClE,MAAM,aAAa,OAAO,gBAAgB,OAAO,KAAI,CAAE,QAAQ,CAAC;CAChE,QAAQ,IACN,wBAAwB,MAAM,YAAY,IAAI,OAAO,MAAM,eAAe,IAAI,SAAS,EACzF;CACA,QAAQ,IAAI,aAAa,QAAQ,QAAQ,MAAM,CAAC;CAChD,QAAQ,IACN,cAAc,YAAY,OAAO,cAAc,EAAE,WAAW,UAAU,UAAU,QAAQ,GAC1F;CACA,KAAK,MAAM,WAAW,CAAC,GAAG,MAAM,UAAU,GAAG,OAAO,aAAa,GAC/D,QAAQ,IAAI,cAAc,SAAS;CAErC,QAAQ,IAAI,aAAa,MAAM,KAAK;AACtC;AAEA,SAAS,aAAa,SAAkC;CACtD,MAAM,SAAS;EAAE,WAAW;EAAG,SAAS;EAAG,OAAO;EAAG,SAAS;EAAG,QAAQ;CAAE;CAC3E,KAAK,MAAM,UAAU,SACnB,OAAO,OAAO,OAAO;CAEvB,OAAO;AACT;AAEA,SAAgB,WAAW,OAAsB;CAC/C,IAAI,UAAU,QACZ,OAAO;EAAE,OAAO;EAAM,OAAO;CAAK;CAEpC,MAAM,QAAQ,iBAAiB,KAAK,KAAK;CACzC,MAAM,QAAQ,OAAO,QAAQ,EAAE;CAC/B,MAAM,QAAQ,OAAO,QAAQ,EAAE;CAC/B,IAAI,CAAC,SAAS,QAAQ,KAAK,QAAQ,KAAK,QAAQ,OAC9C,MAAM,IAAI,qBAAqB,8BAA8B;CAE/D,OAAO;EAAE;EAAO;CAAM;AACxB"}
|