styleproof 3.1.5 → 3.2.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/CHANGELOG.md +18 -1
- package/README.md +47 -20
- package/bin/styleproof-init.mjs +81 -19
- package/bin/styleproof-map.mjs +95 -4
- package/dist/map-store.js +9 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.2.0] - 2026-06-30
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Added optional `styleproof-map --crawl-base-url ... --crawl-route ...` pre-map
|
|
15
|
+
variant crawling, so automation can refresh the generated variant manifest
|
|
16
|
+
immediately before map capture.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- `styleproof-init` now writes a dedicated `playwright.styleproof.config.ts`,
|
|
21
|
+
scopes discovery to the StyleProof spec, detects Vite/Next production preview
|
|
22
|
+
commands, and respects pnpm/Corepack pins in generated commands.
|
|
23
|
+
- Map upload dirty-tree detection now ignores Next's generated `next-env.d.ts`
|
|
24
|
+
shim, while normal source edits still block upload.
|
|
25
|
+
|
|
10
26
|
## [3.1.5] - 2026-06-29
|
|
11
27
|
|
|
12
28
|
### Added
|
|
@@ -1058,7 +1074,8 @@ number)`), so each viewport band can capture at its own height. Default remains
|
|
|
1058
1074
|
- `styleproof-diff` CLI: certifies a refactor (exit 0) or names the exact element,
|
|
1059
1075
|
property, and state that drifted (exit 1).
|
|
1060
1076
|
|
|
1061
|
-
[Unreleased]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.
|
|
1077
|
+
[Unreleased]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.2.0...HEAD
|
|
1078
|
+
[3.2.0]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.1.5...v3.2.0
|
|
1062
1079
|
[3.1.5]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.1.4...v3.1.5
|
|
1063
1080
|
[3.1.4]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.1.3...v3.1.4
|
|
1064
1081
|
[3.1.3]: https://github.com/BenSheridanEdwards/StyleProof/compare/v3.1.2...v3.1.3
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ report. Intentional visual changes get approved; unexpected ones block or fail.
|
|
|
16
16
|
- [What it catches](#what-it-catches)
|
|
17
17
|
- [What you own](#what-you-own)
|
|
18
18
|
- [What the PR gets](#what-the-pr-gets)
|
|
19
|
-
- [
|
|
19
|
+
- [Auto-discovery](#auto-discovery)
|
|
20
20
|
- [What a report looks like](#what-a-report-looks-like)
|
|
21
21
|
- [Works with any styling system](#works-with-any-styling-system)
|
|
22
22
|
- [Breakpoints, detected automatically](#breakpoints-detected-automatically)
|
|
@@ -58,7 +58,7 @@ silently passing.
|
|
|
58
58
|
|
|
59
59
|
1. A **surface** is one UI state to certify: a route, tab, modal-open state,
|
|
60
60
|
dropdown-open state, toast-visible state, loading state, etc.
|
|
61
|
-
2. You list surfaces in a Playwright-style spec.
|
|
61
|
+
2. You list or auto-discover surfaces in a Playwright-style spec.
|
|
62
62
|
3. StyleProof opens each surface at real breakpoint widths and records computed
|
|
63
63
|
styles for every captured element.
|
|
64
64
|
4. On a PR, it compares base vs head and reports exactly which rendered styles
|
|
@@ -94,6 +94,12 @@ states that matter:
|
|
|
94
94
|
- required-but-not-yet-captured states belong in `expected`, where the coverage
|
|
95
95
|
guard fails until they are captured or explicitly excluded with a reason.
|
|
96
96
|
|
|
97
|
+
Auto-discovery is a key feature, not a side path: StyleProof can discover
|
|
98
|
+
Next.js routes, crawl real nav links, inventory component files, harvest
|
|
99
|
+
one-step variants, click safe popups, detect breakpoints, and flag live or
|
|
100
|
+
volatile regions. You review the app-specific leftovers; StyleProof keeps the
|
|
101
|
+
mechanical inventory from drifting.
|
|
102
|
+
|
|
97
103
|
That boundary is deliberate. StyleProof should not guess destructive flows,
|
|
98
104
|
auth-only fixtures, or which product state your component needs. It should make
|
|
99
105
|
missing coverage loud.
|
|
@@ -114,9 +120,19 @@ status green for that commit. Clean runs still leave a receipt: `No visual
|
|
|
114
120
|
changes detected.` New surfaces are shown as new baselines; coverage gaps are
|
|
115
121
|
handled by `expected`.
|
|
116
122
|
|
|
117
|
-
##
|
|
123
|
+
## Auto-discovery
|
|
124
|
+
|
|
125
|
+
StyleProof's auto-discovery keeps the boring inventory out of your hands where it
|
|
126
|
+
can be inferred safely: Next.js routes, crawlable links, component files,
|
|
127
|
+
semantic popups, one-step variants, breakpoints, and volatile/live candidates.
|
|
128
|
+
The key loop is simple: discover what the app exposes, capture what is safe, and
|
|
129
|
+
fail loudly for anything that still needs an owner.
|
|
118
130
|
|
|
119
|
-
StyleProof diffs the surfaces your spec lists — so a page nobody
|
|
131
|
+
StyleProof diffs the surfaces your spec lists or discovers — so a page nobody
|
|
132
|
+
added to either set is invisible to the gate. Its change has no base capture
|
|
133
|
+
_and_ no head capture, so it never appears in any diff, and the status goes
|
|
134
|
+
green having never looked at it. This is the one thing the captures can't catch
|
|
135
|
+
on their own: a capture that was never taken.
|
|
120
136
|
|
|
121
137
|
Declare your app's route/view universe in `expected` and StyleProof emits a coverage-guard test in your **normal** suite (it runs even without `STYLEMAP_DIR` — it's a static check, no browser). It fails the moment a route exists with no surface, so a new page can't ship uncaptured:
|
|
122
138
|
|
|
@@ -258,9 +274,17 @@ styles. It also reports live-state candidates that need fixtures or opt-outs.
|
|
|
258
274
|
styleproof-variants --base-url http://localhost:3000 --route / --route settings=/settings
|
|
259
275
|
```
|
|
260
276
|
|
|
261
|
-
Use it as a manifest generator, not a replacement for review.
|
|
262
|
-
|
|
263
|
-
|
|
277
|
+
Use it as a manifest generator, not a replacement for review. To refresh that
|
|
278
|
+
manifest as part of the map loop, pass the same crawl inputs to `styleproof-map`;
|
|
279
|
+
it runs the crawler before Playwright captures the maps:
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
styleproof-map --crawl-base-url http://localhost:3000 --crawl-route / --crawl-route settings=/settings
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
The app must already be reachable at `--crawl-base-url`. If Playwright's
|
|
286
|
+
`webServer` is the thing starting the app, keep route-link crawling inside the
|
|
287
|
+
capture run with `defineCrawlCapture`.
|
|
264
288
|
|
|
265
289
|
```json
|
|
266
290
|
{
|
|
@@ -393,7 +417,7 @@ npx styleproof-init
|
|
|
393
417
|
It scaffolds:
|
|
394
418
|
|
|
395
419
|
- 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);
|
|
396
|
-
- a **`playwright.config.ts`** that builds and serves a **production build** (never a flaky dev server) and captures surfaces **in parallel** (`fullyParallel`);
|
|
420
|
+
- a dedicated **`playwright.styleproof.config.ts`** that builds and serves a **production build** (never a flaky dev server), scopes discovery to the StyleProof spec, and captures surfaces **in parallel** (`fullyParallel`) without disturbing your app's existing Playwright config;
|
|
397
421
|
- `.gitignore` entries for `.styleproof/`, `test-results/`, and `playwright-report/`;
|
|
398
422
|
- 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.
|
|
399
423
|
|
|
@@ -637,21 +661,24 @@ Non-visual and framework-injected elements (`<meta>`/`<title>`/`<script>`/`<styl
|
|
|
637
661
|
|
|
638
662
|
**Capture env vars** (wire CI without editing the spec):
|
|
639
663
|
|
|
640
|
-
| Env
|
|
641
|
-
|
|
|
642
|
-
| `STYLEMAP_DIR`
|
|
643
|
-
| `STYLEPROOF_BASEDIR`
|
|
644
|
-
| `STYLEPROOF_SCREENSHOTS`
|
|
645
|
-
| `STYLEPROOF_REPLAY_FROM`
|
|
646
|
-
| `STYLEPROOF_REPLAY_URL`
|
|
647
|
-
| `STYLEPROOF_SELFCHECK`
|
|
648
|
-
| `STYLEPROOF_UPLOAD`
|
|
649
|
-
| `STYLEPROOF_CACHE_BRANCH`
|
|
664
|
+
| Env | Purpose |
|
|
665
|
+
| --------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
666
|
+
| `STYLEMAP_DIR` | Output label; the capture is skipped entirely when unset. |
|
|
667
|
+
| `STYLEPROOF_BASEDIR` | Output root dir (runner default `__stylemaps__`; `styleproof-map` CLI default `.styleproof/maps`). |
|
|
668
|
+
| `STYLEPROOF_SCREENSHOTS` | `0` to skip full-page screenshots. The CLI keeps screenshots by default so reports can crop maps restored from cache. |
|
|
669
|
+
| `STYLEPROOF_REPLAY_FROM` | Baseline dir to replay recorded data from — set this on the **head** capture. |
|
|
670
|
+
| `STYLEPROOF_REPLAY_URL` | Override the `**/api/**` data-boundary glob. |
|
|
671
|
+
| `STYLEPROOF_SELFCHECK` | `1` to capture each surface twice and fail if the two differ. |
|
|
672
|
+
| `STYLEPROOF_UPLOAD` | `1` to require map-store upload; `0` to capture locally only. |
|
|
673
|
+
| `STYLEPROOF_CACHE_BRANCH` | Map store branch (default `styleproof-maps`). |
|
|
674
|
+
| `STYLEPROOF_CRAWL_BASE_URL` | App URL for the optional pre-map `styleproof-variants` crawl. |
|
|
675
|
+
| `STYLEPROOF_CRAWL_ROUTES` | Comma-separated routes for the optional pre-map crawl, e.g. `/,settings=/settings`. |
|
|
676
|
+
| `STYLEPROOF_CRAWL_STRICT` | `1` to fail the optional pre-map crawl on live-state fixtures or skipped candidates. |
|
|
650
677
|
|
|
651
678
|
**CLIs** (every flag accepts `--flag value` and `--flag=value`; `--help` lists all):
|
|
652
679
|
|
|
653
|
-
- `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
|
|
654
|
-
- `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.
|
|
680
|
+
- `styleproof-init` — scaffold the gate: the capture spec, a dedicated `playwright.styleproof.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), respect pnpm/Corepack version pins, and detect Vite/Next production preview commands instead of assuming every repo has `start`.
|
|
681
|
+
- `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 `--crawl-base-url` plus repeated `--crawl-route` to run `styleproof-variants` before capture, `--no-upload`, `--restore --sha <commit>`, `--spec`, `--dir`, `--base-dir`, or `--no-screenshots` for custom flows.
|
|
655
682
|
- `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`.
|
|
656
683
|
- `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).
|
|
657
684
|
- `styleproof-variants` — crawl a running app for one-step state variants and write `styleproof.variants.generated.json`. Pass `--base-url`, repeat `--route`, and use `--strict` when unresolved skipped/live candidates should fail automation.
|
package/bin/styleproof-init.mjs
CHANGED
|
@@ -11,14 +11,15 @@
|
|
|
11
11
|
* the `expected` coverage guard from the app's routes at run time, so a page
|
|
12
12
|
* added later can't ship without a surface; otherwise it writes one sample
|
|
13
13
|
* surface plus a commented guard block to wire to your own route registry.
|
|
14
|
-
* - playwright.config.ts:
|
|
15
|
-
* Playwright
|
|
14
|
+
* - playwright.styleproof.config.ts: a dedicated production-build Playwright
|
|
15
|
+
* config for StyleProof captures, so an existing app Playwright config is
|
|
16
|
+
* never disturbed or accidentally reused.
|
|
16
17
|
* - .github/workflows/styleproof.yml: restores reusable maps from the
|
|
17
18
|
* styleproof-maps branch and only captures in CI when the maps are missing.
|
|
18
19
|
*
|
|
19
20
|
* Idempotent: re-running never overwrites an existing spec (use --force) and
|
|
20
|
-
* never touches an existing playwright.config.ts. Exit 0 = done (or nothing
|
|
21
|
-
* do), 2 = usage error.
|
|
21
|
+
* never touches an existing app playwright.config.ts. Exit 0 = done (or nothing
|
|
22
|
+
* to do), 2 = usage error.
|
|
22
23
|
*/
|
|
23
24
|
import fs from 'node:fs';
|
|
24
25
|
import path from 'node:path';
|
|
@@ -31,7 +32,7 @@ usage: styleproof-init [options]
|
|
|
31
32
|
|
|
32
33
|
options:
|
|
33
34
|
--dir <path> spec output path (default: e2e/styleproof.spec.ts)
|
|
34
|
-
--base-url <url> baseURL for a generated playwright.config.ts
|
|
35
|
+
--base-url <url> baseURL for a generated playwright.styleproof.config.ts
|
|
35
36
|
(default: http://localhost:3000)
|
|
36
37
|
--force overwrite the spec if it already exists
|
|
37
38
|
-h, --help show this help
|
|
@@ -41,7 +42,7 @@ What it writes:
|
|
|
41
42
|
In a Next.js app it discovers your routes at run time and wires both the
|
|
42
43
|
surfaces and the \`expected\` coverage guard to them, so a new page can't ship
|
|
43
44
|
uncaptured. Otherwise it writes one sample surface + a commented guard block.
|
|
44
|
-
- playwright.config.ts,
|
|
45
|
+
- playwright.styleproof.config.ts, a dedicated production-build Playwright config
|
|
45
46
|
- .github/workflows/styleproof.yml, a cache-first PR report workflow
|
|
46
47
|
|
|
47
48
|
After running, build and upload this commit's map outside CI when possible:
|
|
@@ -237,14 +238,15 @@ const PACKAGE_MANAGERS = {
|
|
|
237
238
|
},
|
|
238
239
|
pnpm: {
|
|
239
240
|
label: 'pnpm',
|
|
240
|
-
run: (script) => `
|
|
241
|
-
exec: (command) => `
|
|
242
|
-
install: '
|
|
241
|
+
run: (script) => `pnpm run ${script}`,
|
|
242
|
+
exec: (command) => `pnpm exec ${command}`,
|
|
243
|
+
install: 'pnpm install --frozen-lockfile',
|
|
243
244
|
setup: ` - uses: actions/setup-node@v4
|
|
244
245
|
with:
|
|
245
246
|
node-version: '20'
|
|
246
247
|
cache: pnpm
|
|
247
|
-
cache-dependency-path: pnpm-lock.yaml
|
|
248
|
+
cache-dependency-path: pnpm-lock.yaml
|
|
249
|
+
- run: corepack enable`,
|
|
248
250
|
},
|
|
249
251
|
bun: {
|
|
250
252
|
label: 'Bun',
|
|
@@ -269,6 +271,57 @@ function detectPackageManager(root) {
|
|
|
269
271
|
|
|
270
272
|
const PM = detectPackageManager(process.cwd());
|
|
271
273
|
|
|
274
|
+
function readPackageJson(root) {
|
|
275
|
+
try {
|
|
276
|
+
return JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
277
|
+
} catch {
|
|
278
|
+
return {};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function hasDep(pkg, name) {
|
|
283
|
+
return Boolean(pkg.dependencies?.[name] ?? pkg.devDependencies?.[name]);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function scriptIncludes(pkg, script, text) {
|
|
287
|
+
return typeof pkg.scripts?.[script] === 'string' && pkg.scripts[script].includes(text);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function portFromBaseUrl(url) {
|
|
291
|
+
try {
|
|
292
|
+
const parsed = new URL(url);
|
|
293
|
+
if (parsed.port) return parsed.port;
|
|
294
|
+
return parsed.protocol === 'https:' ? '443' : '80';
|
|
295
|
+
} catch {
|
|
296
|
+
return '3000';
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function productionServerCommand(root, base) {
|
|
301
|
+
const pkg = readPackageJson(root);
|
|
302
|
+
const port = portFromBaseUrl(base);
|
|
303
|
+
const build = pkg.scripts?.build ? `${PM.run('build')} && ` : '';
|
|
304
|
+
const looksLikeVite =
|
|
305
|
+
hasDep(pkg, 'vite') || scriptIncludes(pkg, 'dev', 'vite') || scriptIncludes(pkg, 'build', 'vite');
|
|
306
|
+
const looksLikeNext =
|
|
307
|
+
hasDep(pkg, 'next') || scriptIncludes(pkg, 'dev', 'next') || scriptIncludes(pkg, 'build', 'next');
|
|
308
|
+
|
|
309
|
+
if (looksLikeVite) return `${build}${PM.exec(`vite preview --host 127.0.0.1 --port ${port}`)}`;
|
|
310
|
+
if (looksLikeNext) {
|
|
311
|
+
const start = pkg.scripts?.start ? PM.run('start') : PM.exec(`next start -p ${port}`);
|
|
312
|
+
return `${build}${start}`;
|
|
313
|
+
}
|
|
314
|
+
if (pkg.scripts?.start) return `${build}${PM.run('start')}`;
|
|
315
|
+
if (pkg.scripts?.preview) return `${build}${PM.run('preview')}`;
|
|
316
|
+
return `${PM.run('build')} && ${PM.run('start')}`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function configTestDir(spec) {
|
|
320
|
+
const dir = path.dirname(path.resolve(process.cwd(), spec));
|
|
321
|
+
const rel = path.relative(process.cwd(), dir).replace(/\\/g, '/');
|
|
322
|
+
return rel ? `./${rel}` : '.';
|
|
323
|
+
}
|
|
324
|
+
|
|
272
325
|
const CONFIG = `import { defineConfig, devices } from '@playwright/test';
|
|
273
326
|
|
|
274
327
|
// Generated by styleproof-init.
|
|
@@ -280,6 +333,8 @@ const CONFIG = `import { defineConfig, devices } from '@playwright/test';
|
|
|
280
333
|
// app serves precompiled routes at consistent timing. (StyleProof's settle waits for
|
|
281
334
|
// in-flight data either way, but a production build removes the variance at the source.)
|
|
282
335
|
export default defineConfig({
|
|
336
|
+
testDir: ${JSON.stringify(configTestDir(specPath))},
|
|
337
|
+
testMatch: ${JSON.stringify(path.basename(specPath))},
|
|
283
338
|
timeout: 120_000,
|
|
284
339
|
// Capture surfaces in PARALLEL. StyleProof generates one test per surface × width,
|
|
285
340
|
// each an isolated page writing a uniquely-keyed file (\`<key>@<width>.json.gz\`), with
|
|
@@ -293,11 +348,13 @@ export default defineConfig({
|
|
|
293
348
|
baseURL: process.env.BASE_URL || '${baseUrl}',
|
|
294
349
|
},
|
|
295
350
|
// Build once, then serve THAT production build for the captures — so you can't
|
|
296
|
-
// accidentally capture a dev server.
|
|
297
|
-
//
|
|
351
|
+
// accidentally capture a dev server. styleproof-init detected the production
|
|
352
|
+
// serve command from your package scripts/dependencies; tune it here if your
|
|
353
|
+
// framework needs a custom preview command.
|
|
298
354
|
webServer: {
|
|
299
|
-
command: '${
|
|
355
|
+
command: '${productionServerCommand(process.cwd(), baseUrl)}',
|
|
300
356
|
url: process.env.BASE_URL || '${baseUrl}',
|
|
357
|
+
env: { PORT: '${portFromBaseUrl(baseUrl)}' },
|
|
301
358
|
reuseExistingServer: !process.env.CI,
|
|
302
359
|
timeout: 600_000, // a cold production build can take a few minutes
|
|
303
360
|
},
|
|
@@ -422,13 +479,18 @@ if (spec.wrote) {
|
|
|
422
479
|
console.log(`${specPath} already exists — left untouched (use --force to overwrite)`);
|
|
423
480
|
}
|
|
424
481
|
|
|
425
|
-
const configPath = 'playwright.config.ts';
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
}
|
|
429
|
-
fs.writeFileSync(configPath, CONFIG);
|
|
430
|
-
console.log(`created ${configPath} (baseURL ${baseUrl})`);
|
|
482
|
+
const configPath = 'playwright.styleproof.config.ts';
|
|
483
|
+
const config = writeFileSafe(configPath, CONFIG, { force });
|
|
484
|
+
if (config.wrote) {
|
|
485
|
+
console.log(`${config.exists ? 'overwrote' : 'created'} ${configPath} (dedicated StyleProof capture config)`);
|
|
431
486
|
wroteSomething = true;
|
|
487
|
+
} else {
|
|
488
|
+
console.log(`${configPath} already exists — left untouched (use --force to overwrite)`);
|
|
489
|
+
}
|
|
490
|
+
if (fs.existsSync('playwright.config.ts') || fs.existsSync('playwright.config.js')) {
|
|
491
|
+
console.log(
|
|
492
|
+
'app playwright.config exists — left untouched; styleproof-map uses playwright.styleproof.config.ts by default',
|
|
493
|
+
);
|
|
432
494
|
}
|
|
433
495
|
|
|
434
496
|
const ignored = ['.styleproof/', 'test-results/', 'playwright-report/'].filter((line) => ensureGitignoreLine(line));
|
package/bin/styleproof-map.mjs
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import fs from 'node:fs';
|
|
14
14
|
import { spawnSync } from 'node:child_process';
|
|
15
15
|
import path from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
16
17
|
import {
|
|
17
18
|
isHelpArg,
|
|
18
19
|
missingSpecMessage,
|
|
@@ -34,6 +35,8 @@ import {
|
|
|
34
35
|
writeMapManifest,
|
|
35
36
|
} from '../dist/map-store.js';
|
|
36
37
|
|
|
38
|
+
const STYLEPROOF_PLAYWRIGHT_CONFIG = 'playwright.styleproof.config.ts';
|
|
39
|
+
const STYLEPROOF_VARIANTS_SCRIPT = path.join(path.dirname(fileURLToPath(import.meta.url)), 'styleproof-variants.mjs');
|
|
37
40
|
const HELP = `styleproof-map — capture this branch's computed-style map
|
|
38
41
|
|
|
39
42
|
usage: styleproof-map [options] [-- <playwright args>]
|
|
@@ -49,12 +52,28 @@ options:
|
|
|
49
52
|
--upload require upload to the map store branch after capture
|
|
50
53
|
--no-upload capture locally only (default in CI)
|
|
51
54
|
--restore restore a map from the map store instead of capturing
|
|
55
|
+
--crawl-base-url <url>
|
|
56
|
+
run styleproof-variants before capture against this app URL
|
|
57
|
+
--crawl-route <r> route path or key=path for the pre-map variant crawl; repeatable
|
|
58
|
+
--crawl-out <file> variant crawl manifest (default: styleproof.variants.generated.json)
|
|
59
|
+
--crawl-max-actions <n>
|
|
60
|
+
max attempted variant actions per route (default: 40)
|
|
61
|
+
--crawl-width <px> pre-map crawl viewport width (default: 1280)
|
|
62
|
+
--crawl-height <px> pre-map crawl viewport height (default: 800)
|
|
63
|
+
--crawl-strict fail if live-state fixtures or skipped candidates remain
|
|
52
64
|
--cache-branch <b> map store branch (default: ${DEFAULT_MAP_STORE_BRANCH})
|
|
53
65
|
--remote <name> git remote for the map store (default: ${DEFAULT_REMOTE})
|
|
54
66
|
-h, --help show this help
|
|
55
67
|
|
|
68
|
+
If playwright.styleproof.config.ts exists, styleproof-map passes it to Playwright
|
|
69
|
+
by default. Override with: styleproof-map -- --config playwright.config.ts
|
|
70
|
+
|
|
71
|
+
Set STYLEPROOF_CRAWL_BASE_URL and STYLEPROOF_CRAWL_ROUTES (comma-separated) to
|
|
72
|
+
run the same pre-map crawl from automation.
|
|
73
|
+
|
|
56
74
|
Examples:
|
|
57
75
|
styleproof-map
|
|
76
|
+
styleproof-map --crawl-base-url http://localhost:3000 --crawl-route / --crawl-route settings=/settings
|
|
58
77
|
styleproof-map --upload
|
|
59
78
|
styleproof-map --restore --sha 0123abcd --dir head --base-dir __stylemaps__
|
|
60
79
|
styleproof-map --spec e2e/styleproof.spec.ts
|
|
@@ -73,6 +92,16 @@ let cacheBranch = process.env.STYLEPROOF_CACHE_BRANCH ?? DEFAULT_MAP_STORE_BRANC
|
|
|
73
92
|
let remote = process.env.STYLEPROOF_REMOTE ?? DEFAULT_REMOTE;
|
|
74
93
|
let uploadMode =
|
|
75
94
|
process.env.STYLEPROOF_UPLOAD === '1' ? 'required' : process.env.STYLEPROOF_UPLOAD === '0' ? 'off' : 'auto';
|
|
95
|
+
let crawlBaseUrl = process.env.STYLEPROOF_CRAWL_BASE_URL ?? '';
|
|
96
|
+
const crawlRoutes = (process.env.STYLEPROOF_CRAWL_ROUTES ?? '')
|
|
97
|
+
.split(',')
|
|
98
|
+
.map((route) => route.trim())
|
|
99
|
+
.filter(Boolean);
|
|
100
|
+
let crawlOut = process.env.STYLEPROOF_CRAWL_OUT ?? 'styleproof.variants.generated.json';
|
|
101
|
+
let crawlMaxActions = process.env.STYLEPROOF_CRAWL_MAX_ACTIONS ?? '';
|
|
102
|
+
let crawlWidth = process.env.STYLEPROOF_CRAWL_WIDTH ?? '';
|
|
103
|
+
let crawlHeight = process.env.STYLEPROOF_CRAWL_HEIGHT ?? '';
|
|
104
|
+
let crawlStrict = process.env.STYLEPROOF_CRAWL_STRICT === '1';
|
|
76
105
|
const playwrightArgs = [];
|
|
77
106
|
|
|
78
107
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -95,9 +124,24 @@ for (let i = 0; i < argv.length; i++) {
|
|
|
95
124
|
else if (a === '--upload') uploadMode = 'required';
|
|
96
125
|
else if (a === '--no-upload') uploadMode = 'off';
|
|
97
126
|
else if (a === '--restore') restore = true;
|
|
98
|
-
else if (a === '--
|
|
99
|
-
else if (a.startsWith('--
|
|
100
|
-
else if (a === '--
|
|
127
|
+
else if (a === '--crawl-base-url') crawlBaseUrl = argv[++i];
|
|
128
|
+
else if (a.startsWith('--crawl-base-url=')) crawlBaseUrl = a.slice(17);
|
|
129
|
+
else if (a === '--crawl-route') crawlRoutes.push(argv[++i]);
|
|
130
|
+
else if (a.startsWith('--crawl-route=')) crawlRoutes.push(a.slice(14));
|
|
131
|
+
else if (a === '--crawl-out') crawlOut = argv[++i];
|
|
132
|
+
else if (a.startsWith('--crawl-out=')) crawlOut = a.slice(12);
|
|
133
|
+
else if (a === '--crawl-max-actions') crawlMaxActions = argv[++i];
|
|
134
|
+
else if (a.startsWith('--crawl-max-actions=')) crawlMaxActions = a.slice(20);
|
|
135
|
+
else if (a === '--crawl-width') crawlWidth = argv[++i];
|
|
136
|
+
else if (a.startsWith('--crawl-width=')) crawlWidth = a.slice(14);
|
|
137
|
+
else if (a === '--crawl-height') crawlHeight = argv[++i];
|
|
138
|
+
else if (a.startsWith('--crawl-height=')) crawlHeight = a.slice(15);
|
|
139
|
+
else if (a === '--crawl-strict') crawlStrict = true;
|
|
140
|
+
else if (a === '--cache-branch' || a === '--remote') {
|
|
141
|
+
const value = argv[++i];
|
|
142
|
+
if (a === '--cache-branch') cacheBranch = value;
|
|
143
|
+
else remote = value;
|
|
144
|
+
} else if (a.startsWith('--cache-branch=')) cacheBranch = a.slice(15);
|
|
101
145
|
else if (a.startsWith('--remote=')) remote = a.slice(9);
|
|
102
146
|
else if (a.startsWith('--')) {
|
|
103
147
|
console.error(unknownFlagMessage('styleproof-map', a));
|
|
@@ -123,6 +167,15 @@ if (!fs.existsSync(spec)) {
|
|
|
123
167
|
console.error(missingSpecMessage(spec));
|
|
124
168
|
process.exit(2);
|
|
125
169
|
}
|
|
170
|
+
const crawlEnabled = Boolean(crawlBaseUrl || crawlRoutes.length);
|
|
171
|
+
if (crawlEnabled && !crawlBaseUrl) {
|
|
172
|
+
console.error('styleproof-map: --crawl-base-url is required when --crawl-route is set');
|
|
173
|
+
process.exit(2);
|
|
174
|
+
}
|
|
175
|
+
if (crawlEnabled && !crawlRoutes.length) {
|
|
176
|
+
console.error('styleproof-map: at least one --crawl-route is required when --crawl-base-url is set');
|
|
177
|
+
process.exit(2);
|
|
178
|
+
}
|
|
126
179
|
if (restore && !sha) {
|
|
127
180
|
try {
|
|
128
181
|
sha = currentGitSha(process.cwd());
|
|
@@ -165,6 +218,39 @@ function upload(dirPath) {
|
|
|
165
218
|
}
|
|
166
219
|
}
|
|
167
220
|
|
|
221
|
+
function hasPlaywrightConfigArg(args) {
|
|
222
|
+
return args.some((arg) => arg === '--config' || arg === '-c' || arg.startsWith('--config='));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function variantCrawlArgs() {
|
|
226
|
+
const args = ['--base-url', crawlBaseUrl, '--out', crawlOut];
|
|
227
|
+
for (const route of crawlRoutes) args.push('--route', route);
|
|
228
|
+
if (crawlMaxActions) args.push('--max-actions', crawlMaxActions);
|
|
229
|
+
if (crawlWidth) args.push('--width', crawlWidth);
|
|
230
|
+
if (crawlHeight) args.push('--height', crawlHeight);
|
|
231
|
+
if (crawlStrict) args.push('--strict');
|
|
232
|
+
return args;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function runVariantCrawl(env) {
|
|
236
|
+
if (!crawlEnabled) return;
|
|
237
|
+
console.error('styleproof-map: crawling UI variants before capture');
|
|
238
|
+
const command = process.platform === 'win32' ? 'styleproof-variants.cmd' : 'styleproof-variants';
|
|
239
|
+
let result = spawnSync(command, variantCrawlArgs(), { stdio: 'inherit', env });
|
|
240
|
+
if (result.error?.code === 'ENOENT') {
|
|
241
|
+
result = spawnSync(process.execPath, [STYLEPROOF_VARIANTS_SCRIPT, ...variantCrawlArgs()], {
|
|
242
|
+
stdio: 'inherit',
|
|
243
|
+
env,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
if (result.error) {
|
|
247
|
+
console.error(`styleproof-map: could not run styleproof-variants\n${result.error.message}`);
|
|
248
|
+
process.exit(2);
|
|
249
|
+
}
|
|
250
|
+
const status = result.status ?? 1;
|
|
251
|
+
if (status !== 0) process.exit(status);
|
|
252
|
+
}
|
|
253
|
+
|
|
168
254
|
const targetDir = path.join(baseDir, dir);
|
|
169
255
|
let dirtyBeforeCapture;
|
|
170
256
|
try {
|
|
@@ -199,13 +285,18 @@ if (restore) {
|
|
|
199
285
|
}
|
|
200
286
|
|
|
201
287
|
const command = process.platform === 'win32' ? 'playwright.cmd' : 'playwright';
|
|
288
|
+
const configArgs =
|
|
289
|
+
fs.existsSync(STYLEPROOF_PLAYWRIGHT_CONFIG) && !hasPlaywrightConfigArg(playwrightArgs)
|
|
290
|
+
? ['--config', STYLEPROOF_PLAYWRIGHT_CONFIG]
|
|
291
|
+
: [];
|
|
202
292
|
const env = {
|
|
203
293
|
...process.env,
|
|
204
294
|
STYLEMAP_DIR: dir,
|
|
205
295
|
STYLEPROOF_BASEDIR: baseDir,
|
|
206
296
|
STYLEPROOF_SCREENSHOTS: screenshots,
|
|
207
297
|
};
|
|
208
|
-
|
|
298
|
+
runVariantCrawl(env);
|
|
299
|
+
const result = spawnSync(command, ['test', '--grep', 'styleproof capture', ...configArgs, ...playwrightArgs], {
|
|
209
300
|
stdio: 'inherit',
|
|
210
301
|
env,
|
|
211
302
|
});
|
package/dist/map-store.js
CHANGED
|
@@ -10,6 +10,7 @@ export const DEFAULT_MAP_LABEL = 'current';
|
|
|
10
10
|
export const DEFAULT_MAP_STORE_BRANCH = 'styleproof-maps';
|
|
11
11
|
export const DEFAULT_REMOTE = 'origin';
|
|
12
12
|
export const MAP_MANIFEST = 'styleproof-manifest.json';
|
|
13
|
+
const GENERATED_DIRTY_ALLOWLIST = new Set(['next-env.d.ts']);
|
|
13
14
|
export class MapStoreError extends Error {
|
|
14
15
|
}
|
|
15
16
|
function runGit(cwd, args, maxBuffer = 1 << 28) {
|
|
@@ -112,7 +113,14 @@ export function refSha(ref, cwd = process.cwd()) {
|
|
|
112
113
|
return sha;
|
|
113
114
|
}
|
|
114
115
|
export function workingTreeDirty(cwd = process.cwd()) {
|
|
115
|
-
|
|
116
|
+
const r = runGit(cwd, ['status', '--porcelain']);
|
|
117
|
+
const status = r.status === 0 ? r.stdout.trimEnd() : '';
|
|
118
|
+
if (!status)
|
|
119
|
+
return false;
|
|
120
|
+
return status.split(/\r?\n/).some((line) => {
|
|
121
|
+
const file = line.slice(3).trim();
|
|
122
|
+
return file && !GENERATED_DIRTY_ALLOWLIST.has(file);
|
|
123
|
+
});
|
|
116
124
|
}
|
|
117
125
|
export function remoteExists(remote = DEFAULT_REMOTE, cwd = process.cwd()) {
|
|
118
126
|
return runGit(cwd, ['remote', 'get-url', remote], 1 << 20).status === 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "styleproof",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Catch every CSS change before it ships — review PRs and certify refactors by the browser's computed styles, not pixels. Works with any styling system.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"playwright",
|