visual-regression-compare 0.0.0-stage → 1.0.0-beta.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/dist/job.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ import type { DiffEngine, EngineOptions, EngineResult, SizeMismatch } from './types';
2
+ /** The CPU-bound part of one comparison: plain data in, plain data out, so it can run in a worker. */
3
+ export type CompareJob = {
4
+ baseline: Uint8Array;
5
+ actual: Uint8Array;
6
+ onSizeMismatch: SizeMismatch;
7
+ /** An engine id (see `loadEngine`), so a worker can load it itself. */
8
+ engine: {
9
+ id: string;
10
+ options?: EngineOptions;
11
+ cwd?: string;
12
+ };
13
+ };
14
+ type Size = {
15
+ width: number;
16
+ height: number;
17
+ };
18
+ export type JobResult = {
19
+ kind: 'compared';
20
+ baselineSize: Size;
21
+ actualSize: Size;
22
+ resized?: 'pad' | 'crop';
23
+ diffPixels: number;
24
+ diffRatio: number;
25
+ /** The diff image, already encoded. */
26
+ diffPng?: Uint8Array;
27
+ score?: EngineResult['score'];
28
+ } | {
29
+ kind: 'size-mismatch';
30
+ baselineSize: Size;
31
+ actualSize: Size;
32
+ };
33
+ export declare const runCompare: (job: Pick<CompareJob, "baseline" | "actual" | "onSizeMismatch">, engine: DiffEngine) => Promise<JobResult>;
34
+ /** Runs a job in this thread (what a worker does for each message). */
35
+ export declare const runJob: (job: CompareJob) => Promise<JobResult>;
36
+ export {};
@@ -0,0 +1,16 @@
1
+ import type { Layout, Matcher } from './types';
2
+ /** Replaces characters no file system accepts and drops `.`/`..` segments, so a screenshot name is always a safe relative path. */
3
+ export declare const sanitizeName: (name: string) => string;
4
+ /**
5
+ * The manifest convention: `<name>.png` is the baseline, `<name>.actual.png`
6
+ * and `<name>.diff.png` sit beside it (or in the diff directory).
7
+ */
8
+ export declare const manifestLayout: () => Layout;
9
+ /** The baseline is the same relative path under the baseline directory. */
10
+ export declare const samePathMatcher: () => Matcher;
11
+ /**
12
+ * The baseline path from a template over the incoming path: `{dir}` (with its
13
+ * trailing slash, empty at the top level), `{name}` (file stem) and `{path}`
14
+ * (directory and stem), e.g. `{dir}{name}-baseline.png`.
15
+ */
16
+ export declare const templateMatcher: (template: string) => Matcher;
@@ -0,0 +1,53 @@
1
+ import { type ManifestStatus } from 'visual-regression-manifest';
2
+ import type { OnMissingBaseline, SizeMismatch, UpdatePolicy } from './types';
3
+ export declare class UsageError extends Error {
4
+ }
5
+ export type CompareCliOptions = {
6
+ actualDir: string;
7
+ baselineDir: string;
8
+ diffDir?: string;
9
+ engine: string;
10
+ engineOptions: Record<string, unknown>;
11
+ maxDiffRatio?: number;
12
+ maxDiffPixels?: number;
13
+ onMissingBaseline: OnMissingBaseline;
14
+ onSizeMismatch: SizeMismatch;
15
+ update: UpdatePolicy;
16
+ match?: string;
17
+ layout: string;
18
+ manifest?: string;
19
+ runner?: string;
20
+ variant?: string;
21
+ platform?: string;
22
+ report: string;
23
+ concurrency?: number;
24
+ failOn: ManifestStatus[];
25
+ /** `record` copies every screenshot over its baseline. */
26
+ record: boolean;
27
+ };
28
+ export type ApproveCliOptions = {
29
+ manifests: string[];
30
+ names: string[];
31
+ all: boolean;
32
+ test?: string;
33
+ repoRoot?: string;
34
+ force: boolean;
35
+ dryRun: boolean;
36
+ };
37
+ export type Command = {
38
+ kind: 'help';
39
+ } | {
40
+ kind: 'version';
41
+ } | {
42
+ kind: 'compare';
43
+ options: CompareCliOptions;
44
+ } | {
45
+ kind: 'approve';
46
+ options: ApproveCliOptions;
47
+ };
48
+ /** The keys a config file may set; the flags override them. */
49
+ export type ConfigFile = Partial<Omit<CompareCliOptions, 'actualDir' | 'record'> & {
50
+ actualDir: string;
51
+ }>;
52
+ export declare const helpText = "Compare folders of screenshots against their baselines and write a visual-regression-manifest.\n\nUsage:\n visual-regression-compare [compare] <actual-dir> --baseline <dir> [options]\n visual-regression-compare record <actual-dir> --baseline <dir> [options]\n visual-regression-compare approve --manifest <path>... [<name>...|--all] [options]\n\ncompare options:\n --baseline <dir> directory of baseline images (required)\n --diff <dir> where .actual and .diff files go (default: next to the baselines)\n --engine <id> pixelmatch, or a module path or package name (default: pixelmatch)\n --max-diff-ratio <n> largest accepted share of differing pixels, 0..1 (default: 0)\n --max-diff-pixels <n> largest accepted number of differing pixels\n --color-threshold <n> per-pixel color tolerance of the engine, 0..1 (default: 0.1)\n --include-aa count anti-aliased pixels as differences\n --on-missing fail|create a screenshot without baseline (default: fail)\n --on-size-mismatch fail|pad|crop\n fail records the size change and compares nothing; pad grows both\n images to the larger size, crop cuts both to the smaller (default: fail)\n --update none|failures-only|all\n overwrite baselines (default: none)\n --match <template> baseline path from the screenshot path: {dir}, {name}, {path}\n --layout manifest file naming (default: manifest)\n --manifest <path> default: <actual-dir>/visual-regression-manifest.compare.json\n --runner <name> --variant <id> --platform <os>[/<browser>]\n metadata recorded on every entry\n --report <list> console, ctrf[:<file>], github-summary[:<file>] (default: console)\n --concurrency <n> worker threads for decoding and comparing (default: CPU count)\n --config <file> visual-regression.config.{json,mjs,js}, found in the working directory\n --fail-on <list> statuses that make the exit code 1 (default: failed,missing-baseline)\n\napprove options:\n --manifest <path> a manifest to apply (repeatable; required)\n --all every entry that needs a human (the default without names)\n --test <file> only entries of this test file\n --repo-root <dir> the repository the manifest's paths are applied to (default: the working directory)\n --force approve although the baseline changed since the run\n --dry-run report what would be approved\n\n -h, --help, -v, --version\n";
53
+ export declare const parseCommand: (argv: string[], config?: ConfigFile) => Command;
@@ -0,0 +1,25 @@
1
+ import type { CompareContext, Decision, DiffEngine, EngineModule, Layout, Matcher, Measurement, Plugin, Reporter, Store, WritePlan } from './types';
2
+ import type { Manifest } from 'visual-regression-manifest';
3
+ export type Hooks = {
4
+ beforeCompare: ((ctx: CompareContext) => void | Promise<void>)[];
5
+ afterCompare: ((ctx: CompareContext, measurement: Measurement) => void | Promise<void>)[];
6
+ decide: ((ctx: CompareContext, decision: Decision) => Decision | Promise<Decision>)[];
7
+ beforeWrite: ((ctx: CompareContext, plan: WritePlan) => WritePlan | Promise<WritePlan>)[];
8
+ end: ((manifest: Manifest) => void | Promise<void>)[];
9
+ };
10
+ /** What the plugins configured: replaceable parts and the hooks, owned by one comparer. */
11
+ export type Registry = {
12
+ store: Store;
13
+ layout: Layout;
14
+ matcher: Matcher;
15
+ reporters: Reporter[];
16
+ engines: Map<string, DiffEngine | EngineModule>;
17
+ hooks: Hooks;
18
+ setup(plugins: Plugin[]): Promise<void>;
19
+ };
20
+ export declare const createRegistry: (initial: {
21
+ store: Store;
22
+ layout: Layout;
23
+ matcher: Matcher;
24
+ reporters: Reporter[];
25
+ }) => Registry;
package/dist/pool.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { type CompareJob, type JobResult } from './job';
2
+ /**
3
+ * Runs jobs in worker threads, one at a time per worker. With `size` 1 or no
4
+ * worker file it runs them in this thread, so the same code path serves tests
5
+ * and tiny runs.
6
+ */
7
+ export declare class WorkerPool {
8
+ private readonly size;
9
+ private readonly workerFile;
10
+ private readonly idle;
11
+ private readonly workers;
12
+ private readonly queue;
13
+ private readonly running;
14
+ private nextId;
15
+ private readonly current;
16
+ constructor(size: number, workerFile: URL | string | undefined);
17
+ get threaded(): boolean;
18
+ run(job: CompareJob): Promise<JobResult>;
19
+ private pump;
20
+ private spawn;
21
+ close(): Promise<void>;
22
+ }
@@ -0,0 +1,15 @@
1
+ import { type Manifest } from 'visual-regression-manifest';
2
+ import type { Reporter } from './types';
3
+ type Log = (line: string) => void;
4
+ /** One line per screenshot as it is decided, then the counts. */
5
+ export declare const consoleReporter: (log?: Log) => Reporter;
6
+ /** The finished run as a CTRF report. */
7
+ export declare const ctrfReporter: (file: string) => Reporter;
8
+ /** Markdown for a GitHub job summary: the counts and a table of what needs a human. */
9
+ export declare const summaryMarkdown: ({ summary, entries }: Manifest) => string;
10
+ /** Appends the summary to `file`, by default the job summary GitHub points `GITHUB_STEP_SUMMARY` at. */
11
+ export declare const githubSummaryReporter: (file?: string | undefined) => Reporter;
12
+ export declare const REPORTER_NAMES: readonly ["console", "ctrf", "github-summary"];
13
+ /** `console,ctrf:report.json,github-summary[:<path>]` as reporters. */
14
+ export declare const parseReporters: (spec: string) => Reporter[];
15
+ export {};
package/dist/run.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ type Out = {
2
+ log(line: string): void;
3
+ error(line: string): void;
4
+ };
5
+ /** Runs the CLI and returns its exit code. */
6
+ export declare const runCli: (argv: string[], out: Out) => Promise<number>;
7
+ export {};
@@ -0,0 +1,9 @@
1
+ import type { Store } from './types';
2
+ export type FsStoreOptions = {
3
+ baselineDir: string;
4
+ /** Where the screenshots to compare are; only the directory call needs it. */
5
+ actualDir?: string;
6
+ /** Where `.actual` and `.diff` files are written; defaults to `baselineDir`, next to the baselines. */
7
+ diffDir?: string;
8
+ };
9
+ export declare const fsStore: ({ baselineDir, actualDir, diffDir, }: FsStoreOptions) => Store;
@@ -0,0 +1,2 @@
1
+ /** A solid-color PNG, with `dots` red pixels in its first row. */
2
+ export declare const makePng: (width: number, height: number, color?: [number, number, number, number], dots?: number) => Uint8Array;
@@ -0,0 +1,165 @@
1
+ import type { Manifest, ManifestColorMetric, ManifestEntry, ManifestEntryPlatform, ManifestExtra, ManifestStatus, ManifestViewport } from 'visual-regression-manifest';
2
+ /** Decoded image: 8-bit RGBA, row by row. */
3
+ export type RgbaImage = {
4
+ width: number;
5
+ height: number;
6
+ data: Uint8ClampedArray;
7
+ };
8
+ /** What the engine is given besides the images; must be structured-cloneable (it crosses the worker boundary). */
9
+ export type EngineOptions = Record<string, unknown>;
10
+ export type EngineResult = {
11
+ diffPixels: number;
12
+ /** `diffPixels / (width * height)` after size handling. */
13
+ diffRatio: number;
14
+ /** Omitted by engines that produce none. */
15
+ diff?: RgbaImage;
16
+ /** Engine-specific score, e.g. an SSIM mean or Resemble's mismatch percentage. */
17
+ score?: {
18
+ name: string;
19
+ value: number;
20
+ };
21
+ };
22
+ /** Both images have the same size by the time an engine sees them (see `SizeMismatch`). */
23
+ export type DiffEngine = {
24
+ /** Recorded as `extra.visualRegressionCompare.engine`. */
25
+ name: string;
26
+ /** The manifest's `comparison.colorMetric`. */
27
+ colorMetric: ManifestColorMetric;
28
+ /** The per-pixel color tolerance used, recorded as `comparison.colorThreshold`. */
29
+ colorThreshold?: number;
30
+ compare(baseline: RgbaImage, actual: RgbaImage): EngineResult | Promise<EngineResult>;
31
+ };
32
+ /**
33
+ * A DiffEngine as a module id loads it: the default export is an engine, or a
34
+ * function that makes one from `EngineOptions`.
35
+ */
36
+ export type EngineModule = DiffEngine | ((options: EngineOptions) => DiffEngine);
37
+ /**
38
+ * What to do when baseline and actual differ in size:
39
+ * - `fail`: no pixel comparison; the entry fails and records both sizes, so a
40
+ * resolution change shows up as what it is.
41
+ * - `pad`: grow both to the larger size with transparent pixels and compare.
42
+ * - `crop`: cut both to the smaller size and compare.
43
+ */
44
+ export type SizeMismatch = 'fail' | 'pad' | 'crop';
45
+ export type OnMissingBaseline = 'fail' | 'create';
46
+ export type UpdatePolicy = 'none' | 'failures-only' | 'all';
47
+ export type Limits = {
48
+ /** Largest accepted share of differing pixels, 0..1. Default `0`. */
49
+ maxDiffRatio?: number;
50
+ /** Largest accepted number of differing pixels; the stricter of the two limits wins. */
51
+ maxDiffPixels?: number;
52
+ };
53
+ export type ImageKind = 'baseline' | 'incoming' | 'actual' | 'diff';
54
+ /** A file in a store: its kind and the path relative to that kind's root, with `/` separators. */
55
+ export type ImageRef = {
56
+ kind: ImageKind;
57
+ path: string;
58
+ };
59
+ export type Store = {
60
+ /** `null` when there is no such file. */
61
+ read(ref: ImageRef): Promise<Uint8Array | null>;
62
+ write(ref: ImageRef, bytes: Uint8Array): Promise<void>;
63
+ /** Removing a file that is not there is not an error. */
64
+ remove(ref: ImageRef): Promise<void>;
65
+ /** Every PNG of that kind, sorted. `actual` and `diff` files are never listed as `incoming`. */
66
+ list(kind: 'baseline' | 'incoming'): AsyncIterable<ImageRef>;
67
+ /** Absolute path of the file in the project, for the manifest; `null` for stores that keep files elsewhere. */
68
+ locate(ref: ImageRef): string | null;
69
+ };
70
+ export type Layout = {
71
+ /** Where the files of the screenshot `name` (a relative path without extension) live, relative to each root. */
72
+ paths(name: string): Record<ImageKind, string>;
73
+ /** The reverse: the name an image of that kind belongs to, `null` for a file the layout does not own. */
74
+ nameOf(kind: ImageKind, path: string): string | null;
75
+ };
76
+ export type Matcher = {
77
+ /** Where the baseline of an incoming image is (or should be); `null` skips the image. */
78
+ baselineFor(incoming: ImageRef): ImageRef | null;
79
+ };
80
+ /** Identity of one compared screenshot, as an adapter or the CLI states it. */
81
+ export type CompareInput = {
82
+ /** Screenshot name; becomes the file name through the layout (unsafe characters are replaced). */
83
+ name: string;
84
+ actual: Uint8Array;
85
+ test?: {
86
+ filePath?: string;
87
+ titlePath?: string[];
88
+ retry?: number;
89
+ };
90
+ platform?: ManifestEntryPlatform;
91
+ viewport?: ManifestViewport;
92
+ variant?: string;
93
+ extra?: ManifestExtra;
94
+ /** Capture plus preceding time in milliseconds, recorded as the entry's `duration`. */
95
+ duration?: number;
96
+ };
97
+ /**
98
+ * What hooks see of one comparison. `attachments` is reserved for extra inputs
99
+ * an engine may want next to the pixels (a DOM snapshot for layout-aware
100
+ * comparison); nothing fills it yet.
101
+ */
102
+ export type CompareContext = {
103
+ name: string;
104
+ input: CompareInput;
105
+ attachments?: Record<string, unknown>;
106
+ };
107
+ export type Measurement = {
108
+ kind: 'compared';
109
+ baselineSize: {
110
+ width: number;
111
+ height: number;
112
+ };
113
+ actualSize: {
114
+ width: number;
115
+ height: number;
116
+ };
117
+ result: EngineResult;
118
+ /** `pad` or `crop` when the sizes differed. */
119
+ resized?: 'pad' | 'crop';
120
+ } | {
121
+ kind: 'size-mismatch';
122
+ baselineSize: {
123
+ width: number;
124
+ height: number;
125
+ };
126
+ actualSize: {
127
+ width: number;
128
+ height: number;
129
+ };
130
+ } | {
131
+ kind: 'missing-baseline';
132
+ };
133
+ export type Decision = {
134
+ status: ManifestStatus;
135
+ message: string;
136
+ /** Becomes the `visualRegressionCompare` namespace of the entry's `extra`, merged with the compare's own keys. */
137
+ extra?: Record<string, unknown>;
138
+ };
139
+ export type WritePlan = {
140
+ /** Files to write; a hook may drop or replace entries (a store that keeps baselines elsewhere). */
141
+ write: {
142
+ ref: ImageRef;
143
+ bytes: Uint8Array;
144
+ }[];
145
+ };
146
+ export type Reporter = {
147
+ onEntry?(entry: ManifestEntry): void | Promise<void>;
148
+ onEnd?(manifest: Manifest): void | Promise<void>;
149
+ };
150
+ export type PluginApi = {
151
+ addEngine(name: string, engine: DiffEngine | EngineModule): void;
152
+ addReporter(reporter: Reporter): void;
153
+ setStore(store: Store): void;
154
+ setLayout(layout: Layout): void;
155
+ setMatcher(matcher: Matcher): void;
156
+ on(event: 'beforeCompare', fn: (ctx: CompareContext) => void | Promise<void>): void;
157
+ on(event: 'afterCompare', fn: (ctx: CompareContext, measurement: Measurement) => void | Promise<void>): void;
158
+ on(event: 'decide', fn: (ctx: CompareContext, decision: Decision) => Decision | Promise<Decision>): void;
159
+ on(event: 'beforeWrite', fn: (ctx: CompareContext, plan: WritePlan) => WritePlan | Promise<WritePlan>): void;
160
+ on(event: 'end', fn: (manifest: Manifest) => void | Promise<void>): void;
161
+ };
162
+ export type Plugin = {
163
+ name: string;
164
+ setup(api: PluginApi): void | Promise<void>;
165
+ };
@@ -0,0 +1 @@
1
+ export {};
package/dist/worker.js ADDED
@@ -0,0 +1,167 @@
1
+ // src/worker.ts
2
+ import { parentPort } from "worker_threads";
3
+
4
+ // src/engines/index.ts
5
+ import path from "path";
6
+ import { pathToFileURL } from "url";
7
+
8
+ // src/engines/pixelmatch.ts
9
+ import pixelmatch from "pixelmatch";
10
+ var pixelmatchEngine = ({
11
+ threshold = 0.1,
12
+ includeAA = false
13
+ } = {}) => ({
14
+ name: "pixelmatch",
15
+ colorMetric: "yiq",
16
+ colorThreshold: threshold,
17
+ compare(baseline, actual) {
18
+ const { width, height } = baseline;
19
+ const diff = new Uint8ClampedArray(width * height * 4);
20
+ const diffPixels = pixelmatch(
21
+ baseline.data,
22
+ actual.data,
23
+ diff,
24
+ width,
25
+ height,
26
+ {
27
+ threshold,
28
+ includeAA
29
+ }
30
+ );
31
+ const area = width * height;
32
+ return {
33
+ diffPixels,
34
+ diffRatio: area === 0 ? 0 : diffPixels / area,
35
+ diff: { width, height, data: diff }
36
+ };
37
+ }
38
+ });
39
+ var pixelmatchFrom = (options) => pixelmatchEngine(options);
40
+
41
+ // src/engines/index.ts
42
+ var BUILT_IN = {
43
+ pixelmatch: pixelmatchFrom
44
+ };
45
+ var BUILT_IN_ENGINES = Object.keys(BUILT_IN);
46
+ var isDiffEngine = (value) => typeof value === "object" && value !== null && typeof value.compare === "function" && typeof value.name === "string";
47
+ var loadEngine = async (id, options = {}, cwd = process.cwd()) => {
48
+ const builtIn = BUILT_IN[id];
49
+ if (builtIn) return builtIn(options);
50
+ const isPath = /^\.{0,2}[\\/]/.test(id) || path.isAbsolute(id);
51
+ const specifier = isPath ? pathToFileURL(path.resolve(cwd, id)).href : id;
52
+ let loaded;
53
+ try {
54
+ loaded = await import(specifier);
55
+ } catch (error) {
56
+ throw new Error(
57
+ `Cannot load engine "${id}" (built in: ${BUILT_IN_ENGINES.join(", ")}): ${error.message}`,
58
+ { cause: error }
59
+ );
60
+ }
61
+ const exported = loaded.default;
62
+ const engine = typeof exported === "function" ? exported(options) : exported;
63
+ if (!isDiffEngine(engine)) {
64
+ throw new Error(
65
+ `Engine "${id}" must default-export a { name, colorMetric, compare } object or a function returning one`
66
+ );
67
+ }
68
+ return engine;
69
+ };
70
+
71
+ // src/image.ts
72
+ import { PNG } from "pngjs";
73
+ var LFS_POINTER = "version https://git-lfs.github.com/spec/";
74
+ var decodePng = (bytes) => {
75
+ const head = Buffer.from(bytes.subarray(0, LFS_POINTER.length)).toString(
76
+ "latin1"
77
+ );
78
+ if (head === LFS_POINTER) {
79
+ throw new Error(
80
+ "the file is a Git LFS pointer, not an image; run `git lfs pull`"
81
+ );
82
+ }
83
+ const { width, height, data } = PNG.sync.read(Buffer.from(bytes));
84
+ return { width, height, data: new Uint8ClampedArray(data) };
85
+ };
86
+ var encodePng = ({ width, height, data }) => {
87
+ const png = new PNG({ width, height });
88
+ png.data = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
89
+ return PNG.sync.write(png);
90
+ };
91
+ var resize = (image, width, height) => {
92
+ const data = new Uint8ClampedArray(width * height * 4);
93
+ const rows = Math.min(height, image.height);
94
+ const rowBytes = Math.min(width, image.width) * 4;
95
+ for (let y = 0; y < rows; y++) {
96
+ const from = y * image.width * 4;
97
+ data.set(image.data.subarray(from, from + rowBytes), y * width * 4);
98
+ }
99
+ return { width, height, data };
100
+ };
101
+ var padToSame = (a, b) => {
102
+ const width = Math.max(a.width, b.width);
103
+ const height = Math.max(a.height, b.height);
104
+ return [resize(a, width, height), resize(b, width, height)];
105
+ };
106
+ var cropToSame = (a, b) => {
107
+ const width = Math.min(a.width, b.width);
108
+ const height = Math.min(a.height, b.height);
109
+ return [resize(a, width, height), resize(b, width, height)];
110
+ };
111
+
112
+ // src/job.ts
113
+ var decodeNamed = (which, bytes) => {
114
+ try {
115
+ return decodePng(bytes);
116
+ } catch (error) {
117
+ throw new Error(`the ${which} image: ${error.message}`, {
118
+ cause: error
119
+ });
120
+ }
121
+ };
122
+ var sizeOf = ({ width, height }) => ({ width, height });
123
+ var runCompare = async (job, engine) => {
124
+ let baseline = decodeNamed("baseline", job.baseline);
125
+ let actual = decodeNamed("actual", job.actual);
126
+ const baselineSize = sizeOf(baseline);
127
+ const actualSize = sizeOf(actual);
128
+ let resized;
129
+ if (baseline.width !== actual.width || baseline.height !== actual.height) {
130
+ if (job.onSizeMismatch === "fail") {
131
+ return { kind: "size-mismatch", baselineSize, actualSize };
132
+ }
133
+ resized = job.onSizeMismatch;
134
+ [baseline, actual] = (resized === "pad" ? padToSame : cropToSame)(
135
+ baseline,
136
+ actual
137
+ );
138
+ }
139
+ const { diff, ...result } = await engine.compare(baseline, actual);
140
+ return {
141
+ kind: "compared",
142
+ baselineSize,
143
+ actualSize,
144
+ ...resized && { resized },
145
+ ...result,
146
+ ...diff && { diffPng: encodePng(diff) }
147
+ };
148
+ };
149
+ var runJob = async (job) => runCompare(
150
+ job,
151
+ await loadEngine(job.engine.id, job.engine.options, job.engine.cwd)
152
+ );
153
+
154
+ // src/worker.ts
155
+ parentPort?.on(
156
+ "message",
157
+ async ({ id, job }) => {
158
+ try {
159
+ parentPort?.postMessage({ id, result: await runJob(job) });
160
+ } catch (error) {
161
+ parentPort?.postMessage({
162
+ id,
163
+ error: error instanceof Error ? error.message : String(error)
164
+ });
165
+ }
166
+ }
167
+ );
package/package.json CHANGED
@@ -1,6 +1,72 @@
1
1
  {
2
2
  "name": "visual-regression-compare",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "description": "Tool-agnostic image comparison for visual regression testing: pairs baseline and actual screenshots, runs a pluggable diff engine, applies limits, writes .actual and .diff files and a visual-regression-manifest run manifest. A CLI for folders of images and a library for test-runner adapters.",
4
+ "version": "1.0.0-beta.0",
5
+ "author": "Jakub Freisler <jakub.freisler@frsource.org>",
6
+ "homepage": "https://visual-regression.dev/compare-cli",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/visual-regression/visual-regression.git",
10
+ "directory": "packages/visual-regression-compare"
11
+ },
12
+ "license": "MIT",
13
+ "type": "module",
14
+ "sideEffects": false,
15
+ "main": "dist/index.js",
16
+ "types": "dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "default": "./dist/index.js"
21
+ },
22
+ "./package.json": "./package.json"
23
+ },
24
+ "bin": {
25
+ "visual-regression-compare": "dist/cli.js",
26
+ "vrc": "dist/cli.js"
27
+ },
28
+ "engines": {
29
+ "node": ">=20.9.0"
30
+ },
31
+ "dependencies": {
32
+ "pixelmatch": "^7.2.0",
33
+ "pngjs": "^7.0.0",
34
+ "visual-regression-manifest": "0.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "20.19.43",
38
+ "@types/pngjs": "^6.0.5",
39
+ "@types/tmp": "0.2.6",
40
+ "@vitest/coverage-v8": "4.1.11",
41
+ "del-cli": "7.0.0",
42
+ "esbuild": "0.28.2",
43
+ "tmp-promise": "3.0.3",
44
+ "typescript": "6.0.3",
45
+ "vitest": "4.1.11"
46
+ },
47
+ "keywords": [
48
+ "visual regression testing",
49
+ "visual diff",
50
+ "image diff",
51
+ "screenshot testing",
52
+ "pixelmatch",
53
+ "run manifest",
54
+ "CLI"
55
+ ],
56
+ "publishConfig": {
57
+ "access": "public"
58
+ },
59
+ "files": [
60
+ "dist",
61
+ "package.json",
62
+ "README.md",
63
+ "LICENSE"
64
+ ],
65
+ "scripts": {
66
+ "build": "del-cli dist && tsc -p tsconfig.build.json --emitDeclarationOnly --declaration --outDir dist && esbuild src/index.ts src/worker.ts --bundle --platform=node --target=node20 --format=esm --packages=external --outdir=dist && esbuild src/cli.ts --bundle --platform=node --target=node20 --format=esm --packages=external --banner:js='#!/usr/bin/env node' --outfile=dist/cli.js && node scripts/executable.mjs",
67
+ "test:integration": "vitest run",
68
+ "test:integration:watch": "vitest",
69
+ "test:integration:coverage": "vitest run --coverage",
70
+ "test:integration:ci": "CI=true vitest run --coverage"
71
+ }
6
72
  }