@juangadm/pre-post 0.3.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +174 -233
- package/dist/baseline.d.ts +119 -0
- package/dist/baseline.d.ts.map +1 -0
- package/dist/baseline.js +316 -0
- package/dist/baseline.js.map +1 -0
- package/dist/bin/cli.js +243 -458
- package/dist/bin/cli.js.map +1 -1
- package/dist/browser.d.ts +68 -25
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +500 -105
- package/dist/browser.js.map +1 -1
- package/dist/capture.d.ts +1 -6
- package/dist/capture.d.ts.map +1 -1
- package/dist/capture.js +12 -36
- package/dist/capture.js.map +1 -1
- package/dist/commands/compare.d.ts +18 -0
- package/dist/commands/compare.d.ts.map +1 -0
- package/dist/commands/compare.js +82 -0
- package/dist/commands/compare.js.map +1 -0
- package/dist/commands/detect.d.ts +25 -0
- package/dist/commands/detect.d.ts.map +1 -0
- package/dist/commands/detect.js +22 -0
- package/dist/commands/detect.js.map +1 -0
- package/dist/commands/doctor.d.ts +42 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +119 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/login.d.ts +6 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +34 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/pr.d.ts +32 -0
- package/dist/commands/pr.d.ts.map +1 -0
- package/dist/commands/pr.js +259 -0
- package/dist/commands/pr.js.map +1 -0
- package/dist/commands/prune.d.ts +10 -0
- package/dist/commands/prune.d.ts.map +1 -0
- package/dist/commands/prune.js +13 -0
- package/dist/commands/prune.js.map +1 -0
- package/dist/comparison.d.ts +104 -0
- package/dist/comparison.d.ts.map +1 -0
- package/dist/comparison.js +236 -0
- package/dist/comparison.js.map +1 -0
- package/dist/config.d.ts +46 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +82 -0
- package/dist/config.js.map +1 -0
- package/dist/deployments.d.ts +71 -0
- package/dist/deployments.d.ts.map +1 -0
- package/dist/deployments.js +204 -0
- package/dist/deployments.js.map +1 -0
- package/dist/diff-pool.d.ts +17 -0
- package/dist/diff-pool.d.ts.map +1 -0
- package/dist/diff-pool.js +92 -0
- package/dist/diff-pool.js.map +1 -0
- package/dist/diff-worker.d.ts +32 -0
- package/dist/diff-worker.d.ts.map +1 -0
- package/dist/diff-worker.js +39 -0
- package/dist/diff-worker.js.map +1 -0
- package/dist/diff.d.ts +47 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +281 -0
- package/dist/diff.js.map +1 -0
- package/dist/doctor.d.ts +79 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +138 -0
- package/dist/doctor.js.map +1 -0
- package/dist/errors.d.ts +38 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +59 -0
- package/dist/errors.js.map +1 -0
- package/dist/git.d.ts +61 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +182 -0
- package/dist/git.js.map +1 -0
- package/dist/github.d.ts +163 -0
- package/dist/github.d.ts.map +1 -0
- package/dist/github.js +411 -0
- package/dist/github.js.map +1 -0
- package/dist/index.d.ts +22 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -67
- package/dist/index.js.map +1 -1
- package/dist/landing.d.ts +48 -0
- package/dist/landing.d.ts.map +1 -0
- package/dist/landing.js +93 -0
- package/dist/landing.js.map +1 -0
- package/dist/pkg.d.ts +17 -0
- package/dist/pkg.d.ts.map +1 -0
- package/dist/pkg.js +49 -0
- package/dist/pkg.js.map +1 -0
- package/dist/report.d.ts +20 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +130 -0
- package/dist/report.js.map +1 -0
- package/dist/routes/imports.d.ts +46 -0
- package/dist/routes/imports.d.ts.map +1 -0
- package/dist/routes/imports.js +208 -0
- package/dist/routes/imports.js.map +1 -0
- package/dist/routes/vite.d.ts +15 -0
- package/dist/routes/vite.d.ts.map +1 -0
- package/dist/routes/vite.js +130 -0
- package/dist/routes/vite.js.map +1 -0
- package/dist/routes.d.ts +65 -11
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +346 -141
- package/dist/routes.js.map +1 -1
- package/dist/run.d.ts +116 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.js +316 -0
- package/dist/run.js.map +1 -0
- package/dist/sameness.d.ts +91 -0
- package/dist/sameness.d.ts.map +1 -0
- package/dist/sameness.js +157 -0
- package/dist/sameness.js.map +1 -0
- package/dist/sessions.d.ts +50 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +80 -0
- package/dist/sessions.js.map +1 -0
- package/dist/shift.d.ts +67 -0
- package/dist/shift.d.ts.map +1 -0
- package/dist/shift.js +184 -0
- package/dist/shift.js.map +1 -0
- package/dist/types.d.ts +179 -68
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -1
- package/dist/url.d.ts +12 -0
- package/dist/url.d.ts.map +1 -0
- package/dist/url.js +30 -0
- package/dist/url.js.map +1 -0
- package/dist/viewport.d.ts +6 -1
- package/dist/viewport.d.ts.map +1 -1
- package/dist/viewport.js +13 -7
- package/dist/viewport.js.map +1 -1
- package/package.json +21 -14
- package/skill/SKILL.md +56 -221
- package/dist/clipboard.d.ts +0 -9
- package/dist/clipboard.d.ts.map +0 -1
- package/dist/clipboard.js +0 -26
- package/dist/clipboard.js.map +0 -1
- package/dist/upload.d.ts +0 -39
- package/dist/upload.d.ts.map +0 -1
- package/dist/upload.js +0 -159
- package/dist/upload.js.map +0 -1
- package/dist/video.d.ts +0 -19
- package/dist/video.d.ts.map +0 -1
- package/dist/video.js +0 -263
- package/dist/video.js.map +0 -1
- package/skill/scripts/adapters/0x0st.sh +0 -36
- package/skill/scripts/adapters/blob.sh +0 -55
- package/skill/scripts/adapters/git-native.sh +0 -51
- package/skill/scripts/upload-and-copy.sh +0 -230
package/README.md
CHANGED
|
@@ -1,272 +1,213 @@
|
|
|
1
1
|
# pre-post
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## What Changed from the Original
|
|
8
|
-
|
|
9
|
-
| Area | before-and-after | pre-post |
|
|
10
|
-
|------|-----------------|----------|
|
|
11
|
-
| Browser engine | `agent-browser` (Vercel-proprietary) | **Playwright** (direct dependency) |
|
|
12
|
-
| Screenshot quality | 1x | **2x retina** (`deviceScaleFactor: 2`) |
|
|
13
|
-
| Route detection | Manual | **Automatic** from `git diff` (Next.js App/Pages Router, generic fallback) |
|
|
14
|
-
| Responsive capture | Single viewport | **Desktop + mobile** per route (`--responsive`) |
|
|
15
|
-
| CLI subcommands | URL pairs only | `detect`, `compare`, `run` subcommands |
|
|
16
|
-
| Skill orchestration | Basic capture | Full workflow: route detection, Claude refinement, user approval, PR posting |
|
|
17
|
-
| Font/animation handling | None | Waits for `document.fonts.ready`, disables CSS animations |
|
|
18
|
-
|
|
19
|
-
## How It Works
|
|
20
|
-
|
|
21
|
-
```mermaid
|
|
22
|
-
flowchart TD
|
|
23
|
-
A[pre-post] --> B{Mode?}
|
|
24
|
-
|
|
25
|
-
B -->|Manual| C["pre-post url1 url2"]
|
|
26
|
-
B -->|Subcommand| D{"detect | compare | run"}
|
|
27
|
-
B -->|Claude Code Skill| E["/pre-post"]
|
|
28
|
-
|
|
29
|
-
D -->|detect| F["Git: diff, staged, unstaged, untracked"]
|
|
30
|
-
D -->|compare| J
|
|
31
|
-
D -->|run| F
|
|
32
|
-
|
|
33
|
-
F --> G{Detect framework}
|
|
34
|
-
G -->|Next.js App Router| H[Map files → routes]
|
|
35
|
-
G -->|Next.js Pages Router| H
|
|
36
|
-
G -->|Generic| H
|
|
37
|
-
H --> I{Routes found?}
|
|
38
|
-
I -->|Yes| J[Route list with confidence]
|
|
39
|
-
I -->|No| J2["Default to /"]
|
|
40
|
-
J2 --> J
|
|
41
|
-
|
|
42
|
-
E --> E1[Pre-flight checks]
|
|
43
|
-
E1 --> F
|
|
44
|
-
E1 --> K[Claude refines routes]
|
|
45
|
-
K --> K1[User approves routes]
|
|
46
|
-
K1 --> J
|
|
47
|
-
|
|
48
|
-
C --> L[Playwright]
|
|
49
|
-
J --> L
|
|
50
|
-
|
|
51
|
-
L --> M{Launch Chromium}
|
|
52
|
-
M -->|"PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH"| M1[Custom path — fail hard if invalid]
|
|
53
|
-
M -->|Auto-detect| M2[System Chrome → Bundled → Cached]
|
|
54
|
-
M1 --> P[Navigate + wait for network idle]
|
|
55
|
-
M2 --> P
|
|
56
|
-
|
|
57
|
-
P --> Q[Disable CSS animations]
|
|
58
|
-
Q --> R[Wait for document.fonts.ready]
|
|
59
|
-
R --> S[Take 2x retina screenshot]
|
|
60
|
-
|
|
61
|
-
S --> T[Save to ~/Downloads]
|
|
62
|
-
T --> T1{--markdown?}
|
|
63
|
-
T1 -->|No| U[Done]
|
|
64
|
-
T1 -->|Yes| V{Upload method}
|
|
65
|
-
V -->|Default| W["git-native: commit to .pre-post/"]
|
|
66
|
-
V -->|--upload-url| Y["HTTP: 0x0.st / Vercel Blob / PUT"]
|
|
67
|
-
W --> X[Build blob+SHA URL]
|
|
68
|
-
Y --> X
|
|
69
|
-
X --> Z[Markdown table]
|
|
70
|
-
|
|
71
|
-
Z --> Z1{Skill mode?}
|
|
72
|
-
Z1 -->|Yes| Z2[User approves screenshots]
|
|
73
|
-
Z2 --> Z3["gh pr edit — append to PR body"]
|
|
74
|
-
Z1 -->|No| Z4[Copy to clipboard]
|
|
75
|
-
|
|
76
|
-
style A fill:#4f46e5,color:#fff
|
|
77
|
-
style L fill:#2563eb,color:#fff
|
|
78
|
-
style S fill:#059669,color:#fff
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
## Prerequisites
|
|
82
|
-
|
|
83
|
-
- **Node.js** 18+
|
|
84
|
-
- **Chromium** — install once via Playwright:
|
|
85
|
-
|
|
86
|
-
```bash
|
|
87
|
-
npx playwright install chromium
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
## CI / Sandboxed Environments
|
|
91
|
-
|
|
92
|
-
pre-post needs a Chromium binary. In restricted environments where `npx playwright install chromium` fails (CDN blocked):
|
|
93
|
-
|
|
94
|
-
```bash
|
|
95
|
-
# Point to a pre-installed Chrome/Chromium binary
|
|
96
|
-
export PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/path/to/chrome
|
|
97
|
-
|
|
98
|
-
# This is treated as an explicit override — pre-post will fail with a
|
|
99
|
-
# clear error if the path is wrong, rather than silently falling back.
|
|
100
|
-
```
|
|
3
|
+
Before/after screenshots for pull requests. One command detects the routes your branch
|
|
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.
|
|
101
6
|
|
|
102
|
-
|
|
7
|
+
It works out both sides itself, cheapest option first:
|
|
103
8
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
pre-post
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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.
|
|
33
|
+
|
|
34
|
+
> Originally forked from [before-and-after](https://github.com/vercel-labs/before-and-after) by James Clements / Vercel Labs.
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
$ npx -y @juangadm/pre-post@latest pr
|
|
38
|
+
|
|
39
|
+
Routes (nextjs-app, 41ms): /, /pricing
|
|
40
|
+
/ medium src/app/page.tsx imports src/components/ui/button.tsx (2 hops)
|
|
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)
|
|
44
|
+
changed /pricing @ desktop (0.64%, 2036ms)
|
|
45
|
+
same / @ mobile (0.00%, 1990ms)
|
|
46
|
+
same / @ desktop (0.00%, 2211ms)
|
|
47
|
+
Publishing 10 image(s) to acme/web@pre-post-assets ...
|
|
48
|
+
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
|
+
```
|
|
56
|
+
|
|
57
|
+
## How it works
|
|
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
|
+
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.
|
|
119
104
|
|
|
120
105
|
## Install
|
|
121
106
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
## Basic Use
|
|
127
|
-
|
|
128
|
-
### As a Claude Code Skill (recommended)
|
|
129
|
-
|
|
130
|
-
After making visual UI changes, say `/pre-post` or "take pre and post screenshots". Claude will:
|
|
131
|
-
|
|
132
|
-
1. Detect affected routes from your git diff
|
|
133
|
-
2. Propose routes for your approval
|
|
134
|
-
3. Capture desktop + mobile screenshots (production vs localhost)
|
|
135
|
-
4. Show you the screenshots for approval
|
|
136
|
-
5. Upload and append markdown to your PR
|
|
107
|
+
Nothing to install. `npx -y @juangadm/pre-post@latest pr` downloads the CLI and, on first
|
|
108
|
+
use, the Chromium headless shell (~80 MB). You need Node 20+ and a GitHub token: either
|
|
109
|
+
`gh auth login` or `GH_TOKEN`.
|
|
137
110
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
Capture any two URLs:
|
|
141
|
-
|
|
142
|
-
```bash
|
|
143
|
-
pre-post site.com localhost:3000
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
Use existing images:
|
|
111
|
+
As a Claude Code skill:
|
|
147
112
|
|
|
148
113
|
```bash
|
|
149
|
-
pre-post
|
|
114
|
+
npx skills add juangadm/pre-post -y
|
|
150
115
|
```
|
|
151
116
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
### `detect` -- Route Detection
|
|
117
|
+
Then say `/pre-post` after making UI changes.
|
|
155
118
|
|
|
156
|
-
|
|
119
|
+
## Usage
|
|
157
120
|
|
|
158
121
|
```bash
|
|
159
|
-
pre-post
|
|
160
|
-
pre-post
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
122
|
+
pre-post pr # everything, on the current branch's PR
|
|
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
|
|
125
|
+
pre-post pr --routes /pricing,/docs # explicit routes
|
|
126
|
+
pre-post pr --viewports desktop,1440x900 # custom viewports
|
|
127
|
+
pre-post pr --dry-run # capture + diff locally, post nothing
|
|
128
|
+
pre-post pr --json # machine-readable output
|
|
164
129
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
pre-post
|
|
171
|
-
pre-post compare --before-base URL --after-base URL --routes /dashboard,/settings
|
|
172
|
-
pre-post compare --before-base URL --after-base URL --responsive # Desktop + mobile
|
|
130
|
+
pre-post https://acme.com http://localhost:3000 --routes /pricing # ad-hoc comparison
|
|
131
|
+
pre-post before.png after.png # diff two images
|
|
132
|
+
pre-post detect # which routes does this branch touch?
|
|
133
|
+
pre-post login https://staging.acme.com # sign in once; the session is reused
|
|
134
|
+
pre-post prune --days 90 # clean up the assets branch
|
|
135
|
+
pre-post doctor # browser, token, dev server, config
|
|
173
136
|
```
|
|
174
137
|
|
|
175
|
-
|
|
138
|
+
When something needs a human, the CLI exits with code 3 and one sentence saying what to do
|
|
139
|
+
(log in, start the dev server, pass `--before`). Re-running picks up where it left off.
|
|
176
140
|
|
|
177
|
-
|
|
141
|
+
Exit codes, so a script or an agent can branch on them:
|
|
178
142
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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 |
|
|
182
149
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
```bash
|
|
188
|
-
pre-post url1 url2 ".hero"
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
Use different selectors for pre and post:
|
|
192
|
-
|
|
193
|
-
```bash
|
|
194
|
-
pre-post url1 url2 ".old" ".new"
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
Capture at mobile (375x812), tablet (768x1024), or custom viewport:
|
|
198
|
-
|
|
199
|
-
```bash
|
|
200
|
-
pre-post url1 url2 --mobile
|
|
201
|
-
pre-post url1 url2 --size 1920x1080
|
|
202
|
-
```
|
|
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.
|
|
203
154
|
|
|
204
|
-
|
|
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.
|
|
205
158
|
|
|
206
|
-
|
|
207
|
-
pre-post url1 url2 --full
|
|
208
|
-
```
|
|
159
|
+
## Configuration
|
|
209
160
|
|
|
210
|
-
|
|
161
|
+
Optional `.pre-post.json` in the repo root:
|
|
211
162
|
|
|
212
|
-
```
|
|
213
|
-
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"before": "https://acme.com",
|
|
166
|
+
"routes": ["/"],
|
|
167
|
+
"samples": { "/blog/[slug]": "/blog/hello-world" },
|
|
168
|
+
"viewports": ["desktop"],
|
|
169
|
+
"fullPage": true,
|
|
170
|
+
"maxHeight": 2400,
|
|
171
|
+
"scale": 2,
|
|
172
|
+
"threshold": 0.001,
|
|
173
|
+
"minChangedArea": 100,
|
|
174
|
+
"maxRoutes": 6,
|
|
175
|
+
"ignore": ["apps/docs"],
|
|
176
|
+
"headers": {},
|
|
177
|
+
"assetsBranch": "pre-post-assets"
|
|
178
|
+
}
|
|
214
179
|
```
|
|
215
180
|
|
|
216
|
-
|
|
181
|
+
Environment:
|
|
217
182
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
183
|
+
| Variable | Purpose |
|
|
184
|
+
|---|---|
|
|
185
|
+
| `GH_TOKEN` / `GITHUB_TOKEN` | GitHub token (default: `gh auth token`) |
|
|
186
|
+
| `VERCEL_AUTOMATION_BYPASS_SECRET` | Bypass Vercel Deployment Protection on preview and production URLs |
|
|
187
|
+
| `PRE_POST_CONCURRENCY` | Parallel pages (default 6) |
|
|
188
|
+
| `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` | Use a specific Chromium binary |
|
|
189
|
+
| `GH_REPO` | `owner/repo` when the remote URL cannot be parsed |
|
|
221
190
|
|
|
222
|
-
|
|
191
|
+
## Library
|
|
223
192
|
|
|
224
|
-
```
|
|
225
|
-
|
|
193
|
+
```ts
|
|
194
|
+
import { runPr, captureScreenshot, diffImages, detectRoutesForRepo } from '@juangadm/pre-post';
|
|
226
195
|
```
|
|
227
196
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
## Route Detection
|
|
231
|
-
|
|
232
|
-
Pre-post automatically maps `git diff --name-only` to affected UI routes:
|
|
233
|
-
|
|
234
|
-
| Changed File | Detected Route | Confidence |
|
|
235
|
-
|---|---|---|
|
|
236
|
-
| `app/page.tsx` | `/` | high |
|
|
237
|
-
| `app/dashboard/page.tsx` | `/dashboard` | high |
|
|
238
|
-
| `app/(marketing)/about/page.tsx` | `/about` (strips route groups) | high |
|
|
239
|
-
| `app/blog/[slug]/page.tsx` | `/blog/[slug]` | high |
|
|
240
|
-
| `app/dashboard/layout.tsx` | `/dashboard` | medium |
|
|
241
|
-
| `app/dashboard/components/Chart.tsx` | `/dashboard` | medium |
|
|
242
|
-
| `globals.css`, `tailwind.config.ts` | `/` | low |
|
|
243
|
-
| `app/api/*`, `middleware.ts` | Skipped (no visual) | -- |
|
|
244
|
-
|
|
245
|
-
Supports:
|
|
246
|
-
- **Next.js App Router** (route groups, dynamic segments, parallel routes, catch-all)
|
|
247
|
-
- **Next.js Pages Router** (`pages/`, `_app.tsx`, `_document.tsx`)
|
|
248
|
-
- **Generic fallback** (defaults to `/`)
|
|
249
|
-
|
|
250
|
-
## Add Skill
|
|
251
|
-
|
|
252
|
-
Install as an agent skill using the [`skills` CLI](https://github.com/vercel-labs/skills):
|
|
197
|
+
## Development
|
|
253
198
|
|
|
254
199
|
```bash
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
200
|
+
pnpm install
|
|
201
|
+
pnpm build
|
|
202
|
+
pnpm test:unit
|
|
203
|
+
TEST_BROWSER=true pnpm test # needs a Chromium; npx playwright-core install chromium-headless-shell
|
|
258
204
|
```
|
|
259
205
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
## Credits
|
|
267
|
-
|
|
268
|
-
- Original [before-and-after](https://github.com/vercel-labs/before-and-after) by [James Clements](https://github.com/jamesclement) at [Vercel Labs](https://github.com/vercel-labs)
|
|
269
|
-
- Browser automation powered by [Playwright](https://playwright.dev/)
|
|
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.
|
|
270
211
|
|
|
271
212
|
## License
|
|
272
213
|
|
|
@@ -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"}
|