@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.
Files changed (153) hide show
  1. package/README.md +174 -233
  2. package/dist/baseline.d.ts +119 -0
  3. package/dist/baseline.d.ts.map +1 -0
  4. package/dist/baseline.js +316 -0
  5. package/dist/baseline.js.map +1 -0
  6. package/dist/bin/cli.js +243 -458
  7. package/dist/bin/cli.js.map +1 -1
  8. package/dist/browser.d.ts +68 -25
  9. package/dist/browser.d.ts.map +1 -1
  10. package/dist/browser.js +500 -105
  11. package/dist/browser.js.map +1 -1
  12. package/dist/capture.d.ts +1 -6
  13. package/dist/capture.d.ts.map +1 -1
  14. package/dist/capture.js +12 -36
  15. package/dist/capture.js.map +1 -1
  16. package/dist/commands/compare.d.ts +18 -0
  17. package/dist/commands/compare.d.ts.map +1 -0
  18. package/dist/commands/compare.js +82 -0
  19. package/dist/commands/compare.js.map +1 -0
  20. package/dist/commands/detect.d.ts +25 -0
  21. package/dist/commands/detect.d.ts.map +1 -0
  22. package/dist/commands/detect.js +22 -0
  23. package/dist/commands/detect.js.map +1 -0
  24. package/dist/commands/doctor.d.ts +42 -0
  25. package/dist/commands/doctor.d.ts.map +1 -0
  26. package/dist/commands/doctor.js +119 -0
  27. package/dist/commands/doctor.js.map +1 -0
  28. package/dist/commands/login.d.ts +6 -0
  29. package/dist/commands/login.d.ts.map +1 -0
  30. package/dist/commands/login.js +34 -0
  31. package/dist/commands/login.js.map +1 -0
  32. package/dist/commands/pr.d.ts +32 -0
  33. package/dist/commands/pr.d.ts.map +1 -0
  34. package/dist/commands/pr.js +259 -0
  35. package/dist/commands/pr.js.map +1 -0
  36. package/dist/commands/prune.d.ts +10 -0
  37. package/dist/commands/prune.d.ts.map +1 -0
  38. package/dist/commands/prune.js +13 -0
  39. package/dist/commands/prune.js.map +1 -0
  40. package/dist/comparison.d.ts +104 -0
  41. package/dist/comparison.d.ts.map +1 -0
  42. package/dist/comparison.js +236 -0
  43. package/dist/comparison.js.map +1 -0
  44. package/dist/config.d.ts +46 -0
  45. package/dist/config.d.ts.map +1 -0
  46. package/dist/config.js +82 -0
  47. package/dist/config.js.map +1 -0
  48. package/dist/deployments.d.ts +71 -0
  49. package/dist/deployments.d.ts.map +1 -0
  50. package/dist/deployments.js +204 -0
  51. package/dist/deployments.js.map +1 -0
  52. package/dist/diff-pool.d.ts +17 -0
  53. package/dist/diff-pool.d.ts.map +1 -0
  54. package/dist/diff-pool.js +92 -0
  55. package/dist/diff-pool.js.map +1 -0
  56. package/dist/diff-worker.d.ts +32 -0
  57. package/dist/diff-worker.d.ts.map +1 -0
  58. package/dist/diff-worker.js +39 -0
  59. package/dist/diff-worker.js.map +1 -0
  60. package/dist/diff.d.ts +47 -0
  61. package/dist/diff.d.ts.map +1 -0
  62. package/dist/diff.js +281 -0
  63. package/dist/diff.js.map +1 -0
  64. package/dist/doctor.d.ts +79 -0
  65. package/dist/doctor.d.ts.map +1 -0
  66. package/dist/doctor.js +138 -0
  67. package/dist/doctor.js.map +1 -0
  68. package/dist/errors.d.ts +38 -0
  69. package/dist/errors.d.ts.map +1 -0
  70. package/dist/errors.js +59 -0
  71. package/dist/errors.js.map +1 -0
  72. package/dist/git.d.ts +61 -0
  73. package/dist/git.d.ts.map +1 -0
  74. package/dist/git.js +182 -0
  75. package/dist/git.js.map +1 -0
  76. package/dist/github.d.ts +163 -0
  77. package/dist/github.d.ts.map +1 -0
  78. package/dist/github.js +411 -0
  79. package/dist/github.js.map +1 -0
  80. package/dist/index.d.ts +22 -23
  81. package/dist/index.d.ts.map +1 -1
  82. package/dist/index.js +17 -67
  83. package/dist/index.js.map +1 -1
  84. package/dist/landing.d.ts +48 -0
  85. package/dist/landing.d.ts.map +1 -0
  86. package/dist/landing.js +93 -0
  87. package/dist/landing.js.map +1 -0
  88. package/dist/pkg.d.ts +17 -0
  89. package/dist/pkg.d.ts.map +1 -0
  90. package/dist/pkg.js +49 -0
  91. package/dist/pkg.js.map +1 -0
  92. package/dist/report.d.ts +20 -0
  93. package/dist/report.d.ts.map +1 -0
  94. package/dist/report.js +130 -0
  95. package/dist/report.js.map +1 -0
  96. package/dist/routes/imports.d.ts +46 -0
  97. package/dist/routes/imports.d.ts.map +1 -0
  98. package/dist/routes/imports.js +208 -0
  99. package/dist/routes/imports.js.map +1 -0
  100. package/dist/routes/vite.d.ts +15 -0
  101. package/dist/routes/vite.d.ts.map +1 -0
  102. package/dist/routes/vite.js +130 -0
  103. package/dist/routes/vite.js.map +1 -0
  104. package/dist/routes.d.ts +65 -11
  105. package/dist/routes.d.ts.map +1 -1
  106. package/dist/routes.js +346 -141
  107. package/dist/routes.js.map +1 -1
  108. package/dist/run.d.ts +116 -0
  109. package/dist/run.d.ts.map +1 -0
  110. package/dist/run.js +316 -0
  111. package/dist/run.js.map +1 -0
  112. package/dist/sameness.d.ts +91 -0
  113. package/dist/sameness.d.ts.map +1 -0
  114. package/dist/sameness.js +157 -0
  115. package/dist/sameness.js.map +1 -0
  116. package/dist/sessions.d.ts +50 -0
  117. package/dist/sessions.d.ts.map +1 -0
  118. package/dist/sessions.js +80 -0
  119. package/dist/sessions.js.map +1 -0
  120. package/dist/shift.d.ts +67 -0
  121. package/dist/shift.d.ts.map +1 -0
  122. package/dist/shift.js +184 -0
  123. package/dist/shift.js.map +1 -0
  124. package/dist/types.d.ts +179 -68
  125. package/dist/types.d.ts.map +1 -1
  126. package/dist/types.js +1 -0
  127. package/dist/types.js.map +1 -1
  128. package/dist/url.d.ts +12 -0
  129. package/dist/url.d.ts.map +1 -0
  130. package/dist/url.js +30 -0
  131. package/dist/url.js.map +1 -0
  132. package/dist/viewport.d.ts +6 -1
  133. package/dist/viewport.d.ts.map +1 -1
  134. package/dist/viewport.js +13 -7
  135. package/dist/viewport.js.map +1 -1
  136. package/package.json +21 -14
  137. package/skill/SKILL.md +56 -221
  138. package/dist/clipboard.d.ts +0 -9
  139. package/dist/clipboard.d.ts.map +0 -1
  140. package/dist/clipboard.js +0 -26
  141. package/dist/clipboard.js.map +0 -1
  142. package/dist/upload.d.ts +0 -39
  143. package/dist/upload.d.ts.map +0 -1
  144. package/dist/upload.js +0 -159
  145. package/dist/upload.js.map +0 -1
  146. package/dist/video.d.ts +0 -19
  147. package/dist/video.d.ts.map +0 -1
  148. package/dist/video.js +0 -263
  149. package/dist/video.js.map +0 -1
  150. package/skill/scripts/adapters/0x0st.sh +0 -36
  151. package/skill/scripts/adapters/blob.sh +0 -55
  152. package/skill/scripts/adapters/git-native.sh +0 -51
  153. package/skill/scripts/upload-and-copy.sh +0 -230
@@ -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"}
@@ -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 ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * package.json access, shared by route detection and the pr command.
3
+ * Parsed once per file version (keyed by path + mtime), since detection asks
4
+ * the same question about the same root several times per run.
5
+ */
6
+ export declare function readPackage(root: string): Record<string, any> | null;
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;
17
+ //# sourceMappingURL=pkg.d.ts.map
@@ -0,0 +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;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIpD"}
package/dist/pkg.js ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * package.json access, shared by route detection and the pr command.
3
+ * Parsed once per file version (keyed by path + mtime), since detection asks
4
+ * the same question about the same root several times per run.
5
+ */
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ const cache = new Map();
9
+ export function readPackage(root) {
10
+ const file = path.join(root, 'package.json');
11
+ let mtimeMs;
12
+ try {
13
+ mtimeMs = fs.statSync(file).mtimeMs;
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ const hit = cache.get(file);
19
+ if (hit && hit.mtimeMs === mtimeMs)
20
+ return hit.pkg;
21
+ let pkg = null;
22
+ try {
23
+ pkg = JSON.parse(fs.readFileSync(file, 'utf-8'));
24
+ }
25
+ catch {
26
+ pkg = null;
27
+ }
28
+ cache.set(file, { mtimeMs, pkg });
29
+ return pkg;
30
+ }
31
+ export function hasDependency(root, name) {
32
+ const pkg = readPackage(root);
33
+ return Boolean(pkg?.dependencies?.[name] || pkg?.devDependencies?.[name]);
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
+ }
49
+ //# sourceMappingURL=pkg.js.map
@@ -0,0 +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;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"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Output formatting: the PR comment (for humans) and the terminal summary
3
+ * (for the developer or the agent that invoked the CLI).
4
+ */
5
+ import { PrRunResult } from './types.js';
6
+ export declare const STICKY_MARKER = "<!-- pre-post:visual-changes -->";
7
+ export interface CommentOptions {
8
+ version?: string;
9
+ headSha?: string | null;
10
+ now?: Date;
11
+ }
12
+ /**
13
+ * Markdown body for the sticky PR comment.
14
+ */
15
+ export declare function buildComment(result: PrRunResult, options?: CommentOptions): string;
16
+ /**
17
+ * Compact terminal summary — designed to be the only thing an agent reads.
18
+ */
19
+ export declare function buildSummary(result: PrRunResult): string;
20
+ //# sourceMappingURL=report.d.ts.map
@@ -0,0 +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;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,CA6BxD"}
package/dist/report.js ADDED
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Output formatting: the PR comment (for humans) and the terminal summary
3
+ * (for the developer or the agent that invoked the CLI).
4
+ */
5
+ import { describeShift } from './run.js';
6
+ import { hostOf, isLocalUrl } from './url.js';
7
+ export const STICKY_MARKER = '<!-- pre-post:visual-changes -->';
8
+ function code(s) {
9
+ return `\`${s}\``;
10
+ }
11
+ function viewportLabel(v) {
12
+ return v.charAt(0).toUpperCase() + v.slice(1);
13
+ }
14
+ function groupByRoute(outcomes) {
15
+ const map = new Map();
16
+ for (const o of outcomes)
17
+ map.set(o.route, [...(map.get(o.route) ?? []), o]);
18
+ return map;
19
+ }
20
+ /**
21
+ * Markdown body for the sticky PR comment.
22
+ */
23
+ export function buildComment(result, options = {}) {
24
+ const lines = [STICKY_MARKER, '## Visual changes', ''];
25
+ const routes = groupByRoute(result.outcomes);
26
+ const changed = result.outcomes.filter(o => o.status === 'changed');
27
+ const unchanged = result.outcomes.filter(o => o.status === 'unchanged');
28
+ const errors = result.outcomes.filter(o => o.status === 'error');
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.', '');
40
+ for (const [route, outcomes] of routes) {
41
+ const changedHere = outcomes.filter(o => o.status === 'changed' && (o.urls || o.files));
42
+ if (changedHere.length === 0)
43
+ continue;
44
+ for (const o of changedHere) {
45
+ const u = o.urls ?? o.files;
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);
56
+ const pre = u.cropBefore ?? u.before;
57
+ const post = u.cropAfter ?? u.after;
58
+ lines.push('| Pre | Post |', '|:---:|:---:|', `| ![Pre](${pre}) | ![Post](${post}) |`, '');
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) |', '|:---:|:---:|', `| ![Pre full](${u.before}) | ![Post full](${u.after}) |`, '');
64
+ lines.push('</details>', '');
65
+ }
66
+ }
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(`![${side}](${image})`, '');
80
+ }
81
+ if (unchanged.length) {
82
+ const byRoute = groupByRoute(unchanged);
83
+ const parts = Array.from(byRoute.entries()).map(([route, os]) => `${code(route)} (${os.map(o => o.viewport).join(', ')})`);
84
+ lines.push(`**No visual change:** ${parts.join(' · ')}`, '');
85
+ }
86
+ if (errors.length) {
87
+ lines.push('**Could not capture:**');
88
+ for (const o of errors)
89
+ lines.push(`- ${code(o.route)} ${o.viewport}: ${o.error}`);
90
+ lines.push('');
91
+ }
92
+ if (result.skippedDynamic.length) {
93
+ lines.push(`**Needs a sample URL:** ${result.skippedDynamic.map(code).join(', ')} — add them under ${code('"samples"')} in ${code('.pre-post.json')}.`, '');
94
+ }
95
+ return lines.join('\n');
96
+ }
97
+ /**
98
+ * Compact terminal summary — designed to be the only thing an agent reads.
99
+ */
100
+ export function buildSummary(result) {
101
+ const lines = [];
102
+ const header = [
103
+ result.prNumber ? `PR #${result.prNumber}` : 'no PR',
104
+ `${new Set(result.outcomes.map(o => o.route)).size} route(s)`,
105
+ `${new Set(result.outcomes.map(o => o.viewport)).size} viewport(s)`,
106
+ `${(result.durationMs / 1000).toFixed(1)}s`,
107
+ ];
108
+ lines.push(`pre-post · ${header.join(' · ')}`);
109
+ const rows = result.outcomes.map(o => [
110
+ o.route,
111
+ o.viewport,
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 || ''),
118
+ ]);
119
+ const widths = [0, 1, 2].map(i => Math.max(...rows.map(r => r[i].length), 0));
120
+ for (const r of rows) {
121
+ lines.push(` ${r[0].padEnd(widths[0])} ${r[1].padEnd(widths[1])} ${r[2].padEnd(widths[2])} ${r[3]}`.trimEnd());
122
+ }
123
+ if (result.skippedDynamic.length) {
124
+ lines.push(` needs sample URL: ${result.skippedDynamic.join(', ')} (add to .pre-post.json "samples")`);
125
+ }
126
+ if (result.commentUrl)
127
+ lines.push(`Comment: ${result.commentUrl}`);
128
+ return lines.join('\n');
129
+ }
130
+ //# sourceMappingURL=report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Lightweight import graph. Regex-based (no TypeScript compiler), fast enough
3
+ * to scan a few thousand files in well under a second, and good enough to
4
+ * answer "which page files transitively import this changed file?".
5
+ */
6
+ export declare const SOURCE_EXTENSIONS: string[];
7
+ export declare const SKIP_DIRS: Set<string>;
8
+ export declare function toPosix(p: string): string;
9
+ export interface ImportGraph {
10
+ /** Absolute paths of every scanned source file */
11
+ files: Set<string>;
12
+ /** file → files that import it (absolute paths) */
13
+ importers: Map<string, Set<string>>;
14
+ }
15
+ export interface Alias {
16
+ /** Prefix such as "@/" or "~/" */
17
+ prefix: string;
18
+ /** Absolute directories to try, in order */
19
+ targets: string[];
20
+ }
21
+ /**
22
+ * Read path aliases from tsconfig.json / jsconfig.json (one level of `extends`).
23
+ */
24
+ export declare function readAliases(appRoot: string): Alias[];
25
+ export declare function resolveSpecifier(spec: string, fromFile: string, files: Set<string>, aliases: Alias[]): string | null;
26
+ export declare function walkSourceFiles(root: string, maxFiles?: number): string[];
27
+ export declare function extractSpecifiers(source: string): string[];
28
+ /**
29
+ * Build the reverse import graph for every source file under `appRoot`.
30
+ */
31
+ export declare function buildImportGraph(appRoot: string, options?: {
32
+ files?: string[];
33
+ aliases?: Alias[];
34
+ }): ImportGraph;
35
+ export interface AffectedEntry {
36
+ /** The changed file that led here */
37
+ via: string;
38
+ /** Number of import hops from the changed file (0 = the entry itself changed) */
39
+ depth: number;
40
+ }
41
+ /**
42
+ * Walk importers from each changed file until entry files are reached.
43
+ * Returns entry → closest (changed file, depth).
44
+ */
45
+ export declare function findAffectedEntries(graph: ImportGraph, changed: string[], isEntry: (file: string) => boolean, maxDepth?: number): Map<string, AffectedEntry>;
46
+ //# sourceMappingURL=imports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"imports.d.ts","sourceRoot":"","sources":["../../src/routes/imports.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,eAAO,MAAM,iBAAiB,UAA6G,CAAC;AAG5I,eAAO,MAAM,SAAS,aAGpB,CAAC;AAEH,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAEzC;AAED,MAAM,WAAW,WAAW;IAC1B,kDAAkD;IAClD,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACnB,mDAAmD;IACnD,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;CACrC;AAED,MAAM,WAAW,KAAK;IACpB,kCAAkC;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAqBD;;GAEG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,KAAK,EAAE,CAuCpD;AAcD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,MAAM,GAAG,IAAI,CAiBpH;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,SAAO,GAAG,MAAM,EAAE,CAoBvE;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAQ1D;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,CAAC,EAAE,KAAK,EAAE,CAAA;CAAO,GAAG,WAAW,CAsBpH;AAED,MAAM,WAAW,aAAa;IAC5B,qCAAqC;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,MAAM,EAAE,EACjB,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,EAClC,QAAQ,SAAI,GACX,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAyB5B"}
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Lightweight import graph. Regex-based (no TypeScript compiler), fast enough
3
+ * to scan a few thousand files in well under a second, and good enough to
4
+ * answer "which page files transitively import this changed file?".
5
+ */
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ export const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts', '.vue', '.svelte', '.astro', '.mdx', '.md'];
9
+ const RESOLVE_EXTENSIONS = ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.cjs', '.mts', '.cts', '.vue', '.svelte', '.astro', '.mdx', '.md', '.css', '.scss', '.json'];
10
+ export const SKIP_DIRS = new Set([
11
+ '.git', '.next', '.nuxt', '.svelte-kit', '.turbo', '.vercel', '.cache', 'node_modules',
12
+ 'dist', 'build', 'out', 'coverage', 'public', 'storybook-static', '.storybook', '__snapshots__',
13
+ ]);
14
+ export function toPosix(p) {
15
+ return p.split(path.sep).join('/');
16
+ }
17
+ const SPECIFIER_PATTERNS = [
18
+ /(?:^|[^\w$.])(?:import|export)\s+(?:type\s+)?(?:[\w*{}\s,$]+?\s+from\s+)?['"]([^'"\n]+)['"]/g,
19
+ /import\s*\(\s*['"]([^'"\n]+)['"]\s*\)/g,
20
+ /require\s*\(\s*['"]([^'"\n]+)['"]\s*\)/g,
21
+ ];
22
+ /** Very lenient JSON parse for tsconfig/jsconfig (comments + trailing commas). */
23
+ function parseJsonc(text) {
24
+ const stripped = text
25
+ .replace(/\/\*[\s\S]*?\*\//g, '')
26
+ .replace(/(^|[^:\\])\/\/.*$/gm, '$1')
27
+ .replace(/,\s*([}\]])/g, '$1');
28
+ try {
29
+ return JSON.parse(stripped);
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ /**
36
+ * Read path aliases from tsconfig.json / jsconfig.json (one level of `extends`).
37
+ */
38
+ export function readAliases(appRoot) {
39
+ const aliases = [];
40
+ const seen = new Set();
41
+ const visit = (file, depth) => {
42
+ if (depth > 2 || seen.has(file) || !fs.existsSync(file))
43
+ return;
44
+ seen.add(file);
45
+ const cfg = parseJsonc(fs.readFileSync(file, 'utf-8'));
46
+ if (!cfg)
47
+ return;
48
+ const dir = path.dirname(file);
49
+ const baseUrl = cfg.compilerOptions?.baseUrl ? path.resolve(dir, cfg.compilerOptions.baseUrl) : dir;
50
+ const paths = cfg.compilerOptions?.paths || {};
51
+ for (const [pattern, targets] of Object.entries(paths)) {
52
+ const prefix = pattern.replace(/\*$/, '');
53
+ aliases.push({
54
+ prefix,
55
+ targets: (targets || []).map(t => path.resolve(baseUrl, t.replace(/\*$/, ''))),
56
+ });
57
+ }
58
+ if (cfg.compilerOptions?.baseUrl) {
59
+ aliases.push({ prefix: '', targets: [baseUrl] });
60
+ }
61
+ if (typeof cfg.extends === 'string' && cfg.extends.startsWith('.')) {
62
+ visit(path.resolve(dir, cfg.extends.endsWith('.json') ? cfg.extends : `${cfg.extends}.json`), depth + 1);
63
+ }
64
+ };
65
+ visit(path.join(appRoot, 'tsconfig.json'), 0);
66
+ visit(path.join(appRoot, 'jsconfig.json'), 0);
67
+ // Common conventions even without tsconfig paths.
68
+ for (const conv of ['@/', '~/']) {
69
+ if (!aliases.some(a => a.prefix === conv)) {
70
+ const targets = [path.join(appRoot, 'src'), appRoot].filter(d => fs.existsSync(d));
71
+ aliases.push({ prefix: conv, targets });
72
+ }
73
+ }
74
+ // Longest prefix first; the bare baseUrl fallback last.
75
+ return aliases.sort((a, b) => b.prefix.length - a.prefix.length);
76
+ }
77
+ function resolveFile(base, files) {
78
+ if (files.has(base))
79
+ return base;
80
+ for (const ext of RESOLVE_EXTENSIONS) {
81
+ if (files.has(base + ext))
82
+ return base + ext;
83
+ }
84
+ for (const ext of RESOLVE_EXTENSIONS) {
85
+ const idx = path.join(base, `index${ext}`);
86
+ if (files.has(idx))
87
+ return idx;
88
+ }
89
+ return null;
90
+ }
91
+ export function resolveSpecifier(spec, fromFile, files, aliases) {
92
+ const clean = spec.split('?')[0].split('#')[0];
93
+ if (!clean)
94
+ return null;
95
+ if (clean.startsWith('.') || clean.startsWith('/')) {
96
+ const base = clean.startsWith('/') ? clean : path.resolve(path.dirname(fromFile), clean);
97
+ return resolveFile(base, files);
98
+ }
99
+ for (const alias of aliases) {
100
+ if (alias.prefix && !clean.startsWith(alias.prefix))
101
+ continue;
102
+ if (!alias.prefix && (clean.startsWith('@') || !/^[\w-]+[/]/.test(clean) && !/^[\w-]+$/.test(clean)))
103
+ continue;
104
+ const rest = clean.slice(alias.prefix.length);
105
+ for (const target of alias.targets) {
106
+ const found = resolveFile(path.join(target, rest), files);
107
+ if (found)
108
+ return found;
109
+ }
110
+ }
111
+ return null;
112
+ }
113
+ export function walkSourceFiles(root, maxFiles = 8000) {
114
+ const out = [];
115
+ const stack = [root];
116
+ while (stack.length && out.length < maxFiles) {
117
+ const dir = stack.pop();
118
+ let entries;
119
+ try {
120
+ entries = fs.readdirSync(dir, { withFileTypes: true });
121
+ }
122
+ catch {
123
+ continue;
124
+ }
125
+ for (const entry of entries) {
126
+ if (entry.isDirectory()) {
127
+ if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith('.'))
128
+ stack.push(path.join(dir, entry.name));
129
+ }
130
+ else if (entry.isFile() && SOURCE_EXTENSIONS.includes(path.extname(entry.name))) {
131
+ out.push(path.join(dir, entry.name));
132
+ }
133
+ }
134
+ }
135
+ return out;
136
+ }
137
+ export function extractSpecifiers(source) {
138
+ const specs = [];
139
+ for (const pattern of SPECIFIER_PATTERNS) {
140
+ pattern.lastIndex = 0;
141
+ let m;
142
+ while ((m = pattern.exec(source)))
143
+ specs.push(m[1]);
144
+ }
145
+ return specs;
146
+ }
147
+ /**
148
+ * Build the reverse import graph for every source file under `appRoot`.
149
+ */
150
+ export function buildImportGraph(appRoot, options = {}) {
151
+ const list = options.files ?? walkSourceFiles(appRoot);
152
+ const files = new Set(list);
153
+ const aliases = options.aliases ?? readAliases(appRoot);
154
+ const importers = new Map();
155
+ for (const file of list) {
156
+ let source;
157
+ try {
158
+ source = fs.readFileSync(file, 'utf-8');
159
+ }
160
+ catch {
161
+ continue;
162
+ }
163
+ for (const spec of extractSpecifiers(source)) {
164
+ const target = resolveSpecifier(spec, file, files, aliases);
165
+ if (!target || target === file)
166
+ continue;
167
+ let set = importers.get(target);
168
+ if (!set)
169
+ importers.set(target, (set = new Set()));
170
+ set.add(file);
171
+ }
172
+ }
173
+ return { files, importers };
174
+ }
175
+ /**
176
+ * Walk importers from each changed file until entry files are reached.
177
+ * Returns entry → closest (changed file, depth).
178
+ */
179
+ export function findAffectedEntries(graph, changed, isEntry, maxDepth = 8) {
180
+ const result = new Map();
181
+ const record = (entry, via, depth) => {
182
+ const existing = result.get(entry);
183
+ if (!existing || depth < existing.depth)
184
+ result.set(entry, { via, depth });
185
+ };
186
+ for (const origin of changed) {
187
+ const visited = new Set([origin]);
188
+ let frontier = [origin];
189
+ if (isEntry(origin))
190
+ record(origin, origin, 0);
191
+ for (let depth = 1; depth <= maxDepth && frontier.length; depth++) {
192
+ const next = [];
193
+ for (const file of frontier) {
194
+ for (const importer of graph.importers.get(file) ?? []) {
195
+ if (visited.has(importer))
196
+ continue;
197
+ visited.add(importer);
198
+ if (isEntry(importer))
199
+ record(importer, origin, depth);
200
+ next.push(importer);
201
+ }
202
+ }
203
+ frontier = next;
204
+ }
205
+ }
206
+ return result;
207
+ }
208
+ //# sourceMappingURL=imports.js.map