styleproof 3.1.2 → 3.1.4
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/CHANGELOG.md +53 -1
- package/README.md +86 -70
- package/bin/styleproof-diff.mjs +46 -32
- package/bin/styleproof-init.mjs +78 -160
- package/bin/styleproof-map.mjs +139 -12
- package/bin/styleproof-report.mjs +41 -23
- package/dist/capture.d.ts +1 -1
- package/dist/cli-errors.d.ts +0 -1
- package/dist/cli-errors.js +2 -8
- package/dist/diff.js +1 -1
- package/dist/gitref.d.ts +2 -12
- package/dist/gitref.js +2 -42
- package/dist/index.d.ts +1 -1
- package/dist/map-store.d.ts +85 -0
- package/dist/map-store.js +342 -0
- package/dist/report.d.ts +6 -0
- package/dist/report.js +79 -9
- package/dist/runner.d.ts +31 -5
- package/dist/runner.js +187 -6
- package/package.json +5 -3
- package/dist/cli-base-ref.d.ts +0 -15
- package/dist/cli-base-ref.js +0 -53
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,57 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Popup capture no longer collapses distinct triggers that reuse the same
|
|
13
|
+
overlay mount.** Each visible trigger slot that opens a persistent overlay now
|
|
14
|
+
gets its own `<surface>-popup-XX` map, so separate modals or menus rendered at
|
|
15
|
+
the same DOM path are captured instead of being deduped away.
|
|
16
|
+
|
|
17
|
+
## [3.1.3] - 2026-06-29
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **Optional automatic popup capture.** `defineStyleMapCapture` and
|
|
22
|
+
`defineCrawlCapture` now accept `popups: true` or `{ max, triggers, overlays,
|
|
23
|
+
timeoutMs }`. When enabled, StyleProof clicks visible safe triggers after each
|
|
24
|
+
base surface capture and saves persistent dialogs, popovers, menus, listboxes,
|
|
25
|
+
tooltips, and open data-state overlays as `<surface>-popup-XX` captures with
|
|
26
|
+
popup labels in reports.
|
|
27
|
+
- **Local-first reusable map bundles are now the default v3 flow.** `styleproof-map`
|
|
28
|
+
writes `.styleproof/maps/current`, records a `styleproof-manifest.json` with the
|
|
29
|
+
commit SHA and capture compatibility key, and uploads the bundle to a dedicated
|
|
30
|
+
`styleproof-maps` branch when the working tree was clean and a git remote is
|
|
31
|
+
available. The upload happens from the explicit `styleproof-map` command, not
|
|
32
|
+
from a git hook, so generated maps no longer enter PR branch history.
|
|
33
|
+
- **The report now surfaces tiny changes by default.** Every changed region shows
|
|
34
|
+
the clean before/after crop **and** a highlighted twin (magenta boxes marking each
|
|
35
|
+
change) without expanding anything, and names the changed element next to the image
|
|
36
|
+
(e.g. `changed: span.caret`). When the changed element's footprint is small
|
|
37
|
+
(≤ `zoomBelow`, default 64px) a magnified zoom crop is added so a sub-pixel change
|
|
38
|
+
(e.g. a 2px caret bump) is obvious at a glance instead of hiding in a collapsed
|
|
39
|
+
section. New `zoomBelow` report option tunes or disables it. ([#97](https://github.com/BenSheridanEdwards/StyleProof/issues/97))
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- **`styleproof-init` no longer generates or activates a pre-push hook.** It now
|
|
44
|
+
scaffolds `.gitignore` cache entries and a cache-first PR workflow that restores
|
|
45
|
+
base/head maps from `styleproof-maps`, runs the full StyleProof report action
|
|
46
|
+
when both bundles are present, and recaptures both sides in CI only on cache miss
|
|
47
|
+
or incompatible cache.
|
|
48
|
+
- **`styleproof-diff` and `styleproof-report` now default to cached maps by commit
|
|
49
|
+
SHA.** No-arg and single base argument usage restores base/head bundles from the map
|
|
50
|
+
store, while explicit `<beforeDir> <afterDir>` usage remains for CI fallback
|
|
51
|
+
captures and custom comparisons. The old committed-map `--base-ref`/`--maps-dir`
|
|
52
|
+
mode is removed from v3 so generated maps no longer have any supported path into
|
|
53
|
+
PR branch history.
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- `styleproof-diff` and report generation now ignore `styleproof-manifest.json`
|
|
58
|
+
when discovering surface map files, so manifest-backed bundles do not get parsed
|
|
59
|
+
as captures.
|
|
60
|
+
|
|
10
61
|
## [3.1.2] - 2026-06-28
|
|
11
62
|
|
|
12
63
|
### Added
|
|
@@ -958,7 +1009,8 @@ number)`), so each viewport band can capture at its own height. Default remains
|
|
|
958
1009
|
- `styleproof-diff` CLI: certifies a refactor (exit 0) or names the exact element,
|
|
959
1010
|
property, and state that drifted (exit 1).
|
|
960
1011
|
|
|
961
|
-
[Unreleased]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.1.
|
|
1012
|
+
[Unreleased]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.1.3...HEAD
|
|
1013
|
+
[3.1.3]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.1.2...v3.1.3
|
|
962
1014
|
[3.1.2]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.1.1...v3.1.2
|
|
963
1015
|
[3.1.1]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.1.0...v3.1.1
|
|
964
1016
|
[3.1.0]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.0.2...v3.1.0
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ On every PR, StyleProof diffs the PR head's computed-style map against the base
|
|
|
18
18
|
- A single **Approve all changes** checkbox in the comment, driving a `StyleProof` commit status: red until one tick signs off every change, green when there are none. The reviewer who ticks it is recorded inline (_approved by @them_), sourced from the commit status so it survives a report re-run.
|
|
19
19
|
- **Clean runs leave a receipt too.** When no visual changes are detected, StyleProof still creates or updates the PR comment with `✓ No visual changes detected.`, so a green status has a visible record instead of only a silent check.
|
|
20
20
|
- **New surfaces don't block.** A surface that exists only on the PR head (no baseline to diff — e.g. the bootstrap PR that first adds the capture spec, or a brand-new page) is shown in the report under a `🆕 new surface` heading but never holds the status red and needs no sign-off. It becomes part of the baseline once merged.
|
|
21
|
-
- The diff is always **head-vs-base**, so the report is _exactly what this PR changes_ — whether the maps are
|
|
21
|
+
- The diff is always **head-vs-base**, so the report is _exactly what this PR changes_ — whether the maps are restored from the `styleproof-maps` cache or recaptured in CI on a cache miss. See [Quickstart](#quickstart).
|
|
22
22
|
|
|
23
23
|
## Don't let a new page ship uncaptured
|
|
24
24
|
|
|
@@ -118,10 +118,12 @@ defineStyleMapCapture({
|
|
|
118
118
|
|
|
119
119
|
## What a report looks like
|
|
120
120
|
|
|
121
|
-
One change — the hero CTA recoloured cyan → amber — appears as a single section in the report: a side-by-side before/after cropped screenshot, a one-line summary, then the exact property change folded under a toggle.
|
|
121
|
+
One change — the hero CTA recoloured cyan → amber — appears as a single section in the report: a side-by-side before/after cropped screenshot, the same crop again with magenta boxes marking exactly what changed, a one-line summary, then the exact property change folded under a toggle. A change too small to see at 1:1 (say a 2px icon tweak) also gets a magnified zoom crop, so a sub-pixel change can't slip past a reviewer.
|
|
122
122
|
|
|
123
123
|

|
|
124
124
|
|
|
125
|
+
📄 **[See a full live report](docs/demo/report.md)** — rendered by the current code with real images (clean before/after, the highlighted twin, a magnified zoom for a sub-pixel change, and a `🆕 new surface`). It's regenerated and verified on every PR (`npm run demo:report`), so it always reflects exactly what StyleProof produces today.
|
|
126
|
+
|
|
125
127
|
As it renders in the committed report (a plain-English bullet first — naming the theme token and showing the hex with a live colour swatch — then the exact table inside the toggle). The PR comment itself stays lean — a summary plus the approval box — and links here:
|
|
126
128
|
|
|
127
129
|
```text
|
|
@@ -158,7 +160,7 @@ Requires **Node ≥ 18** (ESM), **`@playwright/test` ≥ 1.40** (peer dep). Forc
|
|
|
158
160
|
|
|
159
161
|
## Quickstart
|
|
160
162
|
|
|
161
|
-
After installing (above), one command sets up
|
|
163
|
+
After installing (above), one command sets up the whole gate:
|
|
162
164
|
|
|
163
165
|
```bash
|
|
164
166
|
npx styleproof-init
|
|
@@ -168,8 +170,8 @@ It scaffolds:
|
|
|
168
170
|
|
|
169
171
|
- a **capture spec** (`e2e/styleproof.spec.ts`) describing your surfaces (a Next.js app gets its routes _and_ the coverage guard wired automatically — see below);
|
|
170
172
|
- a **`playwright.config.ts`** that builds and serves a **production build** (never a flaky dev server) and captures surfaces **in parallel** (`fullyParallel`);
|
|
171
|
-
-
|
|
172
|
-
- a **
|
|
173
|
+
- `.gitignore` entries for `.styleproof/`, `test-results/`, and `playwright-report/`;
|
|
174
|
+
- a **cache-first CI workflow** that restores reusable maps from the `styleproof-maps` branch and generates the report without a browser when both maps are already built.
|
|
173
175
|
|
|
174
176
|
Describe your surfaces — **omit `widths`** and StyleProof sweeps your real `@media` breakpoints automatically:
|
|
175
177
|
|
|
@@ -196,43 +198,54 @@ npx styleproof-map
|
|
|
196
198
|
npx styleproof-diff
|
|
197
199
|
```
|
|
198
200
|
|
|
199
|
-
`styleproof-map` captures
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
`styleproof-diff
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
201
|
+
`styleproof-map` captures the current commit into `.styleproof/maps/current`,
|
|
202
|
+
writes a manifest, and uploads the bundle
|
|
203
|
+
to the dedicated `styleproof-maps` branch when the working tree was clean and a
|
|
204
|
+
git remote is available. Nothing under `.styleproof/` belongs in the PR branch.
|
|
205
|
+
HAR recordings are removed before upload by default so private API responses do
|
|
206
|
+
not land in the map store. Keep them locally only for an explicit record/replay
|
|
207
|
+
workflow with `styleproof-map --keep-har` (or `STYLEPROOF_KEEP_HAR=1`).
|
|
208
|
+
|
|
209
|
+
`styleproof-diff` restores the base and head maps from `styleproof-maps`
|
|
210
|
+
automatically: in GitHub Actions it uses the PR base/head SHAs; locally it checks
|
|
211
|
+
`branch.<name>.gh-merge-base`, then the current GitHub PR base via `gh pr view`
|
|
212
|
+
(handy for stacked PRs), then `origin/main`, `origin/master`, `main`, and
|
|
213
|
+
`master`. Pin the base with `styleproof-diff main` or `styleproof-diff master`.
|
|
214
|
+
|
|
215
|
+
**That's the whole loop.** Build the map outside CI when possible by running
|
|
216
|
+
`styleproof-map` after committing. On the PR, CI first restores the base/head
|
|
217
|
+
bundles and only generates the report — no build, no browser. If either bundle
|
|
218
|
+
is missing or incompatible, CI recaptures both sides in the same pinned
|
|
219
|
+
environment before reporting. Correctness wins over a stale cache, but the hot
|
|
220
|
+
path is report-only.
|
|
221
|
+
|
|
222
|
+
> **Same-environment note.** Computed styles depend on the browser build and installed fonts, so maps are only comparable when captured in the same runtime environment. StyleProof records a compatibility key to select the right cached bundle and refuses to compare maps captured under different browser/platform settings; CI then recaptures both sides instead of producing a bogus report.
|
|
213
223
|
|
|
214
224
|
**Want the local side-by-side report** (not just a pass/fail diff)? Run `npx
|
|
215
225
|
styleproof-report` after `styleproof-map`; it uses the same inferred base ref and
|
|
216
|
-
|
|
226
|
+
the same cached-map defaults as `styleproof-diff`. Pin the base with
|
|
217
227
|
`styleproof-report main` or keep the manual form with `styleproof-report before
|
|
218
228
|
after --out report`.
|
|
219
229
|
|
|
220
|
-
**Want the side-by-side report + one-click approval**?
|
|
230
|
+
**Want the side-by-side report + one-click approval**? `styleproof-init` scaffolds
|
|
231
|
+
this for you. If you wire it by hand, restore or capture two dirs first, then use
|
|
232
|
+
the Action on those dirs:
|
|
221
233
|
|
|
222
234
|
```yaml
|
|
223
235
|
# .github/workflows/styleproof.yml
|
|
224
236
|
- uses: actions/checkout@v4
|
|
225
|
-
|
|
237
|
+
- run: npx styleproof-map --restore --sha "${{ github.event.pull_request.base.sha }}" --dir base --base-dir __stylemaps__
|
|
238
|
+
- run: npx styleproof-map --restore --sha "${{ github.event.pull_request.head.sha }}" --dir head --base-dir __stylemaps__
|
|
226
239
|
- uses: BenSheridanEdwards/StyleProof@v3
|
|
227
240
|
with:
|
|
228
|
-
|
|
229
|
-
fresh-dir:
|
|
241
|
+
baseline-dir: __stylemaps__/base
|
|
242
|
+
fresh-dir: __stylemaps__/head
|
|
230
243
|
require-approval: true # review-gate mode (omit / use fail-on-diff: true to certify)
|
|
231
244
|
```
|
|
232
245
|
|
|
233
246
|
Then copy [`example/styleproof-approve.yml`](https://github.com/BenSheridanEdwards/StyleProof/blob/main/example/styleproof-approve.yml) to `.github/workflows/` **on your default branch** (GitHub only runs `issue_comment` workflows from there, so the approval checkbox is inert until it's merged).
|
|
234
247
|
|
|
235
|
-
**Prefer to capture in CI
|
|
248
|
+
**Prefer to always capture in CI?** For a repo with many outside contributors on different machines, StyleProof can capture **both** base and head in CI and diff them there. See **[Forks and Dependabot](#forks-and-dependabot)** for that flow (it's also the fork-safe split). The default cache-first flow is faster for same-repo teams because local `styleproof-map` can build the head map before CI starts.
|
|
236
249
|
|
|
237
250
|
**Want to skip work safely?** Skip the **whole** StyleProof workflow only for
|
|
238
251
|
changes that cannot affect rendered output, such as docs-only edits, using your
|
|
@@ -254,7 +267,7 @@ on:
|
|
|
254
267
|
|
|
255
268
|
## Forks and Dependabot
|
|
256
269
|
|
|
257
|
-
If you **capture in CI** rather than
|
|
270
|
+
If you **always capture in CI** rather than restoring maps from `styleproof-maps` (a better fit when many outside contributors push from different machines), the simplest setup runs the whole gate in one `pull_request` job that captures base + head and diffs them. That job needs a **write** token to push the report branch, post the comment, and set the `StyleProof` status. That's fine for same-repo PRs, but **fork and Dependabot PRs run with a read-only `GITHUB_TOKEN`** (GitHub's security default for untrusted PRs). So the job can't post the status — and a required `StyleProof` check then sits `pending` forever, blocking the PR even though a dependency or fork change usually touches no UI at all.
|
|
258
271
|
|
|
259
272
|
Fix it by splitting capture from reporting, the way the approve workflow is already split out:
|
|
260
273
|
|
|
@@ -299,15 +312,16 @@ Anything still moving on its own after that is detected as a volatile region and
|
|
|
299
312
|
| Live / volatile regions (tickers, third-party embeds) | auto-detected as still-moving and excluded from direct element comparison |
|
|
300
313
|
| Non-deterministic capture (replay gap, unseeded randomness) | self-check flags it _while recording_, with a named error |
|
|
301
314
|
|
|
302
|
-
| You set this — only because it's app-specific | Why it exists
|
|
303
|
-
| --------------------------------------------- |
|
|
304
|
-
| `STYLEPROOF_REPLAY_FROM` (record / replay) | Base and head capture at different times against a live backend; replaying the base's recorded data pins the head to the same inputs, so the diff is **your code, not data drift**. The one piece of real setup.
|
|
305
|
-
| `replayUrl` / `STYLEPROOF_REPLAY_URL` | Your data endpoints aren't under `**/api/**`.
|
|
306
|
-
| `ignore: ['.selector']` | You want a region gone **explicitly** — auto-exclude already handles most live regions, but a known-noisy element reads clearer named.
|
|
307
|
-
| `liveStates: [{ key, setup, go }]` | A live feature has real states to certify. Capture each state on base and head (`surface-loading`, `surface-loaded`) instead of relying on a single moving page state.
|
|
308
|
-
| `variants: [{ key, setup, go }]` | Non-live deterministic variants, such as nav-open or modal-open states.
|
|
309
|
-
| `
|
|
310
|
-
| `
|
|
315
|
+
| You set this — only because it's app-specific | Why it exists |
|
|
316
|
+
| --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
317
|
+
| `STYLEPROOF_REPLAY_FROM` (record / replay) | Base and head capture at different times against a live backend; replaying the base's recorded data pins the head to the same inputs, so the diff is **your code, not data drift**. The one piece of real setup. |
|
|
318
|
+
| `replayUrl` / `STYLEPROOF_REPLAY_URL` | Your data endpoints aren't under `**/api/**`. |
|
|
319
|
+
| `ignore: ['.selector']` | You want a region gone **explicitly** — auto-exclude already handles most live regions, but a known-noisy element reads clearer named. |
|
|
320
|
+
| `liveStates: [{ key, setup, go }]` | A live feature has real states to certify. Capture each state on base and head (`surface-loading`, `surface-loaded`) instead of relying on a single moving page state. |
|
|
321
|
+
| `variants: [{ key, setup, go }]` | Non-live deterministic variants, such as nav-open or modal-open states. |
|
|
322
|
+
| `popups: true` | Visible click-triggered overlays should be discovered automatically. Captures each matching trigger's persistent dialogs, popovers, menus, listboxes, and open data-state overlays as `surface-popup-XX`; keep hover-only or destructive states as explicit `variants`. |
|
|
323
|
+
| `clockTime` | Your styling keys off a **specific** date, not just "now". |
|
|
324
|
+
| `stabilize: { quietFor, timeout }` | An unusually slow surface needs a longer quiet window before the map is read. |
|
|
311
325
|
|
|
312
326
|
## Optional: content layer (advisory)
|
|
313
327
|
|
|
@@ -350,14 +364,13 @@ When a PR **adds** an element, StyleProof now reports its **full resting compute
|
|
|
350
364
|
|
|
351
365
|
**Action `BenSheridanEdwards/StyleProof@v3`** — key inputs:
|
|
352
366
|
|
|
353
|
-
| Input | Default | Purpose
|
|
354
|
-
| ------------------ | ------------ |
|
|
355
|
-
| `fresh-dir` | _required_ | PR-head
|
|
356
|
-
| `
|
|
357
|
-
| `
|
|
358
|
-
| `
|
|
359
|
-
| `
|
|
360
|
-
| `status-context` | `StyleProof` | Commit-status name. Must match the approve workflow and branch protection. |
|
|
367
|
+
| Input | Default | Purpose |
|
|
368
|
+
| ------------------ | ------------ | ----------------------------------------------------------------------------------- |
|
|
369
|
+
| `fresh-dir` | _required_ | PR-head captures restored from `styleproof-maps` or freshly captured in CI. |
|
|
370
|
+
| `baseline-dir` | _required_ | Base-branch captures dir restored from `styleproof-maps` or freshly captured in CI. |
|
|
371
|
+
| `require-approval` | `false` | Review-gate mode: set the `StyleProof` status instead of failing. |
|
|
372
|
+
| `fail-on-diff` | `true` | Certify mode: fail on any diff. Ignored when `require-approval` is true. |
|
|
373
|
+
| `status-context` | `StyleProof` | Commit-status name. Must match the approve workflow and branch protection. |
|
|
361
374
|
|
|
362
375
|
Outputs: `changed` (`"true"` when anything changed), `report-url`. Other inputs (`report-branch`, `github-token`) have sensible defaults — see [`action.yml`](https://github.com/BenSheridanEdwards/StyleProof/blob/main/action.yml).
|
|
363
376
|
|
|
@@ -379,41 +392,44 @@ It's **asynchronous by design**: approval is a checkbox tick handled by a separa
|
|
|
379
392
|
|
|
380
393
|
**Capture spec `defineStyleMapCapture({ surfaces, … })`** — determinism is on by default; you rarely set more than `surfaces` and `dir`:
|
|
381
394
|
|
|
382
|
-
| Option | Default | Purpose
|
|
383
|
-
| ------------- | --------------------------- |
|
|
384
|
-
| `surfaces` | _required_ | Page states to certify — each `{ key, go, widths?, ignore?, height?, liveStates?, variants? }`. `go(page)` drives to a settled state. Omit `widths` to auto-detect the app's `@media` breakpoints and sweep one width per band. |
|
|
385
|
-
| `liveStates` | _none_ | Optional pinned live product states. Each `{ key, setup?, go?, widths?, height?, ignore? }` becomes `<surface>-<state>` and is labeled as a live state in reports.
|
|
386
|
-
| `variants` | _none_ | Optional non-live deterministic states under a surface. Each `{ key, setup?, go?, widths?, height?, ignore? }` becomes `<surface>-<variant>` so base/head compare matching states.
|
|
387
|
-
| `
|
|
388
|
-
| `
|
|
389
|
-
| `
|
|
390
|
-
| `
|
|
391
|
-
| `
|
|
392
|
-
| `
|
|
393
|
-
| `
|
|
394
|
-
| `
|
|
395
|
-
| `
|
|
396
|
-
| `
|
|
395
|
+
| Option | Default | Purpose |
|
|
396
|
+
| ------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
397
|
+
| `surfaces` | _required_ | Page states to certify — each `{ key, go, widths?, ignore?, height?, liveStates?, variants?, popups? }`. `go(page)` drives to a settled state. Omit `widths` to auto-detect the app's `@media` breakpoints and sweep one width per band. |
|
|
398
|
+
| `liveStates` | _none_ | Optional pinned live product states. Each `{ key, setup?, go?, widths?, height?, ignore? }` becomes `<surface>-<state>` and is labeled as a live state in reports. |
|
|
399
|
+
| `variants` | _none_ | Optional non-live deterministic states under a surface. Each `{ key, setup?, go?, widths?, height?, ignore? }` becomes `<surface>-<variant>` so base/head compare matching states. |
|
|
400
|
+
| `popups` | `false` | Optional automatic popup capture. Set `true` or `{ max, triggers, overlays, timeoutMs }` to click visible safe triggers and save each opened overlay state as `<surface>-popup-XX`. |
|
|
401
|
+
| `expected` | _none_ | Your route/view universe. Emits a coverage-guard test (runs without a capture dir) that fails when a route has no surface and isn't excluded — so a new page can't ship uncaptured. |
|
|
402
|
+
| `exclude` | `{}` | `key → reason` for routes deliberately not captured. Keeps the guard green for known gaps; a key absent from `expected` fails the guard, so the ledger can't go stale. |
|
|
403
|
+
| `dir` | `STYLEMAP_DIR` | Output label (`base`/`head`); the spec is **inert until set**, so it sits safely beside your other specs. |
|
|
404
|
+
| `replayFrom` | `STYLEPROOF_REPLAY_FROM` | Baseline dir whose recorded responses to replay. Unset → this run **records** its HAR for the comparison to use. |
|
|
405
|
+
| `replayUrl` | `**/api/**` (`…REPLAY_URL`) | URL glob for the data boundary to record/replay; everything else (JS/CSS/fonts) loads live so the code runs. |
|
|
406
|
+
| `freezeClock` | `true` | Pin `Date.now()`/`new Date()` so time-derived styling can't drift; timers keep running so settling still works. |
|
|
407
|
+
| `clockTime` | `2025-01-01T00:00:00Z` | The frozen instant. |
|
|
408
|
+
| `selfCheck` | on while recording | Capture each surface twice and fail on any difference — proves the capture is deterministic. Off on the replay run; `STYLEPROOF_SELFCHECK=1` forces both. |
|
|
409
|
+
| `screenshots` | `true` | Save full-page screenshots for the report's before/after crops. |
|
|
410
|
+
| `baseDir` | `__stylemaps__` | Output root directory. |
|
|
397
411
|
|
|
398
412
|
Non-visual and framework-injected elements (`<meta>`/`<title>`/`<script>`/`<style>`/… and `next-route-announcer`) are skipped automatically; a surface's `ignore` adds to that default, it doesn't replace it.
|
|
399
413
|
|
|
400
414
|
**Capture env vars** (wire CI without editing the spec):
|
|
401
415
|
|
|
402
|
-
| Env
|
|
403
|
-
|
|
|
404
|
-
| `STYLEMAP_DIR`
|
|
405
|
-
| `STYLEPROOF_BASEDIR`
|
|
406
|
-
| `STYLEPROOF_SCREENSHOTS`
|
|
407
|
-
| `STYLEPROOF_REPLAY_FROM`
|
|
408
|
-
| `STYLEPROOF_REPLAY_URL`
|
|
409
|
-
| `STYLEPROOF_SELFCHECK`
|
|
416
|
+
| Env | Purpose |
|
|
417
|
+
| ------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
418
|
+
| `STYLEMAP_DIR` | Output label; the capture is skipped entirely when unset. |
|
|
419
|
+
| `STYLEPROOF_BASEDIR` | Output root dir (runner default `__stylemaps__`; `styleproof-map` CLI default `.styleproof/maps`). |
|
|
420
|
+
| `STYLEPROOF_SCREENSHOTS` | `0` to skip full-page screenshots. The CLI keeps screenshots by default so reports can crop maps restored from cache. |
|
|
421
|
+
| `STYLEPROOF_REPLAY_FROM` | Baseline dir to replay recorded data from — set this on the **head** capture. |
|
|
422
|
+
| `STYLEPROOF_REPLAY_URL` | Override the `**/api/**` data-boundary glob. |
|
|
423
|
+
| `STYLEPROOF_SELFCHECK` | `1` to capture each surface twice and fail if the two differ. |
|
|
424
|
+
| `STYLEPROOF_UPLOAD` | `1` to require map-store upload; `0` to capture locally only. |
|
|
425
|
+
| `STYLEPROOF_CACHE_BRANCH` | Map store branch (default `styleproof-maps`). |
|
|
410
426
|
|
|
411
427
|
**CLIs** (every flag accepts `--flag value` and `--flag=value`; `--help` lists all):
|
|
412
428
|
|
|
413
|
-
- `styleproof-init` — scaffold
|
|
414
|
-
- `styleproof-map` — capture the current
|
|
415
|
-
- `styleproof-diff` — the certify gate. With no args, it
|
|
416
|
-
- `styleproof-report` — render the diff to a Markdown report with before/after crops. With no args, it reports
|
|
429
|
+
- `styleproof-init` — scaffold the gate: the capture spec, a `playwright.config.ts` (production-build `webServer`, parallel capture), `.gitignore` cache entries, and the cache-first report workflow. One command. Generated commands follow the repo's lockfile (`bun.lock`/`bun.lockb`, `pnpm-lock.yaml`, `yarn.lock`, or npm by default) instead of assuming npm.
|
|
430
|
+
- `styleproof-map` — capture the current commit's computed-style map through Playwright. By default it writes `.styleproof/maps/current`, keeps screenshots for reports, writes a manifest, and uploads to `styleproof-maps` outside CI when the working tree was clean and a git remote exists. Pass `--no-upload`, `--restore --sha <commit>`, `--spec`, `--dir`, `--base-dir`, or `--no-screenshots` for custom flows.
|
|
431
|
+
- `styleproof-diff` — the certify gate. With no args, it restores cached maps for the current commit and inferred base (`GITHUB_BASE_REF`, `branch.<name>.gh-merge-base`, `gh pr view`, then main/master fallbacks); `styleproof-diff main` / `styleproof-diff master` pins the base; `styleproof-diff <beforeDir> <afterDir>` keeps the manual two-directory form for CI fallback captures. Exits `0` certified (identical), `1` on a diff, `2` on a usage/capture error, `3` when only new surfaces are present (no baseline to diff against). A clean run prints `0 changed surfaces across N captured surface(s)`, and `--json` includes `compared`.
|
|
432
|
+
- `styleproof-report` — render the diff to a Markdown report with before/after crops. With no args, it reports cached maps for the current commit against the inferred base; `styleproof-report main` / `styleproof-report master` pins the base; `styleproof-report <beforeDir> <afterDir> --out <dir>` keeps the manual two-directory form. Add `--include-content` for the opt-in, advisory content section (see above).
|
|
417
433
|
|
|
418
434
|
A programmatic API is also exported — `captureStyleMap`, `diffStyleMaps`, `generateStyleMapReport`, and the breakpoint helpers `detectViewportWidths` / `widthsFromBoundaries`, among others. For the capture internals, the approve-workflow trust model, and how to contribute, see [CONTRIBUTING](https://github.com/BenSheridanEdwards/StyleProof/blob/main/CONTRIBUTING.md) and the [`example/`](https://github.com/BenSheridanEdwards/StyleProof/tree/main/example) workflows.
|
|
419
435
|
|
package/bin/styleproof-diff.mjs
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Diff two computed-style map captures (see styleproof).
|
|
4
4
|
*
|
|
5
|
-
* styleproof-diff [baseRef] [--
|
|
6
|
-
* styleproof-diff --base-ref <gitref> [mapsDir] [--max N] [--json <file>]
|
|
5
|
+
* styleproof-diff [baseRef] [--max N] [--json <file>]
|
|
7
6
|
* styleproof-diff <beforeDir> <afterDir> [--max N] [--json <file>]
|
|
8
7
|
*
|
|
9
8
|
* Reports, per surface:
|
|
@@ -15,12 +14,9 @@
|
|
|
15
14
|
* longer does (or now changes differently) — the classic dropped
|
|
16
15
|
* `hover:` variant a screenshot can never catch.
|
|
17
16
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* main, CI just diffs" flow — commit each branch's maps (pre-push), and the gate
|
|
22
|
-
* never recomputes the base. Both sides must be captured in the same environment
|
|
23
|
-
* (browser + fonts) for the diff to be meaningful.
|
|
17
|
+
* No-arg and single base argument usage restore base/head maps from the StyleProof
|
|
18
|
+
* map store branch by commit SHA. To compare already-restored/captured maps,
|
|
19
|
+
* pass explicit before/after directories.
|
|
24
20
|
*
|
|
25
21
|
* Custom properties (--*) are ignored: they are inputs, not outcomes (see
|
|
26
22
|
* README). Exit code 0 = identical, 1 = reviewable differences, 2 = usage/capture
|
|
@@ -29,7 +25,13 @@
|
|
|
29
25
|
import fs from 'node:fs';
|
|
30
26
|
import path from 'node:path';
|
|
31
27
|
import { diffStyleMapDirs, findingLabel } from '../dist/diff.js';
|
|
32
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
DEFAULT_MAP_STORE_BRANCH,
|
|
30
|
+
DEFAULT_REMOTE,
|
|
31
|
+
assertCompatibleMapDirs,
|
|
32
|
+
cleanupCachedCaptureDirs,
|
|
33
|
+
resolveCachedCaptureDirs,
|
|
34
|
+
} from '../dist/map-store.js';
|
|
33
35
|
import {
|
|
34
36
|
cliErrorMessage,
|
|
35
37
|
isHelpArg,
|
|
@@ -39,19 +41,19 @@ import {
|
|
|
39
41
|
} from '../dist/cli-errors.js';
|
|
40
42
|
|
|
41
43
|
const COMMAND = path.basename(process.argv[1] ?? 'styleproof-diff').replace(/\.mjs$/, '');
|
|
42
|
-
const DEFAULT_MAPS_DIR = 'stylemaps/current';
|
|
43
44
|
|
|
44
45
|
const HELP = `${COMMAND} — certify a CSS refactor by diffing two computed-style map captures
|
|
45
46
|
|
|
46
47
|
usage: ${COMMAND} [baseRef] [options]
|
|
47
|
-
${COMMAND} --base-ref <gitref> [mapsDir] [options]
|
|
48
48
|
${COMMAND} <beforeDir> <afterDir> [options]
|
|
49
49
|
|
|
50
50
|
options:
|
|
51
|
-
--
|
|
52
|
-
|
|
53
|
-
--
|
|
54
|
-
|
|
51
|
+
--spec <path> StyleProof spec used to select compatible cached maps
|
|
52
|
+
(default: e2e/styleproof.spec.ts)
|
|
53
|
+
--cache-branch <b>
|
|
54
|
+
map store branch for default cached-map mode
|
|
55
|
+
(default: ${DEFAULT_MAP_STORE_BRANCH})
|
|
56
|
+
--remote <name> git remote for the map store (default: ${DEFAULT_REMOTE})
|
|
55
57
|
--max <n> max lines printed per surface before truncating (default: 40)
|
|
56
58
|
--json <file> also write the full structured diff to <file>
|
|
57
59
|
-h, --help show this help
|
|
@@ -64,18 +66,21 @@ const argv = process.argv.slice(2);
|
|
|
64
66
|
const args = [];
|
|
65
67
|
let MAX = 40;
|
|
66
68
|
let jsonOut = null;
|
|
67
|
-
let
|
|
68
|
-
let
|
|
69
|
+
let spec = 'e2e/styleproof.spec.ts';
|
|
70
|
+
let cacheBranch = process.env.STYLEPROOF_CACHE_BRANCH ?? DEFAULT_MAP_STORE_BRANCH;
|
|
71
|
+
let remote = process.env.STYLEPROOF_REMOTE ?? DEFAULT_REMOTE;
|
|
69
72
|
for (let i = 0; i < argv.length; i++) {
|
|
70
73
|
if (isHelpArg(argv[i])) showHelpAndExit(HELP);
|
|
71
74
|
else if (argv[i] === '--max') MAX = Number(argv[++i]);
|
|
72
75
|
else if (argv[i].startsWith('--max=')) MAX = Number(argv[i].slice(6));
|
|
73
76
|
else if (argv[i] === '--json') jsonOut = argv[++i];
|
|
74
77
|
else if (argv[i].startsWith('--json=')) jsonOut = argv[i].slice(7);
|
|
75
|
-
else if (argv[i] === '--
|
|
76
|
-
else if (argv[i].startsWith('--
|
|
77
|
-
else if (argv[i] === '--
|
|
78
|
-
else if (argv[i].startsWith('--
|
|
78
|
+
else if (argv[i] === '--spec') spec = argv[++i];
|
|
79
|
+
else if (argv[i].startsWith('--spec=')) spec = argv[i].slice(7);
|
|
80
|
+
else if (argv[i] === '--cache-branch') cacheBranch = argv[++i];
|
|
81
|
+
else if (argv[i].startsWith('--cache-branch=')) cacheBranch = argv[i].slice(15);
|
|
82
|
+
else if (argv[i] === '--remote') remote = argv[++i];
|
|
83
|
+
else if (argv[i].startsWith('--remote=')) remote = argv[i].slice(9);
|
|
79
84
|
else if (argv[i].startsWith('--')) {
|
|
80
85
|
console.error(unknownFlagMessage(COMMAND, argv[i]));
|
|
81
86
|
process.exit(2);
|
|
@@ -84,26 +89,34 @@ for (let i = 0; i < argv.length; i++) {
|
|
|
84
89
|
|
|
85
90
|
let dirA;
|
|
86
91
|
let dirB;
|
|
87
|
-
let
|
|
88
|
-
if (
|
|
92
|
+
let cacheCapture = null;
|
|
93
|
+
if (args.length <= 1) {
|
|
89
94
|
if (!Number.isFinite(MAX)) {
|
|
90
|
-
console.error(`usage: ${COMMAND} [baseRef] [--
|
|
95
|
+
console.error(`usage: ${COMMAND} [baseRef] [--max N] [--json <file>]`);
|
|
91
96
|
process.exit(2);
|
|
92
97
|
}
|
|
93
98
|
try {
|
|
94
|
-
|
|
99
|
+
cacheCapture = resolveCachedCaptureDirs({
|
|
95
100
|
command: COMMAND,
|
|
96
|
-
baseRef,
|
|
97
|
-
mapsDir,
|
|
98
101
|
args,
|
|
99
|
-
|
|
102
|
+
spec,
|
|
103
|
+
branch: cacheBranch,
|
|
104
|
+
remote,
|
|
105
|
+
baseUrl: process.env.BASE_URL,
|
|
106
|
+
usage: `usage: ${COMMAND} [baseRef] [--max N] [--json <file>]`,
|
|
100
107
|
});
|
|
108
|
+
dirA = cacheCapture.beforeDir;
|
|
109
|
+
dirB = cacheCapture.afterDir;
|
|
101
110
|
} catch (e) {
|
|
102
|
-
console.error(
|
|
111
|
+
console.error(
|
|
112
|
+
[
|
|
113
|
+
`${COMMAND}: cached maps are not available for this comparison`,
|
|
114
|
+
cliErrorMessage(e),
|
|
115
|
+
'Next: run styleproof-map on the base and head commits to upload maps, or let CI recapture both sides.',
|
|
116
|
+
].join('\n'),
|
|
117
|
+
);
|
|
103
118
|
process.exit(2);
|
|
104
119
|
}
|
|
105
|
-
dirA = baseCapture.beforeDir;
|
|
106
|
-
dirB = baseCapture.afterDir;
|
|
107
120
|
} else {
|
|
108
121
|
if (args.length !== 2 || !Number.isFinite(MAX)) {
|
|
109
122
|
console.error(`usage: ${COMMAND} <beforeDir> <afterDir> [--max N] [--json <file>] (--help for all options)`);
|
|
@@ -120,12 +133,13 @@ if (baseRef || args.length <= 1) {
|
|
|
120
133
|
|
|
121
134
|
let result;
|
|
122
135
|
try {
|
|
136
|
+
assertCompatibleMapDirs(dirA, dirB);
|
|
123
137
|
result = diffStyleMapDirs(dirA, dirB);
|
|
124
138
|
} catch (e) {
|
|
125
139
|
console.error(e.message);
|
|
126
140
|
process.exit(2);
|
|
127
141
|
} finally {
|
|
128
|
-
|
|
142
|
+
cleanupCachedCaptureDirs(cacheCapture);
|
|
129
143
|
}
|
|
130
144
|
const { surfaces, counts, compared } = result;
|
|
131
145
|
|