renderdiff 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +490 -2
- package/dist/capture.d.ts +34 -0
- package/dist/capture.js +338 -0
- package/dist/capture.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +288 -0
- package/dist/cli.js.map +1 -0
- package/dist/compare.d.ts +93 -0
- package/dist/compare.js +537 -0
- package/dist/compare.js.map +1 -0
- package/dist/config.d.ts +223 -0
- package/dist/config.js +388 -0
- package/dist/config.js.map +1 -0
- package/dist/format.d.ts +20 -0
- package/dist/format.js +26 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/locate.d.ts +24 -0
- package/dist/locate.js +316 -0
- package/dist/locate.js.map +1 -0
- package/dist/report.d.ts +118 -0
- package/dist/report.js +192 -0
- package/dist/report.js.map +1 -0
- package/dist/run.d.ts +17 -0
- package/dist/run.js +390 -0
- package/dist/run.js.map +1 -0
- package/package.json +59 -4
- package/renderdiff.config.example.ts +111 -0
package/dist/capture.js
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
2
|
+
import sharp from "sharp";
|
|
3
|
+
import { label, MASK_COLOR, masksFor } from "./config.js";
|
|
4
|
+
/**
|
|
5
|
+
* Chromium's screenshot backing store caps at this in any dimension; a single
|
|
6
|
+
* `fullPage` or clip shot past it can come out truncated or wrapped. Rather than refuse, a
|
|
7
|
+
* taller page or element is captured in vertical tiles of at most this height and stitched
|
|
8
|
+
* back together (`captureTiled`), so it still diffs in full.
|
|
9
|
+
*/
|
|
10
|
+
const MAX_CAPTURE_HEIGHT = 16_384;
|
|
11
|
+
/** Tile height for the stitched path. Under the cap, with margin. */
|
|
12
|
+
const TILE_HEIGHT = 16_000;
|
|
13
|
+
/**
|
|
14
|
+
* Sanity ceiling. Past this a page is far likelier a broken/runaway layout than a real
|
|
15
|
+
* one, and we would rather error than stitch a >100MB image. Named in the throw.
|
|
16
|
+
*/
|
|
17
|
+
const MAX_PAGE_HEIGHT = 40_000;
|
|
18
|
+
/** Requests still open per page, and when the last one opened or closed. */
|
|
19
|
+
const network = new WeakMap();
|
|
20
|
+
/** Never finish, so they would hold network idle open until its timeout. */
|
|
21
|
+
const STREAMS = new Set(["eventsource", "websocket", "ping"]);
|
|
22
|
+
/** Playwright's `networkidle` waits a fixed 500ms; below 250ms, late request events under load slip past. */
|
|
23
|
+
async function networkIdle(page, quiet, timeout) {
|
|
24
|
+
const net = network.get(page);
|
|
25
|
+
const end = Date.now() + timeout;
|
|
26
|
+
while (Date.now() < end && (net.open.size || Date.now() - net.last < quiet))
|
|
27
|
+
await sleep(25);
|
|
28
|
+
}
|
|
29
|
+
/** A `before` hook threw. Never retried: the hook may have half-run against the page. */
|
|
30
|
+
export class HookError extends Error {
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Install network blocking and tracking, the fake clock and seeded randomness. Once per
|
|
34
|
+
* page, before any navigation.
|
|
35
|
+
*/
|
|
36
|
+
export async function prepare(page, cfg, now) {
|
|
37
|
+
const net = { open: new Set(), last: Date.now() };
|
|
38
|
+
network.set(page, net);
|
|
39
|
+
page.on("request", (r) => {
|
|
40
|
+
if (STREAMS.has(r.resourceType()))
|
|
41
|
+
return;
|
|
42
|
+
net.open.add(r);
|
|
43
|
+
net.last = Date.now();
|
|
44
|
+
});
|
|
45
|
+
const done = (r) => {
|
|
46
|
+
if (net.open.delete(r))
|
|
47
|
+
net.last = Date.now();
|
|
48
|
+
};
|
|
49
|
+
page.on("requestfinished", done);
|
|
50
|
+
page.on("requestfailed", done);
|
|
51
|
+
// A request the old document left open never finishes or fails, so it can't hold the next load.
|
|
52
|
+
page.on("framenavigated", (f) => {
|
|
53
|
+
if (f !== page.mainFrame())
|
|
54
|
+
return;
|
|
55
|
+
net.open.clear();
|
|
56
|
+
net.last = Date.now();
|
|
57
|
+
});
|
|
58
|
+
for (const pattern of cfg.blockUrlPatterns) {
|
|
59
|
+
await page.route(pattern, (route) => route.abort());
|
|
60
|
+
}
|
|
61
|
+
// Time flows, so Date-based debounce and busy-waits work; `capture` rewinds it per load.
|
|
62
|
+
await page.clock.install({ time: now });
|
|
63
|
+
// mulberry32 with a fixed seed, re-seeded on every navigation.
|
|
64
|
+
await page.addInitScript(() => {
|
|
65
|
+
let s = 0x9e3779b9;
|
|
66
|
+
Math.random = () => {
|
|
67
|
+
s = (s + 0x6d2b79f5) | 0;
|
|
68
|
+
let t = Math.imul(s ^ (s >>> 15), 1 | s);
|
|
69
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
70
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
export async function capture(page, url, route, side, width, cfg, now) {
|
|
75
|
+
await page.setViewportSize({ width, height: 900 });
|
|
76
|
+
await page.clock.setSystemTime(now);
|
|
77
|
+
const res = await page.goto(url, {
|
|
78
|
+
waitUntil: "domcontentloaded",
|
|
79
|
+
timeout: 45_000,
|
|
80
|
+
});
|
|
81
|
+
if (res && res.status() >= 400) {
|
|
82
|
+
throw new Error(`HTTP ${res.status()} at ${url}`);
|
|
83
|
+
}
|
|
84
|
+
if (route.before) {
|
|
85
|
+
try {
|
|
86
|
+
await route.before(page, { side, route: label(route), width });
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
throw new HookError(`before hook failed on ${side}: ${e instanceof Error ? e.message : String(e)}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
await page.addStyleTag({ content: cfg.neutralizeCss });
|
|
93
|
+
// Native lazy images go eager; JS lazy loaders see every viewport-height slice, one
|
|
94
|
+
// frame each, since a scroll inside one synchronous loop never paints.
|
|
95
|
+
await page.evaluate(async (maxHeight) => {
|
|
96
|
+
// The fake clock drives rAF too, so add wall time for a real paint per slice.
|
|
97
|
+
const frame = () => new Promise((r) => requestAnimationFrame(() => setTimeout(r, 32)));
|
|
98
|
+
Array.from(document.images).forEach((img) => (img.loading = "eager"));
|
|
99
|
+
document.querySelectorAll("video").forEach((v) => {
|
|
100
|
+
v.pause();
|
|
101
|
+
v.currentTime = 0;
|
|
102
|
+
});
|
|
103
|
+
const h = Math.min(document.documentElement.scrollHeight, maxHeight);
|
|
104
|
+
for (let y = 0; y < h; y += window.innerHeight) {
|
|
105
|
+
window.scrollTo(0, y);
|
|
106
|
+
await frame();
|
|
107
|
+
}
|
|
108
|
+
window.scrollTo(0, 0);
|
|
109
|
+
await frame();
|
|
110
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
111
|
+
const decoded = Promise.all(Array.from(document.images).map((img) => img.decode().catch(() => { })));
|
|
112
|
+
await Promise.race([decoded, new Promise((r) => setTimeout(r, 5_000))]);
|
|
113
|
+
}, MAX_PAGE_HEIGHT);
|
|
114
|
+
await page.evaluate(() => document.fonts.ready);
|
|
115
|
+
await networkIdle(page, 250, 3_000);
|
|
116
|
+
await page.addStyleTag({ content: cfg.neutralizeCss }); // anything injected late
|
|
117
|
+
const selector = route.selector?.[side];
|
|
118
|
+
const where = `on ${side} (${route[side]})`;
|
|
119
|
+
// count() doesn't wait, so a wrong selector fails now rather than after a 30s timeout.
|
|
120
|
+
const el = selector ? page.locator(selector) : null;
|
|
121
|
+
const n = await el?.count();
|
|
122
|
+
if (n !== undefined && n !== 1) {
|
|
123
|
+
throw new Error(`selector "${selector}" matched ${n} elements ${where}, expected 1`);
|
|
124
|
+
}
|
|
125
|
+
// Masks only paint the PNG, so hide them for the text read too; visibility keeps layout.
|
|
126
|
+
const masks = masksFor(cfg, page, route);
|
|
127
|
+
const hidden = [];
|
|
128
|
+
for (const l of masks) {
|
|
129
|
+
hidden.push(await l.evaluateAll((els) => els.map((e) => {
|
|
130
|
+
const prev = e.style.visibility;
|
|
131
|
+
e.style.visibility = "hidden";
|
|
132
|
+
return prev;
|
|
133
|
+
})));
|
|
134
|
+
}
|
|
135
|
+
const { scrollHeight, innerText, pageText, media } = await (el ?? page.locator(":root")).evaluate((root) => {
|
|
136
|
+
const body = document.body ?? document.documentElement;
|
|
137
|
+
return {
|
|
138
|
+
scrollHeight: document.documentElement.scrollHeight,
|
|
139
|
+
innerText: (root === document.documentElement ? body : root).innerText || "",
|
|
140
|
+
pageText: body.innerText?.length ?? 0,
|
|
141
|
+
media: Array.from(document.querySelectorAll("img, svg, canvas, video, iframe, input, textarea, select")).some((e) => {
|
|
142
|
+
const r = e.getBoundingClientRect();
|
|
143
|
+
return r.width * r.height >= 1024;
|
|
144
|
+
}),
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
// Reverse order, so an element two masks share gets its original value back.
|
|
148
|
+
for (let i = masks.length - 1; i >= 0; i--) {
|
|
149
|
+
await masks[i].evaluateAll((els, prev) => els.forEach((e, j) => (e.style.visibility = prev[j] ?? "")), hidden[i]);
|
|
150
|
+
}
|
|
151
|
+
const text = innerText
|
|
152
|
+
.split("\n")
|
|
153
|
+
.map((line) => line.replace(/\s+/g, " ").trim())
|
|
154
|
+
.filter(Boolean);
|
|
155
|
+
const blank = pageText < 200 && !media ? `height=${scrollHeight}, text=${pageText} chars` : null;
|
|
156
|
+
if (scrollHeight > MAX_PAGE_HEIGHT) {
|
|
157
|
+
throw new Error(`page is ${scrollHeight}px tall, past the ${MAX_PAGE_HEIGHT}px sanity ceiling ` +
|
|
158
|
+
`at ${url} — likely a broken layout, not a real page`);
|
|
159
|
+
}
|
|
160
|
+
const measure = async (l) => {
|
|
161
|
+
const b = await l.boundingBox();
|
|
162
|
+
if (!b?.width || !b.height)
|
|
163
|
+
throw new Error(`selector "${selector}" has no box ${where}`);
|
|
164
|
+
return b;
|
|
165
|
+
};
|
|
166
|
+
let clip;
|
|
167
|
+
// Brings in what lazy-loads near it; then scroll 0, so sticky chrome is as in a page shot.
|
|
168
|
+
if (el) {
|
|
169
|
+
await measure(el);
|
|
170
|
+
await el.scrollIntoViewIfNeeded();
|
|
171
|
+
}
|
|
172
|
+
// At scroll 0 viewport coordinates are page coordinates, which every path shoots in.
|
|
173
|
+
await page.evaluate(() => window.scrollTo(0, 0));
|
|
174
|
+
if (el) {
|
|
175
|
+
const b = await measure(el);
|
|
176
|
+
const x = Math.max(0, Math.floor(b.x));
|
|
177
|
+
const y = Math.max(0, Math.floor(b.y));
|
|
178
|
+
clip = { x, y, width: Math.ceil(b.x + b.width) - x, height: Math.ceil(b.y + b.height) - y };
|
|
179
|
+
if (clip.width <= 0 || clip.height <= 0) {
|
|
180
|
+
throw new Error(`selector "${selector}" is off the page ${where}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const boxes = (await Promise.all((await Promise.all(masks.map((l) => l.all()))).flat().map((m) => m.boundingBox())))
|
|
184
|
+
.filter((b) => b !== null)
|
|
185
|
+
.map((b) => (clip ? { ...b, x: b.x - clip.x, y: b.y - clip.y } : b));
|
|
186
|
+
const area = clip ?? { x: 0, y: 0, width, height: scrollHeight };
|
|
187
|
+
const tiled = area.height > MAX_CAPTURE_HEIGHT;
|
|
188
|
+
const shoot = async () => paintBoxes(tiled
|
|
189
|
+
? await captureTiled(page, area)
|
|
190
|
+
: await page.screenshot({ fullPage: true, clip, animations: "disabled" }), boxes);
|
|
191
|
+
// A tiled shot is expensive, so it gets one comparison.
|
|
192
|
+
const tries = tiled ? 2 : 3;
|
|
193
|
+
const origin = { x: clip?.x ?? 0, y: clip?.y ?? 0 };
|
|
194
|
+
let prev = await shoot();
|
|
195
|
+
for (let n = 2;; n++) {
|
|
196
|
+
await page.waitForTimeout(50);
|
|
197
|
+
const png = await shoot();
|
|
198
|
+
if (png.equals(prev))
|
|
199
|
+
return { png, unstable: null, blank, text, origin };
|
|
200
|
+
if (n === tries)
|
|
201
|
+
return { png, unstable: await differing(prev, png), blank, text, origin };
|
|
202
|
+
prev = png;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const decode = (png) => sharp(png).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
206
|
+
/** Pixels that differ between two shots of the same load. Null if none, or if the size changed. */
|
|
207
|
+
async function differing(a, b) {
|
|
208
|
+
const [ra, rb] = await Promise.all([decode(a), decode(b)]);
|
|
209
|
+
// A page whose size changed between shots isn't masked; that diff still counts.
|
|
210
|
+
if (ra.info.width !== rb.info.width || ra.info.height !== rb.info.height) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
const mask = new Uint8Array(ra.info.width * ra.info.height);
|
|
214
|
+
const pa = new Uint32Array(ra.data.buffer, ra.data.byteOffset, mask.length);
|
|
215
|
+
const pb = new Uint32Array(rb.data.buffer, rb.data.byteOffset, mask.length);
|
|
216
|
+
let any = false;
|
|
217
|
+
for (let i = 0; i < mask.length; i++) {
|
|
218
|
+
if (pa[i] !== pb[i]) {
|
|
219
|
+
mask[i] = 1;
|
|
220
|
+
any = true;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return any ? mask : null;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Paint every pixel either side found unstable onto both sides, so that noise is left
|
|
227
|
+
* out of the measurement. Rewrites `left.png` / `right.png`; returns the painted pixel count.
|
|
228
|
+
*/
|
|
229
|
+
export async function cancelUnstable(left, right) {
|
|
230
|
+
if (!left.unstable && !right.unstable)
|
|
231
|
+
return 0;
|
|
232
|
+
const raws = await Promise.all([decode(left.png), decode(right.png)]);
|
|
233
|
+
const w = Math.max(raws[0].info.width, raws[1].info.width);
|
|
234
|
+
const h = Math.max(raws[0].info.height, raws[1].info.height);
|
|
235
|
+
const union = new Uint8Array(w * h);
|
|
236
|
+
let count = 0;
|
|
237
|
+
[left, right].forEach((shot, s) => {
|
|
238
|
+
const u = shot.unstable;
|
|
239
|
+
if (!u)
|
|
240
|
+
return;
|
|
241
|
+
const sw = raws[s].info.width;
|
|
242
|
+
for (let i = 0; i < u.length; i++) {
|
|
243
|
+
if (!u[i])
|
|
244
|
+
continue;
|
|
245
|
+
const j = Math.floor(i / sw) * w + (i % sw);
|
|
246
|
+
if (!union[j]) {
|
|
247
|
+
union[j] = 1;
|
|
248
|
+
count++;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
await Promise.all([left, right].map(async (shot, s) => {
|
|
253
|
+
const { data, info } = raws[s];
|
|
254
|
+
for (let y = 0; y < info.height; y++) {
|
|
255
|
+
for (let x = 0; x < info.width; x++) {
|
|
256
|
+
if (!union[y * w + x])
|
|
257
|
+
continue;
|
|
258
|
+
const o = (y * info.width + x) * 4;
|
|
259
|
+
data[o] = 0xff;
|
|
260
|
+
data[o + 1] = 0x00;
|
|
261
|
+
data[o + 2] = 0xff;
|
|
262
|
+
data[o + 3] = 0xff;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
shot.png = await sharp(data, {
|
|
266
|
+
raw: { width: info.width, height: info.height, channels: 4 },
|
|
267
|
+
})
|
|
268
|
+
.png()
|
|
269
|
+
.toBuffer();
|
|
270
|
+
}));
|
|
271
|
+
return count;
|
|
272
|
+
}
|
|
273
|
+
async function paintBoxes(png, boxes) {
|
|
274
|
+
if (!boxes.length)
|
|
275
|
+
return png;
|
|
276
|
+
const { width: W = 0, height: H = 0 } = await sharp(png).metadata();
|
|
277
|
+
const rects = boxes.flatMap((b) => {
|
|
278
|
+
const left = Math.max(0, Math.floor(b.x));
|
|
279
|
+
const top = Math.max(0, Math.floor(b.y));
|
|
280
|
+
const width = Math.min(W, Math.ceil(b.x + b.width)) - left;
|
|
281
|
+
const height = Math.min(H, Math.ceil(b.y + b.height)) - top;
|
|
282
|
+
if (width <= 0 || height <= 0)
|
|
283
|
+
return [];
|
|
284
|
+
return [
|
|
285
|
+
{
|
|
286
|
+
input: {
|
|
287
|
+
create: { width, height, channels: 4, background: MASK_COLOR },
|
|
288
|
+
},
|
|
289
|
+
left,
|
|
290
|
+
top,
|
|
291
|
+
},
|
|
292
|
+
];
|
|
293
|
+
});
|
|
294
|
+
return rects.length ? sharp(png).composite(rects).png().toBuffer() : png;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Capture a page, or an element's box, taller than Chromium's one-shot limit as vertical
|
|
298
|
+
* tiles, then stitch them into a single full-height PNG.
|
|
299
|
+
*
|
|
300
|
+
* Playwright's `page.screenshot({ clip })` clips against the viewport, so it cannot
|
|
301
|
+
* reach a band below the fold. We drop to a raw CDP `Page.captureScreenshot` with
|
|
302
|
+
* `captureBeyondViewport: true`, which renders the whole page offscreen and clips in
|
|
303
|
+
* page coordinates — no per-tile scrolling. That matters here: the page renders at
|
|
304
|
+
* scroll-0 for every tile, exactly as a `fullPage` shot would, so a pinned/sticky
|
|
305
|
+
* element and its spacer stay put instead of shifting between tiles.
|
|
306
|
+
*
|
|
307
|
+
* `deviceScaleFactor` is 1 (`CONTEXT_OPTIONS` in config.ts), so clip coords are CSS px.
|
|
308
|
+
*/
|
|
309
|
+
async function captureTiled(page, box) {
|
|
310
|
+
const client = await page.context().newCDPSession(page);
|
|
311
|
+
try {
|
|
312
|
+
const tiles = [];
|
|
313
|
+
for (let y = 0; y < box.height; y += TILE_HEIGHT) {
|
|
314
|
+
const height = Math.min(TILE_HEIGHT, box.height - y);
|
|
315
|
+
const { data } = await client.send("Page.captureScreenshot", {
|
|
316
|
+
format: "png",
|
|
317
|
+
captureBeyondViewport: true,
|
|
318
|
+
clip: { x: box.x, y: box.y + y, width: box.width, height, scale: 1 },
|
|
319
|
+
});
|
|
320
|
+
tiles.push(Buffer.from(data, "base64"));
|
|
321
|
+
}
|
|
322
|
+
return sharp({
|
|
323
|
+
create: {
|
|
324
|
+
width: box.width,
|
|
325
|
+
height: box.height,
|
|
326
|
+
channels: 4,
|
|
327
|
+
background: MASK_COLOR,
|
|
328
|
+
},
|
|
329
|
+
})
|
|
330
|
+
.composite(tiles.map((input, i) => ({ input, left: 0, top: i * TILE_HEIGHT })))
|
|
331
|
+
.png()
|
|
332
|
+
.toBuffer();
|
|
333
|
+
}
|
|
334
|
+
finally {
|
|
335
|
+
await client.detach();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
//# sourceMappingURL=capture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capture.js","sourceRoot":"","sources":["../src/capture.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Screenshot diff between two origins.
|
|
4
|
+
*
|
|
5
|
+
* renderdiff <left-origin> <right-origin> <route…>
|
|
6
|
+
*
|
|
7
|
+
* Screenshots the given routes on both origins at several viewport widths and
|
|
8
|
+
* pixel-diffs them. Routes are required and literal — the tool does no route discovery.
|
|
9
|
+
*
|
|
10
|
+
* Per-site knobs (masks for content that drifts, extra selectors to hide, defaults for
|
|
11
|
+
* the flags below) live in a `renderdiff.config.ts` / `.mjs` in the working directory.
|
|
12
|
+
*/
|
|
13
|
+
import { label, loadConfig, parseRoutes, resolveConfig, selectorText, version, } from "./config.js";
|
|
14
|
+
/** `Number(raw)`, but reject anything that isn't a finite number with a flag-named error. */
|
|
15
|
+
const num = (flag, raw) => {
|
|
16
|
+
const n = Number(raw);
|
|
17
|
+
if (!Number.isFinite(n)) {
|
|
18
|
+
throw new Error(`${flag} expects a number, got "${raw}"`);
|
|
19
|
+
}
|
|
20
|
+
return n;
|
|
21
|
+
};
|
|
22
|
+
function parseArgs(argv) {
|
|
23
|
+
const a = {
|
|
24
|
+
dryRun: false,
|
|
25
|
+
open: false,
|
|
26
|
+
help: false,
|
|
27
|
+
version: false,
|
|
28
|
+
routes: [],
|
|
29
|
+
};
|
|
30
|
+
const origins = [];
|
|
31
|
+
const sel = {};
|
|
32
|
+
const state = {};
|
|
33
|
+
for (let i = 0; i < argv.length; i++) {
|
|
34
|
+
const raw = argv[i];
|
|
35
|
+
// Accept both `--flag value` and `--flag=value`.
|
|
36
|
+
const eq = raw.startsWith("--") ? raw.indexOf("=") : -1;
|
|
37
|
+
const v = eq === -1 ? raw : raw.slice(0, eq);
|
|
38
|
+
const inline = eq === -1 ? undefined : raw.slice(eq + 1);
|
|
39
|
+
// Require a value: the inline `--flag=value`, or the next argv token — but not
|
|
40
|
+
// another flag, which means the value was simply omitted. Only advance `i` when
|
|
41
|
+
// a positional token is actually consumed.
|
|
42
|
+
const next = () => {
|
|
43
|
+
if (inline !== undefined)
|
|
44
|
+
return inline;
|
|
45
|
+
const peek = argv[i + 1];
|
|
46
|
+
if (peek === undefined || peek.startsWith("-")) {
|
|
47
|
+
throw new Error(`${v} needs a value`);
|
|
48
|
+
}
|
|
49
|
+
return argv[++i];
|
|
50
|
+
};
|
|
51
|
+
if (v === "--help" || v === "-h")
|
|
52
|
+
a.help = true;
|
|
53
|
+
else if (v === "--version" || v === "-v")
|
|
54
|
+
a.version = true;
|
|
55
|
+
else if (v === "--dry-run")
|
|
56
|
+
a.dryRun = true;
|
|
57
|
+
else if (v === "--open")
|
|
58
|
+
a.open = true;
|
|
59
|
+
else if (v === "--progress")
|
|
60
|
+
a.progress = true;
|
|
61
|
+
else if (v === "--no-progress")
|
|
62
|
+
a.progress = false;
|
|
63
|
+
else if (v === "--left")
|
|
64
|
+
a.left = next();
|
|
65
|
+
else if (v === "--right")
|
|
66
|
+
a.right = next();
|
|
67
|
+
else if (v === "--out")
|
|
68
|
+
a.outDir = next();
|
|
69
|
+
else if (v === "--config")
|
|
70
|
+
a.config = next();
|
|
71
|
+
else if (v === "--selector" || v === "--left-selector" || v === "--right-selector") {
|
|
72
|
+
const s = next();
|
|
73
|
+
if (!s)
|
|
74
|
+
throw new Error(`${v} needs a value`);
|
|
75
|
+
sel[v === "--selector" ? "both" : v === "--left-selector" ? "left" : "right"] = s;
|
|
76
|
+
}
|
|
77
|
+
else if (v === "--storage-state" ||
|
|
78
|
+
v === "--left-storage-state" ||
|
|
79
|
+
v === "--right-storage-state") {
|
|
80
|
+
const s = next();
|
|
81
|
+
if (!s)
|
|
82
|
+
throw new Error(`${v} needs a value`);
|
|
83
|
+
state[v === "--storage-state" ? "both" : v === "--left-storage-state" ? "left" : "right"] = s;
|
|
84
|
+
}
|
|
85
|
+
else if (v === "--width") {
|
|
86
|
+
const w = num("--width", next());
|
|
87
|
+
if (w <= 0)
|
|
88
|
+
throw new Error(`--width expects a positive number, got ${w}`);
|
|
89
|
+
(a.widths ??= []).push(w);
|
|
90
|
+
}
|
|
91
|
+
else if (v === "--concurrency") {
|
|
92
|
+
const n = num("--concurrency", next());
|
|
93
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
94
|
+
throw new Error(`--concurrency expects a positive integer, got ${n}`);
|
|
95
|
+
}
|
|
96
|
+
a.concurrency = n;
|
|
97
|
+
}
|
|
98
|
+
else if (v === "--threshold") {
|
|
99
|
+
const t = num("--threshold", next());
|
|
100
|
+
if (t < 0 || t > 1)
|
|
101
|
+
throw new Error(`--threshold expects 0..1, got ${t}`);
|
|
102
|
+
a.threshold = t;
|
|
103
|
+
}
|
|
104
|
+
else if (v.startsWith("-"))
|
|
105
|
+
throw new Error(`unknown flag: ${v}`);
|
|
106
|
+
else if (/^https?:\/\//i.test(v))
|
|
107
|
+
origins.push(v);
|
|
108
|
+
else
|
|
109
|
+
a.routes.push(v);
|
|
110
|
+
}
|
|
111
|
+
if (origins.length > 2) {
|
|
112
|
+
throw new Error(`at most two origins (left, right), got ${origins.length}: ${origins.join(" ")}`);
|
|
113
|
+
}
|
|
114
|
+
for (const [i, side] of ["left", "right"].entries()) {
|
|
115
|
+
if (origins[i] === undefined)
|
|
116
|
+
continue;
|
|
117
|
+
if (a[side] !== undefined) {
|
|
118
|
+
throw new Error(`${side} origin given twice: ${origins[i]} and --${side} ${a[side]}`);
|
|
119
|
+
}
|
|
120
|
+
a[side] = origins[i];
|
|
121
|
+
}
|
|
122
|
+
const left = sel.left ?? sel.both;
|
|
123
|
+
const right = sel.right ?? sel.both;
|
|
124
|
+
if (!left !== !right) {
|
|
125
|
+
const [has, lacks] = left ? ["left", "right"] : ["right", "left"];
|
|
126
|
+
throw new Error(`--${has}-selector needs --${lacks}-selector or --selector`);
|
|
127
|
+
}
|
|
128
|
+
if (left && right)
|
|
129
|
+
a.selector = { left, right };
|
|
130
|
+
if (state.both || state.left || state.right) {
|
|
131
|
+
a.storageState = { left: state.left ?? state.both, right: state.right ?? state.both };
|
|
132
|
+
}
|
|
133
|
+
return a;
|
|
134
|
+
}
|
|
135
|
+
const HELP = `
|
|
136
|
+
renderdiff — screenshot diff between two origins
|
|
137
|
+
|
|
138
|
+
Usage
|
|
139
|
+
renderdiff <left-origin> <right-origin> <route…> [options]
|
|
140
|
+
|
|
141
|
+
renderdiff http://localhost:3000 https://example.com / /about
|
|
142
|
+
renderdiff http://localhost:3000 https://example.com /pricing::/plans
|
|
143
|
+
|
|
144
|
+
An argument starting with http:// or https:// is an origin: the first is left, the
|
|
145
|
+
second right (or pass --left / --right). A route is a literal path, loaded on both sides; /a::/b loads
|
|
146
|
+
/a on the left and /b on the right. No globs, no discovery.
|
|
147
|
+
|
|
148
|
+
With --selector, each side compares the one element it matches instead of the page;
|
|
149
|
+
it must match exactly one. It applies to every route, over the config's own selectors.
|
|
150
|
+
|
|
151
|
+
With --storage-state, each side starts with that Playwright storage state (cookies,
|
|
152
|
+
localStorage), e.g. signed in. Make one with:
|
|
153
|
+
npx playwright codegen --save-storage=auth.json <url>
|
|
154
|
+
A before hook in the config runs on each page before it is shot, e.g. to open a menu.
|
|
155
|
+
|
|
156
|
+
Writes report.html, results.json and crops of each change to --out. Exits 1 if a
|
|
157
|
+
route falls below --threshold, 3 if one errored (unmeasured) or the run crashed, 2 on
|
|
158
|
+
bad arguments or an origin that is down. A diff without --threshold exits 0.
|
|
159
|
+
|
|
160
|
+
Options
|
|
161
|
+
--left <url> Left origin, instead of the first positional one.
|
|
162
|
+
--right <url> Right origin, instead of the second positional one.
|
|
163
|
+
--width <n> Viewport width; repeatable. Replaces the defaults.
|
|
164
|
+
--threshold <0..1> Fail if any comparison scores below this.
|
|
165
|
+
--out <dir> Output directory (default: ./renderdiff-out)
|
|
166
|
+
--concurrency <n> Parallel workers (default: half the cores, 2–8)
|
|
167
|
+
--config <path> Config file. Default: renderdiff.config.{ts,mts,js,mjs} in cwd.
|
|
168
|
+
--selector <sel> Compare one element (a Playwright selector) on both sides.
|
|
169
|
+
--left-selector <sel>, --right-selector <sel>
|
|
170
|
+
That side's selector, over --selector.
|
|
171
|
+
--storage-state <file>
|
|
172
|
+
Start both sides with this storage state JSON. Never commit it.
|
|
173
|
+
--left-storage-state <file>, --right-storage-state <file>
|
|
174
|
+
That side's storage state, over --storage-state.
|
|
175
|
+
--dry-run Print the URL pairs and exit. No browser.
|
|
176
|
+
--open Open the report when the run finishes.
|
|
177
|
+
--progress A line per shot as it finishes. The default on a terminal;
|
|
178
|
+
otherwise one summary of the differing routes at the end.
|
|
179
|
+
--no-progress The end-of-run summary, even on a terminal.
|
|
180
|
+
-h, --help Show this.
|
|
181
|
+
-v, --version Print the version.
|
|
182
|
+
|
|
183
|
+
Default widths: 1440, 1152, 896, 704, 375
|
|
184
|
+
|
|
185
|
+
Masks, hidden selectors, blocked URLs and route lists go in a config file; CLI flags
|
|
186
|
+
win over it. See the README, or skills/renderdiff/reference.md.
|
|
187
|
+
`;
|
|
188
|
+
const usage = () => {
|
|
189
|
+
console.error(" Usage: renderdiff <left-origin> <right-origin> <route…>");
|
|
190
|
+
console.error(" Try: renderdiff --help");
|
|
191
|
+
};
|
|
192
|
+
// A function declaration, not a const arrow: TypeScript only lets a `never`-returning
|
|
193
|
+
// call narrow control flow when the callee is declared this way.
|
|
194
|
+
function die(message) {
|
|
195
|
+
console.error(`✗ ${message}\n`);
|
|
196
|
+
usage();
|
|
197
|
+
process.exit(2);
|
|
198
|
+
}
|
|
199
|
+
const alive = async (origin) => {
|
|
200
|
+
try {
|
|
201
|
+
return ((await fetch(origin, {
|
|
202
|
+
method: "HEAD",
|
|
203
|
+
signal: AbortSignal.timeout(3000),
|
|
204
|
+
})).status < 500);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* Refuse to start unless both origins answer — two error pages diffed against each
|
|
212
|
+
* other score well, which is the likeliest way this tool lies.
|
|
213
|
+
*/
|
|
214
|
+
async function preflight(left, right) {
|
|
215
|
+
const [leftUp, rightUp] = await Promise.all([alive(left), alive(right)]);
|
|
216
|
+
if (leftUp && rightUp)
|
|
217
|
+
return;
|
|
218
|
+
const down = [!leftUp && `left (${left})`, !rightUp && `right (${right})`]
|
|
219
|
+
.filter(Boolean)
|
|
220
|
+
.join(" and ");
|
|
221
|
+
console.error(`✗ ${down} not responding.\n`);
|
|
222
|
+
console.error(" If that is a local dev server, start it first — and make sure it serves the");
|
|
223
|
+
console.error(" same assets as the other origin (a bare preview server that 404s images will");
|
|
224
|
+
console.error(" collapse every page and diff as garbage).\n");
|
|
225
|
+
process.exit(2);
|
|
226
|
+
}
|
|
227
|
+
let args;
|
|
228
|
+
try {
|
|
229
|
+
args = parseArgs(process.argv.slice(2));
|
|
230
|
+
}
|
|
231
|
+
catch (e) {
|
|
232
|
+
die(e instanceof Error ? e.message : String(e));
|
|
233
|
+
}
|
|
234
|
+
if (args.help) {
|
|
235
|
+
console.log(HELP.trimEnd());
|
|
236
|
+
process.exit(0);
|
|
237
|
+
}
|
|
238
|
+
if (args.version) {
|
|
239
|
+
console.log(version());
|
|
240
|
+
process.exit(0);
|
|
241
|
+
}
|
|
242
|
+
const { config: fileConfig, path: configPath } = await loadConfig(args.config).catch((e) => {
|
|
243
|
+
console.error(`✗ ${e instanceof Error ? e.message : String(e)}`);
|
|
244
|
+
process.exit(2);
|
|
245
|
+
});
|
|
246
|
+
let cfg;
|
|
247
|
+
try {
|
|
248
|
+
cfg = resolveConfig(fileConfig, args);
|
|
249
|
+
}
|
|
250
|
+
catch (e) {
|
|
251
|
+
die(e instanceof Error ? e.message : String(e));
|
|
252
|
+
}
|
|
253
|
+
const rawRoutes = args.routes.length ? args.routes : (fileConfig.routes ?? []);
|
|
254
|
+
if (!rawRoutes.length) {
|
|
255
|
+
die("no routes given.");
|
|
256
|
+
}
|
|
257
|
+
let routes;
|
|
258
|
+
try {
|
|
259
|
+
routes = parseRoutes(rawRoutes, args.selector, fileConfig.before);
|
|
260
|
+
}
|
|
261
|
+
catch (e) {
|
|
262
|
+
die(e instanceof Error ? e.message : String(e));
|
|
263
|
+
}
|
|
264
|
+
if (configPath)
|
|
265
|
+
console.log(`Config: ${configPath}`);
|
|
266
|
+
console.log(`Routes: ${routes.length}`);
|
|
267
|
+
if (args.dryRun) {
|
|
268
|
+
for (const r of routes) {
|
|
269
|
+
console.log(` ${label(r).padEnd(52)} ${cfg.left}${r.left} | ${cfg.right}${r.right}` +
|
|
270
|
+
(r.selector ? ` [${selectorText(r.selector)}]` : ""));
|
|
271
|
+
}
|
|
272
|
+
process.exit(0);
|
|
273
|
+
}
|
|
274
|
+
await preflight(cfg.left, cfg.right);
|
|
275
|
+
const { run } = await import("./run.js");
|
|
276
|
+
try {
|
|
277
|
+
process.exit(await run({
|
|
278
|
+
routes,
|
|
279
|
+
cfg,
|
|
280
|
+
open: args.open,
|
|
281
|
+
progress: args.progress ?? !!process.stdout.isTTY,
|
|
282
|
+
}));
|
|
283
|
+
}
|
|
284
|
+
catch (e) {
|
|
285
|
+
console.error(`✗ ${e instanceof Error ? e.message : String(e)}`);
|
|
286
|
+
process.exit(3);
|
|
287
|
+
}
|
|
288
|
+
//# sourceMappingURL=cli.js.map
|