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/dist/cli-errors.d.ts
CHANGED
|
@@ -7,4 +7,3 @@ export declare function playwrightMissingMessage(message: string): string;
|
|
|
7
7
|
export declare function missingWorkingMapsMessage(command: string, dir: string): string;
|
|
8
8
|
export declare function missingManualCaptureMessage(command: string, dir: string): string;
|
|
9
9
|
export declare function baseInferenceMessage(command: string, message: string): string;
|
|
10
|
-
export declare function baseMapsMessage(command: string, message: string, baseRef: string, mapsDir: string): string;
|
package/dist/cli-errors.js
CHANGED
|
@@ -26,8 +26,8 @@ export function playwrightMissingMessage(message) {
|
|
|
26
26
|
export function missingWorkingMapsMessage(command, dir) {
|
|
27
27
|
return [
|
|
28
28
|
`${command}: no capture at ${dir}`,
|
|
29
|
-
`Next: run styleproof-map to create ${dir},
|
|
30
|
-
'If your maps live elsewhere, pass
|
|
29
|
+
`Next: run styleproof-map to create ${dir}, or run styleproof-map --restore --sha <commit> to restore it from the map store.`,
|
|
30
|
+
'If your maps live elsewhere, pass explicit capture directories.',
|
|
31
31
|
].join('\n');
|
|
32
32
|
}
|
|
33
33
|
export function missingManualCaptureMessage(command, dir) {
|
|
@@ -42,9 +42,3 @@ export function baseInferenceMessage(command, message) {
|
|
|
42
42
|
`Next: pass a base ref explicitly, e.g. ${command} main, or set git config branch.<name>.gh-merge-base main.`,
|
|
43
43
|
].join('\n');
|
|
44
44
|
}
|
|
45
|
-
export function baseMapsMessage(command, message, baseRef, mapsDir) {
|
|
46
|
-
return [
|
|
47
|
-
`${command}: ${message}`,
|
|
48
|
-
`Next: make sure ${baseRef} contains committed captures at ${mapsDir}. On the base branch, run styleproof-map and commit ${mapsDir}.`,
|
|
49
|
-
].join('\n');
|
|
50
|
-
}
|
package/dist/diff.js
CHANGED
|
@@ -153,7 +153,7 @@ function tallyCounts(findings, counts) {
|
|
|
153
153
|
function indexDir(dir) {
|
|
154
154
|
return Object.fromEntries(fs
|
|
155
155
|
.readdirSync(dir)
|
|
156
|
-
.filter((f) => /\.json(\.gz)?$/.test(f))
|
|
156
|
+
.filter((f) => f !== 'styleproof-manifest.json' && /\.json(\.gz)?$/.test(f))
|
|
157
157
|
.map((f) => [f.replace(/\.json(\.gz)?$/, ''), path.join(dir, f)]));
|
|
158
158
|
}
|
|
159
159
|
/** Diff every same-named capture between two directories. `volatile` is the
|
package/dist/gitref.d.ts
CHANGED
|
@@ -1,15 +1,5 @@
|
|
|
1
|
-
/** A readable failure
|
|
1
|
+
/** A readable failure inferring a base branch — CLIs map this to exit 2. */
|
|
2
2
|
export declare class GitRefError extends Error {
|
|
3
3
|
}
|
|
4
|
-
/** Infer
|
|
4
|
+
/** Infer a PR/base ref for local and GitHub Actions CLI runs. */
|
|
5
5
|
export declare function inferBaseRef(env?: NodeJS.ProcessEnv): string;
|
|
6
|
-
/**
|
|
7
|
-
* Materialise the captures committed at `dir` as of `ref` into a fresh temp dir,
|
|
8
|
-
* so a diff/report can use e.g. `main`'s committed map as the base with no checkout
|
|
9
|
-
* and no recapture — the "base map lives on main, CI just diffs" model.
|
|
10
|
-
*
|
|
11
|
-
* Reads purely through git (`ls-tree`/`show`, no `tar`/deps); binary `.json.gz`
|
|
12
|
-
* bytes come straight from `git show`. The caller owns cleanup of the returned dir.
|
|
13
|
-
* Throws {@link GitRefError} (never exits) so it's reusable from any entry point.
|
|
14
|
-
*/
|
|
15
|
-
export declare function materializeRef(ref: string, dir: string): string;
|
package/dist/gitref.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
-
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
/** A readable failure materialising a base from git — CLIs map this to exit 2. */
|
|
2
|
+
/** A readable failure inferring a base branch — CLIs map this to exit 2. */
|
|
6
3
|
export class GitRefError extends Error {
|
|
7
4
|
}
|
|
8
5
|
function git(args) {
|
|
@@ -29,7 +26,7 @@ function ghPrBaseRef() {
|
|
|
29
26
|
});
|
|
30
27
|
return r.status === 0 ? r.stdout.trim() : '';
|
|
31
28
|
}
|
|
32
|
-
/** Infer
|
|
29
|
+
/** Infer a PR/base ref for local and GitHub Actions CLI runs. */
|
|
33
30
|
export function inferBaseRef(env = process.env) {
|
|
34
31
|
if (env.GITHUB_BASE_REF) {
|
|
35
32
|
return baseRefCandidate(env.GITHUB_BASE_REF);
|
|
@@ -49,40 +46,3 @@ export function inferBaseRef(env = process.env) {
|
|
|
49
46
|
return fallback;
|
|
50
47
|
throw new GitRefError('could not infer a base branch (tried GITHUB_BASE_REF, branch.<name>.gh-merge-base, gh pr view, origin/main, origin/master, main, master)');
|
|
51
48
|
}
|
|
52
|
-
/**
|
|
53
|
-
* Materialise the captures committed at `dir` as of `ref` into a fresh temp dir,
|
|
54
|
-
* so a diff/report can use e.g. `main`'s committed map as the base with no checkout
|
|
55
|
-
* and no recapture — the "base map lives on main, CI just diffs" model.
|
|
56
|
-
*
|
|
57
|
-
* Reads purely through git (`ls-tree`/`show`, no `tar`/deps); binary `.json.gz`
|
|
58
|
-
* bytes come straight from `git show`. The caller owns cleanup of the returned dir.
|
|
59
|
-
* Throws {@link GitRefError} (never exits) so it's reusable from any entry point.
|
|
60
|
-
*/
|
|
61
|
-
export function materializeRef(ref, dir) {
|
|
62
|
-
const top = git(['rev-parse', '--show-toplevel']);
|
|
63
|
-
if (top.status !== 0)
|
|
64
|
-
throw new GitRefError('must run inside a git repository');
|
|
65
|
-
const toplevel = top.stdout.trim();
|
|
66
|
-
// -z: NUL-separated, unquoted paths (handles spaces/unicode).
|
|
67
|
-
const ls = spawnSync('git', ['ls-tree', '-r', '-z', '--name-only', ref, '--', dir], {
|
|
68
|
-
encoding: 'utf8',
|
|
69
|
-
maxBuffer: 1 << 28,
|
|
70
|
-
});
|
|
71
|
-
if (ls.status !== 0)
|
|
72
|
-
throw new GitRefError(`cannot read ${dir} at ${ref}: ${(ls.stderr || '').trim()}`);
|
|
73
|
-
const files = ls.stdout.split('\0').filter(Boolean);
|
|
74
|
-
if (files.length === 0) {
|
|
75
|
-
throw new GitRefError(`no committed captures at ${dir} in ${ref} — commit the maps so the base lives in git`);
|
|
76
|
-
}
|
|
77
|
-
const dirRepoRel = path.relative(toplevel, path.resolve(dir));
|
|
78
|
-
const dest = fs.mkdtempSync(path.join(os.tmpdir(), 'styleproof-baseref-'));
|
|
79
|
-
for (const f of files) {
|
|
80
|
-
const show = spawnSync('git', ['show', `${ref}:${f}`], { maxBuffer: 1 << 28 }); // buffer — preserves .json.gz
|
|
81
|
-
if (show.status !== 0)
|
|
82
|
-
throw new GitRefError(`cannot read ${f} at ${ref}`);
|
|
83
|
-
const out = path.join(dest, path.relative(dirRepoRel, f));
|
|
84
|
-
fs.mkdirSync(path.dirname(out), { recursive: true });
|
|
85
|
-
fs.writeFileSync(out, show.stdout);
|
|
86
|
-
}
|
|
87
|
-
return dest;
|
|
88
|
-
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { captureStyleMap, saveStyleMap, loadStyleMap, trackInflightRequests } from './capture.js';
|
|
2
2
|
export type { StyleMap, CaptureOptions, CaptureMetadata, ElementEntry, LiveRegionCandidate, Rect } from './capture.js';
|
|
3
3
|
export { defineStyleMapCapture, defineCrawlCapture } from './runner.js';
|
|
4
|
-
export type { Surface, SurfaceLiveState, SurfaceVariant, DefineOptions, CrawlOptions } from './runner.js';
|
|
4
|
+
export type { Surface, SurfaceLiveState, SurfaceVariant, PopupCaptureOptions, DefineOptions, CrawlOptions, } from './runner.js';
|
|
5
5
|
export { coverageGaps } from './coverage.js';
|
|
6
6
|
export type { CoverageGaps } from './coverage.js';
|
|
7
7
|
export { detectViewportWidths, mediaTextWidthBoundaries, widthsFromBoundaries } from './breakpoints.js';
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export declare const DEFAULT_MAP_DIR = ".styleproof/maps";
|
|
2
|
+
export declare const DEFAULT_MAP_LABEL = "current";
|
|
3
|
+
export declare const DEFAULT_MAP_STORE_BRANCH = "styleproof-maps";
|
|
4
|
+
export declare const DEFAULT_REMOTE = "origin";
|
|
5
|
+
export declare const MAP_MANIFEST = "styleproof-manifest.json";
|
|
6
|
+
export declare class MapStoreError extends Error {
|
|
7
|
+
}
|
|
8
|
+
export interface MapManifest {
|
|
9
|
+
version: 1;
|
|
10
|
+
packageVersion: string;
|
|
11
|
+
sha: string;
|
|
12
|
+
dirty: boolean;
|
|
13
|
+
spec: string;
|
|
14
|
+
specHash: string;
|
|
15
|
+
lockfile?: string;
|
|
16
|
+
lockfileHash?: string;
|
|
17
|
+
playwrightVersion?: string;
|
|
18
|
+
platform: string;
|
|
19
|
+
arch: string;
|
|
20
|
+
nodeMajor: string;
|
|
21
|
+
baseUrl?: string;
|
|
22
|
+
screenshots: boolean;
|
|
23
|
+
har: boolean;
|
|
24
|
+
compatibilityKey: string;
|
|
25
|
+
createdAt: string;
|
|
26
|
+
}
|
|
27
|
+
export interface CachedCaptureDirs {
|
|
28
|
+
beforeDir: string;
|
|
29
|
+
afterDir: string;
|
|
30
|
+
baseRef: string;
|
|
31
|
+
baseSha: string;
|
|
32
|
+
headSha: string;
|
|
33
|
+
compatibilityKey: string;
|
|
34
|
+
tmpRoot: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function expectedCompatibilityKey(options?: {
|
|
37
|
+
cwd?: string;
|
|
38
|
+
spec?: string;
|
|
39
|
+
baseUrl?: string;
|
|
40
|
+
}): string;
|
|
41
|
+
export declare function currentGitSha(cwd?: string, env?: NodeJS.ProcessEnv): string;
|
|
42
|
+
export declare function refSha(ref: string, cwd?: string): string;
|
|
43
|
+
export declare function workingTreeDirty(cwd?: string): boolean;
|
|
44
|
+
export declare function remoteExists(remote?: string, cwd?: string): boolean;
|
|
45
|
+
export declare function writeMapManifest(options: {
|
|
46
|
+
dir: string;
|
|
47
|
+
spec: string;
|
|
48
|
+
sha?: string;
|
|
49
|
+
screenshots: boolean;
|
|
50
|
+
dirty?: boolean;
|
|
51
|
+
cwd?: string;
|
|
52
|
+
env?: NodeJS.ProcessEnv;
|
|
53
|
+
}): MapManifest;
|
|
54
|
+
export declare function readMapManifest(dir: string): MapManifest | null;
|
|
55
|
+
export declare function assertCompatibleMapDirs(beforeDir: string, afterDir: string): void;
|
|
56
|
+
export declare function publishMapBundle(options: {
|
|
57
|
+
dir: string;
|
|
58
|
+
branch?: string;
|
|
59
|
+
remote?: string;
|
|
60
|
+
cwd?: string;
|
|
61
|
+
includeHar?: boolean;
|
|
62
|
+
}): {
|
|
63
|
+
sha: string;
|
|
64
|
+
compatibilityKey: string;
|
|
65
|
+
branch: string;
|
|
66
|
+
};
|
|
67
|
+
export declare function restoreMapBundle(options: {
|
|
68
|
+
sha: string;
|
|
69
|
+
outDir: string;
|
|
70
|
+
branch?: string;
|
|
71
|
+
remote?: string;
|
|
72
|
+
cwd?: string;
|
|
73
|
+
compatibilityKey?: string;
|
|
74
|
+
}): MapManifest;
|
|
75
|
+
export declare function resolveCachedCaptureDirs(options: {
|
|
76
|
+
command: string;
|
|
77
|
+
args: string[];
|
|
78
|
+
spec: string;
|
|
79
|
+
branch?: string;
|
|
80
|
+
remote?: string;
|
|
81
|
+
cwd?: string;
|
|
82
|
+
baseUrl?: string;
|
|
83
|
+
usage: string;
|
|
84
|
+
}): CachedCaptureDirs;
|
|
85
|
+
export declare function cleanupCachedCaptureDirs(captureDirs: CachedCaptureDirs | null): void;
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { inferBaseRef } from './gitref.js';
|
|
8
|
+
export const DEFAULT_MAP_DIR = '.styleproof/maps';
|
|
9
|
+
export const DEFAULT_MAP_LABEL = 'current';
|
|
10
|
+
export const DEFAULT_MAP_STORE_BRANCH = 'styleproof-maps';
|
|
11
|
+
export const DEFAULT_REMOTE = 'origin';
|
|
12
|
+
export const MAP_MANIFEST = 'styleproof-manifest.json';
|
|
13
|
+
export class MapStoreError extends Error {
|
|
14
|
+
}
|
|
15
|
+
function runGit(cwd, args, maxBuffer = 1 << 28) {
|
|
16
|
+
return spawnSync('git', args, { cwd, encoding: 'utf8', maxBuffer });
|
|
17
|
+
}
|
|
18
|
+
function gitOutput(cwd, args) {
|
|
19
|
+
const r = runGit(cwd, args);
|
|
20
|
+
return r.status === 0 ? r.stdout.trim() : '';
|
|
21
|
+
}
|
|
22
|
+
function hash(input) {
|
|
23
|
+
return createHash('sha256').update(input).digest('hex');
|
|
24
|
+
}
|
|
25
|
+
function hashFile(file) {
|
|
26
|
+
try {
|
|
27
|
+
return hash(fs.readFileSync(file));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function styleProofPackageVersion() {
|
|
34
|
+
try {
|
|
35
|
+
const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
36
|
+
return pkg.version ?? 'unknown';
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return 'unknown';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function consumerRequire(cwd) {
|
|
43
|
+
return createRequire(path.join(cwd, 'package.json'));
|
|
44
|
+
}
|
|
45
|
+
function playwrightVersion(cwd) {
|
|
46
|
+
try {
|
|
47
|
+
const pkgPath = consumerRequire(cwd).resolve('@playwright/test/package.json');
|
|
48
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
49
|
+
return pkg.version;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function detectLockfile(cwd) {
|
|
56
|
+
for (const file of ['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lock', 'bun.lockb']) {
|
|
57
|
+
const full = path.join(cwd, file);
|
|
58
|
+
const h = hashFile(full);
|
|
59
|
+
if (h)
|
|
60
|
+
return { file, hash: h };
|
|
61
|
+
}
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
function hasHar(dir) {
|
|
65
|
+
if (!fs.existsSync(dir))
|
|
66
|
+
return false;
|
|
67
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
68
|
+
const full = path.join(dir, entry.name);
|
|
69
|
+
if (entry.isDirectory() && hasHar(full))
|
|
70
|
+
return true;
|
|
71
|
+
if (entry.isFile() && entry.name.endsWith('.har'))
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
function compatibilityInput(options) {
|
|
77
|
+
const specPath = path.resolve(options.cwd, options.spec);
|
|
78
|
+
const lock = detectLockfile(options.cwd);
|
|
79
|
+
return {
|
|
80
|
+
packageVersion: styleProofPackageVersion(),
|
|
81
|
+
spec: path.relative(options.cwd, specPath) || options.spec,
|
|
82
|
+
specHash: hashFile(specPath) ?? 'missing',
|
|
83
|
+
lockfile: lock.file,
|
|
84
|
+
lockfileHash: lock.hash,
|
|
85
|
+
playwrightVersion: playwrightVersion(options.cwd),
|
|
86
|
+
platform: process.platform,
|
|
87
|
+
arch: process.arch,
|
|
88
|
+
nodeMajor: process.versions.node.split('.')[0] ?? process.versions.node,
|
|
89
|
+
baseUrl: options.baseUrl,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export function expectedCompatibilityKey(options = {}) {
|
|
93
|
+
return hash(JSON.stringify(compatibilityInput({
|
|
94
|
+
cwd: options.cwd ?? process.cwd(),
|
|
95
|
+
spec: options.spec ?? 'e2e/styleproof.spec.ts',
|
|
96
|
+
baseUrl: options.baseUrl,
|
|
97
|
+
}))).slice(0, 16);
|
|
98
|
+
}
|
|
99
|
+
export function currentGitSha(cwd = process.cwd(), env = process.env) {
|
|
100
|
+
const fromEnv = env.GITHUB_SHA || env.GITHUB_HEAD_SHA;
|
|
101
|
+
if (fromEnv && /^[0-9a-f]{7,40}$/i.test(fromEnv))
|
|
102
|
+
return fromEnv;
|
|
103
|
+
const sha = gitOutput(cwd, ['rev-parse', 'HEAD']);
|
|
104
|
+
if (!sha)
|
|
105
|
+
throw new MapStoreError('must run inside a git repository, or pass --sha <commit>');
|
|
106
|
+
return sha;
|
|
107
|
+
}
|
|
108
|
+
export function refSha(ref, cwd = process.cwd()) {
|
|
109
|
+
const sha = gitOutput(cwd, ['rev-parse', `${ref}^{commit}`]);
|
|
110
|
+
if (!sha)
|
|
111
|
+
throw new MapStoreError(`could not resolve ${ref} to a commit`);
|
|
112
|
+
return sha;
|
|
113
|
+
}
|
|
114
|
+
export function workingTreeDirty(cwd = process.cwd()) {
|
|
115
|
+
return gitOutput(cwd, ['status', '--porcelain']) !== '';
|
|
116
|
+
}
|
|
117
|
+
export function remoteExists(remote = DEFAULT_REMOTE, cwd = process.cwd()) {
|
|
118
|
+
return runGit(cwd, ['remote', 'get-url', remote], 1 << 20).status === 0;
|
|
119
|
+
}
|
|
120
|
+
export function writeMapManifest(options) {
|
|
121
|
+
const cwd = options.cwd ?? process.cwd();
|
|
122
|
+
const input = compatibilityInput({ cwd, spec: options.spec, baseUrl: options.env?.BASE_URL ?? process.env.BASE_URL });
|
|
123
|
+
const manifest = {
|
|
124
|
+
version: 1,
|
|
125
|
+
packageVersion: input.packageVersion,
|
|
126
|
+
sha: options.sha ?? currentGitSha(cwd, options.env),
|
|
127
|
+
dirty: options.dirty ?? workingTreeDirty(cwd),
|
|
128
|
+
spec: input.spec,
|
|
129
|
+
specHash: input.specHash,
|
|
130
|
+
...(input.lockfile ? { lockfile: input.lockfile } : {}),
|
|
131
|
+
...(input.lockfileHash ? { lockfileHash: input.lockfileHash } : {}),
|
|
132
|
+
...(input.playwrightVersion ? { playwrightVersion: input.playwrightVersion } : {}),
|
|
133
|
+
platform: input.platform,
|
|
134
|
+
arch: input.arch,
|
|
135
|
+
nodeMajor: input.nodeMajor,
|
|
136
|
+
...(input.baseUrl ? { baseUrl: input.baseUrl } : {}),
|
|
137
|
+
screenshots: options.screenshots,
|
|
138
|
+
har: hasHar(options.dir),
|
|
139
|
+
compatibilityKey: hash(JSON.stringify(input)).slice(0, 16),
|
|
140
|
+
createdAt: new Date().toISOString(),
|
|
141
|
+
};
|
|
142
|
+
fs.writeFileSync(path.join(options.dir, MAP_MANIFEST), JSON.stringify(manifest, null, 2));
|
|
143
|
+
return manifest;
|
|
144
|
+
}
|
|
145
|
+
export function readMapManifest(dir) {
|
|
146
|
+
try {
|
|
147
|
+
return JSON.parse(fs.readFileSync(path.join(dir, MAP_MANIFEST), 'utf8'));
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
export function assertCompatibleMapDirs(beforeDir, afterDir) {
|
|
154
|
+
const before = readMapManifest(beforeDir);
|
|
155
|
+
const after = readMapManifest(afterDir);
|
|
156
|
+
if (!before || !after)
|
|
157
|
+
return;
|
|
158
|
+
const beforeRuntime = {
|
|
159
|
+
platform: before.platform,
|
|
160
|
+
arch: before.arch,
|
|
161
|
+
nodeMajor: before.nodeMajor,
|
|
162
|
+
playwrightVersion: before.playwrightVersion ?? '',
|
|
163
|
+
baseUrl: before.baseUrl ?? '',
|
|
164
|
+
};
|
|
165
|
+
const afterRuntime = {
|
|
166
|
+
platform: after.platform,
|
|
167
|
+
arch: after.arch,
|
|
168
|
+
nodeMajor: after.nodeMajor,
|
|
169
|
+
playwrightVersion: after.playwrightVersion ?? '',
|
|
170
|
+
baseUrl: after.baseUrl ?? '',
|
|
171
|
+
};
|
|
172
|
+
if (JSON.stringify(beforeRuntime) === JSON.stringify(afterRuntime))
|
|
173
|
+
return;
|
|
174
|
+
throw new MapStoreError([
|
|
175
|
+
'maps were captured in different runtime environments',
|
|
176
|
+
`before ${before.sha.slice(0, 12)}: ${before.compatibilityKey} (${before.platform}/${before.arch}, Playwright ${before.playwrightVersion ?? 'unknown'})`,
|
|
177
|
+
`after ${after.sha.slice(0, 12)}: ${after.compatibilityKey} (${after.platform}/${after.arch}, Playwright ${after.playwrightVersion ?? 'unknown'})`,
|
|
178
|
+
'Next: rebuild one side with styleproof-map in the same environment, or let CI recapture both maps.',
|
|
179
|
+
].join('\n'));
|
|
180
|
+
}
|
|
181
|
+
function safeSegment(value, name) {
|
|
182
|
+
if (!/^[A-Za-z0-9._-]+$/.test(value))
|
|
183
|
+
throw new MapStoreError(`${name} contains unsupported characters: ${value}`);
|
|
184
|
+
return value;
|
|
185
|
+
}
|
|
186
|
+
function copyDir(src, dest, includeHar) {
|
|
187
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
188
|
+
fs.cpSync(src, dest, {
|
|
189
|
+
recursive: true,
|
|
190
|
+
filter: (source) => includeHar || !source.endsWith('.har'),
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
function checkoutMapStore(cwd, remote, branch) {
|
|
194
|
+
if (!remoteExists(remote, cwd))
|
|
195
|
+
throw new MapStoreError(`git remote ${remote} was not found`);
|
|
196
|
+
const remoteUrl = gitOutput(cwd, ['remote', 'get-url', remote]);
|
|
197
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'styleproof-map-store-'));
|
|
198
|
+
if (runGit(cwd, ['ls-remote', '--exit-code', '--heads', remote, branch], 1 << 20).status === 0) {
|
|
199
|
+
const clone = spawnSync('git', ['clone', '-q', '--depth', '1', '--branch', branch, remoteUrl, tmp], {
|
|
200
|
+
encoding: 'utf8',
|
|
201
|
+
maxBuffer: 1 << 20,
|
|
202
|
+
});
|
|
203
|
+
if (clone.status !== 0) {
|
|
204
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
205
|
+
throw new MapStoreError(clone.stderr.trim() || 'could not clone map store branch');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
runGit(tmp, ['init', '-q', '-b', branch]);
|
|
210
|
+
runGit(tmp, ['remote', 'add', 'origin', remoteUrl]);
|
|
211
|
+
}
|
|
212
|
+
return tmp;
|
|
213
|
+
}
|
|
214
|
+
export function publishMapBundle(options) {
|
|
215
|
+
const cwd = options.cwd ?? process.cwd();
|
|
216
|
+
const branch = options.branch ?? DEFAULT_MAP_STORE_BRANCH;
|
|
217
|
+
const remote = options.remote ?? DEFAULT_REMOTE;
|
|
218
|
+
const manifest = readMapManifest(options.dir);
|
|
219
|
+
if (!manifest)
|
|
220
|
+
throw new MapStoreError(`no ${MAP_MANIFEST} in ${options.dir}`);
|
|
221
|
+
if (manifest.dirty) {
|
|
222
|
+
throw new MapStoreError(`not uploading ${options.dir}: working tree was dirty when the map was captured. Commit first, then rerun styleproof-map.`);
|
|
223
|
+
}
|
|
224
|
+
if (!remoteExists(remote, cwd))
|
|
225
|
+
throw new MapStoreError(`git remote ${remote} was not found`);
|
|
226
|
+
const sha = safeSegment(manifest.sha, 'sha');
|
|
227
|
+
const compatibilityKey = safeSegment(manifest.compatibilityKey, 'compatibility key');
|
|
228
|
+
const target = `${sha}/${compatibilityKey}`;
|
|
229
|
+
let ok = false;
|
|
230
|
+
let lastError = '';
|
|
231
|
+
for (let attempt = 1; attempt <= 5; attempt++) {
|
|
232
|
+
const tmp = checkoutMapStore(cwd, remote, branch);
|
|
233
|
+
try {
|
|
234
|
+
runGit(tmp, ['config', 'user.name', 'github-actions[bot]']);
|
|
235
|
+
runGit(tmp, ['config', 'user.email', '41898282+github-actions[bot]@users.noreply.github.com']);
|
|
236
|
+
fs.writeFileSync(path.join(tmp, 'README.md'), '# StyleProof maps\n\nMachine-generated reusable map bundles. Each folder is keyed by commit SHA and capture compatibility.\n');
|
|
237
|
+
fs.rmSync(path.join(tmp, target), { recursive: true, force: true });
|
|
238
|
+
copyDir(options.dir, path.join(tmp, target), options.includeHar === true);
|
|
239
|
+
if (!options.includeHar) {
|
|
240
|
+
fs.writeFileSync(path.join(tmp, target, MAP_MANIFEST), JSON.stringify({ ...manifest, har: false }, null, 2));
|
|
241
|
+
}
|
|
242
|
+
runGit(tmp, ['add', '-A']);
|
|
243
|
+
runGit(tmp, ['commit', '-q', '-m', `StyleProof map ${sha.slice(0, 12)} ${compatibilityKey}`], 1 << 20);
|
|
244
|
+
const push = runGit(tmp, ['push', '-q', 'origin', `HEAD:${branch}`], 1 << 20);
|
|
245
|
+
if (push.status === 0) {
|
|
246
|
+
ok = true;
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
lastError = push.stderr.trim();
|
|
250
|
+
}
|
|
251
|
+
finally {
|
|
252
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
253
|
+
}
|
|
254
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, attempt * 250);
|
|
255
|
+
}
|
|
256
|
+
if (!ok)
|
|
257
|
+
throw new MapStoreError(lastError || `could not push ${branch}`);
|
|
258
|
+
return { sha, compatibilityKey, branch };
|
|
259
|
+
}
|
|
260
|
+
export function restoreMapBundle(options) {
|
|
261
|
+
const cwd = options.cwd ?? process.cwd();
|
|
262
|
+
const branch = options.branch ?? DEFAULT_MAP_STORE_BRANCH;
|
|
263
|
+
const remote = options.remote ?? DEFAULT_REMOTE;
|
|
264
|
+
const sha = safeSegment(options.sha, 'sha');
|
|
265
|
+
const compatibilityKey = options.compatibilityKey
|
|
266
|
+
? safeSegment(options.compatibilityKey, 'compatibility key')
|
|
267
|
+
: undefined;
|
|
268
|
+
if (!remoteExists(remote, cwd))
|
|
269
|
+
throw new MapStoreError(`git remote ${remote} was not found`);
|
|
270
|
+
if (runGit(cwd, ['ls-remote', '--exit-code', '--heads', remote, branch], 1 << 20).status !== 0) {
|
|
271
|
+
throw new MapStoreError(`map store branch ${branch} does not exist`);
|
|
272
|
+
}
|
|
273
|
+
const tmp = checkoutMapStore(cwd, remote, branch);
|
|
274
|
+
try {
|
|
275
|
+
const shaDir = path.join(tmp, sha);
|
|
276
|
+
if (!fs.existsSync(shaDir))
|
|
277
|
+
throw new MapStoreError(`no cached map for ${sha} on ${branch}`);
|
|
278
|
+
const candidates = fs
|
|
279
|
+
.readdirSync(shaDir, { withFileTypes: true })
|
|
280
|
+
.filter((entry) => entry.isDirectory())
|
|
281
|
+
.map((entry) => entry.name)
|
|
282
|
+
.filter((name) => !compatibilityKey || name === compatibilityKey)
|
|
283
|
+
.sort();
|
|
284
|
+
if (!candidates.length) {
|
|
285
|
+
throw new MapStoreError(compatibilityKey
|
|
286
|
+
? `no cached map for ${sha} with compatibility ${compatibilityKey} on ${branch}`
|
|
287
|
+
: `no cached map bundle under ${sha} on ${branch}`);
|
|
288
|
+
}
|
|
289
|
+
const src = path.join(shaDir, candidates[0]);
|
|
290
|
+
fs.rmSync(options.outDir, { recursive: true, force: true });
|
|
291
|
+
copyDir(src, options.outDir, true);
|
|
292
|
+
const manifest = readMapManifest(options.outDir);
|
|
293
|
+
if (!manifest)
|
|
294
|
+
throw new MapStoreError(`cached map for ${sha} is missing ${MAP_MANIFEST}`);
|
|
295
|
+
return manifest;
|
|
296
|
+
}
|
|
297
|
+
finally {
|
|
298
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
export function resolveCachedCaptureDirs(options) {
|
|
302
|
+
const cwd = options.cwd ?? process.cwd();
|
|
303
|
+
if (options.args.length > 1)
|
|
304
|
+
throw new MapStoreError(options.usage);
|
|
305
|
+
if (!fs.existsSync(path.resolve(cwd, options.spec))) {
|
|
306
|
+
throw new MapStoreError(`${options.command}: no StyleProof spec at ${options.spec}`);
|
|
307
|
+
}
|
|
308
|
+
const baseRef = options.args[0] ?? inferBaseRef();
|
|
309
|
+
const baseSha = refSha(baseRef, cwd);
|
|
310
|
+
const headSha = currentGitSha(cwd);
|
|
311
|
+
const compatibilityKey = expectedCompatibilityKey({ cwd, spec: options.spec, baseUrl: options.baseUrl });
|
|
312
|
+
const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'styleproof-cache-'));
|
|
313
|
+
const beforeDir = path.join(tmpRoot, 'base');
|
|
314
|
+
const afterDir = path.join(tmpRoot, 'head');
|
|
315
|
+
try {
|
|
316
|
+
restoreMapBundle({
|
|
317
|
+
sha: baseSha,
|
|
318
|
+
outDir: beforeDir,
|
|
319
|
+
branch: options.branch,
|
|
320
|
+
remote: options.remote,
|
|
321
|
+
cwd,
|
|
322
|
+
compatibilityKey,
|
|
323
|
+
});
|
|
324
|
+
restoreMapBundle({
|
|
325
|
+
sha: headSha,
|
|
326
|
+
outDir: afterDir,
|
|
327
|
+
branch: options.branch,
|
|
328
|
+
remote: options.remote,
|
|
329
|
+
cwd,
|
|
330
|
+
compatibilityKey,
|
|
331
|
+
});
|
|
332
|
+
return { beforeDir, afterDir, baseRef, baseSha, headSha, compatibilityKey, tmpRoot };
|
|
333
|
+
}
|
|
334
|
+
catch (e) {
|
|
335
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
336
|
+
throw e;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
export function cleanupCachedCaptureDirs(captureDirs) {
|
|
340
|
+
if (captureDirs)
|
|
341
|
+
fs.rmSync(captureDirs.tmpRoot, { recursive: true, force: true });
|
|
342
|
+
}
|
package/dist/report.d.ts
CHANGED
|
@@ -24,6 +24,12 @@ export type ReportOptions = {
|
|
|
24
24
|
minHeight?: number;
|
|
25
25
|
/** Crops taller than this are clamped (default 1600px). */
|
|
26
26
|
maxHeight?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Changed-element footprint (max of its width/height, in px) at or below which a
|
|
29
|
+
* magnified zoom crop is added so a sub-pixel change is visible by default
|
|
30
|
+
* (default 64). Set to 0 to disable zoom crops.
|
|
31
|
+
*/
|
|
32
|
+
zoomBelow?: number;
|
|
27
33
|
/** Max crop regions per surface before collapsing into one union crop (default 8). */
|
|
28
34
|
maxCrops?: number;
|
|
29
35
|
/**
|