@juangadm/pre-post 1.0.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 (103) hide show
  1. package/README.md +99 -20
  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 +80 -41
  7. package/dist/bin/cli.js.map +1 -1
  8. package/dist/browser.d.ts +31 -3
  9. package/dist/browser.d.ts.map +1 -1
  10. package/dist/browser.js +239 -97
  11. package/dist/browser.js.map +1 -1
  12. package/dist/commands/compare.d.ts.map +1 -1
  13. package/dist/commands/compare.js +21 -3
  14. package/dist/commands/compare.js.map +1 -1
  15. package/dist/commands/detect.d.ts +5 -0
  16. package/dist/commands/detect.d.ts.map +1 -1
  17. package/dist/commands/detect.js +4 -1
  18. package/dist/commands/detect.js.map +1 -1
  19. package/dist/commands/doctor.d.ts +32 -0
  20. package/dist/commands/doctor.d.ts.map +1 -1
  21. package/dist/commands/doctor.js +90 -9
  22. package/dist/commands/doctor.js.map +1 -1
  23. package/dist/commands/login.js +1 -1
  24. package/dist/commands/pr.d.ts +4 -0
  25. package/dist/commands/pr.d.ts.map +1 -1
  26. package/dist/commands/pr.js +150 -50
  27. package/dist/commands/pr.js.map +1 -1
  28. package/dist/commands/prune.js +1 -1
  29. package/dist/commands/prune.js.map +1 -1
  30. package/dist/comparison.d.ts +104 -0
  31. package/dist/comparison.d.ts.map +1 -0
  32. package/dist/comparison.js +236 -0
  33. package/dist/comparison.js.map +1 -0
  34. package/dist/config.d.ts +12 -2
  35. package/dist/config.d.ts.map +1 -1
  36. package/dist/config.js +25 -3
  37. package/dist/config.js.map +1 -1
  38. package/dist/deployments.d.ts +71 -0
  39. package/dist/deployments.d.ts.map +1 -0
  40. package/dist/deployments.js +204 -0
  41. package/dist/deployments.js.map +1 -0
  42. package/dist/diff-pool.d.ts.map +1 -1
  43. package/dist/diff-pool.js +5 -2
  44. package/dist/diff-pool.js.map +1 -1
  45. package/dist/diff-worker.d.ts +3 -0
  46. package/dist/diff-worker.d.ts.map +1 -1
  47. package/dist/diff-worker.js +1 -0
  48. package/dist/diff-worker.js.map +1 -1
  49. package/dist/diff.d.ts +10 -0
  50. package/dist/diff.d.ts.map +1 -1
  51. package/dist/diff.js +117 -7
  52. package/dist/diff.js.map +1 -1
  53. package/dist/doctor.d.ts +42 -1
  54. package/dist/doctor.d.ts.map +1 -1
  55. package/dist/doctor.js +73 -7
  56. package/dist/doctor.js.map +1 -1
  57. package/dist/git.d.ts +41 -3
  58. package/dist/git.d.ts.map +1 -1
  59. package/dist/git.js +103 -7
  60. package/dist/git.js.map +1 -1
  61. package/dist/github.d.ts +92 -2
  62. package/dist/github.d.ts.map +1 -1
  63. package/dist/github.js +191 -20
  64. package/dist/github.js.map +1 -1
  65. package/dist/landing.d.ts +48 -0
  66. package/dist/landing.d.ts.map +1 -0
  67. package/dist/landing.js +93 -0
  68. package/dist/landing.js.map +1 -0
  69. package/dist/pkg.d.ts +9 -0
  70. package/dist/pkg.d.ts.map +1 -1
  71. package/dist/pkg.js +14 -0
  72. package/dist/pkg.js.map +1 -1
  73. package/dist/report.d.ts.map +1 -1
  74. package/dist/report.js +49 -26
  75. package/dist/report.js.map +1 -1
  76. package/dist/routes.d.ts +27 -0
  77. package/dist/routes.d.ts.map +1 -1
  78. package/dist/routes.js +98 -23
  79. package/dist/routes.js.map +1 -1
  80. package/dist/run.d.ts +88 -8
  81. package/dist/run.d.ts.map +1 -1
  82. package/dist/run.js +214 -26
  83. package/dist/run.js.map +1 -1
  84. package/dist/sameness.d.ts +91 -0
  85. package/dist/sameness.d.ts.map +1 -0
  86. package/dist/sameness.js +157 -0
  87. package/dist/sameness.js.map +1 -0
  88. package/dist/sessions.d.ts.map +1 -1
  89. package/dist/sessions.js +7 -1
  90. package/dist/sessions.js.map +1 -1
  91. package/dist/shift.d.ts +67 -0
  92. package/dist/shift.d.ts.map +1 -0
  93. package/dist/shift.js +184 -0
  94. package/dist/shift.js.map +1 -0
  95. package/dist/types.d.ts +67 -5
  96. package/dist/types.d.ts.map +1 -1
  97. package/dist/types.js.map +1 -1
  98. package/dist/url.d.ts +12 -0
  99. package/dist/url.d.ts.map +1 -0
  100. package/dist/url.js +30 -0
  101. package/dist/url.js.map +1 -0
  102. package/package.json +1 -1
  103. package/skill/SKILL.md +40 -7
package/README.md CHANGED
@@ -1,9 +1,35 @@
1
1
  # pre-post
2
2
 
3
3
  Before/after screenshots for pull requests. One command detects the routes your branch
4
- changed, captures them on production ("Pre") and on your dev server ("Post"), pixel-diffs
5
- them, uploads the images to GitHub, and posts a single comment on the PR that a reviewer
6
- can judge at a glance.
4
+ changed, captures each one before and after, uploads the images to GitHub, and puts them at
5
+ the top of the PR description where a reviewer can judge them at a glance.
6
+
7
+ It works out both sides itself, cheapest option first:
8
+
9
+ | | Pre (the baseline) | Post (this branch) |
10
+ |---|---|---|
11
+ | 1 | `--before` | `--after` |
12
+ | 2 | `before` in `.pre-post.json` | the preview deployment for this commit |
13
+ | 3 | the production deployment for the base commit | a local dev server |
14
+ | 4 | whatever is on production now | |
15
+ | 5 | the base commit, served locally | |
16
+
17
+ Rows 2 to 4 need no dev environment at all, which is the point: a preview deployment and a
18
+ production URL are enough for anyone who can open the PR. Deployments come from the GitHub
19
+ Deployments API, so Vercel, Cloudflare Pages, Netlify and Render all work with no extra
20
+ token and nothing to configure; a host that records only a commit status is read from the
21
+ deployment bot's own PR comment instead. Row 4 covers repositories that do not deploy every
22
+ push to their default branch, so nothing is deployed at the fork point — it prints which
23
+ commit Pre actually came from rather than implying the base.
24
+
25
+ pre-post never guesses a baseline. If no deployment can be found it says so and tells you
26
+ the one flag that fixes it, because a baseline that is quietly the wrong site reads as 100%
27
+ changed on every route and looks exactly like a real result.
28
+
29
+ The last baseline needs no network at all: it checks the base commit into a throwaway
30
+ worktree and boots its dev script. That keeps pre-post working inside a sandboxed agent
31
+ container, a CI job, or behind an egress allowlist — and it compares against exactly what
32
+ the branch forked from, rendered in the same browser as the Post side.
7
33
 
8
34
  > Originally forked from [before-and-after](https://github.com/vercel-labs/before-and-after) by James Clements / Vercel Labs.
9
35
 
@@ -19,34 +45,62 @@ Capturing 8 screenshots (2 route(s) × 2 viewport(s)) ...
19
45
  same / @ mobile (0.00%, 1990ms)
20
46
  same / @ desktop (0.00%, 2211ms)
21
47
  Publishing 10 image(s) to acme/web@pre-post-assets ...
22
- Updated PR comment: https://github.com/acme/web/pull/42#issuecomment-1
48
+ Updated PR description: https://github.com/acme/web/pull/42
23
49
  pre-post · PR #42 · 2 route(s) · 2 viewport(s) · 6.8s
24
50
  / desktop no change
25
51
  / mobile no change
26
52
  /pricing desktop 0.64% changed
27
53
  /pricing mobile 1.4% changed
28
- Comment: https://github.com/acme/web/pull/42#issuecomment-1
54
+ Comment: https://github.com/acme/web/pull/42
29
55
  ```
30
56
 
31
57
  ## How it works
32
58
 
33
- 1. **Routes.** Diffs the branch against the merge base with `main`, then follows the import
59
+ 1. **Routes.** Diffs the branch against the merge base with `main` — fetching that branch
60
+ first when the checkout does not have it, which is the normal shape in CI and in the
61
+ web/sandbox editors. When no shared history can be established it stops with one
62
+ sentence rather than reporting an empty diff, and `--base <ref>` names the commit
63
+ directly. Then it follows the import
34
64
  graph: a change to `components/ui/button.tsx` marks every page that imports it. Next.js
35
65
  App Router and Pages Router, Vite apps (React Router, file-based `src/pages`), and a
36
66
  generic fallback. Monorepos are handled by picking the app that owns the changed files.
37
- 2. **Capture.** Playwright + Chromium headless shell. Fixed clock, reduced motion,
38
- animations finished, caret hidden, fonts and images settled, layout stable across frames,
39
- lazy content primed. 2x device scale, full page (capped at 2400 CSS px), desktop + mobile.
67
+ 2. **Capture.** Playwright + Chromium headless shell. The page's own clock is held
68
+ still while it loads and then run forward by a fixed budget, so a page that animates
69
+ on a timer is photographed at the same frame on both sides however fast each host
70
+ answered. Reduced motion, animations finished, caret hidden, fonts and images settled,
71
+ layout stable, lazy content primed. 2x device scale, full page (capped at 2400 CSS
72
+ px), desktop (add mobile with `-r`).
40
73
  All routes and viewports run concurrently.
41
74
  3. **Diff.** Pure-JS pixel comparison in worker threads. Reports the percentage changed,
42
- the bounding box, a red-highlight image, and a tight crop of the changed region.
43
- 4. **Publish.** Images go to a `pre-post-assets` branch in the same repository via the GitHub
75
+ the bounding box, and a tight crop of the changed region. A route counts as changed when
76
+ the painted difference covers at least `minChangedArea` CSS pixels² (default 100, roughly
77
+ a third of a 16px icon) or at least `threshold` of the canvas — the first rule is what
78
+ decides on a page, the second on a small image.
79
+ 4. **Layout shift.** A padding change near the top of a page moves everything below it, and
80
+ pixel diffing counts every moved pixel as changed: a change a designer would call
81
+ "slightly roomier" reads as most of the page repainted, and the crop is suppressed just
82
+ when it would be most useful. So the two sides are checked for a single vertical offset
83
+ first — how far the content moved, and from which row. When one is found, Pre is re-spaced
84
+ into Post's layout and the two are compared there: the crop comes from that pair, and the
85
+ comment says `Content shifted down 48px` instead of quoting a percentage. Rows Post gained
86
+ are left as background rather than skipped, so content inserted above the shift — the
87
+ banner that caused it — still reads as new rather than as nothing. The offset is only
88
+ accepted when it accounts for most of the difference across the rows both sides share.
89
+ The raw numbers are left alone: a move is a visual change, and reporting less of one would
90
+ hide it. Reflow, where content moves both across and down, has no single offset to find,
91
+ so it is reported the way it always was.
92
+ 5. **Publish.** Images go to a `pre-post-assets` branch in the same repository via the GitHub
44
93
  API, as one commit per run. Nothing is committed to the PR branch, no CI is triggered,
45
- and the blob URLs render on private repos. `pre-post prune` removes images for PRs
46
- closed more than 90 days ago.
47
- 5. **Comment.** One sticky comment per PR, updated in place on every run. Changed routes show
48
- a Pre/Post crop with the full page and diff collapsed underneath; unchanged routes fold
49
- into a single line.
94
+ and the blob URLs render on private repos — a screenshot is visible to exactly whoever
95
+ can see the repository. `pre-post prune` removes images for PRs closed more than 90 days
96
+ ago, but note that it commits a deletion rather than rewriting history: the older commits
97
+ still hold the blobs, so a link handed out earlier keeps working. Treat anything captured
98
+ as permanent, and think twice before pointing pre-post at a preview holding real data.
99
+ 6. **Describe.** The images go in a delimited block at the top of the PR description,
100
+ replaced in place on every run and leaving your own text untouched. Changed routes show
101
+ a Pre/Post crop with the full page collapsed underneath; unchanged routes fold into a
102
+ single line. If the PR cannot be edited — a fork, a read-only token — it falls back to
103
+ one sticky comment.
50
104
 
51
105
  ## Install
52
106
 
@@ -66,7 +120,8 @@ Then say `/pre-post` after making UI changes.
66
120
 
67
121
  ```bash
68
122
  pre-post pr # everything, on the current branch's PR
69
- pre-post pr --before https://acme.com # first run in a repo (saved to .pre-post.json)
123
+ pre-post pr --before https://acme.com # pin the baseline (saved to .pre-post.json)
124
+ pre-post pr --no-local-baseline # never build the base commit locally
70
125
  pre-post pr --routes /pricing,/docs # explicit routes
71
126
  pre-post pr --viewports desktop,1440x900 # custom viewports
72
127
  pre-post pr --dry-run # capture + diff locally, post nothing
@@ -83,6 +138,24 @@ pre-post doctor # browser, token, dev server, confi
83
138
  When something needs a human, the CLI exits with code 3 and one sentence saying what to do
84
139
  (log in, start the dev server, pass `--before`). Re-running picks up where it left off.
85
140
 
141
+ Exit codes, so a script or an agent can branch on them:
142
+
143
+ | code | meaning |
144
+ |---|---|
145
+ | 0 | done; for `doctor`, `pre-post pr` can run |
146
+ | 1 | the run failed — every capture errored, or an unexpected error; for `doctor`, a required check failed |
147
+ | 2 | the arguments could not be parsed |
148
+ | 3 | something needs a human; the message says what |
149
+
150
+ `doctor` marks a check **FAIL** only when `pre-post pr` has no way to proceed without it —
151
+ the browser, a GitHub token, and being inside a git repository. Everything else prints as
152
+ **note**: no dev server running, or no `--before` saved, narrows *which* strategy a run
153
+ picks rather than stopping it, so those never change the exit code.
154
+
155
+ Results go into a delimited block at the top of the PR description, which re-runs replace in
156
+ place, leaving your own text untouched. If the PR cannot be edited — a fork, a read-only
157
+ token — it falls back to a single sticky comment.
158
+
86
159
  ## Configuration
87
160
 
88
161
  Optional `.pre-post.json` in the repo root:
@@ -92,12 +165,12 @@ Optional `.pre-post.json` in the repo root:
92
165
  "before": "https://acme.com",
93
166
  "routes": ["/"],
94
167
  "samples": { "/blog/[slug]": "/blog/hello-world" },
95
- "viewports": ["desktop", "mobile"],
168
+ "viewports": ["desktop"],
96
169
  "fullPage": true,
97
170
  "maxHeight": 2400,
98
171
  "scale": 2,
99
172
  "threshold": 0.001,
100
- "minChangedPixels": 40,
173
+ "minChangedArea": 100,
101
174
  "maxRoutes": 6,
102
175
  "ignore": ["apps/docs"],
103
176
  "headers": {},
@@ -110,7 +183,7 @@ Environment:
110
183
  | Variable | Purpose |
111
184
  |---|---|
112
185
  | `GH_TOKEN` / `GITHUB_TOKEN` | GitHub token (default: `gh auth token`) |
113
- | `VERCEL_AUTOMATION_BYPASS_SECRET` | Bypass Vercel Deployment Protection on the production URL |
186
+ | `VERCEL_AUTOMATION_BYPASS_SECRET` | Bypass Vercel Deployment Protection on preview and production URLs |
114
187
  | `PRE_POST_CONCURRENCY` | Parallel pages (default 6) |
115
188
  | `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` | Use a specific Chromium binary |
116
189
  | `GH_REPO` | `owner/repo` when the remote URL cannot be parsed |
@@ -130,6 +203,12 @@ pnpm test:unit
130
203
  TEST_BROWSER=true pnpm test # needs a Chromium; npx playwright-core install chromium-headless-shell
131
204
  ```
132
205
 
206
+ pnpm and npm cannot share a `node_modules`. pnpm builds a symlinked tree that npm
207
+ cannot read, so `npm install` over it fails with `Cannot read properties of null
208
+ (reading 'edgesOut')`, which names none of that. Run `rm -rf node_modules
209
+ site/node_modules` before switching either way. npm also installs the CLI only —
210
+ `site/` is a pnpm workspace member, so use pnpm to work on the site.
211
+
133
212
  ## License
134
213
 
135
214
  MIT
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Serve the PR's base commit locally, as the "Pre" side.
3
+ *
4
+ * This is the baseline that always works: it needs no deployment, no
5
+ * production URL and no network, so it is what keeps pre-post usable inside a
6
+ * sandbox — a cloud agent container, a CI job, or any box behind an egress
7
+ * allowlist. It is also the most honest comparison available, because both
8
+ * sides then render in the same environment with the same fonts and the same
9
+ * browser, leaving the diff as the only variable.
10
+ *
11
+ * The cost is time: a checkout, an install and a dev-server boot. So callers
12
+ * reach for it after a reachable deployment, not before one.
13
+ */
14
+ export interface LocalBaseline {
15
+ url: string;
16
+ /** Stop the dev server and delete the worktree. Safe to call twice. */
17
+ stop: () => Promise<void>;
18
+ }
19
+ interface PackageManager {
20
+ bin: string;
21
+ install: string[];
22
+ /** Build `run <script>` argv, appending extra args the way this manager wants. */
23
+ run: (script: string, args: string[]) => string[];
24
+ }
25
+ /**
26
+ * Which package manager this tree *declares*, from its `packageManager` field
27
+ * or its lockfile. Defaults to npm.
28
+ *
29
+ * The field wins over the lockfile because it is the deliberate statement: a
30
+ * repository mid-migration can carry two lockfiles, but it names one manager.
31
+ */
32
+ export declare function detectPackageManager(dir: string, repoRoot?: string): PackageManager;
33
+ /**
34
+ * Is `bin` runnable here?
35
+ *
36
+ * A PATH scan rather than a `--version` subprocess: this answers before every
37
+ * local baseline, and spawning a process to learn that a process cannot be
38
+ * spawned is the wrong shape.
39
+ */
40
+ export declare function onPath(bin: string, env?: NodeJS.ProcessEnv): boolean;
41
+ export interface ManagerChoice {
42
+ /** The manager to run, or null when neither it nor npm is installed here. */
43
+ pm: PackageManager | null;
44
+ /** What the repository declares. Differs from `pm` when we fell back. */
45
+ declared: PackageManager;
46
+ }
47
+ /**
48
+ * The manager to actually run: what the repository declares, if it is installed.
49
+ *
50
+ * Declaring pnpm does not put pnpm on PATH. On a real machine it was not there,
51
+ * and the local baseline — the fallback that exists for when nothing is
52
+ * deployed — died on `pnpm install` with no hint that the missing piece was
53
+ * pnpm itself. The same shell could still build the CLI, so the tool ran while
54
+ * its last resort could not.
55
+ *
56
+ * npm ships with Node, so it is the fallback with the best odds of being
57
+ * present. It will not honour a pnpm or yarn lockfile, so the baseline it
58
+ * installs can drift from the locked versions — worth saying out loud, and
59
+ * still far better than no baseline at all.
60
+ */
61
+ export declare function resolvePackageManager(dir: string, repoRoot?: string, has?: (bin: string) => boolean): ManagerChoice;
62
+ /**
63
+ * The directory to actually serve.
64
+ *
65
+ * The app root is detected from the files a branch changed, which answers
66
+ * "which routes moved" — not "which package can be served". A PR touching only
67
+ * the CLI resolves to the repo root, which has no dev script even though the
68
+ * site next door does. So prefer the detected directory and otherwise take the
69
+ * nearest package that can start a server.
70
+ */
71
+ export declare function servableDir(treeRoot: string, appPrefix?: string): {
72
+ dir: string;
73
+ script: string;
74
+ } | null;
75
+ /**
76
+ * Copy the working checkout's local env files into a throwaway worktree.
77
+ *
78
+ * .env files are gitignored, so a fresh worktree of the base commit has none.
79
+ * An app that needs one to boot then either never starts or serves an error
80
+ * page — and an error page diffed against the real branch reports a wall of
81
+ * changes the PR never made, which is worse than reporting no baseline at all.
82
+ *
83
+ * Only ever writes inside `to`, and returns names, never values: these files
84
+ * are exactly the ones that hold secrets.
85
+ */
86
+ export declare function copyEnvFiles(from: string, to: string, appPrefix?: string): string[];
87
+ /** An OS-assigned free port. */
88
+ export declare function freePort(): Promise<number>;
89
+ export interface BaselineOptions {
90
+ repoRoot: string;
91
+ /** Commit to serve. Omit to serve the working tree as it stands. */
92
+ sha?: string;
93
+ /** Directory holding the app's package.json, relative to the repo root. */
94
+ appPrefix?: string;
95
+ /** Budget for install + boot. */
96
+ timeoutMs?: number;
97
+ /** Injectable for tests; defaults to a real PATH scan. */
98
+ pathHas?: (bin: string) => boolean;
99
+ log?: (msg: string) => void;
100
+ }
101
+ /**
102
+ * Check out `sha` into a throwaway worktree, install, and start its dev server.
103
+ *
104
+ * Returns null when this repo cannot be served this way (no dev script, no
105
+ * install, server never came up). Callers treat that as "try the next option",
106
+ * so every failure path cleans up after itself and stays quiet.
107
+ */
108
+ export declare function serveBaseCommit(opts: BaselineOptions): Promise<LocalBaseline | null>;
109
+ /**
110
+ * Boot this repository's dev server without one already running.
111
+ *
112
+ * The working tree is what the author is looking at, uncommitted edits and all,
113
+ * so it is the honest "Post" when nothing is deployed. Serving it needs no
114
+ * worktree and usually no install, which is why it is cheaper than the base
115
+ * side it gets paired with.
116
+ */
117
+ export declare function serveWorkingTree(opts: Omit<BaselineOptions, 'sha'>): Promise<LocalBaseline | null>;
118
+ export {};
119
+ //# sourceMappingURL=baseline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline.d.ts","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAUH,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,uEAAuE;IACvE,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAGD,UAAU,cAAc;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,kFAAkF;IAClF,GAAG,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,MAAM,EAAE,CAAC;CACnD;AAgBD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,cAAc,CAUnF;AAED;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAUjF;AAED,MAAM,WAAW,aAAa;IAC5B,6EAA6E;IAC7E,EAAE,EAAE,cAAc,GAAG,IAAI,CAAC;IAC1B,yEAAyE;IACzE,QAAQ,EAAE,cAAc,CAAC;CAC1B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,MAAM,EACX,QAAQ,CAAC,EAAE,MAAM,EACjB,GAAG,GAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAgB,GACrC,aAAa,CAIf;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAUxG;AAKD;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAkBnF;AAED,gCAAgC;AAChC,wBAAgB,QAAQ,IAAI,OAAO,CAAC,MAAM,CAAC,CAS1C;AAgBD,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iCAAiC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,0DAA0D;IAC1D,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACnC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAE1F;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,IAAI,CAAC,eAAe,EAAE,KAAK,CAAC,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAExG"}
@@ -0,0 +1,316 @@
1
+ /**
2
+ * Serve the PR's base commit locally, as the "Pre" side.
3
+ *
4
+ * This is the baseline that always works: it needs no deployment, no
5
+ * production URL and no network, so it is what keeps pre-post usable inside a
6
+ * sandbox — a cloud agent container, a CI job, or any box behind an egress
7
+ * allowlist. It is also the most honest comparison available, because both
8
+ * sides then render in the same environment with the same fonts and the same
9
+ * browser, leaving the diff as the only variable.
10
+ *
11
+ * The cost is time: a checkout, an install and a dev-server boot. So callers
12
+ * reach for it after a reachable deployment, not before one.
13
+ */
14
+ import { spawn, execFileSync } from 'child_process';
15
+ import fs from 'fs';
16
+ import net from 'net';
17
+ import os from 'os';
18
+ import path from 'path';
19
+ import { devScript, readPackage } from './pkg.js';
20
+ import { findAppRoots } from './routes.js';
21
+ const NPM = { bin: 'npm', install: ['install'], run: (s, a) => ['run', s, '--', ...a] };
22
+ const PNPM = { bin: 'pnpm', install: ['install', '--prefer-offline'], run: (s, a) => ['run', s, ...a] };
23
+ const YARN = { bin: 'yarn', install: ['install'], run: (s, a) => ['run', s, ...a] };
24
+ const BUN = { bin: 'bun', install: ['install'], run: (s, a) => ['run', s, ...a] };
25
+ const MANAGERS = [
26
+ { lockfile: 'pnpm-lock.yaml', pm: PNPM },
27
+ { lockfile: 'yarn.lock', pm: YARN },
28
+ { lockfile: 'bun.lockb', pm: BUN },
29
+ { lockfile: 'package-lock.json', pm: NPM },
30
+ ];
31
+ const BY_NAME = { npm: NPM, pnpm: PNPM, yarn: YARN, bun: BUN };
32
+ /**
33
+ * Which package manager this tree *declares*, from its `packageManager` field
34
+ * or its lockfile. Defaults to npm.
35
+ *
36
+ * The field wins over the lockfile because it is the deliberate statement: a
37
+ * repository mid-migration can carry two lockfiles, but it names one manager.
38
+ */
39
+ export function detectPackageManager(dir, repoRoot) {
40
+ for (const candidate of [dir, repoRoot].filter(Boolean)) {
41
+ const field = readPackage(candidate)?.packageManager;
42
+ const named = typeof field === 'string' ? BY_NAME[field.split('@')[0].trim()] : undefined;
43
+ if (named)
44
+ return named;
45
+ for (const { lockfile, pm } of MANAGERS) {
46
+ if (fs.existsSync(path.join(candidate, lockfile)))
47
+ return pm;
48
+ }
49
+ }
50
+ return NPM;
51
+ }
52
+ /**
53
+ * Is `bin` runnable here?
54
+ *
55
+ * A PATH scan rather than a `--version` subprocess: this answers before every
56
+ * local baseline, and spawning a process to learn that a process cannot be
57
+ * spawned is the wrong shape.
58
+ */
59
+ export function onPath(bin, env = process.env) {
60
+ const dirs = (env.PATH ?? '').split(path.delimiter).filter(Boolean);
61
+ const exts = process.platform === 'win32'
62
+ ? (env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean)
63
+ : [''];
64
+ return dirs.some(dir => exts.some(ext => {
65
+ // throwIfNoEntry keeps the common case (a miss) from constructing an
66
+ // exception; the catch stays for the rarer EACCES/ELOOP.
67
+ try {
68
+ return fs.statSync(path.join(dir, bin + ext), { throwIfNoEntry: false })?.isFile() ?? false;
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ }));
74
+ }
75
+ /**
76
+ * The manager to actually run: what the repository declares, if it is installed.
77
+ *
78
+ * Declaring pnpm does not put pnpm on PATH. On a real machine it was not there,
79
+ * and the local baseline — the fallback that exists for when nothing is
80
+ * deployed — died on `pnpm install` with no hint that the missing piece was
81
+ * pnpm itself. The same shell could still build the CLI, so the tool ran while
82
+ * its last resort could not.
83
+ *
84
+ * npm ships with Node, so it is the fallback with the best odds of being
85
+ * present. It will not honour a pnpm or yarn lockfile, so the baseline it
86
+ * installs can drift from the locked versions — worth saying out loud, and
87
+ * still far better than no baseline at all.
88
+ */
89
+ export function resolvePackageManager(dir, repoRoot, has = onPath) {
90
+ const declared = detectPackageManager(dir, repoRoot);
91
+ if (has(declared.bin))
92
+ return { pm: declared, declared };
93
+ return { pm: has(NPM.bin) ? NPM : null, declared };
94
+ }
95
+ /**
96
+ * The directory to actually serve.
97
+ *
98
+ * The app root is detected from the files a branch changed, which answers
99
+ * "which routes moved" — not "which package can be served". A PR touching only
100
+ * the CLI resolves to the repo root, which has no dev script even though the
101
+ * site next door does. So prefer the detected directory and otherwise take the
102
+ * nearest package that can start a server.
103
+ */
104
+ export function servableDir(treeRoot, appPrefix) {
105
+ const preferred = appPrefix ? path.join(treeRoot, appPrefix) : treeRoot;
106
+ const preferredScript = devScript(preferred);
107
+ if (preferredScript)
108
+ return { dir: preferred, script: preferredScript };
109
+ // Deepest first: a workspace app beats the repo root that merely contains it.
110
+ for (const dir of findAppRoots(treeRoot).sort((a, b) => b.length - a.length)) {
111
+ const script = devScript(dir);
112
+ if (script)
113
+ return { dir, script };
114
+ }
115
+ return null;
116
+ }
117
+ /** Local env files, in the order a framework would layer them. */
118
+ const ENV_FILES = ['.env', '.env.local', '.env.development', '.env.development.local'];
119
+ /**
120
+ * Copy the working checkout's local env files into a throwaway worktree.
121
+ *
122
+ * .env files are gitignored, so a fresh worktree of the base commit has none.
123
+ * An app that needs one to boot then either never starts or serves an error
124
+ * page — and an error page diffed against the real branch reports a wall of
125
+ * changes the PR never made, which is worse than reporting no baseline at all.
126
+ *
127
+ * Only ever writes inside `to`, and returns names, never values: these files
128
+ * are exactly the ones that hold secrets.
129
+ */
130
+ export function copyEnvFiles(from, to, appPrefix) {
131
+ const copied = [];
132
+ const root = path.resolve(to);
133
+ for (const dir of appPrefix ? ['', appPrefix] : ['']) {
134
+ for (const name of ENV_FILES) {
135
+ const src = path.join(from, dir, name);
136
+ const dest = path.resolve(root, dir, name);
137
+ // The worktree is the only place this may write.
138
+ if (dest !== root && !dest.startsWith(root + path.sep))
139
+ continue;
140
+ if (!fs.existsSync(src) || fs.existsSync(dest))
141
+ continue;
142
+ try {
143
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
144
+ fs.copyFileSync(src, dest);
145
+ copied.push(path.posix.join(dir, name));
146
+ }
147
+ catch { /* best effort: a missing env file is not fatal on its own */ }
148
+ }
149
+ }
150
+ return copied;
151
+ }
152
+ /** An OS-assigned free port. */
153
+ export function freePort() {
154
+ return new Promise((resolve, reject) => {
155
+ const srv = net.createServer();
156
+ srv.once('error', reject);
157
+ srv.listen(0, '127.0.0.1', () => {
158
+ const port = srv.address().port;
159
+ srv.close(() => resolve(port));
160
+ });
161
+ });
162
+ }
163
+ async function waitForServer(url, timeoutMs, alive) {
164
+ const deadline = Date.now() + timeoutMs;
165
+ while (Date.now() < deadline) {
166
+ if (!alive())
167
+ return false;
168
+ try {
169
+ await fetch(url, { signal: AbortSignal.timeout(2000) });
170
+ return true;
171
+ }
172
+ catch {
173
+ await new Promise(r => setTimeout(r, 500));
174
+ }
175
+ }
176
+ return false;
177
+ }
178
+ /**
179
+ * Check out `sha` into a throwaway worktree, install, and start its dev server.
180
+ *
181
+ * Returns null when this repo cannot be served this way (no dev script, no
182
+ * install, server never came up). Callers treat that as "try the next option",
183
+ * so every failure path cleans up after itself and stays quiet.
184
+ */
185
+ export async function serveBaseCommit(opts) {
186
+ return serveLocally(opts);
187
+ }
188
+ /**
189
+ * Boot this repository's dev server without one already running.
190
+ *
191
+ * The working tree is what the author is looking at, uncommitted edits and all,
192
+ * so it is the honest "Post" when nothing is deployed. Serving it needs no
193
+ * worktree and usually no install, which is why it is cheaper than the base
194
+ * side it gets paired with.
195
+ */
196
+ export async function serveWorkingTree(opts) {
197
+ return serveLocally({ ...opts, sha: undefined });
198
+ }
199
+ async function serveLocally(opts) {
200
+ const log = opts.log ?? (() => undefined);
201
+ const what = opts.sha ? `base commit ${opts.sha.slice(0, 7)}` : 'the working tree';
202
+ /** Every caller treats null as "try the next option", so say why before returning it. */
203
+ const skip = (detail) => {
204
+ log(`Could not serve ${what}: ${detail}`);
205
+ return null;
206
+ };
207
+ const timeoutMs = opts.timeoutMs ?? 300_000;
208
+ const deadline = Date.now() + timeoutMs;
209
+ const worktree = opts.sha ? fs.mkdtempSync(path.join(os.tmpdir(), 'pre-post-base-')) : opts.repoRoot;
210
+ let child = null;
211
+ let stopped = false;
212
+ // A detached dev server survives Ctrl-C and keeps its port, so tear it down
213
+ // on the signals that end the run as well as on the normal path.
214
+ const onSignal = () => { void cleanup(); };
215
+ const untrap = () => {
216
+ process.off('SIGINT', onSignal);
217
+ process.off('SIGTERM', onSignal);
218
+ process.off('exit', onSignal);
219
+ };
220
+ const cleanup = async () => {
221
+ if (stopped)
222
+ return;
223
+ stopped = true;
224
+ untrap();
225
+ if (child && child.exitCode === null) {
226
+ try {
227
+ process.kill(-child.pid, 'SIGTERM');
228
+ }
229
+ catch { /* already gone */ }
230
+ }
231
+ // Guard on the thing that actually matters: only a throwaway worktree may be
232
+ // removed. Checking `opts.sha` instead would let any future caller that
233
+ // points `worktree` at a real checkout delete uncommitted work.
234
+ if (worktree === opts.repoRoot)
235
+ return;
236
+ try {
237
+ execFileSync('git', ['worktree', 'remove', '--force', worktree], { cwd: opts.repoRoot, stdio: 'ignore' });
238
+ }
239
+ catch {
240
+ fs.rmSync(worktree, { recursive: true, force: true });
241
+ }
242
+ };
243
+ if (opts.sha) {
244
+ try {
245
+ execFileSync('git', ['worktree', 'add', '--detach', '--force', worktree, opts.sha], { cwd: opts.repoRoot, stdio: 'ignore' });
246
+ }
247
+ catch {
248
+ await cleanup();
249
+ return skip('the worktree checkout failed.');
250
+ }
251
+ const copied = copyEnvFiles(opts.repoRoot, worktree, opts.appPrefix);
252
+ if (copied.length)
253
+ log(`Using local env file(s) for the baseline: ${copied.join(', ')}`);
254
+ }
255
+ const app = servableDir(worktree, opts.appPrefix);
256
+ if (!app) {
257
+ await cleanup();
258
+ return skip('no package with a dev, serve or start script was found.');
259
+ }
260
+ const { dir: appDir, script } = app;
261
+ const { pm, declared } = resolvePackageManager(appDir, worktree, opts.pathHas);
262
+ const rel = path.relative(worktree, appDir);
263
+ // The worktree is a throwaway that cleanup deletes on the way out, so every
264
+ // "run it by hand" below names the same directory in the caller's own
265
+ // checkout instead — the one that will still be there to run it in.
266
+ const appIn = path.join(opts.repoRoot, rel);
267
+ const where = rel || 'the repository';
268
+ if (!pm) {
269
+ await cleanup();
270
+ return skip(`neither ${declared.bin} nor npm is on PATH, so nothing can install ${where}.`);
271
+ }
272
+ const install = !fs.existsSync(path.join(appDir, 'node_modules'));
273
+ // Substituting npm for the declared manager is safe in a throwaway worktree
274
+ // and not in the caller's own checkout: npm cannot share a node_modules with
275
+ // pnpm, so installing over their tree leaves the working copy broken — the
276
+ // state the README tells people to `rm -rf node_modules` out of. Taking
277
+ // screenshots must not cost someone their install, so this stops instead.
278
+ // Only the install is dangerous; running a script against a tree that is
279
+ // already installed is not, which is why this asks about both.
280
+ if (pm !== declared && install && worktree === opts.repoRoot) {
281
+ await cleanup();
282
+ return skip(`${declared.bin} is not on PATH, and installing ${where} with npm instead would leave a node_modules your ${declared.bin} cannot use. Install ${declared.bin}, or run \`${declared.bin} install\` in ${appIn}.`);
283
+ }
284
+ if (pm !== declared) {
285
+ log(`${declared.bin} is not on PATH; installing the baseline with ${pm.bin} instead (it will not honour the ${declared.bin} lockfile).`);
286
+ }
287
+ log(`Starting a dev server for ${what} (${pm.bin} ${script}) ...`);
288
+ if (install) {
289
+ try {
290
+ execFileSync(pm.bin, pm.install, { cwd: appDir, stdio: 'ignore', timeout: Math.max(1, deadline - Date.now()) });
291
+ }
292
+ catch {
293
+ await cleanup();
294
+ return skip(`\`${pm.bin} ${pm.install.join(' ')}\` failed. Run it in ${appIn} to see why.`);
295
+ }
296
+ }
297
+ const port = await freePort();
298
+ const url = `http://localhost:${port}`;
299
+ child = spawn(pm.bin, pm.run(script, ['--port', String(port)]), {
300
+ cwd: appDir,
301
+ stdio: 'ignore',
302
+ detached: true,
303
+ env: { ...process.env, PORT: String(port), BROWSER: 'none' },
304
+ });
305
+ child.unref();
306
+ const ready = await waitForServer(url, Math.max(1, deadline - Date.now()), () => !!child && child.exitCode === null);
307
+ if (!ready) {
308
+ await cleanup();
309
+ return skip(`${pm.bin} ${script} did not start serving within ${Math.round(timeoutMs / 1000)}s (missing env vars are the usual cause).`);
310
+ }
311
+ process.once('SIGINT', onSignal);
312
+ process.once('SIGTERM', onSignal);
313
+ process.once('exit', onSignal);
314
+ return { url, stop: cleanup };
315
+ }
316
+ //# sourceMappingURL=baseline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline.js","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"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"}