@juangadm/pre-post 1.0.0 → 1.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/README.md +99 -20
- package/dist/baseline.d.ts +208 -0
- package/dist/baseline.d.ts.map +1 -0
- package/dist/baseline.js +413 -0
- package/dist/baseline.js.map +1 -0
- package/dist/bin/cli.js +87 -42
- package/dist/bin/cli.js.map +1 -1
- package/dist/browser.d.ts +31 -3
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +291 -97
- package/dist/browser.js.map +1 -1
- package/dist/commands/compare.d.ts.map +1 -1
- package/dist/commands/compare.js +21 -3
- package/dist/commands/compare.js.map +1 -1
- package/dist/commands/detect.d.ts +5 -0
- package/dist/commands/detect.d.ts.map +1 -1
- package/dist/commands/detect.js +4 -1
- package/dist/commands/detect.js.map +1 -1
- package/dist/commands/doctor.d.ts +32 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +90 -9
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/login.js +1 -1
- package/dist/commands/pr.d.ts +4 -0
- package/dist/commands/pr.d.ts.map +1 -1
- package/dist/commands/pr.js +157 -51
- package/dist/commands/pr.js.map +1 -1
- package/dist/commands/prune.js +1 -1
- package/dist/commands/prune.js.map +1 -1
- package/dist/comparison.d.ts +104 -0
- package/dist/comparison.d.ts.map +1 -0
- package/dist/comparison.js +241 -0
- package/dist/comparison.js.map +1 -0
- package/dist/config.d.ts +12 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +25 -3
- package/dist/config.js.map +1 -1
- 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.map +1 -1
- package/dist/diff-pool.js +5 -2
- package/dist/diff-pool.js.map +1 -1
- package/dist/diff-worker.d.ts +3 -0
- package/dist/diff-worker.d.ts.map +1 -1
- package/dist/diff-worker.js +1 -0
- package/dist/diff-worker.js.map +1 -1
- package/dist/diff.d.ts +10 -0
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +117 -7
- package/dist/diff.js.map +1 -1
- package/dist/doctor.d.ts +42 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +73 -7
- package/dist/doctor.js.map +1 -1
- package/dist/git.d.ts +41 -3
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +103 -7
- package/dist/git.js.map +1 -1
- package/dist/github.d.ts +92 -2
- package/dist/github.d.ts.map +1 -1
- package/dist/github.js +191 -20
- package/dist/github.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 +9 -0
- package/dist/pkg.d.ts.map +1 -1
- package/dist/pkg.js +14 -0
- package/dist/pkg.js.map +1 -1
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +59 -28
- package/dist/report.js.map +1 -1
- package/dist/routes.d.ts +27 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +98 -23
- package/dist/routes.js.map +1 -1
- package/dist/run.d.ts +88 -8
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +224 -28
- package/dist/run.js.map +1 -1
- 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.map +1 -1
- package/dist/sessions.js +7 -1
- package/dist/sessions.js.map +1 -1
- 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 +83 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +17 -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/package.json +1 -1
- package/skill/SKILL.md +40 -7
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Did a capture reach the page it asked for, or land somewhere else?
|
|
3
|
+
*
|
|
4
|
+
* Deployment protection, corporate SSO and IP allow-lists answer a request for
|
|
5
|
+
* a page with a sign-in screen, and they do it with HTTP 200 after a redirect —
|
|
6
|
+
* so the 401/403 guard in `browser.ts` never sees them. Screenshotting that and
|
|
7
|
+
* publishing the difference as a visual change is the worst failure this tool
|
|
8
|
+
* has: confident, and wrong. It happened on a real run, twice, before anyone
|
|
9
|
+
* noticed the "Post" screenshot was a login form.
|
|
10
|
+
*
|
|
11
|
+
* The test is deliberately narrow. A redirect on its own proves nothing — a
|
|
12
|
+
* site legitimately sends `example.xyz` to `example.org`, or apex to `www` —
|
|
13
|
+
* so only a redirect that ends somewhere *sign-in shaped* counts. Missing an
|
|
14
|
+
* exotic auth wall is recoverable; refusing to run against a site that redirects
|
|
15
|
+
* for ordinary reasons is not.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* The registrable-ish part of a host, so `www.example.com` and `example.com`
|
|
19
|
+
* read as one site. Two labels is a heuristic — it treats `a.co.uk` and
|
|
20
|
+
* `b.co.uk` as the same site — but it is only ever used to make the sign-in
|
|
21
|
+
* test *stricter*, never to reject anything on its own.
|
|
22
|
+
*/
|
|
23
|
+
export declare function siteOf(url: string): string;
|
|
24
|
+
/** Did the navigation end up on a different site than the one requested? */
|
|
25
|
+
export declare function leftTheSite(requested: string, final: string): boolean;
|
|
26
|
+
/** Does this URL and title read as a sign-in page rather than a site's own page? */
|
|
27
|
+
export declare function looksLikeSignIn(url: string, title?: string): boolean;
|
|
28
|
+
export interface Landing {
|
|
29
|
+
/** Where the browser actually ended up. */
|
|
30
|
+
finalUrl: string;
|
|
31
|
+
/** True when the capture is of a sign-in wall, not of the requested page. */
|
|
32
|
+
blocked: boolean;
|
|
33
|
+
/** The wall is on another site entirely — usually an identity provider. */
|
|
34
|
+
offSite: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Judge where a capture landed.
|
|
38
|
+
*
|
|
39
|
+
* Blocked requires *both* that the browser moved and that where it moved to is
|
|
40
|
+
* sign-in shaped: asking for `/login` and getting `/login` is not a wall, and
|
|
41
|
+
* neither is a plain canonical-domain redirect.
|
|
42
|
+
*/
|
|
43
|
+
export declare function checkLanding(requested: string, finalUrl: string, title?: string): Landing;
|
|
44
|
+
/**
|
|
45
|
+
* The one sentence a human needs. Names the fix, not the symptom.
|
|
46
|
+
*/
|
|
47
|
+
export declare function signInHint(url: string, vercel: boolean): string;
|
|
48
|
+
//# sourceMappingURL=landing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"landing.d.ts","sourceRoot":"","sources":["../src/landing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;;;;;GAKG;AACH,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAQ1C;AAED,4EAA4E;AAC5E,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAIrE;AAMD,oFAAoF;AACpF,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,OAAO,CAShE;AAED,MAAM,WAAW,OAAO;IACtB,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,6EAA6E;IAC7E,OAAO,EAAE,OAAO,CAAC;IACjB,2EAA2E;IAC3E,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,OAAO,CAOrF;AAcD;;GAEG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,CAO/D"}
|
package/dist/landing.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Did a capture reach the page it asked for, or land somewhere else?
|
|
3
|
+
*
|
|
4
|
+
* Deployment protection, corporate SSO and IP allow-lists answer a request for
|
|
5
|
+
* a page with a sign-in screen, and they do it with HTTP 200 after a redirect —
|
|
6
|
+
* so the 401/403 guard in `browser.ts` never sees them. Screenshotting that and
|
|
7
|
+
* publishing the difference as a visual change is the worst failure this tool
|
|
8
|
+
* has: confident, and wrong. It happened on a real run, twice, before anyone
|
|
9
|
+
* noticed the "Post" screenshot was a login form.
|
|
10
|
+
*
|
|
11
|
+
* The test is deliberately narrow. A redirect on its own proves nothing — a
|
|
12
|
+
* site legitimately sends `example.xyz` to `example.org`, or apex to `www` —
|
|
13
|
+
* so only a redirect that ends somewhere *sign-in shaped* counts. Missing an
|
|
14
|
+
* exotic auth wall is recoverable; refusing to run against a site that redirects
|
|
15
|
+
* for ordinary reasons is not.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* The registrable-ish part of a host, so `www.example.com` and `example.com`
|
|
19
|
+
* read as one site. Two labels is a heuristic — it treats `a.co.uk` and
|
|
20
|
+
* `b.co.uk` as the same site — but it is only ever used to make the sign-in
|
|
21
|
+
* test *stricter*, never to reject anything on its own.
|
|
22
|
+
*/
|
|
23
|
+
export function siteOf(url) {
|
|
24
|
+
try {
|
|
25
|
+
const { hostname } = new URL(url);
|
|
26
|
+
const labels = hostname.split('.');
|
|
27
|
+
return labels.length <= 2 ? hostname : labels.slice(-2).join('.');
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return '';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Did the navigation end up on a different site than the one requested? */
|
|
34
|
+
export function leftTheSite(requested, final) {
|
|
35
|
+
const from = siteOf(requested);
|
|
36
|
+
const to = siteOf(final);
|
|
37
|
+
return Boolean(from && to && from !== to);
|
|
38
|
+
}
|
|
39
|
+
/** Words that appear in the URL or title of a sign-in page and little else. */
|
|
40
|
+
const SIGN_IN_URL = /(login|log-in|signin|sign-in|sso|oauth|authorize|authenticate)/i;
|
|
41
|
+
const SIGN_IN_TITLE = /(sign[ -]?in|log[ -]?in|authentication required|authorization required|access denied)/i;
|
|
42
|
+
/** Does this URL and title read as a sign-in page rather than a site's own page? */
|
|
43
|
+
export function looksLikeSignIn(url, title = '') {
|
|
44
|
+
let path = '';
|
|
45
|
+
try {
|
|
46
|
+
const u = new URL(url);
|
|
47
|
+
path = u.pathname + u.search;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return SIGN_IN_URL.test(path) || SIGN_IN_TITLE.test(title);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Judge where a capture landed.
|
|
56
|
+
*
|
|
57
|
+
* Blocked requires *both* that the browser moved and that where it moved to is
|
|
58
|
+
* sign-in shaped: asking for `/login` and getting `/login` is not a wall, and
|
|
59
|
+
* neither is a plain canonical-domain redirect.
|
|
60
|
+
*/
|
|
61
|
+
export function checkLanding(requested, finalUrl, title = '') {
|
|
62
|
+
const moved = normalize(finalUrl) !== normalize(requested);
|
|
63
|
+
return {
|
|
64
|
+
finalUrl,
|
|
65
|
+
blocked: moved && looksLikeSignIn(finalUrl, title),
|
|
66
|
+
offSite: leftTheSite(requested, finalUrl),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** Ignore the trailing-slash and fragment differences a redirect adds for free. */
|
|
70
|
+
function normalize(url) {
|
|
71
|
+
try {
|
|
72
|
+
const u = new URL(url);
|
|
73
|
+
u.hash = '';
|
|
74
|
+
if (u.pathname.endsWith('/') && u.pathname !== '/')
|
|
75
|
+
u.pathname = u.pathname.slice(0, -1);
|
|
76
|
+
return u.toString();
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return url;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The one sentence a human needs. Names the fix, not the symptom.
|
|
84
|
+
*/
|
|
85
|
+
export function signInHint(url, vercel) {
|
|
86
|
+
if (vercel) {
|
|
87
|
+
return `${url} answered with a sign-in page, so the site was never captured. ` +
|
|
88
|
+
'Set VERCEL_AUTOMATION_BYPASS_SECRET (Vercel → Project → Settings → Deployment Protection → Protection Bypass for Automation) and re-run.';
|
|
89
|
+
}
|
|
90
|
+
return `${url} answered with a sign-in page, so the site was never captured. ` +
|
|
91
|
+
`Run: npx pre-post login ${url} (opens a browser once, saves the session locally)`;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=landing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"landing.js","sourceRoot":"","sources":["../src/landing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH;;;;;GAKG;AACH,MAAM,UAAU,MAAM,CAAC,GAAW;IAChC,IAAI,CAAC;QACH,MAAM,EAAE,QAAQ,EAAE,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QAClC,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACnC,OAAO,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACpE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,WAAW,CAAC,SAAiB,EAAE,KAAa;IAC1D,MAAM,IAAI,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;IAC/B,MAAM,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IACzB,OAAO,OAAO,CAAC,IAAI,IAAI,EAAE,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC;AAC5C,CAAC;AAED,+EAA+E;AAC/E,MAAM,WAAW,GAAG,iEAAiE,CAAC;AACtF,MAAM,aAAa,GAAG,wFAAwF,CAAC;AAE/G,oFAAoF;AACpF,MAAM,UAAU,eAAe,CAAC,GAAW,EAAE,KAAK,GAAG,EAAE;IACrD,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QACvB,IAAI,GAAG,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,MAAM,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC7D,CAAC;AAWD;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,SAAiB,EAAE,QAAgB,EAAE,KAAK,GAAG,EAAE;IAC1E,MAAM,KAAK,GAAG,SAAS,CAAC,QAAQ,CAAC,KAAK,SAAS,CAAC,SAAS,CAAC,CAAC;IAC3D,OAAO;QACL,QAAQ;QACR,OAAO,EAAE,KAAK,IAAI,eAAe,CAAC,QAAQ,EAAE,KAAK,CAAC;QAClD,OAAO,EAAE,WAAW,CAAC,SAAS,EAAE,QAAQ,CAAC;KAC1C,CAAC;AACJ,CAAC;AAED,mFAAmF;AACnF,SAAS,SAAS,CAAC,GAAW;IAC5B,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;QACvB,CAAC,CAAC,IAAI,GAAG,EAAE,CAAC;QACZ,IAAI,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,QAAQ,KAAK,GAAG;YAAE,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACzF,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC;IACtB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,CAAC;IACb,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,UAAU,CAAC,GAAW,EAAE,MAAe;IACrD,IAAI,MAAM,EAAE,CAAC;QACX,OAAO,GAAG,GAAG,iEAAiE;YAC5E,0IAA0I,CAAC;IAC/I,CAAC;IACD,OAAO,GAAG,GAAG,iEAAiE;QAC5E,2BAA2B,GAAG,sDAAsD,CAAC;AACzF,CAAC"}
|
package/dist/pkg.d.ts
CHANGED
|
@@ -5,4 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export declare function readPackage(root: string): Record<string, any> | null;
|
|
7
7
|
export declare function hasDependency(root: string, name: string): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* The script that starts a dev server for this package, preferring `dev`.
|
|
10
|
+
*
|
|
11
|
+
* This is the test for "is this directory an app we could actually serve?",
|
|
12
|
+
* used both when picking the app root to detect routes for and when serving a
|
|
13
|
+
* base commit. The two must agree: a directory that cannot be served is not
|
|
14
|
+
* the app a PR changed, however many of its files the diff touched.
|
|
15
|
+
*/
|
|
16
|
+
export declare function devScript(dir: string): string | null;
|
|
8
17
|
//# sourceMappingURL=pkg.d.ts.map
|
package/dist/pkg.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pkg.d.ts","sourceRoot":"","sources":["../src/pkg.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAkBpE;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAGjE"}
|
|
1
|
+
{"version":3,"file":"pkg.d.ts","sourceRoot":"","sources":["../src/pkg.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAOH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAkBpE;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAGjE;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIpD"}
|
package/dist/pkg.js
CHANGED
|
@@ -32,4 +32,18 @@ export function hasDependency(root, name) {
|
|
|
32
32
|
const pkg = readPackage(root);
|
|
33
33
|
return Boolean(pkg?.dependencies?.[name] || pkg?.devDependencies?.[name]);
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* The script that starts a dev server for this package, preferring `dev`.
|
|
37
|
+
*
|
|
38
|
+
* This is the test for "is this directory an app we could actually serve?",
|
|
39
|
+
* used both when picking the app root to detect routes for and when serving a
|
|
40
|
+
* base commit. The two must agree: a directory that cannot be served is not
|
|
41
|
+
* the app a PR changed, however many of its files the diff touched.
|
|
42
|
+
*/
|
|
43
|
+
export function devScript(dir) {
|
|
44
|
+
const scripts = readPackage(dir)?.scripts;
|
|
45
|
+
if (!scripts)
|
|
46
|
+
return null;
|
|
47
|
+
return ['dev', 'start:dev', 'serve', 'start'].find(name => typeof scripts[name] === 'string') ?? null;
|
|
48
|
+
}
|
|
35
49
|
//# sourceMappingURL=pkg.js.map
|
package/dist/pkg.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pkg.js","sourceRoot":"","sources":["../src/pkg.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,MAAM,KAAK,GAAG,IAAI,GAAG,EAAgE,CAAC;AAEtF,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC;IAC7C,IAAI,OAAe,CAAC;IACpB,IAAI,CAAC;QACH,OAAO,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC;IACtC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC5B,IAAI,GAAG,IAAI,GAAG,CAAC,OAAO,KAAK,OAAO;QAAE,OAAO,GAAG,CAAC,GAAG,CAAC;IACnD,IAAI,GAAG,GAA+B,IAAI,CAAC;IAC3C,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;IACnD,CAAC;IAAC,MAAM,CAAC;QACP,GAAG,GAAG,IAAI,CAAC;IACb,CAAC;IACD,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC;IAClC,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,IAAY,EAAE,IAAY;IACtD,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;IAC9B,OAAO,OAAO,CAAC,GAAG,EAAE,YAAY,EAAE,CAAC,IAAI,CAAC,IAAI,GAAG,EAAE,eAAe,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;AAC5E,CAAC"}
|
|
1
|
+
{"version":3,"file":"pkg.js","sourceRoot":"","sources":["../src/pkg.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,MAAM,KAAK,GAAG,IAAI,GAAG,EAAgE,CAAC;AAEtF,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC;IAC7C,IAAI,OAAe,CAAC;IACpB,IAAI,CAAC;QACH,OAAO,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC;IACtC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC5B,IAAI,GAAG,IAAI,GAAG,CAAC,OAAO,KAAK,OAAO;QAAE,OAAO,GAAG,CAAC,GAAG,CAAC;IACnD,IAAI,GAAG,GAA+B,IAAI,CAAC;IAC3C,IAAI,CAAC;QACH,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC;IACnD,CAAC;IAAC,MAAM,CAAC;QACP,GAAG,GAAG,IAAI,CAAC;IACb,CAAC;IACD,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC;IAClC,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,IAAY,EAAE,IAAY;IACtD,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;IAC9B,OAAO,OAAO,CAAC,GAAG,EAAE,YAAY,EAAE,CAAC,IAAI,CAAC,IAAI,GAAG,EAAE,eAAe,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;AAC5E,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,SAAS,CAAC,GAAW;IACnC,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,CAAC,EAAE,OAA6C,CAAC;IAChF,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAC1B,OAAO,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,OAAO,CAAC,IAAI,CAAC,KAAK,QAAQ,CAAC,IAAI,IAAI,CAAC;AACxG,CAAC"}
|
package/dist/report.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,WAAW,EAAuB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,WAAW,EAAuB,MAAM,YAAY,CAAC;AAI9D,eAAO,MAAM,aAAa,qCAAqC,CAAC;AAgBhE,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,GAAG,CAAC,EAAE,IAAI,CAAC;CACZ;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,GAAE,cAAmB,GAAG,MAAM,CA+EtF;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAsCxD"}
|
package/dist/report.js
CHANGED
|
@@ -2,17 +2,9 @@
|
|
|
2
2
|
* Output formatting: the PR comment (for humans) and the terminal summary
|
|
3
3
|
* (for the developer or the agent that invoked the CLI).
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
5
|
+
import { describeShift } from './run.js';
|
|
6
|
+
import { hostOf, isLocalUrl } from './url.js';
|
|
6
7
|
export const STICKY_MARKER = '<!-- pre-post:visual-changes -->';
|
|
7
|
-
function pct(ratio) {
|
|
8
|
-
if (ratio === undefined)
|
|
9
|
-
return '';
|
|
10
|
-
if (ratio === 0)
|
|
11
|
-
return '0%';
|
|
12
|
-
if (ratio < 0.0001)
|
|
13
|
-
return '<0.01%';
|
|
14
|
-
return `${(ratio * 100).toFixed(ratio < 0.01 ? 2 : 1)}%`;
|
|
15
|
-
}
|
|
16
8
|
function code(s) {
|
|
17
9
|
return `\`${s}\``;
|
|
18
10
|
}
|
|
@@ -31,31 +23,61 @@ function groupByRoute(outcomes) {
|
|
|
31
23
|
export function buildComment(result, options = {}) {
|
|
32
24
|
const lines = [STICKY_MARKER, '## Visual changes', ''];
|
|
33
25
|
const routes = groupByRoute(result.outcomes);
|
|
34
|
-
const viewportCount = new Set(result.outcomes.map(o => o.viewport)).size;
|
|
35
26
|
const changed = result.outcomes.filter(o => o.status === 'changed');
|
|
36
27
|
const unchanged = result.outcomes.filter(o => o.status === 'unchanged');
|
|
37
28
|
const errors = result.outcomes.filter(o => o.status === 'error');
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
// A page that only one side has is still something a reviewer must see, so it
|
|
30
|
+
// counts as a change for "did anything happen" — but it is shown as one
|
|
31
|
+
// screenshot, never as a Pre/Post pair, because there is no pair.
|
|
32
|
+
const oneSided = result.outcomes.filter(o => (o.status === 'added' || o.status === 'removed') && (o.urls || o.files));
|
|
33
|
+
// Both sides can now be a deployment, so name the actual hosts rather than
|
|
34
|
+
// assuming Post is the reader's own checkout.
|
|
35
|
+
const postLabel = isLocalUrl(result.afterBase) ? 'this branch (local)' : hostOf(result.afterBase);
|
|
36
|
+
const sha = options.headSha ? ` @ ${code(options.headSha.slice(0, 7))}` : '';
|
|
37
|
+
lines.push(`**Pre** = ${hostOf(result.beforeBase)} · **Post** = ${postLabel}${sha} · <a href="https://github.com/juangadm/pre-post">pre-post</a>`, '');
|
|
38
|
+
if (changed.length === 0 && oneSided.length === 0)
|
|
39
|
+
lines.push('No visual changes.', '');
|
|
42
40
|
for (const [route, outcomes] of routes) {
|
|
43
41
|
const changedHere = outcomes.filter(o => o.status === 'changed' && (o.urls || o.files));
|
|
44
42
|
if (changedHere.length === 0)
|
|
45
43
|
continue;
|
|
46
44
|
for (const o of changedHere) {
|
|
47
45
|
const u = o.urls ?? o.files;
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
lines.push(`### ${code(route)} — ${viewportLabel(o.viewport)}`, '');
|
|
47
|
+
// A move repaints everything below it, so the percentage says nothing a
|
|
48
|
+
// reviewer can use. Say how far it moved instead, and whether the branch
|
|
49
|
+
// changed anything else.
|
|
50
|
+
if (o.shift) {
|
|
51
|
+
lines.push(o.shift.otherChange
|
|
52
|
+
? `Content ${describeShift(o.shift.px)}. The pair below is aligned on that move, so it shows what changed besides it.`
|
|
53
|
+
: `Content ${describeShift(o.shift.px)}. Nothing else changed.`, '');
|
|
54
|
+
}
|
|
55
|
+
const cropped = Boolean(u.cropBefore && u.cropAfter);
|
|
50
56
|
const pre = u.cropBefore ?? u.before;
|
|
51
57
|
const post = u.cropAfter ?? u.after;
|
|
52
58
|
lines.push('| Pre | Post |', '|:---:|:---:|', `|  |  |`, '');
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
// Without a crop the pair above is already the full page; repeating it
|
|
60
|
+
// under a fold gives a reviewer two more identical images to scroll past.
|
|
61
|
+
if (cropped) {
|
|
62
|
+
lines.push('<details>', `<summary>Full page</summary>`, '');
|
|
63
|
+
lines.push('| Pre (full) | Post (full) |', '|:---:|:---:|', `|  |  |`, '');
|
|
64
|
+
lines.push('</details>', '');
|
|
65
|
+
}
|
|
57
66
|
}
|
|
58
67
|
}
|
|
68
|
+
for (const o of oneSided) {
|
|
69
|
+
const u = o.urls ?? o.files;
|
|
70
|
+
const image = o.status === 'added' ? u.after : u.before;
|
|
71
|
+
if (!image)
|
|
72
|
+
continue;
|
|
73
|
+
const what = o.status === 'added' ? 'New page' : 'Page removed';
|
|
74
|
+
const side = o.status === 'added' ? 'Post' : 'Pre';
|
|
75
|
+
lines.push(`### ${code(o.route)} — ${viewportLabel(o.viewport)} · ${what}`, '');
|
|
76
|
+
lines.push(o.status === 'added'
|
|
77
|
+
? `This route has no baseline — ${hostOf(result.beforeBase)} returns 404 for it — so there is no "before" to show and no percentage to quote. ${side} only:`
|
|
78
|
+
: `This route is gone on this branch, so there is no "after". ${side} only:`, '');
|
|
79
|
+
lines.push(``, '');
|
|
80
|
+
}
|
|
59
81
|
if (unchanged.length) {
|
|
60
82
|
const byRoute = groupByRoute(unchanged);
|
|
61
83
|
const parts = Array.from(byRoute.entries()).map(([route, os]) => `${code(route)} (${os.map(o => o.viewport).join(', ')})`);
|
|
@@ -70,9 +92,6 @@ export function buildComment(result, options = {}) {
|
|
|
70
92
|
if (result.skippedDynamic.length) {
|
|
71
93
|
lines.push(`**Needs a sample URL:** ${result.skippedDynamic.map(code).join(', ')} — add them under ${code('"samples"')} in ${code('.pre-post.json')}.`, '');
|
|
72
94
|
}
|
|
73
|
-
const stamp = (options.now ?? new Date()).toISOString().replace('T', ' ').slice(0, 16) + ' UTC';
|
|
74
|
-
const sha = options.headSha ? ` · ${code(options.headSha.slice(0, 7))}` : '';
|
|
75
|
-
lines.push('---', `<sub>Captured by <a href="https://github.com/juangadm/pre-post">pre-post</a>${options.version ? ` v${options.version}` : ''}${sha} · ${stamp} · ${(result.durationMs / 1000).toFixed(1)}s</sub>`);
|
|
76
95
|
return lines.join('\n');
|
|
77
96
|
}
|
|
78
97
|
/**
|
|
@@ -90,8 +109,12 @@ export function buildSummary(result) {
|
|
|
90
109
|
const rows = result.outcomes.map(o => [
|
|
91
110
|
o.route,
|
|
92
111
|
o.viewport,
|
|
93
|
-
o.status === 'error' ? 'error'
|
|
94
|
-
|
|
112
|
+
o.status === 'error' ? 'error'
|
|
113
|
+
: o.status === 'changed' ? 'changed'
|
|
114
|
+
: o.status === 'added' ? 'new page'
|
|
115
|
+
: o.status === 'removed' ? 'removed'
|
|
116
|
+
: 'no change',
|
|
117
|
+
o.status === 'error' ? (o.error ?? '') : (o.note || ''),
|
|
95
118
|
]);
|
|
96
119
|
const widths = [0, 1, 2].map(i => Math.max(...rows.map(r => r[i].length), 0));
|
|
97
120
|
for (const r of rows) {
|
|
@@ -100,8 +123,16 @@ export function buildSummary(result) {
|
|
|
100
123
|
if (result.skippedDynamic.length) {
|
|
101
124
|
lines.push(` needs sample URL: ${result.skippedDynamic.join(', ')} (add to .pre-post.json "samples")`);
|
|
102
125
|
}
|
|
103
|
-
|
|
104
|
-
|
|
126
|
+
// Named for what actually happened. The images normally go in the PR
|
|
127
|
+
// description and only fall back to a comment, so "Comment:" sent a reader
|
|
128
|
+
// looking for a comment that a run with zero comments had never created.
|
|
129
|
+
//
|
|
130
|
+
// Only an explicit 'description' changes the label: `commentKind` is optional
|
|
131
|
+
// and `buildSummary` is exported, so a caller that predates it — or a result
|
|
132
|
+
// persisted before it existed — still means what it always meant.
|
|
133
|
+
if (result.commentUrl) {
|
|
134
|
+
lines.push(`${result.commentKind === 'description' ? 'PR description' : 'Comment'}: ${result.commentUrl}`);
|
|
135
|
+
}
|
|
105
136
|
return lines.join('\n');
|
|
106
137
|
}
|
|
107
138
|
//# sourceMappingURL=report.js.map
|
package/dist/report.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"report.js","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"report.js","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"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"}
|
package/dist/routes.d.ts
CHANGED
|
@@ -10,7 +10,30 @@
|
|
|
10
10
|
* 5. Dedupe, rank by confidence, cap, and substitute samples for dynamic routes.
|
|
11
11
|
*/
|
|
12
12
|
import { DetectedRoute, Framework, PrePostConfig, RouteDetectionOptions } from './types.js';
|
|
13
|
+
import { BaseResolution } from './git.js';
|
|
13
14
|
export type { Framework };
|
|
15
|
+
/**
|
|
16
|
+
* Does this directory hold an app whose routes are worth detecting?
|
|
17
|
+
*
|
|
18
|
+
* A dev script is the test baseline.ts applies before serving a base commit; a
|
|
19
|
+
* recognised framework counts too, since an app can be captured from a
|
|
20
|
+
* deployment without ever being served here. Both are stricter than "owns some
|
|
21
|
+
* changed files", which is the point: counting alone once picked this repo's
|
|
22
|
+
* tests/fixtures over its marketing site by a single file and then reported
|
|
23
|
+
* routes for a directory nothing can serve.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isServableApp(root: string): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Is this directory a package at all, rather than a folder that merely looks
|
|
28
|
+
* like one?
|
|
29
|
+
*
|
|
30
|
+
* findAppRoots accepts any directory holding `app/` or `pages/`, which is how
|
|
31
|
+
* this repo's own tests/fixtures — a pile of static HTML under pages/ with no
|
|
32
|
+
* package.json — became a candidate and, on a diff dominated by fixture files,
|
|
33
|
+
* the winner. Declaring a package (or matching a real framework) is the line
|
|
34
|
+
* between an app and a directory shaped like one.
|
|
35
|
+
*/
|
|
36
|
+
export declare function isAppLike(root: string): boolean;
|
|
14
37
|
export declare function frameworkForRoot(root: string): Framework;
|
|
15
38
|
/** Directories (depth ≤ 3) that look like an app: package.json, or app/ pages/ route folders. */
|
|
16
39
|
export declare function findAppRoots(baseDir: string): string[];
|
|
@@ -32,6 +55,8 @@ export interface RepoRouteDetection {
|
|
|
32
55
|
routes: DetectedRoute[];
|
|
33
56
|
/** Dynamic routes with no sample URL configured */
|
|
34
57
|
skippedDynamic: string[];
|
|
58
|
+
/** What the working tree was compared with; null when files were supplied. */
|
|
59
|
+
base: BaseResolution | null;
|
|
35
60
|
/** Milliseconds spent */
|
|
36
61
|
durationMs: number;
|
|
37
62
|
}
|
|
@@ -43,6 +68,8 @@ export interface RepoDetectionOptions {
|
|
|
43
68
|
diffTarget?: string;
|
|
44
69
|
/** Override changed files (tests) */
|
|
45
70
|
changedFiles?: string[];
|
|
71
|
+
/** Where warnings go. Left unset, they are silent — --json must stay parseable. */
|
|
72
|
+
log?: (msg: string) => void;
|
|
46
73
|
}
|
|
47
74
|
export declare function detectRoutesForRepo(options?: RepoDetectionOptions): RepoRouteDetection;
|
|
48
75
|
//# sourceMappingURL=routes.d.ts.map
|
package/dist/routes.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAK5F,OAAO,EAAE,cAAc,EAAyE,MAAM,UAAU,CAAC;AAIjH,YAAY,EAAE,SAAS,EAAE,CAAC;AAgH1B;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAE/C;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAExD;AASD,iGAAiG;AACjG,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAsBtD;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAG3D;AAQD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,aAAa,EAAE,CAS1E;AAaD,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAErD;AAED,uFAAuF;AACvF,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAAG,MAAM,CAEzF;AAMD,wBAAgB,eAAe,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,GAAG,SAAgB,GAAG,MAAM,EAAE,CAUlF;AAED,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,qBAA0B,GAAG,aAAa,EAAE,CAIzG;AAMD,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,SAAS,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,mDAAmD;IACnD,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,8EAA8E;IAC9E,IAAI,EAAE,cAAc,GAAG,IAAI,CAAC;IAC5B,yBAAyB;IACzB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qCAAqC;IACrC,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,mFAAmF;IACnF,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AASD,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,oBAAyB,GAAG,kBAAkB,CA6I1F"}
|
package/dist/routes.js
CHANGED
|
@@ -15,9 +15,9 @@ import { detectAppRouterRoutes, detectPagesRouterRoutes } from './routes/nextjs.
|
|
|
15
15
|
import { detectGenericRoutes } from './routes/generic.js';
|
|
16
16
|
import { buildImportGraph, findAffectedEntries, readAliases, walkSourceFiles, toPosix, SKIP_DIRS } from './routes/imports.js';
|
|
17
17
|
import { viteRouteEntries, isViteApp } from './routes/vite.js';
|
|
18
|
-
import { changedFiles as gitChangedFiles, repoRoot as gitRepoRoot } from './git.js';
|
|
19
|
-
import { hasDependency } from './pkg.js';
|
|
20
|
-
import { CONFIG_DEFAULTS } from './config.js';
|
|
18
|
+
import { changedFiles as gitChangedFiles, repoRoot as gitRepoRoot, requireBase } from './git.js';
|
|
19
|
+
import { devScript, hasDependency, readPackage } from './pkg.js';
|
|
20
|
+
import { CONFIG_DEFAULTS, CONFIG_FILENAME } from './config.js';
|
|
21
21
|
const NEXT_APP_PAGE = /^app\/(.+\/)?page\.(tsx|ts|jsx|js|mdx|md)$/;
|
|
22
22
|
const NEXT_PAGES_HIGH = /^pages\/(?!api\/|_)(.+)\.(tsx|ts|jsx|js|mdx|md)$/;
|
|
23
23
|
const NEXT_PAGE_FILE = /^(page|layout)\.(tsx|ts|jsx|js|mdx)$/;
|
|
@@ -115,6 +115,32 @@ const FRAMEWORKS = [
|
|
|
115
115
|
function adapterFor(name) {
|
|
116
116
|
return FRAMEWORKS.find(f => f.name === name) ?? FRAMEWORKS[FRAMEWORKS.length - 1];
|
|
117
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Does this directory hold an app whose routes are worth detecting?
|
|
120
|
+
*
|
|
121
|
+
* A dev script is the test baseline.ts applies before serving a base commit; a
|
|
122
|
+
* recognised framework counts too, since an app can be captured from a
|
|
123
|
+
* deployment without ever being served here. Both are stricter than "owns some
|
|
124
|
+
* changed files", which is the point: counting alone once picked this repo's
|
|
125
|
+
* tests/fixtures over its marketing site by a single file and then reported
|
|
126
|
+
* routes for a directory nothing can serve.
|
|
127
|
+
*/
|
|
128
|
+
export function isServableApp(root) {
|
|
129
|
+
return devScript(root) !== null || frameworkForRoot(root) !== 'generic';
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Is this directory a package at all, rather than a folder that merely looks
|
|
133
|
+
* like one?
|
|
134
|
+
*
|
|
135
|
+
* findAppRoots accepts any directory holding `app/` or `pages/`, which is how
|
|
136
|
+
* this repo's own tests/fixtures — a pile of static HTML under pages/ with no
|
|
137
|
+
* package.json — became a candidate and, on a diff dominated by fixture files,
|
|
138
|
+
* the winner. Declaring a package (or matching a real framework) is the line
|
|
139
|
+
* between an app and a directory shaped like one.
|
|
140
|
+
*/
|
|
141
|
+
export function isAppLike(root) {
|
|
142
|
+
return readPackage(root) !== null || frameworkForRoot(root) !== 'generic';
|
|
143
|
+
}
|
|
118
144
|
export function frameworkForRoot(root) {
|
|
119
145
|
return FRAMEWORKS.find(f => f.matches(root)).name;
|
|
120
146
|
}
|
|
@@ -171,14 +197,13 @@ export function deduplicateRoutes(routes) {
|
|
|
171
197
|
}
|
|
172
198
|
return Array.from(byPath.values());
|
|
173
199
|
}
|
|
174
|
-
function rankAndCap(routes, maxRoutes, warn
|
|
200
|
+
function rankAndCap(routes, maxRoutes, warn) {
|
|
175
201
|
let out = deduplicateRoutes(routes);
|
|
176
202
|
out.sort((a, b) => CONFIDENCE_ORDER[a.confidence] - CONFIDENCE_ORDER[b.confidence] || a.path.localeCompare(b.path));
|
|
177
203
|
if (out.length > maxRoutes) {
|
|
178
204
|
const original = out.length;
|
|
179
205
|
out = out.slice(0, maxRoutes);
|
|
180
|
-
|
|
181
|
-
console.warn(`Detected ${original} routes, capping at ${maxRoutes}. Use --max-routes to increase.`);
|
|
206
|
+
warn?.(`Detected ${original} routes, capping at ${maxRoutes}. Use --max-routes to increase.`);
|
|
182
207
|
}
|
|
183
208
|
return out;
|
|
184
209
|
}
|
|
@@ -193,8 +218,12 @@ export function resolveSample(route, samples = {}) {
|
|
|
193
218
|
// File-list API (framework rules only, no import graph)
|
|
194
219
|
// ============================================================
|
|
195
220
|
export function getChangedFiles(diffTarget, cwd = process.cwd()) {
|
|
221
|
+
// requireBase throws NeedsHumanError when the base cannot be established;
|
|
222
|
+
// that has to travel, because the alternative is an empty list that reads
|
|
223
|
+
// as "nothing changed".
|
|
224
|
+
const target = diffTarget ?? requireBase(cwd).sha;
|
|
196
225
|
try {
|
|
197
|
-
return gitChangedFiles(cwd,
|
|
226
|
+
return gitChangedFiles(cwd, target);
|
|
198
227
|
}
|
|
199
228
|
catch {
|
|
200
229
|
return [];
|
|
@@ -204,7 +233,7 @@ export function detectRoutes(changedFiles, options = {}) {
|
|
|
204
233
|
if (changedFiles.length === 0)
|
|
205
234
|
return [];
|
|
206
235
|
const adapter = adapterFor(options.framework || detectFramework());
|
|
207
|
-
return rankAndCap(adapter.directRoutes(changedFiles.map(adapter.normalize)), options.maxRoutes ?? CONFIG_DEFAULTS.maxRoutes);
|
|
236
|
+
return rankAndCap(adapter.directRoutes(changedFiles.map(adapter.normalize)), options.maxRoutes ?? CONFIG_DEFAULTS.maxRoutes, options.log);
|
|
208
237
|
}
|
|
209
238
|
/** For a layout-only route (no page of its own), the closest static page beneath it. */
|
|
210
239
|
function nearestPageRoute(route, knownRoutes) {
|
|
@@ -219,21 +248,66 @@ export function detectRoutesForRepo(options = {}) {
|
|
|
219
248
|
const config = options.config || {};
|
|
220
249
|
const maxRoutes = options.maxRoutes ?? config.maxRoutes ?? CONFIG_DEFAULTS.maxRoutes;
|
|
221
250
|
const ignore = (config.ignore || []).map(p => p.replace(/^\.?\//, ''));
|
|
222
|
-
|
|
251
|
+
// Establish the base before reading the diff. Getting this wrong is silent:
|
|
252
|
+
// diffing HEAD reports only uncommitted work, so a branch whose changes are
|
|
253
|
+
// committed — every branch in a shallow CI or sandbox checkout — comes back
|
|
254
|
+
// empty and the run reports no visual changes.
|
|
255
|
+
const base = options.changedFiles
|
|
256
|
+
? null
|
|
257
|
+
: requireBase(root, { explicit: options.diffTarget, log: options.log });
|
|
258
|
+
const allChanged = (options.changedFiles ?? gitChangedFiles(root, base.sha))
|
|
259
|
+
// Our own config file is written mid-run; it never affects which routes changed.
|
|
260
|
+
.filter(f => f !== CONFIG_FILENAME)
|
|
223
261
|
.filter(f => !ignore.some(prefix => f === prefix || f.startsWith(prefix.replace(/\/?$/, '/'))));
|
|
224
|
-
// Choose the app root
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
262
|
+
// Choose the app root the PR is about. The repo root contains every changed
|
|
263
|
+
// file, so it would always match at least as many as any app nested inside
|
|
264
|
+
// it; score the nested app roots only, and fall back to the repo root when
|
|
265
|
+
// none of them owns a changed file.
|
|
266
|
+
//
|
|
267
|
+
// Servability outranks file count. Counting alone once picked this repo's
|
|
268
|
+
// own tests/fixtures over its marketing site by a single file, and then
|
|
269
|
+
// reported routes for a directory nothing can serve. A directory that cannot
|
|
270
|
+
// be served is not the app a branch changed, however much of the diff lands
|
|
271
|
+
// in it; among candidates that can be served, the diff decides, deepest
|
|
272
|
+
// first on a tie.
|
|
273
|
+
const owners = findAppRoots(root)
|
|
274
|
+
.map(dir => {
|
|
275
|
+
const rel = toPosix(path.relative(root, dir));
|
|
276
|
+
return { dir, count: rel ? allChanged.filter(f => f.startsWith(rel + '/')).length : 0 };
|
|
277
|
+
})
|
|
278
|
+
// Probing the filesystem is the expensive part, so only candidates that own
|
|
279
|
+
// some of the diff pay for it.
|
|
280
|
+
.filter(c => c.count > 0)
|
|
281
|
+
.map(c => {
|
|
282
|
+
const framework = frameworkForRoot(c.dir);
|
|
283
|
+
return {
|
|
284
|
+
...c,
|
|
285
|
+
framework,
|
|
286
|
+
appLike: readPackage(c.dir) !== null || framework !== 'generic',
|
|
287
|
+
servable: devScript(c.dir) !== null || framework !== 'generic',
|
|
288
|
+
};
|
|
289
|
+
})
|
|
290
|
+
// A folder shaped like an app is not one; see isAppLike.
|
|
291
|
+
.filter(c => c.appLike)
|
|
292
|
+
.sort((a, b) => b.count - a.count || b.dir.length - a.dir.length);
|
|
293
|
+
// Among roots that own a comparable share of the diff, prefer one that can be
|
|
294
|
+
// served; outside that band the diff wins outright.
|
|
295
|
+
//
|
|
296
|
+
// Servability breaks near-ties, which is the case it was added for: this
|
|
297
|
+
// repo's tests/fixtures beat site/ by a single file and produced routes for a
|
|
298
|
+
// directory nothing can serve. It must not override a clear majority, though
|
|
299
|
+
// — a deployed or --before/--after comparison never serves anything locally,
|
|
300
|
+
// so a static app owning most of the diff still has the routes that changed,
|
|
301
|
+
// and handing detection to an incidental package with a start script would
|
|
302
|
+
// discard them.
|
|
303
|
+
const CONTENDER_SHARE = 0.5;
|
|
304
|
+
const leader = owners[0]?.count ?? 0;
|
|
305
|
+
const contenders = owners.filter(c => c.count >= leader * CONTENDER_SHARE);
|
|
306
|
+
const winner = contenders.find(c => c.servable) ?? owners[0];
|
|
307
|
+
const appRoot = winner?.dir ?? root;
|
|
308
|
+
// The winner's framework was already resolved while scoring; frameworkForRoot
|
|
309
|
+
// walks directories, so don't ask twice.
|
|
310
|
+
const adapter = adapterFor(options.framework || winner?.framework || frameworkForRoot(appRoot));
|
|
237
311
|
const appPrefix = toPosix(path.relative(root, appRoot));
|
|
238
312
|
const appRel = allChanged
|
|
239
313
|
.filter(f => !appPrefix || f.startsWith(appPrefix + '/'))
|
|
@@ -296,8 +370,9 @@ export function detectRoutesForRepo(options = {}) {
|
|
|
296
370
|
framework: adapter.name,
|
|
297
371
|
appRoot,
|
|
298
372
|
changedFiles: allChanged,
|
|
299
|
-
routes: rankAndCap(resolved, maxRoutes,
|
|
373
|
+
routes: rankAndCap(resolved, maxRoutes, options.log),
|
|
300
374
|
skippedDynamic: Array.from(skippedDynamic),
|
|
375
|
+
base,
|
|
301
376
|
durationMs: Date.now() - started,
|
|
302
377
|
};
|
|
303
378
|
}
|