@juangadm/pre-post 1.3.0 → 1.4.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 +42 -288
- package/dist/baseline.d.ts +26 -1
- package/dist/baseline.d.ts.map +1 -1
- package/dist/baseline.js +92 -19
- package/dist/baseline.js.map +1 -1
- package/dist/bin/cli.js +20 -16
- package/dist/bin/cli.js.map +1 -1
- package/dist/browser.d.ts +31 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +119 -7
- package/dist/browser.js.map +1 -1
- package/dist/commands/compare.d.ts.map +1 -1
- package/dist/commands/compare.js +5 -5
- 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 +3 -0
- package/dist/commands/detect.js.map +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +7 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/pr.d.ts.map +1 -1
- package/dist/commands/pr.js +119 -97
- package/dist/commands/pr.js.map +1 -1
- package/dist/comparison.d.ts +5 -0
- package/dist/comparison.d.ts.map +1 -1
- package/dist/comparison.js +5 -5
- package/dist/comparison.js.map +1 -1
- package/dist/diff-worker.d.ts +1 -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 +42 -0
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +142 -24
- package/dist/diff.js.map +1 -1
- package/dist/doctor.d.ts +4 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +5 -1
- package/dist/doctor.js.map +1 -1
- package/dist/drift.d.ts +29 -0
- package/dist/drift.d.ts.map +1 -0
- package/dist/drift.js +106 -0
- package/dist/drift.js.map +1 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +6 -0
- package/dist/errors.js.map +1 -1
- package/dist/report.d.ts +20 -1
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +118 -39
- package/dist/report.js.map +1 -1
- package/dist/routes/imports.d.ts +21 -1
- package/dist/routes/imports.d.ts.map +1 -1
- package/dist/routes/imports.js +32 -2
- package/dist/routes/imports.js.map +1 -1
- package/dist/routes.d.ts +18 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +110 -29
- package/dist/routes.js.map +1 -1
- package/dist/run.d.ts +46 -12
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +138 -17
- package/dist/run.js.map +1 -1
- package/dist/types.d.ts +61 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
- package/skill/SKILL.md +18 -9
package/README.md
CHANGED
|
@@ -1,330 +1,84 @@
|
|
|
1
1
|
# pre-post
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
3
|
+
**See the change before you read the diff.**
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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.
|
|
33
|
-
|
|
34
|
-
> Originally forked from [before-and-after](https://github.com/vercel-labs/before-and-after) by James Clements / Vercel Labs.
|
|
5
|
+
pre-post puts before/after screenshots of every page your branch touched at the top of the
|
|
6
|
+
pull request. One command finds the changed routes, captures Pre and Post, highlights what
|
|
7
|
+
moved, and updates the PR description. No setup, no pasting images.
|
|
35
8
|
|
|
36
9
|
```
|
|
37
10
|
$ npx -y @juangadm/pre-post@latest pr
|
|
38
11
|
|
|
39
12
|
Routes (nextjs-app, 41ms): /, /pricing
|
|
40
|
-
|
|
41
|
-
/pricing medium src/app/pricing/page.tsx imports src/components/ui/button.tsx (2 hops)
|
|
42
|
-
Capturing 8 screenshots (2 route(s) × 2 viewport(s)) ...
|
|
43
|
-
changed /pricing @ mobile (1.42%, 1811ms)
|
|
13
|
+
Capturing 4 screenshots (2 route(s) × 1 viewport(s)) ...
|
|
44
14
|
changed /pricing @ desktop (0.64%, 2036ms)
|
|
45
|
-
same / @ mobile (0.00%, 1990ms)
|
|
46
15
|
same / @ desktop (0.00%, 2211ms)
|
|
47
|
-
Publishing 10 image(s) to acme/web@pre-post-assets ...
|
|
48
16
|
Updated PR description: https://github.com/acme/web/pull/42
|
|
49
|
-
pre-post · PR #42 · 2 route(s) · 2 viewport(s) · 6.8s
|
|
50
|
-
/ desktop no change
|
|
51
|
-
/ mobile no change
|
|
52
|
-
/pricing desktop 0.64% changed
|
|
53
|
-
/pricing mobile 1.4% changed
|
|
54
|
-
Comment: https://github.com/acme/web/pull/42
|
|
55
17
|
```
|
|
56
18
|
|
|
57
|
-
##
|
|
58
|
-
|
|
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
|
|
64
|
-
graph: a change to `components/ui/button.tsx` marks every page that imports it. Next.js
|
|
65
|
-
App Router and Pages Router, Vite apps (React Router, file-based `src/pages`), and a
|
|
66
|
-
generic fallback. Monorepos are handled by picking the app that owns the changed files.
|
|
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`).
|
|
73
|
-
All routes and viewports run concurrently.
|
|
74
|
-
3. **Diff.** Pure-JS pixel comparison in worker threads. Reports the percentage changed,
|
|
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
|
|
93
|
-
API, as one commit per run. Nothing is committed to the PR branch, no CI is triggered,
|
|
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
|
-
See [Screenshot storage](#screenshot-storage) for how fast the branch grows.
|
|
100
|
-
6. **Describe.** The images go in a delimited block at the top of the PR description,
|
|
101
|
-
replaced in place on every run and leaving your own text untouched. Changed routes show
|
|
102
|
-
a Pre/Post crop with the full page collapsed underneath; unchanged routes fold into a
|
|
103
|
-
single line. A pure layout shift — nothing changed once the move is undone — has no
|
|
104
|
-
region to crop, so it shows the full pages directly under the `Content shifted down 40px.
|
|
105
|
-
Nothing else changed.` line, with no collapsed section repeating them. If the PR cannot be edited — a fork, a read-only token — it falls back to
|
|
106
|
-
one sticky comment.
|
|
19
|
+
## Get started
|
|
107
20
|
|
|
108
|
-
|
|
21
|
+
Pick one. None of them installs anything permanently.
|
|
109
22
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
### Claude Code — easiest
|
|
23
|
+
**In Claude Code**: install the skill once, then say `/pre-post` after a UI change.
|
|
113
24
|
|
|
114
25
|
```bash
|
|
115
26
|
npx skills add juangadm/pre-post -y
|
|
116
27
|
```
|
|
117
28
|
|
|
118
|
-
|
|
119
|
-
knowledge needed.
|
|
120
|
-
|
|
121
|
-
### Terminal
|
|
29
|
+
**In a terminal**: run it on a branch with an open PR.
|
|
122
30
|
|
|
123
31
|
```bash
|
|
124
32
|
npx -y @juangadm/pre-post@latest pr
|
|
125
33
|
```
|
|
126
34
|
|
|
127
|
-
|
|
35
|
+
**On every PR, automatically**: add the [GitHub Action](https://github.com/juangadm/pre-post/blob/main/docs/github-action.md). No tokens or
|
|
36
|
+
secrets needed.
|
|
128
37
|
|
|
129
|
-
###
|
|
38
|
+
### You'll need
|
|
130
39
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
| **A GitHub token** | `gh auth login`, or set `GH_TOKEN`. Needs write access — it publishes images and edits the PR. |
|
|
135
|
-
| **An open PR** | Where the images go. Without one, pre-post prints the markdown to paste. |
|
|
40
|
+
- **Node 20+**
|
|
41
|
+
- **A GitHub token** with write access: `gh auth login`, or set `GH_TOKEN`
|
|
42
|
+
- **An open PR**: without one, pre-post prints the markdown for you to paste
|
|
136
43
|
|
|
137
|
-
|
|
44
|
+
The first run downloads a small browser (~80 MB). Stuck? Run
|
|
138
45
|
`npx -y @juangadm/pre-post@latest doctor`.
|
|
139
46
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
For a team, or for work done in cloud agent sessions: GitHub runs pre-post on each push to a
|
|
143
|
-
PR and posts with the job's own token. Nobody sets up anything per person, and there are no
|
|
144
|
-
secrets to add. Add `.github/workflows/pre-post.yml`:
|
|
145
|
-
|
|
146
|
-
```yaml
|
|
147
|
-
name: pre-post
|
|
148
|
-
on: pull_request
|
|
149
|
-
permissions: { contents: write, pull-requests: write }
|
|
150
|
-
concurrency: { group: 'pre-post-${{ github.event.pull_request.number }}', cancel-in-progress: true }
|
|
151
|
-
jobs:
|
|
152
|
-
pre-post:
|
|
153
|
-
if: github.event.pull_request.head.repo.full_name == github.repository
|
|
154
|
-
runs-on: ubuntu-latest
|
|
155
|
-
steps:
|
|
156
|
-
- uses: actions/checkout@v4
|
|
157
|
-
with: { ref: '${{ github.event.pull_request.head.sha }}', fetch-depth: 0 }
|
|
158
|
-
- uses: juangadm/pre-post@v1
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
The runner builds the base commit and the PR's commit and serves both, the same way pre-post
|
|
162
|
-
does on a laptop with nothing deployed. No preview deployment is involved, so Vercel's
|
|
163
|
-
Deployment Protection never gets in the way.
|
|
164
|
-
|
|
165
|
-
- **Your app must start with its dev script without secrets.** If it needs environment
|
|
166
|
-
variables, give them to the job with `env:`.
|
|
167
|
-
- **pnpm or yarn** comes from the `packageManager` field in package.json. Without one, set
|
|
168
|
-
it up before the pre-post step (e.g. `pnpm/action-setup`), as any other job would.
|
|
169
|
-
- **Fork PRs are skipped:** their token cannot write, so there is nowhere to post.
|
|
170
|
-
- **Flags** go in `with: { args: '--responsive' }`.
|
|
171
|
-
- **The run's images**, including `sheet.png`, are kept as a workflow artifact even when
|
|
172
|
-
posting fails.
|
|
173
|
-
- **Pinned:** `@v1` runs the pre-post version released with that tag, never whatever npm
|
|
174
|
-
calls `latest`.
|
|
175
|
-
|
|
176
|
-
## Usage
|
|
47
|
+
## Everyday commands
|
|
177
48
|
|
|
178
49
|
```bash
|
|
179
|
-
pre-post pr
|
|
180
|
-
pre-post pr --
|
|
181
|
-
pre-post pr
|
|
182
|
-
pre-post pr --
|
|
183
|
-
pre-post pr --
|
|
184
|
-
pre-post
|
|
185
|
-
pre-post
|
|
186
|
-
|
|
187
|
-
pre-post https://acme.com http://localhost:3000 --routes /pricing # ad-hoc comparison
|
|
188
|
-
pre-post before.png after.png # diff two images
|
|
189
|
-
pre-post detect # which routes does this branch touch?
|
|
190
|
-
pre-post login https://staging.acme.com # sign in once; the session is reused
|
|
191
|
-
pre-post prune --days 90 # clean up the assets branch
|
|
192
|
-
pre-post doctor # browser, token, dev server, config
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
When something needs a human, the CLI exits with code 3 and one sentence saying what to do
|
|
196
|
-
(log in, start the dev server, pass `--before`). Re-running picks up where it left off.
|
|
197
|
-
|
|
198
|
-
Exit codes, so a script or an agent can branch on them:
|
|
199
|
-
|
|
200
|
-
| code | meaning |
|
|
201
|
-
|---|---|
|
|
202
|
-
| 0 | done; for `doctor`, `pre-post pr` can run |
|
|
203
|
-
| 1 | the run failed — every capture errored, or an unexpected error; for `doctor`, a required check failed |
|
|
204
|
-
| 2 | the arguments could not be parsed |
|
|
205
|
-
| 3 | something needs a human; the message says what |
|
|
206
|
-
|
|
207
|
-
`doctor` marks a check **FAIL** only when `pre-post pr` has no way to proceed without it —
|
|
208
|
-
the browser, a GitHub token, and being inside a git repository. Everything else prints as
|
|
209
|
-
**note**: no dev server running, or no `--before` saved, narrows *which* strategy a run
|
|
210
|
-
picks rather than stopping it, so those never change the exit code.
|
|
211
|
-
|
|
212
|
-
Results go into a delimited block at the top of the PR description, which re-runs replace in
|
|
213
|
-
place, leaving your own text untouched. If the PR cannot be edited — a fork, a read-only
|
|
214
|
-
token — it falls back to a single sticky comment.
|
|
215
|
-
|
|
216
|
-
## Screenshot storage
|
|
217
|
-
|
|
218
|
-
Every real run adds one commit to the `pre-post-assets` branch holding that run's images —
|
|
219
|
-
roughly 100–200 KB for a small PR, and again on every re-run. As a rough guide, 20 PRs a
|
|
220
|
-
week at 3 runs each is about 9 MB a week, or ~450 MB a year.
|
|
221
|
-
|
|
222
|
-
Two things to know before that adds up:
|
|
223
|
-
|
|
224
|
-
- **`prune` tidies the branch; it does not shrink the repository.** It commits a deletion,
|
|
225
|
-
so the images stay in history and every link already posted in a PR keeps working. The
|
|
226
|
-
repository's size only ever grows.
|
|
227
|
-
- **A plain `git clone` downloads every branch**, screenshots included. Teammates who never
|
|
228
|
-
look at them can skip the branch in an existing clone (git 2.29+):
|
|
229
|
-
|
|
230
|
-
```bash
|
|
231
|
-
git config --add remote.origin.fetch '^refs/heads/pre-post-assets'
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
A weekly prune keeps the branch's own listing short. As a scheduled workflow:
|
|
235
|
-
|
|
236
|
-
```yaml
|
|
237
|
-
# .github/workflows/pre-post-prune.yml
|
|
238
|
-
on:
|
|
239
|
-
schedule: [{ cron: '17 6 * * 1' }] # Mondays
|
|
240
|
-
workflow_dispatch:
|
|
241
|
-
permissions: { contents: write, pull-requests: read }
|
|
242
|
-
jobs:
|
|
243
|
-
prune:
|
|
244
|
-
runs-on: ubuntu-latest
|
|
245
|
-
steps:
|
|
246
|
-
- uses: actions/checkout@v4
|
|
247
|
-
- run: npx -y @juangadm/pre-post@latest prune --days 90
|
|
248
|
-
env: { GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' }
|
|
50
|
+
pre-post pr # screenshot this branch's PR
|
|
51
|
+
pre-post pr --routes /pricing,/docs # only these pages
|
|
52
|
+
pre-post pr -r # desktop and mobile
|
|
53
|
+
pre-post pr --before https://acme.com # set the "before" site (remembered)
|
|
54
|
+
pre-post pr --dry-run # try it without posting
|
|
55
|
+
pre-post login https://staging.acme.com # sign in once for protected sites
|
|
56
|
+
pre-post doctor # check your setup
|
|
249
57
|
```
|
|
250
58
|
|
|
251
|
-
|
|
252
|
-
opened once they are that old. Actually reclaiming the space means rewriting the branch's
|
|
253
|
-
history, which also breaks the images in those older PRs — so it is left as your call.
|
|
254
|
-
|
|
255
|
-
## Configuration
|
|
256
|
-
|
|
257
|
-
Optional `.pre-post.json` in the repo root:
|
|
59
|
+
If pre-post needs something from you, it stops with one sentence saying exactly what.
|
|
258
60
|
|
|
259
|
-
|
|
260
|
-
{
|
|
261
|
-
"before": "https://acme.com",
|
|
262
|
-
"routes": ["/"],
|
|
263
|
-
"samples": { "/blog/[slug]": "/blog/hello-world" },
|
|
264
|
-
"viewports": ["desktop"],
|
|
265
|
-
"fullPage": true,
|
|
266
|
-
"maxHeight": 2400,
|
|
267
|
-
"scale": 2,
|
|
268
|
-
"threshold": 0.001,
|
|
269
|
-
"minChangedArea": 100,
|
|
270
|
-
"maxRoutes": 6,
|
|
271
|
-
"ignore": ["apps/docs"],
|
|
272
|
-
"headers": {},
|
|
273
|
-
"assetsBranch": "pre-post-assets",
|
|
274
|
-
"baselineSetup": "pnpm run build:packages"
|
|
275
|
-
}
|
|
276
|
-
```
|
|
277
|
-
|
|
278
|
-
`baselineSetup` runs in the app directory between the install and the dev server when the
|
|
279
|
-
baseline is built from source. Use it for whatever the app needs before it can boot — a
|
|
280
|
-
workspace build, a codegen step. In a turborepo it is inferred: `turbo run build
|
|
281
|
-
--filter=<app>^...`, which builds the packages the app imports and leaves the app to the
|
|
282
|
-
dev server. Setting it turns the guess off.
|
|
61
|
+
## How it decides what "before" is
|
|
283
62
|
|
|
284
|
-
|
|
285
|
-
|
|
63
|
+
Post (your branch) is the PR's preview deployment, or a local dev server. Pre is the
|
|
64
|
+
production site your branch forked from. When nothing is deployed, pre-post builds the base
|
|
65
|
+
commit itself, so it works in CI and in sandboxes too. It never guesses: if it can't find a
|
|
66
|
+
trustworthy baseline, it tells you which flag to pass.
|
|
286
67
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
the old one. Without this an auth-gated app rejects its own callbacks and every capture is a
|
|
290
|
-
loading skeleton. The names rewritten are the ones that can only mean the app's own address
|
|
291
|
-
— `BETTER_AUTH_URL`, `NEXTAUTH_URL`, `AUTH_URL`, `APP_URL`, `SITE_URL`, `CANONICAL_URL` and
|
|
292
|
-
their `_ORIGIN`/`_BASE_URL` forms, with a `NEXT_PUBLIC_` / `VITE_` / `PUBLIC_` prefix
|
|
293
|
-
optional. Generic names are left alone: `BASE_URL`, `SERVER_URL` and `PUBLIC_URL` are as
|
|
294
|
-
often a backend on another port or a path prefix, and `DATABASE_URL` is never an origin.
|
|
68
|
+
Screenshots are stored on a `pre-post-assets` branch in your own repo, so only people who can
|
|
69
|
+
see the repo can see them. Nothing is committed to your PR branch.
|
|
295
70
|
|
|
296
|
-
|
|
71
|
+
## Learn more
|
|
297
72
|
|
|
298
|
-
|
|
|
73
|
+
| | |
|
|
299
74
|
|---|---|
|
|
300
|
-
|
|
|
301
|
-
|
|
|
302
|
-
|
|
|
303
|
-
|
|
|
304
|
-
|
|
|
305
|
-
| `GH_REPO` | `owner/repo` when the remote URL cannot be parsed |
|
|
306
|
-
|
|
307
|
-
## Library
|
|
308
|
-
|
|
309
|
-
```ts
|
|
310
|
-
import { runPr, captureScreenshot, diffImages, detectRoutesForRepo } from '@juangadm/pre-post';
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
## Development
|
|
314
|
-
|
|
315
|
-
```bash
|
|
316
|
-
pnpm install
|
|
317
|
-
pnpm build
|
|
318
|
-
pnpm test:unit
|
|
319
|
-
TEST_BROWSER=true pnpm test # needs a Chromium; npx playwright-core install chromium-headless-shell
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
pnpm and npm cannot share a `node_modules`. pnpm builds a symlinked tree that npm
|
|
323
|
-
cannot read, so `npm install` over it fails with `Cannot read properties of null
|
|
324
|
-
(reading 'edgesOut')`, which names none of that. Run `rm -rf node_modules
|
|
325
|
-
site/node_modules` before switching either way. npm also installs the CLI only —
|
|
326
|
-
`site/` is a pnpm workspace member, so use pnpm to work on the site.
|
|
75
|
+
| [How it works](https://github.com/juangadm/pre-post/blob/main/docs/how-it-works.md) | How Pre and Post are chosen, capture, diffing, layout shifts |
|
|
76
|
+
| [GitHub Action](https://github.com/juangadm/pre-post/blob/main/docs/github-action.md) | Run on every PR with no per-person setup |
|
|
77
|
+
| [Reference](https://github.com/juangadm/pre-post/blob/main/docs/reference.md) | All commands, exit codes, `.pre-post.json`, environment variables |
|
|
78
|
+
| [Screenshot storage](https://github.com/juangadm/pre-post/blob/main/docs/storage.md) | How much space it uses and how to prune it |
|
|
79
|
+
| [Contributing](https://github.com/juangadm/pre-post/blob/main/CONTRIBUTING.md) | Build and test locally |
|
|
327
80
|
|
|
328
|
-
##
|
|
81
|
+
## Credits
|
|
329
82
|
|
|
330
|
-
|
|
83
|
+
Forked from [before-and-after](https://github.com/vercel-labs/before-and-after) by James
|
|
84
|
+
Clements / Vercel Labs. MIT licensed.
|
package/dist/baseline.d.ts
CHANGED
|
@@ -11,13 +11,24 @@
|
|
|
11
11
|
* The cost is time: a checkout, an install and a dev-server boot. So callers
|
|
12
12
|
* reach for it after a reachable deployment, not before one.
|
|
13
13
|
*/
|
|
14
|
+
import { ChildProcess } from 'child_process';
|
|
14
15
|
import { NeedsHumanError } from './errors.js';
|
|
15
16
|
import { Stopwatch } from './timings.js';
|
|
16
17
|
export interface LocalBaseline {
|
|
17
18
|
url: string;
|
|
18
19
|
/** Stop the dev server and delete the worktree. Safe to call twice. */
|
|
19
20
|
stop: () => Promise<void>;
|
|
21
|
+
/** Everything the dev server printed, so a failure can be explained with its own words. */
|
|
22
|
+
logFile?: string;
|
|
20
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Where a dev server's output goes: a file per server, never the terminal.
|
|
26
|
+
*
|
|
27
|
+
* It used to go nowhere (`stdio: 'ignore'`), so when a server answered 500
|
|
28
|
+
* there was no way to say why. A file rather than a pipe: the server is
|
|
29
|
+
* detached and must not stall when this process stops reading.
|
|
30
|
+
*/
|
|
31
|
+
export declare function serverLogFile(side: 'pre' | 'post', port: number): string;
|
|
21
32
|
interface PackageManager {
|
|
22
33
|
bin: string;
|
|
23
34
|
install: string[];
|
|
@@ -172,7 +183,7 @@ export declare function installDeps(pm: PackageManager, cwd: string, timeoutMs:
|
|
|
172
183
|
/**
|
|
173
184
|
* The install failed and nothing else can rescue this run.
|
|
174
185
|
*
|
|
175
|
-
* Its own error rather than a quiet null, for the reason docs/portability.md
|
|
186
|
+
* Its own error rather than a quiet null, for the reason docs/internal/portability.md
|
|
176
187
|
* §1 gives about base resolution: a run that compared nothing must not exit
|
|
177
188
|
* clean. A null here fell through to "no baseline", and on a repository with a
|
|
178
189
|
* configured production URL it fell through to comparing against that instead
|
|
@@ -257,6 +268,20 @@ export declare function reusableInstall(repoRoot: string, sha: string): Promise<
|
|
|
257
268
|
* removed so the normal install starts from a clean tree.
|
|
258
269
|
*/
|
|
259
270
|
export declare function reuseInstall(repoRoot: string, worktree: string, dirs: string[]): Promise<boolean>;
|
|
271
|
+
/** How long a dev server gets to exit on SIGTERM before it is killed. */
|
|
272
|
+
export declare const SERVER_STOP_TIMEOUT_MS = 5000;
|
|
273
|
+
/**
|
|
274
|
+
* SIGTERM the server's process group, wait for it to exit, then make sure.
|
|
275
|
+
*
|
|
276
|
+
* The wait is on the child itself — the package manager, which exits once the
|
|
277
|
+
* server under it has — because Node reaps it, so its exit is a fact. Asking
|
|
278
|
+
* the group with signal 0 was not: in a container whose PID 1 does not reap,
|
|
279
|
+
* dead members linger as zombies that still answer, and every stop waited out
|
|
280
|
+
* the full timeout. Once the child is gone, or `timeoutMs` has passed, the
|
|
281
|
+
* whole group is SIGKILLed, so nothing it started is left writing into a
|
|
282
|
+
* worktree about to be removed.
|
|
283
|
+
*/
|
|
284
|
+
export declare function stopGroup(child: ChildProcess, timeoutMs?: number): Promise<void>;
|
|
260
285
|
export interface BaselineOptions {
|
|
261
286
|
repoRoot: string;
|
|
262
287
|
/** Commit to serve. Omit to serve the working tree as it stands. */
|
package/dist/baseline.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"baseline.d.ts","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;
|
|
1
|
+
{"version":3,"file":"baseline.d.ts","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"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"}
|
package/dist/baseline.js
CHANGED
|
@@ -19,6 +19,18 @@ import path from 'path';
|
|
|
19
19
|
import { NeedsHumanError } from './errors.js';
|
|
20
20
|
import { devScript, readPackage } from './pkg.js';
|
|
21
21
|
import { findAppRoots } from './routes.js';
|
|
22
|
+
/**
|
|
23
|
+
* Where a dev server's output goes: a file per server, never the terminal.
|
|
24
|
+
*
|
|
25
|
+
* It used to go nowhere (`stdio: 'ignore'`), so when a server answered 500
|
|
26
|
+
* there was no way to say why. A file rather than a pipe: the server is
|
|
27
|
+
* detached and must not stall when this process stops reading.
|
|
28
|
+
*/
|
|
29
|
+
export function serverLogFile(side, port) {
|
|
30
|
+
const dir = path.join(os.tmpdir(), 'pre-post', 'servers');
|
|
31
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
32
|
+
return path.join(dir, `${side}-${port}-${Date.now()}.log`);
|
|
33
|
+
}
|
|
22
34
|
const NPM = { bin: 'npm', install: ['install'], retry: { argv: ['install', '--legacy-peer-deps'], when: isPeerConflict }, run: (s, a) => ['run', s, '--', ...a] };
|
|
23
35
|
const PNPM = { bin: 'pnpm', install: ['install', '--prefer-offline'], run: (s, a) => ['run', s, ...a] };
|
|
24
36
|
const YARN = { bin: 'yarn', install: ['install'], run: (s, a) => ['run', s, ...a] };
|
|
@@ -317,7 +329,7 @@ export async function installDeps(pm, cwd, timeoutMs, run = runInstall, signal)
|
|
|
317
329
|
/**
|
|
318
330
|
* The install failed and nothing else can rescue this run.
|
|
319
331
|
*
|
|
320
|
-
* Its own error rather than a quiet null, for the reason docs/portability.md
|
|
332
|
+
* Its own error rather than a quiet null, for the reason docs/internal/portability.md
|
|
321
333
|
* §1 gives about base resolution: a run that compared nothing must not exit
|
|
322
334
|
* clean. A null here fell through to "no baseline", and on a repository with a
|
|
323
335
|
* configured production URL it fell through to comparing against that instead
|
|
@@ -542,6 +554,44 @@ export async function reuseInstall(repoRoot, worktree, dirs) {
|
|
|
542
554
|
await Promise.all(targets.map(t => fs.promises.rm(t.to, { recursive: true, force: true })));
|
|
543
555
|
return false;
|
|
544
556
|
}
|
|
557
|
+
/** Signal a detached child's whole process group; false when it is already gone. */
|
|
558
|
+
function signalGroup(child, signal) {
|
|
559
|
+
if (!child.pid)
|
|
560
|
+
return false;
|
|
561
|
+
try {
|
|
562
|
+
process.kill(-child.pid, signal);
|
|
563
|
+
return true;
|
|
564
|
+
}
|
|
565
|
+
catch {
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
/** How long a dev server gets to exit on SIGTERM before it is killed. */
|
|
570
|
+
export const SERVER_STOP_TIMEOUT_MS = 5_000;
|
|
571
|
+
/**
|
|
572
|
+
* SIGTERM the server's process group, wait for it to exit, then make sure.
|
|
573
|
+
*
|
|
574
|
+
* The wait is on the child itself — the package manager, which exits once the
|
|
575
|
+
* server under it has — because Node reaps it, so its exit is a fact. Asking
|
|
576
|
+
* the group with signal 0 was not: in a container whose PID 1 does not reap,
|
|
577
|
+
* dead members linger as zombies that still answer, and every stop waited out
|
|
578
|
+
* the full timeout. Once the child is gone, or `timeoutMs` has passed, the
|
|
579
|
+
* whole group is SIGKILLed, so nothing it started is left writing into a
|
|
580
|
+
* worktree about to be removed.
|
|
581
|
+
*/
|
|
582
|
+
export async function stopGroup(child, timeoutMs = SERVER_STOP_TIMEOUT_MS) {
|
|
583
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
584
|
+
signalGroup(child, 'SIGKILL');
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
const exited = new Promise(resolve => child.once('exit', () => resolve()));
|
|
588
|
+
if (!signalGroup(child, 'SIGTERM'))
|
|
589
|
+
return;
|
|
590
|
+
let timer;
|
|
591
|
+
await Promise.race([exited, new Promise(resolve => { timer = setTimeout(resolve, timeoutMs); })]);
|
|
592
|
+
clearTimeout(timer);
|
|
593
|
+
signalGroup(child, 'SIGKILL');
|
|
594
|
+
}
|
|
545
595
|
async function waitForServer(url, timeoutMs, alive) {
|
|
546
596
|
const deadline = Date.now() + timeoutMs;
|
|
547
597
|
while (Date.now() < deadline) {
|
|
@@ -602,28 +652,28 @@ async function serveLocally(opts) {
|
|
|
602
652
|
let child = null;
|
|
603
653
|
let stopped = false;
|
|
604
654
|
// A detached dev server survives Ctrl-C and keeps its port, so tear it down
|
|
605
|
-
// on the signals that end the run as well as on the normal path.
|
|
606
|
-
|
|
655
|
+
// on the signals that end the run as well as on the normal path. Those
|
|
656
|
+
// paths cannot wait for anything — an 'exit' handler runs no more event
|
|
657
|
+
// loop — so they get the synchronous teardown.
|
|
658
|
+
const onSignal = () => { cleanupNow(); };
|
|
607
659
|
const untrap = () => {
|
|
608
660
|
process.off('SIGINT', onSignal);
|
|
609
661
|
process.off('SIGTERM', onSignal);
|
|
610
662
|
process.off('exit', onSignal);
|
|
611
663
|
};
|
|
612
|
-
|
|
664
|
+
// Guard on the thing that actually matters: only a throwaway worktree may be
|
|
665
|
+
// removed. Checking `opts.sha` instead would let any future caller that
|
|
666
|
+
// points `worktree` at a real checkout delete uncommitted work.
|
|
667
|
+
const throwaway = worktree !== opts.repoRoot;
|
|
668
|
+
/** The process is gone, a best-effort worktree removal, no waiting. */
|
|
669
|
+
const cleanupNow = () => {
|
|
613
670
|
if (stopped)
|
|
614
671
|
return;
|
|
615
672
|
stopped = true;
|
|
616
673
|
untrap();
|
|
617
|
-
if (child
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
}
|
|
621
|
-
catch { /* already gone */ }
|
|
622
|
-
}
|
|
623
|
-
// Guard on the thing that actually matters: only a throwaway worktree may be
|
|
624
|
-
// removed. Checking `opts.sha` instead would let any future caller that
|
|
625
|
-
// points `worktree` at a real checkout delete uncommitted work.
|
|
626
|
-
if (worktree === opts.repoRoot)
|
|
674
|
+
if (child)
|
|
675
|
+
signalGroup(child, 'SIGKILL');
|
|
676
|
+
if (!throwaway)
|
|
627
677
|
return;
|
|
628
678
|
try {
|
|
629
679
|
execFileSync('git', ['worktree', 'remove', '--force', worktree], { cwd: opts.repoRoot, stdio: 'ignore' });
|
|
@@ -632,6 +682,26 @@ async function serveLocally(opts) {
|
|
|
632
682
|
fs.rmSync(worktree, { recursive: true, force: true });
|
|
633
683
|
}
|
|
634
684
|
};
|
|
685
|
+
/**
|
|
686
|
+
* Stop the server, and only once it has exited, remove its worktree.
|
|
687
|
+
*
|
|
688
|
+
* The order is the point. Removing the tree under a dev server that is
|
|
689
|
+
* still shutting down races its last writes into `.next`, and the removal
|
|
690
|
+
* blocked the event loop while it walked the copied node_modules.
|
|
691
|
+
*/
|
|
692
|
+
const cleanup = async () => {
|
|
693
|
+
if (stopped)
|
|
694
|
+
return;
|
|
695
|
+
stopped = true;
|
|
696
|
+
untrap();
|
|
697
|
+
if (child)
|
|
698
|
+
await stopGroup(child);
|
|
699
|
+
if (!throwaway)
|
|
700
|
+
return;
|
|
701
|
+
if (await gitAsync(['worktree', 'remove', '--force', worktree], opts.repoRoot) === null) {
|
|
702
|
+
await fs.promises.rm(worktree, { recursive: true, force: true });
|
|
703
|
+
}
|
|
704
|
+
};
|
|
635
705
|
if (opts.sha) {
|
|
636
706
|
// Asynchronous: in a large repository a checkout takes seconds, and the
|
|
637
707
|
// other side is booting meanwhile.
|
|
@@ -746,7 +816,7 @@ async function serveLocally(opts) {
|
|
|
746
816
|
log(attempt.output);
|
|
747
817
|
await cleanup();
|
|
748
818
|
// A command the repository asked for is the install's case exactly
|
|
749
|
-
// (docs/portability.md §1): yielding here lets the run fall through to a
|
|
819
|
+
// (docs/internal/portability.md §1): yielding here lets the run fall through to a
|
|
750
820
|
// configured production URL and publish a different comparison as if it
|
|
751
821
|
// were this one, or die further along on an error about dev servers that
|
|
752
822
|
// names nothing the reader can fix. Only the inferred build yields —
|
|
@@ -756,12 +826,15 @@ async function serveLocally(opts) {
|
|
|
756
826
|
return skip(`\`${setup.label}\` failed.`);
|
|
757
827
|
}
|
|
758
828
|
}
|
|
829
|
+
const logFile = serverLogFile(side, port);
|
|
830
|
+
const out = fs.openSync(logFile, 'a');
|
|
759
831
|
child = spawn(pm.bin, pm.run(script, ['--port', String(port)]), {
|
|
760
832
|
cwd: appDir,
|
|
761
|
-
stdio: 'ignore',
|
|
833
|
+
stdio: ['ignore', out, out],
|
|
762
834
|
detached: true,
|
|
763
835
|
env: { ...process.env, PORT: String(port), BROWSER: 'none' },
|
|
764
836
|
});
|
|
837
|
+
fs.closeSync(out);
|
|
765
838
|
child.unref();
|
|
766
839
|
const ready = await timed('boot', waitForServer(url, Math.max(1, deadline - Date.now()), () => !!child && child.exitCode === null && !opts.signal?.aborted));
|
|
767
840
|
if (await aborted())
|
|
@@ -773,13 +846,13 @@ async function serveLocally(opts) {
|
|
|
773
846
|
const exitCode = child?.exitCode ?? null;
|
|
774
847
|
await cleanup();
|
|
775
848
|
if (exitCode !== null) {
|
|
776
|
-
return skip(`${pm.bin} ${script} exited with code ${exitCode} before serving;
|
|
849
|
+
return skip(`${pm.bin} ${script} exited with code ${exitCode} before serving; its output is in ${logFile}.`);
|
|
777
850
|
}
|
|
778
|
-
return skip(`${pm.bin} ${script} did not start serving within ${Math.round(timeoutMs / 1000)}s (missing env vars are the usual cause).`);
|
|
851
|
+
return skip(`${pm.bin} ${script} did not start serving within ${Math.round(timeoutMs / 1000)}s (missing env vars are the usual cause); its output is in ${logFile}.`);
|
|
779
852
|
}
|
|
780
853
|
process.once('SIGINT', onSignal);
|
|
781
854
|
process.once('SIGTERM', onSignal);
|
|
782
855
|
process.once('exit', onSignal);
|
|
783
|
-
return { url, stop: cleanup };
|
|
856
|
+
return { url, stop: cleanup, logFile };
|
|
784
857
|
}
|
|
785
858
|
//# sourceMappingURL=baseline.js.map
|