@juangadm/pre-post 0.3.0 → 1.1.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 +174 -233
- package/dist/baseline.d.ts +119 -0
- package/dist/baseline.d.ts.map +1 -0
- package/dist/baseline.js +316 -0
- package/dist/baseline.js.map +1 -0
- package/dist/bin/cli.js +243 -458
- package/dist/bin/cli.js.map +1 -1
- package/dist/browser.d.ts +68 -25
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +500 -105
- package/dist/browser.js.map +1 -1
- package/dist/capture.d.ts +1 -6
- package/dist/capture.d.ts.map +1 -1
- package/dist/capture.js +12 -36
- package/dist/capture.js.map +1 -1
- package/dist/commands/compare.d.ts +18 -0
- package/dist/commands/compare.d.ts.map +1 -0
- package/dist/commands/compare.js +82 -0
- package/dist/commands/compare.js.map +1 -0
- package/dist/commands/detect.d.ts +25 -0
- package/dist/commands/detect.d.ts.map +1 -0
- package/dist/commands/detect.js +22 -0
- package/dist/commands/detect.js.map +1 -0
- package/dist/commands/doctor.d.ts +42 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +119 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/login.d.ts +6 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +34 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/pr.d.ts +32 -0
- package/dist/commands/pr.d.ts.map +1 -0
- package/dist/commands/pr.js +259 -0
- package/dist/commands/pr.js.map +1 -0
- package/dist/commands/prune.d.ts +10 -0
- package/dist/commands/prune.d.ts.map +1 -0
- package/dist/commands/prune.js +13 -0
- package/dist/commands/prune.js.map +1 -0
- package/dist/comparison.d.ts +104 -0
- package/dist/comparison.d.ts.map +1 -0
- package/dist/comparison.js +236 -0
- package/dist/comparison.js.map +1 -0
- package/dist/config.d.ts +46 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +82 -0
- package/dist/config.js.map +1 -0
- package/dist/deployments.d.ts +71 -0
- package/dist/deployments.d.ts.map +1 -0
- package/dist/deployments.js +204 -0
- package/dist/deployments.js.map +1 -0
- package/dist/diff-pool.d.ts +17 -0
- package/dist/diff-pool.d.ts.map +1 -0
- package/dist/diff-pool.js +92 -0
- package/dist/diff-pool.js.map +1 -0
- package/dist/diff-worker.d.ts +32 -0
- package/dist/diff-worker.d.ts.map +1 -0
- package/dist/diff-worker.js +39 -0
- package/dist/diff-worker.js.map +1 -0
- package/dist/diff.d.ts +47 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +281 -0
- package/dist/diff.js.map +1 -0
- package/dist/doctor.d.ts +79 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +138 -0
- package/dist/doctor.js.map +1 -0
- package/dist/errors.d.ts +38 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +59 -0
- package/dist/errors.js.map +1 -0
- package/dist/git.d.ts +61 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +182 -0
- package/dist/git.js.map +1 -0
- package/dist/github.d.ts +163 -0
- package/dist/github.d.ts.map +1 -0
- package/dist/github.js +411 -0
- package/dist/github.js.map +1 -0
- package/dist/index.d.ts +22 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -67
- package/dist/index.js.map +1 -1
- package/dist/landing.d.ts +48 -0
- package/dist/landing.d.ts.map +1 -0
- package/dist/landing.js +93 -0
- package/dist/landing.js.map +1 -0
- package/dist/pkg.d.ts +17 -0
- package/dist/pkg.d.ts.map +1 -0
- package/dist/pkg.js +49 -0
- package/dist/pkg.js.map +1 -0
- package/dist/report.d.ts +20 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +130 -0
- package/dist/report.js.map +1 -0
- package/dist/routes/imports.d.ts +46 -0
- package/dist/routes/imports.d.ts.map +1 -0
- package/dist/routes/imports.js +208 -0
- package/dist/routes/imports.js.map +1 -0
- package/dist/routes/vite.d.ts +15 -0
- package/dist/routes/vite.d.ts.map +1 -0
- package/dist/routes/vite.js +130 -0
- package/dist/routes/vite.js.map +1 -0
- package/dist/routes.d.ts +65 -11
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +346 -141
- package/dist/routes.js.map +1 -1
- package/dist/run.d.ts +116 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +316 -0
- package/dist/run.js.map +1 -0
- package/dist/sameness.d.ts +91 -0
- package/dist/sameness.d.ts.map +1 -0
- package/dist/sameness.js +157 -0
- package/dist/sameness.js.map +1 -0
- package/dist/sessions.d.ts +50 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +80 -0
- package/dist/sessions.js.map +1 -0
- package/dist/shift.d.ts +67 -0
- package/dist/shift.d.ts.map +1 -0
- package/dist/shift.js +184 -0
- package/dist/shift.js.map +1 -0
- package/dist/types.d.ts +179 -68
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -1
- package/dist/url.d.ts +12 -0
- package/dist/url.d.ts.map +1 -0
- package/dist/url.js +30 -0
- package/dist/url.js.map +1 -0
- package/dist/viewport.d.ts +6 -1
- package/dist/viewport.d.ts.map +1 -1
- package/dist/viewport.js +13 -7
- package/dist/viewport.js.map +1 -1
- package/package.json +21 -14
- package/skill/SKILL.md +56 -221
- package/dist/clipboard.d.ts +0 -9
- package/dist/clipboard.d.ts.map +0 -1
- package/dist/clipboard.js +0 -26
- package/dist/clipboard.js.map +0 -1
- package/dist/upload.d.ts +0 -39
- package/dist/upload.d.ts.map +0 -1
- package/dist/upload.js +0 -159
- package/dist/upload.js.map +0 -1
- package/dist/video.d.ts +0 -19
- package/dist/video.d.ts.map +0 -1
- package/dist/video.js +0 -263
- package/dist/video.js.map +0 -1
- package/skill/scripts/adapters/0x0st.sh +0 -36
- package/skill/scripts/adapters/blob.sh +0 -55
- package/skill/scripts/adapters/git-native.sh +0 -51
- package/skill/scripts/upload-and-copy.sh +0 -230
package/dist/run.d.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The capture → diff pipeline shared by `pr` and the two-URL mode.
|
|
3
|
+
* Every route/viewport pair runs concurrently; the browser's page pool bounds it.
|
|
4
|
+
*/
|
|
5
|
+
import { AuthOptions, RouteCaptureOutcome, ViewportSize } from './types.js';
|
|
6
|
+
export interface CaptureTask {
|
|
7
|
+
route: string;
|
|
8
|
+
resolvedRoute: string;
|
|
9
|
+
viewport: string;
|
|
10
|
+
size: ViewportSize;
|
|
11
|
+
beforeUrl: string;
|
|
12
|
+
afterUrl: string;
|
|
13
|
+
}
|
|
14
|
+
/** Where the two sides came from, so a verdict about them can say which is which. */
|
|
15
|
+
export interface RunSides {
|
|
16
|
+
before: {
|
|
17
|
+
url: string;
|
|
18
|
+
detail?: string;
|
|
19
|
+
};
|
|
20
|
+
after: {
|
|
21
|
+
url: string;
|
|
22
|
+
detail?: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* How this caller's user would fix a wrong pairing. Only the command knows:
|
|
26
|
+
* `pr` chose the baseline and can offer `--before`, while the two-URL mode
|
|
27
|
+
* was handed both sides as positional arguments, where that flag means
|
|
28
|
+
* nothing.
|
|
29
|
+
*/
|
|
30
|
+
fix?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Why this run cannot be reported as a comparison.
|
|
34
|
+
*
|
|
35
|
+
* Two ways to reach the same conclusion, kept apart because they need
|
|
36
|
+
* different advice: a wall wants credentials, a wrong baseline wants a URL.
|
|
37
|
+
*/
|
|
38
|
+
export interface RunVerdict {
|
|
39
|
+
kind: 'walled' | 'different-sites';
|
|
40
|
+
/** The single actionable sentence a human needs. */
|
|
41
|
+
hint: string;
|
|
42
|
+
}
|
|
43
|
+
export interface RunResult {
|
|
44
|
+
outcomes: RouteCaptureOutcome[];
|
|
45
|
+
/** Non-null when the run did not compare what it claims to have compared. */
|
|
46
|
+
verdict: RunVerdict | null;
|
|
47
|
+
}
|
|
48
|
+
export interface PipelineOptions {
|
|
49
|
+
outputDir: string;
|
|
50
|
+
fullPage: boolean;
|
|
51
|
+
maxHeight: number;
|
|
52
|
+
scale: number;
|
|
53
|
+
threshold: number;
|
|
54
|
+
minChangedArea: number;
|
|
55
|
+
wait?: number;
|
|
56
|
+
auth?: AuthOptions;
|
|
57
|
+
/**
|
|
58
|
+
* How resolution chose the two sides. Optional: without it a verdict still
|
|
59
|
+
* fires and names the URLs the captures actually used, which is enough to
|
|
60
|
+
* act on — it just cannot say *why* that baseline was picked.
|
|
61
|
+
*/
|
|
62
|
+
sides?: RunSides;
|
|
63
|
+
log?: (msg: string) => void;
|
|
64
|
+
}
|
|
65
|
+
export declare function routeSlug(route: string): string;
|
|
66
|
+
/**
|
|
67
|
+
* Did this capture change?
|
|
68
|
+
*
|
|
69
|
+
* Two arms, because they cover different regimes and neither covers both:
|
|
70
|
+
*
|
|
71
|
+
* - Painted area, in CSS pixels² — the rule for pages. Measured against the
|
|
72
|
+
* fixture ladder, every deliberate change covers 242 CSS px² or more while
|
|
73
|
+
* every no-op covers exactly 0, so the floor sits in a wide empty band. CSS
|
|
74
|
+
* pixels rather than device pixels so it means the same at --scale 1 as at
|
|
75
|
+
* --scale 2; the old device-pixel constant was silently four times stricter
|
|
76
|
+
* at scale 1, and at ~10 CSS px² amounted to a third of one letter.
|
|
77
|
+
* - Share of the canvas — the rule for small images. One pixel of a 20x20 icon
|
|
78
|
+
* is a quarter of a percent of it and obviously a change, but it will never
|
|
79
|
+
* reach an area floor sized for pages.
|
|
80
|
+
*
|
|
81
|
+
* On a full page the ratio arm effectively never fires on its own (a recoloured
|
|
82
|
+
* 16px icon is 0.024% of one), and on a small image the area arm never does.
|
|
83
|
+
* Dropping either one loses real changes, which is the worse error: reporting
|
|
84
|
+
* "no visual changes" on a PR that changed something is the one answer that
|
|
85
|
+
* makes a reviewer stop looking.
|
|
86
|
+
*/
|
|
87
|
+
export declare function isChanged(diff: {
|
|
88
|
+
changedRatio: number;
|
|
89
|
+
changedPixels: number;
|
|
90
|
+
}, opts: {
|
|
91
|
+
minChangedArea: number;
|
|
92
|
+
threshold: number;
|
|
93
|
+
scale: number;
|
|
94
|
+
}): boolean;
|
|
95
|
+
/** "shifted down 48px" — the direction spelled out, because the sign is not. */
|
|
96
|
+
export declare function describeShift(px: number): string;
|
|
97
|
+
/**
|
|
98
|
+
* Did the baseline answer unrelated routes with the same page?
|
|
99
|
+
*
|
|
100
|
+
* This is the soft 404 — a not-found view served with status 200, which no
|
|
101
|
+
* status check can see and which a keyword hunt for "404" would catch only in
|
|
102
|
+
* English, only when the page says it, and only when no real page happens to
|
|
103
|
+
* mention it. The durable signal is not the wording but the sameness: a host
|
|
104
|
+
* that returns pixel-identical bytes for several different routes is serving a
|
|
105
|
+
* catch-all, and every comparison against it is measuring the distance from
|
|
106
|
+
* that catch-all rather than from the page.
|
|
107
|
+
*
|
|
108
|
+
* It warns rather than reclassifies. Two routes can legitimately render the
|
|
109
|
+
* same page, and silently turning a real diff into "new page" on a heuristic
|
|
110
|
+
* would hide exactly the change this tool exists to show. The authoritative
|
|
111
|
+
* signal (a 404 status) acts; this one only tells the reader what it sees.
|
|
112
|
+
*/
|
|
113
|
+
export declare function softNotFoundWarning(outcomes: RouteCaptureOutcome[], beforeUrl: string): string | null;
|
|
114
|
+
export declare function verdictFor(outcomes: RouteCaptureOutcome[], sides: RunSides): RunVerdict | null;
|
|
115
|
+
export declare function runTasks(tasks: CaptureTask[], opts: PipelineOptions): Promise<RunResult>;
|
|
116
|
+
//# sourceMappingURL=run.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,EAAE,WAAW,EAA8B,mBAAmB,EAAc,YAAY,EAAE,MAAM,YAAY,CAAC;AASpH,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,YAAY,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,qFAAqF;AACrF,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACzC,KAAK,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACxC;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,QAAQ,GAAG,iBAAiB,CAAC;IACnC,oDAAoD;IACpD,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,6EAA6E;IAC7E,OAAO,EAAE,UAAU,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB;;;;OAIG;IACH,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAG/C;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,EACrD,IAAI,EAAE;IAAE,cAAc,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACjE,OAAO,CAIT;AAED,gFAAgF;AAChF,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAGhD;AA6MD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,mBAAmB,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiBrG;AAED,wBAAgB,UAAU,CAAC,QAAQ,EAAE,mBAAmB,EAAE,EAAE,KAAK,EAAE,QAAQ,GAAG,UAAU,GAAG,IAAI,CAY9F;AAED,wBAAsB,QAAQ,CAAC,KAAK,EAAE,WAAW,EAAE,EAAE,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,SAAS,CAAC,CAkB9F"}
|
package/dist/run.js
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The capture → diff pipeline shared by `pr` and the two-URL mode.
|
|
3
|
+
* Every route/viewport pair runs concurrently; the browser's page pool bounds it.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import { createHash } from 'crypto';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { captureScreenshot } from './browser.js';
|
|
9
|
+
import { checkLanding, signInHint } from './landing.js';
|
|
10
|
+
import { differentSitesHint, looksLikeDifferentSites, textOverlap, titleOverlap } from './sameness.js';
|
|
11
|
+
import { HttpStatusError, NavigationError } from './errors.js';
|
|
12
|
+
import { DiffPool } from './diff-pool.js';
|
|
13
|
+
import { authHint } from './doctor.js';
|
|
14
|
+
import { hostOf } from './url.js';
|
|
15
|
+
export function routeSlug(route) {
|
|
16
|
+
const slug = route.replace(/^\/+|\/+$/g, '').replace(/[^\w.-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
17
|
+
return slug || 'home';
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Did this capture change?
|
|
21
|
+
*
|
|
22
|
+
* Two arms, because they cover different regimes and neither covers both:
|
|
23
|
+
*
|
|
24
|
+
* - Painted area, in CSS pixels² — the rule for pages. Measured against the
|
|
25
|
+
* fixture ladder, every deliberate change covers 242 CSS px² or more while
|
|
26
|
+
* every no-op covers exactly 0, so the floor sits in a wide empty band. CSS
|
|
27
|
+
* pixels rather than device pixels so it means the same at --scale 1 as at
|
|
28
|
+
* --scale 2; the old device-pixel constant was silently four times stricter
|
|
29
|
+
* at scale 1, and at ~10 CSS px² amounted to a third of one letter.
|
|
30
|
+
* - Share of the canvas — the rule for small images. One pixel of a 20x20 icon
|
|
31
|
+
* is a quarter of a percent of it and obviously a change, but it will never
|
|
32
|
+
* reach an area floor sized for pages.
|
|
33
|
+
*
|
|
34
|
+
* On a full page the ratio arm effectively never fires on its own (a recoloured
|
|
35
|
+
* 16px icon is 0.024% of one), and on a small image the area arm never does.
|
|
36
|
+
* Dropping either one loses real changes, which is the worse error: reporting
|
|
37
|
+
* "no visual changes" on a PR that changed something is the one answer that
|
|
38
|
+
* makes a reviewer stop looking.
|
|
39
|
+
*/
|
|
40
|
+
export function isChanged(diff, opts) {
|
|
41
|
+
if (diff.changedPixels === 0)
|
|
42
|
+
return false;
|
|
43
|
+
return diff.changedPixels / (opts.scale * opts.scale) >= opts.minChangedArea
|
|
44
|
+
|| diff.changedRatio >= opts.threshold;
|
|
45
|
+
}
|
|
46
|
+
/** "shifted down 48px" — the direction spelled out, because the sign is not. */
|
|
47
|
+
export function describeShift(px) {
|
|
48
|
+
const rounded = Math.round(Math.abs(px));
|
|
49
|
+
return `shifted ${px < 0 ? 'up' : 'down'} ${rounded}px`;
|
|
50
|
+
}
|
|
51
|
+
function describeError(err, side, url) {
|
|
52
|
+
if (err instanceof HttpStatusError)
|
|
53
|
+
return authHint(err);
|
|
54
|
+
if (err instanceof NavigationError) {
|
|
55
|
+
switch (err.kind) {
|
|
56
|
+
case 'refused': return `connection refused at ${url} (is the ${side === 'after' ? 'dev server' : 'site'} running?)`;
|
|
57
|
+
case 'timeout': return `timed out loading ${url}`;
|
|
58
|
+
case 'dns': return `cannot resolve ${hostOf(url)}`;
|
|
59
|
+
default: return `${side}: ${err.message.slice(0, 160)}`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const msg = err?.message || String(err);
|
|
63
|
+
return `${side}: ${msg.split('\n')[0].slice(0, 160)}`;
|
|
64
|
+
}
|
|
65
|
+
/** Did this side reach the site, or a wall standing in front of it? */
|
|
66
|
+
function blockedSide(requested, result, side) {
|
|
67
|
+
const landing = checkLanding(requested, result.finalUrl ?? requested, result.title ?? '');
|
|
68
|
+
return landing.blocked ? { side, finalUrl: landing.finalUrl, vercel: Boolean(result.vercel) } : null;
|
|
69
|
+
}
|
|
70
|
+
async function runTask(task, opts, pool) {
|
|
71
|
+
const started = Date.now();
|
|
72
|
+
const base = { route: task.route, resolvedRoute: task.resolvedRoute, viewport: task.viewport, status: 'error' };
|
|
73
|
+
const captureOpts = {
|
|
74
|
+
viewport: task.size,
|
|
75
|
+
fullPage: opts.fullPage,
|
|
76
|
+
maxHeight: opts.maxHeight,
|
|
77
|
+
scale: opts.scale,
|
|
78
|
+
wait: opts.wait,
|
|
79
|
+
auth: opts.auth,
|
|
80
|
+
};
|
|
81
|
+
const [beforeRes, afterRes] = await Promise.allSettled([
|
|
82
|
+
captureScreenshot(task.beforeUrl, captureOpts),
|
|
83
|
+
captureScreenshot(task.afterUrl, captureOpts),
|
|
84
|
+
]);
|
|
85
|
+
if (beforeRes.status === 'rejected' || afterRes.status === 'rejected') {
|
|
86
|
+
const errors = [];
|
|
87
|
+
if (beforeRes.status === 'rejected')
|
|
88
|
+
errors.push(describeError(beforeRes.reason, 'before', task.beforeUrl));
|
|
89
|
+
if (afterRes.status === 'rejected')
|
|
90
|
+
errors.push(describeError(afterRes.reason, 'after', task.afterUrl));
|
|
91
|
+
return { ...base, error: errors.join('; '), durationMs: Date.now() - started };
|
|
92
|
+
}
|
|
93
|
+
const before = beforeRes.value;
|
|
94
|
+
const after = afterRes.value;
|
|
95
|
+
// A sign-in wall answers with 200, so both captures "succeeded" — of a login
|
|
96
|
+
// page. Diffing those and calling the result a visual change is the failure
|
|
97
|
+
// this check exists to prevent, so nothing is compared when either side is
|
|
98
|
+
// walled.
|
|
99
|
+
const blocked = blockedSide(task.beforeUrl, before, 'before') ?? blockedSide(task.afterUrl, after, 'after');
|
|
100
|
+
if (blocked) {
|
|
101
|
+
const label = blocked.side === 'before' ? 'Pre' : 'Post';
|
|
102
|
+
return {
|
|
103
|
+
...base,
|
|
104
|
+
blocked,
|
|
105
|
+
error: `${label} landed on a sign-in page (${blocked.finalUrl}), so the site was never captured`,
|
|
106
|
+
durationMs: Date.now() - started,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (before.status === 404 && after.status === 404) {
|
|
110
|
+
return { ...base, error: `404 on both ${task.beforeUrl} and ${task.afterUrl} (wrong route or missing sample?)`, durationMs: Date.now() - started };
|
|
111
|
+
}
|
|
112
|
+
const prefix = `${routeSlug(task.route)}-${task.viewport}`;
|
|
113
|
+
const file = (kind) => path.join(opts.outputDir, `${prefix}-${kind}.png`);
|
|
114
|
+
const outputs = { before: file('pre'), after: file('post'), diff: file('diff'), cropBefore: file('pre-crop'), cropAfter: file('post-crop') };
|
|
115
|
+
// A route that exists on only one side is not a comparison, and the moment to
|
|
116
|
+
// notice is here — before the diff, not after it in a footnote.
|
|
117
|
+
//
|
|
118
|
+
// The old path diffed anyway. For a page this branch adds, "Pre" was a
|
|
119
|
+
// screenshot of the baseline's 404 and the percentage measured the distance
|
|
120
|
+
// between an error page and a new one: 10.81% for a coloured box on white,
|
|
121
|
+
// 0.05% for a light-grey one, neither a fact about the change. Worse, both
|
|
122
|
+
// numbers were published as a Pre/Post pair, so a reviewer was shown a
|
|
123
|
+
// comparison that never existed and four images where one was true.
|
|
124
|
+
//
|
|
125
|
+
// Nothing can be recovered by trying harder — a page the baseline does not
|
|
126
|
+
// serve has no "before" to find — so the honest move is to keep the side that
|
|
127
|
+
// rendered, name why the other is missing, and skip the diff entirely.
|
|
128
|
+
const absent = absentSide(before, after);
|
|
129
|
+
if (absent) {
|
|
130
|
+
const present = absent === 'before' ? after : before;
|
|
131
|
+
const target = absent === 'before' ? outputs.after : outputs.before;
|
|
132
|
+
fs.writeFileSync(target, Buffer.from(present.image));
|
|
133
|
+
const status = absent === 'before' ? 'added' : 'removed';
|
|
134
|
+
const missingHost = hostOf(absent === 'before' ? task.beforeUrl : task.afterUrl);
|
|
135
|
+
opts.log?.(` ${status === 'added' ? 'new ' : 'removed '} ${task.route} @ ${task.viewport} (no ${absent === 'before' ? 'Pre' : 'Post'}, ${Date.now() - started}ms)`);
|
|
136
|
+
return {
|
|
137
|
+
...base,
|
|
138
|
+
status,
|
|
139
|
+
files: absent === 'before' ? { after: outputs.after } : { before: outputs.before },
|
|
140
|
+
// Deliberately no changedRatio: there is nothing to take a ratio of.
|
|
141
|
+
// Deliberately no textOverlap either — a 404's wording is evidence about
|
|
142
|
+
// an error page, not about whether the two sides are the same site.
|
|
143
|
+
note: absent === 'before'
|
|
144
|
+
? `new page — no baseline (${missingHost} returned 404)`
|
|
145
|
+
: `page removed — ${missingHost} returned 404`,
|
|
146
|
+
durationMs: Date.now() - started,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
// Copy into standalone buffers so they can be transferred to the worker.
|
|
150
|
+
const diff = await pool.run({
|
|
151
|
+
before: new Uint8Array(before.image),
|
|
152
|
+
after: new Uint8Array(after.image),
|
|
153
|
+
outputs,
|
|
154
|
+
options: {
|
|
155
|
+
padding: 40 * opts.scale,
|
|
156
|
+
minCrop: { width: 400 * opts.scale, height: 200 * opts.scale },
|
|
157
|
+
highlightDownscale: opts.scale >= 2 ? 2 : 1,
|
|
158
|
+
highlight: false,
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
const notes = [];
|
|
162
|
+
// The 404-on-one-side cases returned above; what is left is a page both sides
|
|
163
|
+
// served with an unhappy status, which is still worth diffing and flagging.
|
|
164
|
+
if (before.status && before.status >= 400)
|
|
165
|
+
notes.push(`production returned ${before.status}`);
|
|
166
|
+
if (after.status && after.status >= 400)
|
|
167
|
+
notes.push(`local returned ${after.status}`);
|
|
168
|
+
const changed = isChanged(diff, opts);
|
|
169
|
+
// Cheap fingerprint of the baseline, kept so the run can notice a host that
|
|
170
|
+
// answers every route with the same page. See softNotFoundWarning.
|
|
171
|
+
const baselineHash = createHash('sha1').update(Buffer.from(before.image)).digest('hex');
|
|
172
|
+
// Recorded on every outcome, judged only across the whole run: one route
|
|
173
|
+
// sharing no words is a rewrite, every route sharing none is the wrong site.
|
|
174
|
+
const overlap = textOverlap(before.text ?? '', after.text ?? '');
|
|
175
|
+
const titles = titleOverlap(before.title ?? '', after.title ?? '');
|
|
176
|
+
// A shift is measured in device pixels; everything a reader sees is in CSS
|
|
177
|
+
// pixels. Whether anything changed besides the move is judged by the same
|
|
178
|
+
// rule as any other capture, against the aligned numbers.
|
|
179
|
+
const shift = diff.shift
|
|
180
|
+
? {
|
|
181
|
+
px: diff.shift.dy / opts.scale,
|
|
182
|
+
otherChange: isChanged({ changedPixels: diff.shift.alignedChangedPixels, changedRatio: diff.shift.alignedChangedRatio }, opts),
|
|
183
|
+
}
|
|
184
|
+
: undefined;
|
|
185
|
+
const shiftNote = shift ? `${describeShift(shift.px)}, ` : '';
|
|
186
|
+
opts.log?.(` ${changed ? 'changed ' : 'same '} ${task.route} @ ${task.viewport} (${shiftNote}${(diff.changedRatio * 100).toFixed(2)}%, ${Date.now() - started}ms)`);
|
|
187
|
+
return {
|
|
188
|
+
...base,
|
|
189
|
+
status: changed ? 'changed' : 'unchanged',
|
|
190
|
+
baselineHash,
|
|
191
|
+
changedRatio: diff.changedRatio,
|
|
192
|
+
sizeChanged: diff.sizeChanged,
|
|
193
|
+
textOverlap: overlap,
|
|
194
|
+
titleOverlap: titles,
|
|
195
|
+
shift,
|
|
196
|
+
files: {
|
|
197
|
+
before: outputs.before,
|
|
198
|
+
after: outputs.after,
|
|
199
|
+
diff: diff.hasHighlight ? outputs.diff : undefined,
|
|
200
|
+
cropBefore: diff.hasCrop ? outputs.cropBefore : undefined,
|
|
201
|
+
cropAfter: diff.hasCrop ? outputs.cropAfter : undefined,
|
|
202
|
+
},
|
|
203
|
+
note: notes.join('; ') || undefined,
|
|
204
|
+
durationMs: Date.now() - started,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Did this run compare the two sites, or something standing in front of them?
|
|
209
|
+
*
|
|
210
|
+
* This lives beside the code that produces the evidence, not in the command
|
|
211
|
+
* that happens to want it. Both checks used to be written out in `pr`, and
|
|
212
|
+
* `compare` — the other caller of this pipeline — therefore had neither, and
|
|
213
|
+
* would print a confident percentage for two unrelated sites. Returning the
|
|
214
|
+
* verdict with the outcomes makes that omission impossible to repeat: a caller
|
|
215
|
+
* cannot get the numbers without also being handed the reason not to trust
|
|
216
|
+
* them.
|
|
217
|
+
*
|
|
218
|
+
* Order matters. A wall is the more specific diagnosis and has the more
|
|
219
|
+
* actionable fix, and a walled run also looks like two different sites — the
|
|
220
|
+
* sign-in page shares no words with the site — so it is answered first.
|
|
221
|
+
*/
|
|
222
|
+
/**
|
|
223
|
+
* Which side, if either, does not have this page at all.
|
|
224
|
+
*
|
|
225
|
+
* The signal is the HTTP status, not the page's wording. A 404 status is a fact
|
|
226
|
+
* the server states about the route; the string "404" in the body is just text,
|
|
227
|
+
* and it is present on pages that are merely *about* error codes and absent
|
|
228
|
+
* from every custom or localised not-found page ever shipped. Sniffing for it
|
|
229
|
+
* would mean this tool's own marketing page — which shows a 404 in a mockup —
|
|
230
|
+
* could be mistaken for a missing route.
|
|
231
|
+
*
|
|
232
|
+
* Soft 404s (a not-found view served with status 200, the usual SPA shape) are
|
|
233
|
+
* therefore invisible here by design; they are caught at the run level instead,
|
|
234
|
+
* where identical baselines across unrelated routes give them away.
|
|
235
|
+
*/
|
|
236
|
+
function absentSide(before, after) {
|
|
237
|
+
const gone = (c) => c.status === 404;
|
|
238
|
+
const served = (c) => !c.status || c.status < 400;
|
|
239
|
+
if (gone(before) && served(after))
|
|
240
|
+
return 'before';
|
|
241
|
+
if (gone(after) && served(before))
|
|
242
|
+
return 'after';
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Did the baseline answer unrelated routes with the same page?
|
|
247
|
+
*
|
|
248
|
+
* This is the soft 404 — a not-found view served with status 200, which no
|
|
249
|
+
* status check can see and which a keyword hunt for "404" would catch only in
|
|
250
|
+
* English, only when the page says it, and only when no real page happens to
|
|
251
|
+
* mention it. The durable signal is not the wording but the sameness: a host
|
|
252
|
+
* that returns pixel-identical bytes for several different routes is serving a
|
|
253
|
+
* catch-all, and every comparison against it is measuring the distance from
|
|
254
|
+
* that catch-all rather than from the page.
|
|
255
|
+
*
|
|
256
|
+
* It warns rather than reclassifies. Two routes can legitimately render the
|
|
257
|
+
* same page, and silently turning a real diff into "new page" on a heuristic
|
|
258
|
+
* would hide exactly the change this tool exists to show. The authoritative
|
|
259
|
+
* signal (a 404 status) acts; this one only tells the reader what it sees.
|
|
260
|
+
*/
|
|
261
|
+
export function softNotFoundWarning(outcomes, beforeUrl) {
|
|
262
|
+
const routesByHash = new Map();
|
|
263
|
+
for (const o of outcomes) {
|
|
264
|
+
if (!o.baselineHash)
|
|
265
|
+
continue;
|
|
266
|
+
const seen = routesByHash.get(o.baselineHash) ?? new Set();
|
|
267
|
+
seen.add(o.route);
|
|
268
|
+
routesByHash.set(o.baselineHash, seen);
|
|
269
|
+
}
|
|
270
|
+
let worst = null;
|
|
271
|
+
for (const routes of routesByHash.values()) {
|
|
272
|
+
if (routes.size > 1 && (!worst || routes.size > worst.size))
|
|
273
|
+
worst = routes;
|
|
274
|
+
}
|
|
275
|
+
if (!worst)
|
|
276
|
+
return null;
|
|
277
|
+
const shown = [...worst].sort().slice(0, 4).join(', ');
|
|
278
|
+
const more = worst.size > 4 ? `, and ${worst.size - 4} more` : '';
|
|
279
|
+
return `Note: ${hostOf(beforeUrl)} served an identical page for ${worst.size} routes (${shown}${more}). `
|
|
280
|
+
+ 'If that is a catch-all or a not-found page, those comparisons are against it, not against the real baseline.';
|
|
281
|
+
}
|
|
282
|
+
export function verdictFor(outcomes, sides) {
|
|
283
|
+
// Every route walled means the run never saw the site. Publishing anything
|
|
284
|
+
// from that, "no visual changes" included, would be a confident lie.
|
|
285
|
+
const walled = outcomes.filter(o => o.blocked);
|
|
286
|
+
if (walled.length > 0 && walled.length === outcomes.length) {
|
|
287
|
+
const { side, vercel } = walled[0].blocked;
|
|
288
|
+
return { kind: 'walled', hint: signInHint(side === 'before' ? sides.before.url : sides.after.url, vercel) };
|
|
289
|
+
}
|
|
290
|
+
if (looksLikeDifferentSites(outcomes)) {
|
|
291
|
+
return { kind: 'different-sites', hint: differentSitesHint(sides.before.url, sides.before.detail, sides.after.url, sides.fix) };
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
export async function runTasks(tasks, opts) {
|
|
296
|
+
fs.mkdirSync(opts.outputDir, { recursive: true });
|
|
297
|
+
const pool = new DiffPool();
|
|
298
|
+
let outcomes;
|
|
299
|
+
try {
|
|
300
|
+
outcomes = await Promise.all(tasks.map(t => runTask(t, opts, pool)));
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
await pool.close();
|
|
304
|
+
}
|
|
305
|
+
// Falling back to the first task's URLs keeps the verdict available to a
|
|
306
|
+
// caller that never resolved a pair — `compare`, given two URLs by hand.
|
|
307
|
+
const sides = opts.sides ?? {
|
|
308
|
+
before: { url: tasks[0]?.beforeUrl ?? '' },
|
|
309
|
+
after: { url: tasks[0]?.afterUrl ?? '' },
|
|
310
|
+
};
|
|
311
|
+
const softNotFound = softNotFoundWarning(outcomes, sides.before.url);
|
|
312
|
+
if (softNotFound)
|
|
313
|
+
opts.log?.(softNotFound);
|
|
314
|
+
return { outcomes, verdict: verdictFor(outcomes, sides) };
|
|
315
|
+
}
|
|
316
|
+
//# sourceMappingURL=run.js.map
|
package/dist/run.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Are the two sides the same site at all?
|
|
3
|
+
*
|
|
4
|
+
* The backstop this file exists to be was first specified as "a diff near 100%
|
|
5
|
+
* on every route means the two sides are different sites". Measured, that rule
|
|
6
|
+
* is wrong in both directions, and it is worth writing down why so it is not
|
|
7
|
+
* proposed again:
|
|
8
|
+
*
|
|
9
|
+
* | pair | changed |
|
|
10
|
+
* |-------------------------------------------------|---------|
|
|
11
|
+
* | the marketing site vs an unrelated real site | 2.66% desktop / 5.02% mobile |
|
|
12
|
+
* | fixture pricing page vs an unrelated fixture | 0.72% – 7.99% |
|
|
13
|
+
* | the same site with a full theme change (fixture) | 99.14% |
|
|
14
|
+
* | the same site redesigned (fixture rung-7) | 99.60% |
|
|
15
|
+
*
|
|
16
|
+
* Web pages are mostly background, and a pixel diff counts pixels that differ,
|
|
17
|
+
* so two *unrelated* pages agree on most of the canvas and read as a small
|
|
18
|
+
* change — the same 1-8% band as the login-page run that started all of this
|
|
19
|
+
* (4.53% / 1.99%). Meanwhile a legitimate redesign, the PR most worth
|
|
20
|
+
* screenshotting, reads ~100%. A threshold at 100% would have missed every
|
|
21
|
+
* real occurrence and fired on the best ones.
|
|
22
|
+
*
|
|
23
|
+
* What does separate them is the words on the page. A redesign changes how a
|
|
24
|
+
* page looks, not what it says; a different site says something else entirely.
|
|
25
|
+
* Measured over the same pairs, with common English words removed:
|
|
26
|
+
*
|
|
27
|
+
* different sites 0.000, 0.000, 0.000, 0.071
|
|
28
|
+
* same site 0.200 (the weakest fixture), 0.417+, 1.000 for real pages
|
|
29
|
+
*
|
|
30
|
+
* The floor below sits in that empty band.
|
|
31
|
+
*/
|
|
32
|
+
/** The distinctive words a page shows a reader. */
|
|
33
|
+
export declare function pageWords(text: string): Set<string>;
|
|
34
|
+
/**
|
|
35
|
+
* A page with almost nothing to say cannot answer this question: an image or a
|
|
36
|
+
* canvas with six words on it scores near zero against anything, including
|
|
37
|
+
* itself one version later. Below this many distinct words, the answer is "do
|
|
38
|
+
* not know" rather than "different".
|
|
39
|
+
*/
|
|
40
|
+
export declare const MIN_WORDS = 10;
|
|
41
|
+
/** The floor, from the measurements above: 0.071 was different, 0.200 was not. */
|
|
42
|
+
export declare const SAME_SITE_FLOOR = 0.1;
|
|
43
|
+
/** Overlap of two pages' body text, or null when either is too text-poor to judge. */
|
|
44
|
+
export declare function textOverlap(before: string, after: string): number | null;
|
|
45
|
+
/**
|
|
46
|
+
* Overlap of two pages' titles, or null when either says nothing distinctive.
|
|
47
|
+
*
|
|
48
|
+
* No `MIN_WORDS` here: a title is a handful of words by nature, and it is the
|
|
49
|
+
* one place a site almost always names itself. That makes it the corroborating
|
|
50
|
+
* signal for a run with only one route to go on — a branch that rewrites a
|
|
51
|
+
* page's copy keeps the site's name in the tab, a different site does not.
|
|
52
|
+
*/
|
|
53
|
+
export declare function titleOverlap(before: string, after: string): number | null;
|
|
54
|
+
/** What this needs from each capture: which page it was, and how the two sides compared. */
|
|
55
|
+
export interface RouteEvidence {
|
|
56
|
+
route?: string;
|
|
57
|
+
textOverlap?: number | null;
|
|
58
|
+
titleOverlap?: number | null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Do the captures, taken together, say these are two different sites?
|
|
62
|
+
*
|
|
63
|
+
* Every judgeable page has to disagree, and the evidence has to be about
|
|
64
|
+
* *pages*, not captures. One route shot at three viewports is one page's word
|
|
65
|
+
* list three times over; counting it as three agreeing witnesses is how a run
|
|
66
|
+
* that detected a single route — an explicit `--routes /`, or the `/` fallback
|
|
67
|
+
* when the diff names none — could reject the one thing this function's own
|
|
68
|
+
* rule permits: a branch that rewrote a page.
|
|
69
|
+
*
|
|
70
|
+
* So captures are folded per route, keeping each route's *strongest* showing of
|
|
71
|
+
* shared words, and a run with only one page to go on asks the title as well. A
|
|
72
|
+
* title is where a site names itself, so a rewritten page keeps it and a
|
|
73
|
+
* different site does not. Without a usable title there is no second witness
|
|
74
|
+
* and the run is published, which is the right way to be wrong here: publishing
|
|
75
|
+
* a diff someone can look at beats refusing to on one page's say-so.
|
|
76
|
+
*/
|
|
77
|
+
export declare function looksLikeDifferentSites(routes: RouteEvidence[]): boolean;
|
|
78
|
+
/**
|
|
79
|
+
* The one sentence a human needs when the two sides are not the same site.
|
|
80
|
+
*
|
|
81
|
+
* `beforeDetail` is how Pre was chosen ("production deployment for 26aa6cd"),
|
|
82
|
+
* absent when a caller was handed both URLs by hand and has no provenance to
|
|
83
|
+
* report. `fix` is the remedy, which is the caller's to name: telling someone
|
|
84
|
+
* to pass `--before` is right when the tool picked the baseline and wrong in
|
|
85
|
+
* the two-URL mode, where both sides came in as positional arguments and that
|
|
86
|
+
* flag is not how the command is invoked. Advice that does not apply to the
|
|
87
|
+
* mode you are in is the failure this project keeps finding — it is what sent
|
|
88
|
+
* someone to sign in to a macOS system service.
|
|
89
|
+
*/
|
|
90
|
+
export declare function differentSitesHint(beforeUrl: string, beforeDetail: string | undefined, afterUrl: string, fix?: string): string;
|
|
91
|
+
//# sourceMappingURL=sameness.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sameness.d.ts","sourceRoot":"","sources":["../src/sameness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAeH,mDAAmD;AACnD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAGnD;AAED;;;;;GAKG;AACH,eAAO,MAAM,SAAS,KAAK,CAAC;AAE5B,kFAAkF;AAClF,eAAO,MAAM,eAAe,MAAM,CAAC;AAgBnC,sFAAsF;AACtF,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKxE;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKzE;AAED,4FAA4F;AAC5F,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,OAAO,CAkBxE;AAYD;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,QAAQ,EAAE,MAAM,EAChB,GAAG,GAAE,MAAoC,GACxC,MAAM,CAGR"}
|