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/bin/styleproof-init.mjs
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* surface plus a commented guard block to wire to your own route registry.
|
|
14
14
|
* - playwright.config.ts: only if one does not already exist, so an existing
|
|
15
15
|
* Playwright project is never disturbed.
|
|
16
|
+
* - .github/workflows/styleproof.yml: restores reusable maps from the
|
|
17
|
+
* styleproof-maps branch and only captures in CI when the maps are missing.
|
|
16
18
|
*
|
|
17
19
|
* Idempotent: re-running never overwrites an existing spec (use --force) and
|
|
18
20
|
* never touches an existing playwright.config.ts. Exit 0 = done (or nothing to
|
|
@@ -20,7 +22,6 @@
|
|
|
20
22
|
*/
|
|
21
23
|
import fs from 'node:fs';
|
|
22
24
|
import path from 'node:path';
|
|
23
|
-
import { spawnSync } from 'node:child_process';
|
|
24
25
|
import { discoverNextRoutes } from '../dist/index.js';
|
|
25
26
|
import { isHelpArg, showHelpAndExit } from '../dist/cli-errors.js';
|
|
26
27
|
|
|
@@ -41,8 +42,9 @@ What it writes:
|
|
|
41
42
|
surfaces and the \`expected\` coverage guard to them, so a new page can't ship
|
|
42
43
|
uncaptured. Otherwise it writes one sample surface + a commented guard block.
|
|
43
44
|
- playwright.config.ts, only if absent (an existing one is left untouched)
|
|
45
|
+
- .github/workflows/styleproof.yml, a cache-first PR report workflow
|
|
44
46
|
|
|
45
|
-
After running,
|
|
47
|
+
After running, build and upload this commit's map outside CI when possible:
|
|
46
48
|
npx styleproof-map
|
|
47
49
|
|
|
48
50
|
To certify a refactor:
|
|
@@ -107,8 +109,8 @@ const HEADER = `/**
|
|
|
107
109
|
* detects your @media breakpoints from the loaded CSS and sweeps one viewport per
|
|
108
110
|
* band — no config. Capture against a PRODUCTION build — dev servers inject styles.
|
|
109
111
|
*
|
|
110
|
-
* npx styleproof-map # capture this
|
|
111
|
-
* npx styleproof-diff # compare
|
|
112
|
+
* npx styleproof-map # capture this commit into the local cache and map store
|
|
113
|
+
* npx styleproof-diff # compare cached base/head maps by commit SHA
|
|
112
114
|
*/`;
|
|
113
115
|
|
|
114
116
|
// Next.js detected: derive both surfaces and the coverage guard from the app's
|
|
@@ -285,81 +287,23 @@ export default defineConfig({
|
|
|
285
287
|
});
|
|
286
288
|
`;
|
|
287
289
|
|
|
288
|
-
// The out-of-the-box gate: capture happens PRE-PUSH on your machine (where the app
|
|
289
|
-
// already builds + serves) and the lean computed-style map is committed, so main
|
|
290
|
-
// always carries a base map and CI is a fast, browser-less diff of two precomputed
|
|
291
|
-
// maps. The hook redirects capture into a committed `stylemaps/` dir and drops
|
|
292
|
-
// screenshots via the STYLEPROOF_BASEDIR / STYLEPROOF_SCREENSHOTS env knobs.
|
|
293
|
-
const HOOK_DIR = '.githooks';
|
|
294
|
-
const HOOK_PATH = `${HOOK_DIR}/pre-push`;
|
|
295
|
-
const PREPUSH_HOOK = `#!/bin/sh
|
|
296
|
-
# StyleProof pre-push hook (generated by styleproof-init).
|
|
297
|
-
#
|
|
298
|
-
# Captures this branch's computed-style map, commits it, and pushes it WITH your
|
|
299
|
-
# push — one \`git push\`, never two. So main always carries a base map and CI just
|
|
300
|
-
# diffs two precomputed maps (no browser). Maps are lean .json.gz under stylemaps/.
|
|
301
|
-
# Activate once per clone: git config core.hooksPath .githooks
|
|
302
|
-
set -e
|
|
303
|
-
|
|
304
|
-
remote="$1"
|
|
305
|
-
|
|
306
|
-
# Re-entry guard: the map commit is pushed by this hook (below), which fires the
|
|
307
|
-
# hook a second time — skip the expensive capture on that pass.
|
|
308
|
-
[ "\${STYLEPROOF_SKIP_CAPTURE:-}" = "1" ] && exit 0
|
|
309
|
-
|
|
310
|
-
${PM.exec(`styleproof-map --spec ${specPath}`)}
|
|
311
|
-
|
|
312
|
-
if git cat-file -e HEAD:stylemaps/current 2>/dev/null; then
|
|
313
|
-
tmp="\${TMPDIR:-/tmp}/styleproof-pre-push-diff.$$"
|
|
314
|
-
set +e
|
|
315
|
-
${PM.exec('styleproof-diff --base-ref HEAD --max=20')} > "$tmp"
|
|
316
|
-
code=$?
|
|
317
|
-
set -e
|
|
318
|
-
|
|
319
|
-
if [ "$code" -eq 2 ]; then
|
|
320
|
-
cat "$tmp" >&2
|
|
321
|
-
rm -f "$tmp"
|
|
322
|
-
exit 2
|
|
323
|
-
fi
|
|
324
|
-
|
|
325
|
-
if [ "$code" -eq 0 ]; then
|
|
326
|
-
rm -f "$tmp"
|
|
327
|
-
git restore --source=HEAD -- stylemaps
|
|
328
|
-
git clean -fdq -- stylemaps/current
|
|
329
|
-
exit 0
|
|
330
|
-
fi
|
|
331
|
-
|
|
332
|
-
cat "$tmp" >&2
|
|
333
|
-
rm -f "$tmp"
|
|
334
|
-
echo "StyleProof: style map changed. If this was not an intended visual change, pin live states or replay/fixture the data boundary before pushing." >&2
|
|
335
|
-
fi
|
|
336
|
-
|
|
337
|
-
git add stylemaps
|
|
338
|
-
git diff --cached --quiet -- stylemaps && exit 0 # map unchanged — let the push proceed
|
|
339
|
-
|
|
340
|
-
# Map changed: commit it and push the updated branch ourselves so you never push
|
|
341
|
-
# twice (git pushes the pre-hook commit, so a new commit here wouldn't otherwise go).
|
|
342
|
-
# The re-entry guard stops the inner push from recapturing; then abort this original,
|
|
343
|
-
# now-stale push so nothing is sent twice.
|
|
344
|
-
git commit -m "chore(styleproof): update computed-style map"
|
|
345
|
-
echo "StyleProof: style map changed — committed it and pushing with your branch…" >&2
|
|
346
|
-
STYLEPROOF_SKIP_CAPTURE=1 git push "$remote" HEAD
|
|
347
|
-
echo "StyleProof: ✓ pushed your branch with the updated map — nothing more to do." >&2
|
|
348
|
-
echo "StyleProof: (git's 'failed to push some refs' below is expected; the push above already succeeded.)" >&2
|
|
349
|
-
exit 1
|
|
350
|
-
`;
|
|
351
|
-
|
|
352
290
|
const CI_PATH = '.github/workflows/styleproof.yml';
|
|
353
291
|
const CI_WORKFLOW = `name: StyleProof
|
|
354
292
|
|
|
355
|
-
#
|
|
356
|
-
#
|
|
293
|
+
# Cache-first v3 flow:
|
|
294
|
+
# - run \`styleproof-map\` locally after committing to build/upload this commit's map
|
|
295
|
+
# outside CI when possible;
|
|
296
|
+
# - CI restores base/head maps from the styleproof-maps branch and generates the
|
|
297
|
+
# report without a browser;
|
|
298
|
+
# - on cache miss, CI recaptures both sides in one pinned environment so the
|
|
299
|
+
# comparison stays valid.
|
|
357
300
|
on: pull_request
|
|
358
301
|
|
|
359
302
|
permissions:
|
|
360
|
-
contents:
|
|
303
|
+
contents: write
|
|
361
304
|
issues: write
|
|
362
305
|
pull-requests: write
|
|
306
|
+
statuses: write
|
|
363
307
|
|
|
364
308
|
jobs:
|
|
365
309
|
styleproof:
|
|
@@ -367,62 +311,55 @@ jobs:
|
|
|
367
311
|
steps:
|
|
368
312
|
- uses: actions/checkout@v4
|
|
369
313
|
with:
|
|
370
|
-
fetch-depth: 0 # need
|
|
314
|
+
fetch-depth: 0 # need base/head commits for cache fallback capture
|
|
371
315
|
${PM.setup}
|
|
372
316
|
- run: ${PM.install}
|
|
373
|
-
- id:
|
|
374
|
-
name:
|
|
317
|
+
- id: maps
|
|
318
|
+
name: Restore cached StyleProof maps
|
|
375
319
|
shell: bash
|
|
376
320
|
run: |
|
|
321
|
+
BASE_SHA="\${{ github.event.pull_request.base.sha }}"
|
|
322
|
+
HEAD_SHA="\${{ github.event.pull_request.head.sha }}"
|
|
323
|
+
rm -rf __stylemaps__
|
|
377
324
|
set +e
|
|
378
|
-
${PM.exec(`styleproof-
|
|
379
|
-
|
|
325
|
+
${PM.exec(`styleproof-map --restore --sha "$BASE_SHA" --dir base --base-dir __stylemaps__ --spec ${specPath}`)}
|
|
326
|
+
base_code=$?
|
|
327
|
+
${PM.exec(`styleproof-map --restore --sha "$HEAD_SHA" --dir head --base-dir __stylemaps__ --spec ${specPath}`)}
|
|
328
|
+
head_code=$?
|
|
380
329
|
set -e
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
echo "result=clean" >> "$GITHUB_OUTPUT"
|
|
384
|
-
elif [ "$code" -eq 1 ]; then
|
|
385
|
-
echo "result=changed" >> "$GITHUB_OUTPUT"
|
|
386
|
-
elif [ "$code" -eq 3 ]; then
|
|
387
|
-
echo "result=new" >> "$GITHUB_OUTPUT"
|
|
330
|
+
if [ "$base_code" -eq 0 ] && [ "$head_code" -eq 0 ]; then
|
|
331
|
+
echo "capture-needed=false" >> "$GITHUB_OUTPUT"
|
|
388
332
|
else
|
|
389
|
-
echo "
|
|
333
|
+
echo "capture-needed=true" >> "$GITHUB_OUTPUT"
|
|
390
334
|
fi
|
|
391
|
-
- name:
|
|
392
|
-
if:
|
|
393
|
-
always() &&
|
|
394
|
-
github.event_name == 'pull_request' &&
|
|
395
|
-
steps.diff.outputs.result != '' &&
|
|
396
|
-
steps.diff.outputs.result != 'error'
|
|
397
|
-
uses: actions/github-script@v9
|
|
398
|
-
with:
|
|
399
|
-
script: |
|
|
400
|
-
const issue_number = context.payload.pull_request.number;
|
|
401
|
-
const marker = '<!-- styleproof-report -->';
|
|
402
|
-
const result = '\${{ steps.diff.outputs.result }}';
|
|
403
|
-
const bodies = {
|
|
404
|
-
clean: '✓ No visual changes detected.',
|
|
405
|
-
new: '🆕 New surface(s) detected — no committed baseline to diff against yet, so nothing to approve. They will be certified once their map is committed.',
|
|
406
|
-
changed: 'Visual changes detected. See the StyleProof check logs for the computed-style diff.',
|
|
407
|
-
};
|
|
408
|
-
const body = marker + '\\n## 🗺️ StyleProof report\\n\\n' + (bodies[result] || bodies.changed);
|
|
409
|
-
const { data: comments } = await github.rest.issues.listComments({
|
|
410
|
-
...context.repo,
|
|
411
|
-
issue_number,
|
|
412
|
-
per_page: 100,
|
|
413
|
-
});
|
|
414
|
-
const existing = comments.find((comment) => comment.body && comment.body.includes(marker));
|
|
415
|
-
if (existing) {
|
|
416
|
-
await github.rest.issues.updateComment({ ...context.repo, comment_id: existing.id, body });
|
|
417
|
-
} else {
|
|
418
|
-
await github.rest.issues.createComment({ ...context.repo, issue_number, body });
|
|
419
|
-
}
|
|
420
|
-
- name: Fail on StyleProof diff
|
|
421
|
-
# Exit 1 = reviewable diffs (gate red); 2 = error. Exit 3 (new surface,
|
|
422
|
-
# no baseline) never blocks — it has nothing to approve yet.
|
|
423
|
-
if: steps.diff.outputs.exit-code != '0' && steps.diff.outputs.exit-code != '3'
|
|
335
|
+
- name: Capture maps in CI on cache miss
|
|
336
|
+
if: steps.maps.outputs.capture-needed == 'true'
|
|
424
337
|
shell: bash
|
|
425
|
-
run:
|
|
338
|
+
run: |
|
|
339
|
+
BASE_SHA="\${{ github.event.pull_request.base.sha }}"
|
|
340
|
+
HEAD_SHA="\${{ github.event.pull_request.head.sha }}"
|
|
341
|
+
rm -rf __stylemaps__
|
|
342
|
+
|
|
343
|
+
git checkout "$BASE_SHA"
|
|
344
|
+
${PM.install}
|
|
345
|
+
if [ -f "${specPath}" ]; then
|
|
346
|
+
${PM.exec(`styleproof-map --spec ${specPath} --dir base --base-dir __stylemaps__ --keep-har --no-upload`)}
|
|
347
|
+
else
|
|
348
|
+
mkdir -p __stylemaps__/base
|
|
349
|
+
fi
|
|
350
|
+
|
|
351
|
+
git checkout "$HEAD_SHA"
|
|
352
|
+
${PM.install}
|
|
353
|
+
if find __stylemaps__/base -name '*.har' -print -quit | grep -q .; then
|
|
354
|
+
STYLEPROOF_REPLAY_FROM=__stylemaps__/base ${PM.exec(`styleproof-map --spec ${specPath} --dir head --base-dir __stylemaps__ --no-upload`)}
|
|
355
|
+
else
|
|
356
|
+
${PM.exec(`styleproof-map --spec ${specPath} --dir head --base-dir __stylemaps__ --no-upload`)}
|
|
357
|
+
fi
|
|
358
|
+
- uses: BenSheridanEdwards/StyleProof@v3
|
|
359
|
+
with:
|
|
360
|
+
baseline-dir: __stylemaps__/base
|
|
361
|
+
fresh-dir: __stylemaps__/head
|
|
362
|
+
require-approval: true
|
|
426
363
|
`;
|
|
427
364
|
|
|
428
365
|
function writeFileSafe(file, contents, { force: f } = {}) {
|
|
@@ -433,6 +370,15 @@ function writeFileSafe(file, contents, { force: f } = {}) {
|
|
|
433
370
|
return { wrote: true, exists };
|
|
434
371
|
}
|
|
435
372
|
|
|
373
|
+
function ensureGitignoreLine(line) {
|
|
374
|
+
const file = '.gitignore';
|
|
375
|
+
const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
|
|
376
|
+
if (existing.split(/\r?\n/).includes(line)) return false;
|
|
377
|
+
const prefix = existing && !existing.endsWith('\n') ? '\n' : '';
|
|
378
|
+
fs.writeFileSync(file, `${existing}${prefix}${line}\n`);
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
|
|
436
382
|
// Choose the scaffold: routes-aware when this is a Next.js app with discoverable
|
|
437
383
|
// routes, else the generic one-surface starter.
|
|
438
384
|
const routes = discoverNextRoutes(process.cwd());
|
|
@@ -467,58 +413,30 @@ if (fs.existsSync(configPath) || fs.existsSync('playwright.config.js')) {
|
|
|
467
413
|
wroteSomething = true;
|
|
468
414
|
}
|
|
469
415
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
fs.chmodSync(HOOK_PATH, 0o755);
|
|
474
|
-
console.log(`${hook.exists ? 'overwrote' : 'created'} ${HOOK_PATH} (pre-push capture + commit)`);
|
|
416
|
+
const ignored = ['.styleproof/', 'test-results/', 'playwright-report/'].filter((line) => ensureGitignoreLine(line));
|
|
417
|
+
if (ignored.length) {
|
|
418
|
+
console.log(`updated .gitignore (${ignored.join(', ')})`);
|
|
475
419
|
wroteSomething = true;
|
|
476
|
-
} else {
|
|
477
|
-
console.log(`${HOOK_PATH} already exists — left untouched (use --force to overwrite)`);
|
|
478
420
|
}
|
|
479
421
|
|
|
480
|
-
//
|
|
422
|
+
// Cache-first CI report — never overwrite an existing workflow.
|
|
481
423
|
const ci = writeFileSafe(CI_PATH, CI_WORKFLOW);
|
|
482
424
|
if (ci.wrote) {
|
|
483
|
-
console.log(`created ${CI_PATH} (
|
|
425
|
+
console.log(`created ${CI_PATH} (cache-first StyleProof report)`);
|
|
484
426
|
wroteSomething = true;
|
|
485
427
|
} else {
|
|
486
428
|
console.log(`${CI_PATH} already exists — left untouched`);
|
|
487
429
|
}
|
|
488
430
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
if (git(['config', 'core.hooksPath', HOOK_DIR]).status === 0) {
|
|
499
|
-
hookActivated = true;
|
|
500
|
-
console.log(`activated the pre-push hook (core.hooksPath → ${HOOK_DIR})`);
|
|
501
|
-
wroteSomething = true;
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
console.log('\nHow the gate works — capture local, CI just diffs:');
|
|
506
|
-
if (hookActivated) {
|
|
507
|
-
console.log(' 1. ✓ Pre-push hook is active — nothing to do.');
|
|
508
|
-
} else if (managesHooks) {
|
|
509
|
-
console.log(` 1. Your repo already manages git hooks (${existingHooksPath || '.husky'}), so the hook`);
|
|
510
|
-
console.log(` wasn't auto-activated. Point your hook runner at ${HOOK_DIR}/pre-push, or run:`);
|
|
511
|
-
console.log(` git config core.hooksPath ${HOOK_DIR}`);
|
|
512
|
-
} else {
|
|
513
|
-
console.log(' 1. Activate the pre-push hook (once per clone):');
|
|
514
|
-
console.log(` git config core.hooksPath ${HOOK_DIR}`);
|
|
515
|
-
}
|
|
516
|
-
console.log(' 2. Push your branch. The hook captures the computed-style map against a');
|
|
517
|
-
console.log(' PRODUCTION build, commits it (lean .json.gz under stylemaps/), and pushes it');
|
|
518
|
-
console.log(' with your branch — one `git push`, never two.');
|
|
519
|
-
console.log(' 3. On the PR, CI diffs your committed map against the base branch — no browser,');
|
|
520
|
-
console.log(' just a fast comparison of precomputed maps. An empty diff certifies no change.');
|
|
521
|
-
console.log(' So main always carries a base map and every PR is a quick diff, not a recapture.');
|
|
431
|
+
console.log('\nHow the gate works — local-first maps, CI report when cached:');
|
|
432
|
+
console.log(' 1. Commit your code, then run:');
|
|
433
|
+
console.log(' npx styleproof-map');
|
|
434
|
+
console.log(' It captures a production-build map into .styleproof/ and uploads the bundle');
|
|
435
|
+
console.log(' to the styleproof-maps branch when the git remote is available.');
|
|
436
|
+
console.log(' 2. Open the PR. CI restores the base/head bundles and generates the report');
|
|
437
|
+
console.log(' without a browser when both maps are present and compatible.');
|
|
438
|
+
console.log(' 3. If a bundle is missing or incompatible, CI recaptures both sides in the same');
|
|
439
|
+
console.log(' environment before generating the report. Correctness beats a stale cache.');
|
|
522
440
|
|
|
523
441
|
if (!wroteSomething) console.log('\nnothing to write — project already scaffolded.');
|
|
524
442
|
process.exit(0);
|
package/bin/styleproof-map.mjs
CHANGED
|
@@ -2,16 +2,17 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Capture the current branch's computed-style map.
|
|
4
4
|
*
|
|
5
|
-
* The zero-config path is the
|
|
5
|
+
* The zero-config path is the local-first cache flow scaffolded by styleproof-init:
|
|
6
6
|
* styleproof-map
|
|
7
7
|
*
|
|
8
8
|
* It runs Playwright against e2e/styleproof.spec.ts with:
|
|
9
9
|
* STYLEMAP_DIR=current
|
|
10
|
-
* STYLEPROOF_BASEDIR
|
|
11
|
-
* STYLEPROOF_SCREENSHOTS=
|
|
10
|
+
* STYLEPROOF_BASEDIR=.styleproof/maps
|
|
11
|
+
* STYLEPROOF_SCREENSHOTS=1
|
|
12
12
|
*/
|
|
13
13
|
import fs from 'node:fs';
|
|
14
14
|
import { spawnSync } from 'node:child_process';
|
|
15
|
+
import path from 'node:path';
|
|
15
16
|
import {
|
|
16
17
|
isHelpArg,
|
|
17
18
|
missingSpecMessage,
|
|
@@ -19,6 +20,19 @@ import {
|
|
|
19
20
|
showHelpAndExit,
|
|
20
21
|
unknownFlagMessage,
|
|
21
22
|
} from '../dist/cli-errors.js';
|
|
23
|
+
import {
|
|
24
|
+
DEFAULT_MAP_DIR,
|
|
25
|
+
DEFAULT_MAP_LABEL,
|
|
26
|
+
DEFAULT_MAP_STORE_BRANCH,
|
|
27
|
+
DEFAULT_REMOTE,
|
|
28
|
+
MapStoreError,
|
|
29
|
+
currentGitSha,
|
|
30
|
+
expectedCompatibilityKey,
|
|
31
|
+
publishMapBundle,
|
|
32
|
+
restoreMapBundle,
|
|
33
|
+
workingTreeDirty,
|
|
34
|
+
writeMapManifest,
|
|
35
|
+
} from '../dist/map-store.js';
|
|
22
36
|
|
|
23
37
|
const HELP = `styleproof-map — capture this branch's computed-style map
|
|
24
38
|
|
|
@@ -26,25 +40,39 @@ usage: styleproof-map [options] [-- <playwright args>]
|
|
|
26
40
|
|
|
27
41
|
options:
|
|
28
42
|
--spec <path> StyleProof spec that must exist (default: e2e/styleproof.spec.ts)
|
|
29
|
-
--dir <label> output label under --base-dir (default:
|
|
30
|
-
--base-dir <path> output root directory (default:
|
|
31
|
-
--screenshots keep screenshots for reports (default
|
|
32
|
-
--no-screenshots write lean .json.gz maps only
|
|
43
|
+
--dir <label> output label under --base-dir (default: ${DEFAULT_MAP_LABEL})
|
|
44
|
+
--base-dir <path> output root directory (default: ${DEFAULT_MAP_DIR})
|
|
45
|
+
--screenshots keep screenshots for reports (default)
|
|
46
|
+
--no-screenshots write lean .json.gz maps only
|
|
33
47
|
--keep-har keep recorded HAR files for advanced replay workflows
|
|
48
|
+
--sha <commit> commit SHA this map belongs to (default: current HEAD)
|
|
49
|
+
--upload require upload to the map store branch after capture
|
|
50
|
+
--no-upload capture locally only (default in CI)
|
|
51
|
+
--restore restore a map from the map store instead of capturing
|
|
52
|
+
--cache-branch <b> map store branch (default: ${DEFAULT_MAP_STORE_BRANCH})
|
|
53
|
+
--remote <name> git remote for the map store (default: ${DEFAULT_REMOTE})
|
|
34
54
|
-h, --help show this help
|
|
35
55
|
|
|
36
56
|
Examples:
|
|
37
57
|
styleproof-map
|
|
58
|
+
styleproof-map --upload
|
|
59
|
+
styleproof-map --restore --sha 0123abcd --dir head --base-dir __stylemaps__
|
|
38
60
|
styleproof-map --spec e2e/styleproof.spec.ts
|
|
39
|
-
styleproof-map --dir review --base-dir __stylemaps__ --
|
|
61
|
+
styleproof-map --dir review --base-dir __stylemaps__ --keep-har --no-upload
|
|
40
62
|
`;
|
|
41
63
|
|
|
42
64
|
const argv = process.argv.slice(2);
|
|
43
65
|
let spec = 'e2e/styleproof.spec.ts';
|
|
44
|
-
let dir = process.env.STYLEMAP_DIR ??
|
|
45
|
-
let baseDir = process.env.STYLEPROOF_BASEDIR ??
|
|
46
|
-
let screenshots = process.env.STYLEPROOF_SCREENSHOTS ?? '
|
|
66
|
+
let dir = process.env.STYLEMAP_DIR ?? DEFAULT_MAP_LABEL;
|
|
67
|
+
let baseDir = process.env.STYLEPROOF_BASEDIR ?? DEFAULT_MAP_DIR;
|
|
68
|
+
let screenshots = process.env.STYLEPROOF_SCREENSHOTS ?? '1';
|
|
47
69
|
let keepHar = process.env.STYLEPROOF_KEEP_HAR === '1';
|
|
70
|
+
let sha = process.env.STYLEPROOF_SHA ?? '';
|
|
71
|
+
let restore = false;
|
|
72
|
+
let cacheBranch = process.env.STYLEPROOF_CACHE_BRANCH ?? DEFAULT_MAP_STORE_BRANCH;
|
|
73
|
+
let remote = process.env.STYLEPROOF_REMOTE ?? DEFAULT_REMOTE;
|
|
74
|
+
let uploadMode =
|
|
75
|
+
process.env.STYLEPROOF_UPLOAD === '1' ? 'required' : process.env.STYLEPROOF_UPLOAD === '0' ? 'off' : 'auto';
|
|
48
76
|
const playwrightArgs = [];
|
|
49
77
|
|
|
50
78
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -62,6 +90,15 @@ for (let i = 0; i < argv.length; i++) {
|
|
|
62
90
|
else if (a === '--screenshots') screenshots = '1';
|
|
63
91
|
else if (a === '--no-screenshots') screenshots = '0';
|
|
64
92
|
else if (a === '--keep-har') keepHar = true;
|
|
93
|
+
else if (a === '--sha') sha = argv[++i];
|
|
94
|
+
else if (a.startsWith('--sha=')) sha = a.slice(6);
|
|
95
|
+
else if (a === '--upload') uploadMode = 'required';
|
|
96
|
+
else if (a === '--no-upload') uploadMode = 'off';
|
|
97
|
+
else if (a === '--restore') restore = true;
|
|
98
|
+
else if (a === '--cache-branch') cacheBranch = argv[++i];
|
|
99
|
+
else if (a.startsWith('--cache-branch=')) cacheBranch = a.slice(15);
|
|
100
|
+
else if (a === '--remote') remote = argv[++i];
|
|
101
|
+
else if (a.startsWith('--remote=')) remote = a.slice(9);
|
|
65
102
|
else if (a.startsWith('--')) {
|
|
66
103
|
console.error(unknownFlagMessage('styleproof-map', a));
|
|
67
104
|
process.exit(2);
|
|
@@ -86,6 +123,14 @@ if (!fs.existsSync(spec)) {
|
|
|
86
123
|
console.error(missingSpecMessage(spec));
|
|
87
124
|
process.exit(2);
|
|
88
125
|
}
|
|
126
|
+
if (restore && !sha) {
|
|
127
|
+
try {
|
|
128
|
+
sha = currentGitSha(process.cwd());
|
|
129
|
+
} catch (e) {
|
|
130
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
131
|
+
process.exit(2);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
89
134
|
|
|
90
135
|
function removeHarFiles(root) {
|
|
91
136
|
if (!fs.existsSync(root)) return;
|
|
@@ -96,6 +141,63 @@ function removeHarFiles(root) {
|
|
|
96
141
|
}
|
|
97
142
|
}
|
|
98
143
|
|
|
144
|
+
function shouldAutoUpload() {
|
|
145
|
+
return uploadMode === 'auto' && !process.env.CI;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function upload(dirPath) {
|
|
149
|
+
if (uploadMode === 'off') return;
|
|
150
|
+
if (!shouldAutoUpload() && uploadMode !== 'required') return;
|
|
151
|
+
try {
|
|
152
|
+
const res = publishMapBundle({ dir: dirPath, branch: cacheBranch, remote });
|
|
153
|
+
console.error(`styleproof-map: uploaded ${res.sha.slice(0, 12)} (${res.compatibilityKey}) to ${res.branch}`);
|
|
154
|
+
} catch (e) {
|
|
155
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
156
|
+
if (uploadMode === 'required') {
|
|
157
|
+
console.error(`styleproof-map: upload failed\n${message}`);
|
|
158
|
+
process.exit(2);
|
|
159
|
+
}
|
|
160
|
+
if (e instanceof MapStoreError) {
|
|
161
|
+
console.error(`styleproof-map: map captured locally; upload skipped (${message})`);
|
|
162
|
+
} else {
|
|
163
|
+
console.error(`styleproof-map: map captured locally; upload skipped`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const targetDir = path.join(baseDir, dir);
|
|
169
|
+
let dirtyBeforeCapture;
|
|
170
|
+
try {
|
|
171
|
+
dirtyBeforeCapture = workingTreeDirty(process.cwd());
|
|
172
|
+
} catch {
|
|
173
|
+
dirtyBeforeCapture = false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (restore) {
|
|
177
|
+
try {
|
|
178
|
+
const compatibilityKey = expectedCompatibilityKey({ spec });
|
|
179
|
+
const manifest = restoreMapBundle({
|
|
180
|
+
sha,
|
|
181
|
+
outDir: targetDir,
|
|
182
|
+
branch: cacheBranch,
|
|
183
|
+
remote,
|
|
184
|
+
compatibilityKey,
|
|
185
|
+
});
|
|
186
|
+
console.log(`styleproof-map: restored ${manifest.sha.slice(0, 12)} (${manifest.compatibilityKey}) to ${targetDir}`);
|
|
187
|
+
process.exit(0);
|
|
188
|
+
} catch (e) {
|
|
189
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
190
|
+
console.error(
|
|
191
|
+
[
|
|
192
|
+
`styleproof-map: could not restore ${sha} from ${cacheBranch}`,
|
|
193
|
+
message,
|
|
194
|
+
`Next: run styleproof-map at that commit to build/upload the map, or let CI recapture both sides.`,
|
|
195
|
+
].join('\n'),
|
|
196
|
+
);
|
|
197
|
+
process.exit(2);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
99
201
|
const command = process.platform === 'win32' ? 'playwright.cmd' : 'playwright';
|
|
100
202
|
const env = {
|
|
101
203
|
...process.env,
|
|
@@ -112,5 +214,30 @@ if (result.error) {
|
|
|
112
214
|
process.exit(2);
|
|
113
215
|
}
|
|
114
216
|
const status = result.status ?? 1;
|
|
115
|
-
if (status === 0
|
|
217
|
+
if (status === 0) {
|
|
218
|
+
if (!keepHar) removeHarFiles(targetDir);
|
|
219
|
+
try {
|
|
220
|
+
let manifestSha = sha || undefined;
|
|
221
|
+
if (!manifestSha) {
|
|
222
|
+
try {
|
|
223
|
+
manifestSha = currentGitSha(process.cwd(), env);
|
|
224
|
+
} catch {
|
|
225
|
+
manifestSha = 'local';
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const manifest = writeMapManifest({
|
|
229
|
+
dir: targetDir,
|
|
230
|
+
spec,
|
|
231
|
+
sha: manifestSha,
|
|
232
|
+
screenshots: screenshots !== '0',
|
|
233
|
+
dirty: dirtyBeforeCapture,
|
|
234
|
+
env,
|
|
235
|
+
});
|
|
236
|
+
console.error(`styleproof-map: wrote ${targetDir} for ${manifest.sha.slice(0, 12)} (${manifest.compatibilityKey})`);
|
|
237
|
+
} catch (e) {
|
|
238
|
+
console.error(`styleproof-map: could not write map manifest\n${e instanceof Error ? e.message : String(e)}`);
|
|
239
|
+
process.exit(2);
|
|
240
|
+
}
|
|
241
|
+
upload(targetDir);
|
|
242
|
+
}
|
|
116
243
|
process.exit(status);
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* region, plus the exact property changes, as markdown ready for a PR
|
|
5
5
|
* comment.
|
|
6
6
|
*
|
|
7
|
+
* styleproof-report [baseRef] --out <dir> [options] # cached map store
|
|
7
8
|
* styleproof-report <beforeDir> <afterDir> --out <dir> [options]
|
|
8
9
|
*
|
|
9
10
|
* Both capture dirs need the .json.gz maps; side-by-side images additionally
|
|
@@ -11,23 +12,28 @@
|
|
|
11
12
|
* Exit code 0 = no changes, 1 = report generated, 2 = usage error.
|
|
12
13
|
*/
|
|
13
14
|
import { generateStyleMapReport } from '../dist/report.js';
|
|
14
|
-
import { cleanupBaseRefCaptureDirs, resolveBaseRefCaptureDirs } from '../dist/cli-base-ref.js';
|
|
15
15
|
import { cliErrorMessage, isHelpArg, showHelpAndExit, unknownFlagMessage } from '../dist/cli-errors.js';
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_MAP_STORE_BRANCH,
|
|
18
|
+
DEFAULT_REMOTE,
|
|
19
|
+
assertCompatibleMapDirs,
|
|
20
|
+
cleanupCachedCaptureDirs,
|
|
21
|
+
resolveCachedCaptureDirs,
|
|
22
|
+
} from '../dist/map-store.js';
|
|
16
23
|
|
|
17
24
|
const COMMAND = 'styleproof-report';
|
|
18
|
-
const DEFAULT_MAPS_DIR = 'stylemaps/current';
|
|
19
25
|
|
|
20
26
|
const HELP = `${COMMAND} — reviewable before/after report from two captures
|
|
21
27
|
|
|
22
28
|
usage: ${COMMAND} [baseRef] [options]
|
|
23
|
-
${COMMAND} --base-ref <gitref> [mapsDir] [options]
|
|
24
29
|
${COMMAND} <beforeDir> <afterDir> [options]
|
|
25
30
|
|
|
26
31
|
options:
|
|
27
|
-
--
|
|
28
|
-
|
|
29
|
-
--
|
|
30
|
-
|
|
32
|
+
--spec <path> StyleProof spec used to select compatible cached maps
|
|
33
|
+
(default: e2e/styleproof.spec.ts)
|
|
34
|
+
--cache-branch <b> map store branch for default cached-map mode
|
|
35
|
+
(default: ${DEFAULT_MAP_STORE_BRANCH})
|
|
36
|
+
--remote <name> git remote for the map store (default: ${DEFAULT_REMOTE})
|
|
31
37
|
--out <dir> output directory (default: styleproof-report)
|
|
32
38
|
--image-base-url <url> prefix for image URLs in report.md (default: relative)
|
|
33
39
|
--pad <px> padding around changed rects when cropping (default: 24)
|
|
@@ -58,8 +64,9 @@ let minWidth;
|
|
|
58
64
|
let minHeight;
|
|
59
65
|
let includeLayoutNoise = false;
|
|
60
66
|
let includeContent = false;
|
|
61
|
-
let
|
|
62
|
-
let
|
|
67
|
+
let spec = 'e2e/styleproof.spec.ts';
|
|
68
|
+
let cacheBranch = process.env.STYLEPROOF_CACHE_BRANCH ?? DEFAULT_MAP_STORE_BRANCH;
|
|
69
|
+
let remote = process.env.STYLEPROOF_REMOTE ?? DEFAULT_REMOTE;
|
|
63
70
|
for (let i = 0; i < argv.length; i++) {
|
|
64
71
|
const a = argv[i];
|
|
65
72
|
if (isHelpArg(a)) showHelpAndExit(HELP);
|
|
@@ -81,10 +88,12 @@ for (let i = 0; i < argv.length; i++) {
|
|
|
81
88
|
else if (a.startsWith('--include-layout-noise=')) includeLayoutNoise = a.slice(23) !== 'false';
|
|
82
89
|
else if (a === '--include-content') includeContent = true;
|
|
83
90
|
else if (a.startsWith('--include-content=')) includeContent = a.slice(18) !== 'false';
|
|
84
|
-
else if (a === '--
|
|
85
|
-
else if (a.startsWith('--
|
|
86
|
-
else if (a === '--
|
|
87
|
-
else if (a.startsWith('--
|
|
91
|
+
else if (a === '--spec') spec = argv[++i];
|
|
92
|
+
else if (a.startsWith('--spec=')) spec = a.slice(7);
|
|
93
|
+
else if (a === '--cache-branch') cacheBranch = argv[++i];
|
|
94
|
+
else if (a.startsWith('--cache-branch=')) cacheBranch = a.slice(15);
|
|
95
|
+
else if (a === '--remote') remote = argv[++i];
|
|
96
|
+
else if (a.startsWith('--remote=')) remote = a.slice(9);
|
|
88
97
|
else if (a.startsWith('--')) {
|
|
89
98
|
console.error(unknownFlagMessage(COMMAND, a));
|
|
90
99
|
process.exit(2);
|
|
@@ -92,22 +101,30 @@ for (let i = 0; i < argv.length; i++) {
|
|
|
92
101
|
}
|
|
93
102
|
let beforeDir;
|
|
94
103
|
let afterDir;
|
|
95
|
-
let
|
|
96
|
-
if (
|
|
104
|
+
let cacheCapture = null;
|
|
105
|
+
if (args.length <= 1) {
|
|
97
106
|
try {
|
|
98
|
-
|
|
107
|
+
cacheCapture = resolveCachedCaptureDirs({
|
|
99
108
|
command: COMMAND,
|
|
100
|
-
baseRef,
|
|
101
|
-
mapsDir,
|
|
102
109
|
args,
|
|
103
|
-
|
|
110
|
+
spec,
|
|
111
|
+
branch: cacheBranch,
|
|
112
|
+
remote,
|
|
113
|
+
baseUrl: process.env.BASE_URL,
|
|
114
|
+
usage: 'usage: styleproof-report [baseRef] [--out <dir>] [options]',
|
|
104
115
|
});
|
|
116
|
+
beforeDir = cacheCapture.beforeDir;
|
|
117
|
+
afterDir = cacheCapture.afterDir;
|
|
105
118
|
} catch (e) {
|
|
106
|
-
console.error(
|
|
119
|
+
console.error(
|
|
120
|
+
[
|
|
121
|
+
`${COMMAND}: cached maps are not available for this report`,
|
|
122
|
+
cliErrorMessage(e),
|
|
123
|
+
'Next: run styleproof-map on the base and head commits to upload maps, or let CI recapture both sides.',
|
|
124
|
+
].join('\n'),
|
|
125
|
+
);
|
|
107
126
|
process.exit(2);
|
|
108
127
|
}
|
|
109
|
-
beforeDir = baseCapture.beforeDir;
|
|
110
|
-
afterDir = baseCapture.afterDir;
|
|
111
128
|
} else {
|
|
112
129
|
if (args.length !== 2) {
|
|
113
130
|
console.error('usage: styleproof-report <beforeDir> <afterDir> --out <dir> [options] (--help for all options)');
|
|
@@ -135,6 +152,7 @@ if (foldDetailsAt !== undefined && Number.isNaN(foldDetailsAt)) {
|
|
|
135
152
|
|
|
136
153
|
let result;
|
|
137
154
|
try {
|
|
155
|
+
assertCompatibleMapDirs(beforeDir, afterDir);
|
|
138
156
|
result = generateStyleMapReport({
|
|
139
157
|
beforeDir,
|
|
140
158
|
afterDir,
|
|
@@ -152,7 +170,7 @@ try {
|
|
|
152
170
|
console.error(e.message);
|
|
153
171
|
process.exit(2);
|
|
154
172
|
} finally {
|
|
155
|
-
|
|
173
|
+
cleanupCachedCaptureDirs(cacheCapture);
|
|
156
174
|
}
|
|
157
175
|
|
|
158
176
|
const newNote = result.newSurfaces ? ` (+${result.newSurfaces} new surface(s) with no baseline)` : '';
|
package/dist/capture.d.ts
CHANGED
|
@@ -58,7 +58,7 @@ export type ElementEntry = {
|
|
|
58
58
|
export type CaptureMetadata = {
|
|
59
59
|
surfaceKey?: string;
|
|
60
60
|
variantKey?: string;
|
|
61
|
-
variantKind?: 'variant' | 'live-state';
|
|
61
|
+
variantKind?: 'variant' | 'live-state' | 'popup';
|
|
62
62
|
};
|
|
63
63
|
export type LiveRegionCandidate = {
|
|
64
64
|
path: string;
|