reg-cli 0.18.16 → 0.19.0-rc1

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/cli.mjs ADDED
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env node
2
+ import { t as writeXimgdiffAssets } from "./ximgdiff-D8GAJaxA.mjs";
3
+ import { dir, run } from "./index.mjs";
4
+ import { createRequire } from "node:module";
5
+ import { copyFile, mkdir } from "node:fs/promises";
6
+ import { dirname, join } from "node:path";
7
+ import { parseArgs } from "node:util";
8
+ //#region src/cli.ts
9
+ const HELP = `
10
+ Usage
11
+ $ reg-cli /path/to/actual-dir /path/to/expected-dir /path/to/diff-dir
12
+ Options
13
+ -U, --update Update expected images (copy actual → expected).
14
+ -R, --report Output html report to specified path.
15
+ -J, --json Output json report to specified path (default ./reg.json).
16
+ -I, --ignoreChange Exit 0 even when image changes are detected.
17
+ -E, --extendedErrors Also treat added/deleted images as failures.
18
+ -F, --from Render HTML report from an existing reg.json (no diff).
19
+ -X, --additionalDetection "none" | "client" — enable browser-side second-pass detection.
20
+ -P, --urlPrefix Prefix for image src in html report.
21
+ -M, --matchingThreshold YIQ threshold (0-1). Default 0.
22
+ -T, --thresholdRate Ratio of pixels that may differ before failing.
23
+ -S, --thresholdPixel Absolute pixel count that may differ before failing.
24
+ -C, --concurrency Parallel worker count. Default 4.
25
+ -A, --enableAntialias Count anti-aliased pixels as different.
26
+ -D, --customDiffMessage Trailing message printed on diff.
27
+ --junit Path to write a JUnit XML test report.
28
+ --diffFormat webp (default) | png
29
+ `;
30
+ if (process.argv.includes("-h") || process.argv.includes("--help")) {
31
+ process.stdout.write(HELP);
32
+ process.exit(0);
33
+ }
34
+ if (process.argv.includes("--version")) {
35
+ try {
36
+ const pkg = createRequire(import.meta.url)("../package.json");
37
+ process.stdout.write(`${pkg.version ?? "unknown"}\n`);
38
+ } catch {
39
+ process.stdout.write("unknown\n");
40
+ }
41
+ process.exit(0);
42
+ }
43
+ let parsed;
44
+ try {
45
+ parsed = parseArgs({
46
+ args: process.argv.slice(2),
47
+ options: {
48
+ update: {
49
+ type: "boolean",
50
+ short: "U"
51
+ },
52
+ json: {
53
+ type: "string",
54
+ short: "J"
55
+ },
56
+ ignoreChange: {
57
+ type: "boolean",
58
+ short: "I"
59
+ },
60
+ extendedErrors: {
61
+ type: "boolean",
62
+ short: "E"
63
+ },
64
+ report: {
65
+ type: "string",
66
+ short: "R"
67
+ },
68
+ urlPrefix: {
69
+ type: "string",
70
+ short: "P"
71
+ },
72
+ matchingThreshold: {
73
+ type: "string",
74
+ short: "M"
75
+ },
76
+ thresholdRate: {
77
+ type: "string",
78
+ short: "T"
79
+ },
80
+ thresholdPixel: {
81
+ type: "string",
82
+ short: "S"
83
+ },
84
+ concurrency: {
85
+ type: "string",
86
+ short: "C"
87
+ },
88
+ enableAntialias: {
89
+ type: "boolean",
90
+ short: "A"
91
+ },
92
+ customDiffMessage: {
93
+ type: "string",
94
+ short: "D"
95
+ },
96
+ diffFormat: { type: "string" },
97
+ junit: { type: "string" },
98
+ from: {
99
+ type: "string",
100
+ short: "F"
101
+ },
102
+ additionalDetection: {
103
+ type: "string",
104
+ short: "X"
105
+ }
106
+ },
107
+ allowPositionals: true
108
+ });
109
+ } catch (err) {
110
+ process.stderr.write(`reg-cli: ${err.message}\n`);
111
+ process.stderr.write(HELP);
112
+ process.exit(1);
113
+ }
114
+ const { values, positionals } = parsed;
115
+ const [actualDir, expectedDir, diffDir] = positionals;
116
+ const fromPath = typeof values.from === "string" ? values.from : void 0;
117
+ if (!fromPath && (!actualDir || !expectedDir || !diffDir)) {
118
+ process.stderr.write("reg-cli: please specify actual, expected and diff directories.\n");
119
+ process.stderr.write(HELP);
120
+ process.exit(1);
121
+ }
122
+ const update = !!values.update;
123
+ const ignoreChange = !!values.ignoreChange;
124
+ const extendedErrors = !!values.extendedErrors;
125
+ const diffFormat = typeof values.diffFormat === "string" ? values.diffFormat : "png";
126
+ const jsonPath = typeof values.json === "string" ? values.json : "./reg.json";
127
+ const customDiffMessage = typeof values.customDiffMessage === "string" ? values.customDiffMessage : `\nInspect your code changes, re-run with \`-U\` to update them. `;
128
+ const wasmArgv = ["--"];
129
+ if (actualDir) wasmArgv.push(actualDir);
130
+ if (expectedDir) wasmArgv.push(expectedDir);
131
+ if (diffDir) wasmArgv.push(diffDir);
132
+ const pushFlag = (name, v) => {
133
+ if (v == null || v === false) return;
134
+ if (v === true) wasmArgv.push(`--${name}`);
135
+ else wasmArgv.push(`--${name}`, String(v));
136
+ };
137
+ pushFlag("report", values.report);
138
+ pushFlag("json", jsonPath);
139
+ pushFlag("junit", values.junit);
140
+ pushFlag("extendedErrors", values.extendedErrors);
141
+ pushFlag("from", fromPath);
142
+ pushFlag("additionalDetection", values.additionalDetection);
143
+ pushFlag("matchingThreshold", values.matchingThreshold);
144
+ pushFlag("thresholdRate", values.thresholdRate);
145
+ pushFlag("thresholdPixel", values.thresholdPixel);
146
+ pushFlag("urlPrefix", values.urlPrefix);
147
+ pushFlag("concurrency", values.concurrency);
148
+ pushFlag("enableAntialias", values.enableAntialias);
149
+ pushFlag("diffFormat", diffFormat);
150
+ const CHECK = "✔";
151
+ const CROSS = "✘";
152
+ const PLUS = "✚";
153
+ const MINUS = "−";
154
+ const formatPath = (p) => actualDir ? join(actualDir, p) : p;
155
+ const emitter = run(wasmArgv);
156
+ emitter.once("complete", async (data) => {
157
+ const failed = data.failedItems ?? [];
158
+ const passed = data.passedItems ?? [];
159
+ const added = data.newItems ?? [];
160
+ const deleted = data.deletedItems ?? [];
161
+ if (values.additionalDetection === "client" && typeof values.report === "string") try {
162
+ await writeXimgdiffAssets({
163
+ reportPath: values.report,
164
+ urlPrefix: typeof values.urlPrefix === "string" ? values.urlPrefix : "",
165
+ distDir: dir()
166
+ });
167
+ } catch (e) {
168
+ process.stderr.write(`reg-cli: failed to write ximgdiff assets — ${e.message}\n`);
169
+ process.exitCode = 1;
170
+ }
171
+ for (const img of passed) process.stdout.write(`${CHECK} pass ${formatPath(img)}\n`);
172
+ for (const img of added) process.stdout.write(`${PLUS} append ${formatPath(img)}\n`);
173
+ for (const img of deleted) process.stdout.write(`${MINUS} delete ${formatPath(img)}\n`);
174
+ for (const img of failed) process.stdout.write(`${CROSS} change ${formatPath(img)}\n`);
175
+ process.stdout.write("\n");
176
+ if (failed.length) process.stdout.write(`${CROSS} ${failed.length} file(s) changed.\n`);
177
+ if (deleted.length) process.stdout.write(`${MINUS} ${deleted.length} file(s) deleted.\n`);
178
+ if (added.length) process.stdout.write(`${PLUS} ${added.length} file(s) appended.\n`);
179
+ if (passed.length) process.stdout.write(`${CHECK} ${passed.length} file(s) passed.\n`);
180
+ if (update) {
181
+ if (!actualDir || !expectedDir) {
182
+ process.stderr.write(`reg-cli: --update requires actual/expected dirs (incompatible with --from).\n`);
183
+ process.exitCode = 1;
184
+ return;
185
+ }
186
+ try {
187
+ await updateExpected(actualDir, expectedDir, {
188
+ newItems: added,
189
+ failedItems: failed,
190
+ deletedItems: deleted
191
+ });
192
+ process.stdout.write("✨ your expected images are updated ✨\n");
193
+ } catch (e) {
194
+ process.stderr.write(`reg-cli: failed to update expected — ${e.message}\n`);
195
+ process.exitCode = 1;
196
+ }
197
+ return;
198
+ }
199
+ if (failed.length > 0 || extendedErrors && (added.length > 0 || deleted.length > 0)) {
200
+ process.stdout.write(`${customDiffMessage}\n`);
201
+ if (!ignoreChange) process.exitCode = 1;
202
+ }
203
+ });
204
+ emitter.once("error", (err) => {
205
+ process.stderr.write(`reg-cli: ${err?.message ?? String(err)}\n`);
206
+ process.exitCode = 1;
207
+ });
208
+ async function updateExpected(actualDir, expectedDir, items) {
209
+ const { rm } = await import("node:fs/promises");
210
+ const toRemove = [...items.deletedItems, ...items.failedItems];
211
+ for (const img of toRemove) await rm(join(expectedDir, img), { force: true });
212
+ const toCopy = [...items.newItems, ...items.failedItems];
213
+ for (const img of toCopy) {
214
+ const src = join(actualDir, img);
215
+ const dst = join(expectedDir, img);
216
+ await mkdir(dirname(dst), { recursive: true });
217
+ await copyFile(src, dst);
218
+ }
219
+ }
220
+ //#endregion
221
+ export {};
package/dist/index.cjs ADDED
@@ -0,0 +1,212 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_tracing = require("./tracing-Cf9TNPXQ.cjs");
3
+ let node_events = require("node:events");
4
+ node_events = require_tracing.__toESM(node_events, 1);
5
+ let node_worker_threads = require("node:worker_threads");
6
+ let url = require("url");
7
+ let node_path = require("node:path");
8
+ //#region src/index.ts
9
+ const dir = () => {
10
+ return require_tracing.isCJS ? __dirname : (0, node_path.dirname)((0, url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
11
+ };
12
+ const mainSpans = [];
13
+ const recordMainSpan = (name, start_ms, attributes) => {
14
+ if (!require_tracing.isTracingEnabled()) return;
15
+ mainSpans.push({
16
+ name,
17
+ start_ms,
18
+ end_ms: Date.now(),
19
+ attributes,
20
+ worker_label: "main"
21
+ });
22
+ };
23
+ const run = (argv) => {
24
+ const emitter = new node_events.default();
25
+ setImmediate(() => emitter.emit("start"));
26
+ runInternal(argv, emitter);
27
+ return emitter;
28
+ };
29
+ const runInternal = async (argv, emitter) => {
30
+ const run_start_ms = Date.now();
31
+ const t_init = Date.now();
32
+ if (require_tracing.isTracingEnabled()) {
33
+ await require_tracing.initTracing();
34
+ recordMainSpan("main.init_tracing", t_init);
35
+ }
36
+ const traceContext = require_tracing.isTracingEnabled() ? require_tracing.startRootSpan("reg-cli") : null;
37
+ const t_new_entry = Date.now();
38
+ const worker = new node_worker_threads.Worker((0, node_path.join)(dir(), `./runner.${require_tracing.resolveExtention()}`), { workerData: {
39
+ argv,
40
+ mode: "entry"
41
+ } });
42
+ recordMainSpan("main.new_entry_worker", t_new_entry);
43
+ let nextTid = 1;
44
+ const workers = [worker];
45
+ const threadWorkerSpans = [];
46
+ const attachCommonHandlers = (w) => {
47
+ w.on("message", (msg) => {
48
+ switch (msg.cmd) {
49
+ case "compare-event":
50
+ if (msg.event) emitter.emit("compare", msg.event);
51
+ return;
52
+ case "thread-spawn":
53
+ spawn(msg.startArg, msg.threadId, msg.memory);
54
+ return;
55
+ case "worker-spans":
56
+ if (Array.isArray(msg.workerSpans)) threadWorkerSpans.push(...msg.workerSpans);
57
+ return;
58
+ case "loaded":
59
+ if (typeof w.unref === "function") w.unref();
60
+ return;
61
+ }
62
+ });
63
+ w.on("error", (e) => {
64
+ if (traceContext) require_tracing.endRootSpan(false);
65
+ workers.forEach((x) => x.terminate());
66
+ emitter.emit("error", e);
67
+ });
68
+ };
69
+ const spawn = (startArg, threadId, memory) => {
70
+ const t_new_worker = Date.now();
71
+ const w = new node_worker_threads.Worker((0, node_path.join)(dir(), `./runner.${require_tracing.resolveExtention()}`), { workerData: {
72
+ argv,
73
+ mode: "thread"
74
+ } });
75
+ const tid = nextTid++;
76
+ recordMainSpan("main.new_thread_worker", t_new_worker, { tid });
77
+ workers.push(w);
78
+ attachCommonHandlers(w);
79
+ if (threadId) {
80
+ Atomics.store(threadId, 0, tid);
81
+ Atomics.notify(threadId, 0);
82
+ }
83
+ w.postMessage({
84
+ startArg,
85
+ tid,
86
+ memory
87
+ });
88
+ return tid;
89
+ };
90
+ attachCommonHandlers(worker);
91
+ worker.on("message", async (msg) => {
92
+ if (msg.cmd !== "complete") return;
93
+ const { data, traceData, workerSpans } = msg;
94
+ const t_post_complete = Date.now();
95
+ if (require_tracing.isTracingEnabled()) {
96
+ if (Array.isArray(workerSpans)) require_tracing.processWorkerSpans(workerSpans, "entry");
97
+ if (threadWorkerSpans.length) require_tracing.processWorkerSpans(threadWorkerSpans, "thread");
98
+ if (traceData) require_tracing.processRustTraceData(traceData);
99
+ recordMainSpan("main.process_trace_and_spans", t_post_complete);
100
+ mainSpans.push({
101
+ name: "main.run_total",
102
+ start_ms: run_start_ms,
103
+ end_ms: Date.now(),
104
+ worker_label: "main"
105
+ });
106
+ require_tracing.processWorkerSpans(mainSpans, "main");
107
+ require_tracing.endRootSpan(true);
108
+ await require_tracing.shutdownTracing();
109
+ }
110
+ workers.forEach((w) => w.terminate());
111
+ emitter.emit("complete", data);
112
+ });
113
+ };
114
+ /** Flags that `compare()` handles itself in JS; they are NOT forwarded to
115
+ * the Wasm binary. `extendedErrors` WAS here but now also feeds the
116
+ * Rust-side junit generator, so it's forwarded below via KEY_REMAP.
117
+ *
118
+ * `ignoreChange` and `enableCliAdditionalDetection` are stripped because
119
+ * reg-suit unconditionally passes them (see
120
+ * reg-viz/reg-suit `packages/reg-suit-core/src/processor.ts` `compare({…})`)
121
+ * and the Rust clap layer doesn't recognise them — forwarding would abort
122
+ * the binary with "unexpected argument". Semantically both are no-ops at
123
+ * the library-event layer:
124
+ * - `ignoreChange` only governs classic reg-cli's process exit code;
125
+ * the EventEmitter surface never needed it.
126
+ * - `enableCliAdditionalDetection` was classic's flag for running an
127
+ * extra CLI-side x-img-diff pass; the Wasm port's diff pipeline
128
+ * already produces the final pass/fail classification, so toggling
129
+ * it changes nothing. (The *client*-side variant is handled via
130
+ * `additionalDetection: 'client'` / the legacy
131
+ * `enableClientAdditionalDetection: true` alias further down.) */
132
+ const CLI_ONLY_KEYS = new Set([
133
+ "update",
134
+ "ignoreChange",
135
+ "enableCliAdditionalDetection"
136
+ ]);
137
+ /** Library option names that must be forwarded to the Wasm binary under a
138
+ * different flag name (Rust uses `--junit`, callers pass `junitReport`). */
139
+ const KEY_REMAP = { junitReport: "junit" };
140
+ const compare = (input) => {
141
+ const { actualDir, expectedDir, diffDir, threshold, update, ...rest } = input;
142
+ if (rest.diffFormat == null) rest.diffFormat = "png";
143
+ if (rest.json == null) rest.json = "./reg.json";
144
+ if (threshold != null && rest.thresholdRate == null) rest.thresholdRate = threshold;
145
+ const restAny = rest;
146
+ if (restAny.enableClientAdditionalDetection && restAny.additionalDetection == null) restAny.additionalDetection = "client";
147
+ delete restAny.enableClientAdditionalDetection;
148
+ for (const k of CLI_ONLY_KEYS) delete rest[k];
149
+ const inner = run([
150
+ "--",
151
+ actualDir,
152
+ expectedDir,
153
+ diffDir,
154
+ ...Object.entries(rest).flatMap(([k, v]) => {
155
+ if (v == null || v === "") return [];
156
+ return [`--${KEY_REMAP[k] ?? k}`, String(v)];
157
+ })
158
+ ]);
159
+ const outer = new node_events.default();
160
+ inner.on("start", () => outer.emit("start"));
161
+ inner.on("compare", (p) => outer.emit("compare", p));
162
+ inner.on("error", (e) => outer.emit("error", e));
163
+ inner.on("complete", async (data) => {
164
+ try {
165
+ if (restAny.additionalDetection === "client" && typeof input.report === "string") {
166
+ const { writeXimgdiffAssets } = await Promise.resolve().then(() => require("./ximgdiff-B16oe7EL.cjs")).then((n) => n.ximgdiff_exports);
167
+ await writeXimgdiffAssets({
168
+ reportPath: input.report,
169
+ urlPrefix: typeof input.urlPrefix === "string" ? input.urlPrefix : "",
170
+ distDir: dir()
171
+ });
172
+ }
173
+ if (update) {
174
+ await updateExpected(actualDir, expectedDir, {
175
+ newItems: data.newItems ?? [],
176
+ failedItems: data.failedItems ?? [],
177
+ deletedItems: data.deletedItems ?? []
178
+ });
179
+ outer.emit("update");
180
+ }
181
+ } catch (e) {
182
+ outer.emit("error", e);
183
+ return;
184
+ }
185
+ outer.emit("complete", data);
186
+ });
187
+ return outer;
188
+ };
189
+ async function writeRegJson(path, data) {
190
+ const { writeFile, mkdir } = await import("node:fs/promises");
191
+ const { dirname } = await import("node:path");
192
+ await mkdir(dirname(path), { recursive: true });
193
+ await writeFile(path, JSON.stringify(data, null, 2) + "\n", "utf8");
194
+ }
195
+ async function updateExpected(actualDir, expectedDir, items) {
196
+ const { mkdir, copyFile, rm } = await import("node:fs/promises");
197
+ const { dirname, join } = await import("node:path");
198
+ const toRemove = [...items.deletedItems, ...items.failedItems];
199
+ for (const img of toRemove) await rm(join(expectedDir, img), { force: true });
200
+ const toCopy = [...items.newItems, ...items.failedItems];
201
+ for (const img of toCopy) {
202
+ const src = join(actualDir, img);
203
+ const dst = join(expectedDir, img);
204
+ await mkdir(dirname(dst), { recursive: true });
205
+ await copyFile(src, dst);
206
+ }
207
+ }
208
+ //#endregion
209
+ exports.compare = compare;
210
+ exports.dir = dir;
211
+ exports.run = run;
212
+ exports.writeRegJson = writeRegJson;
@@ -0,0 +1,52 @@
1
+ import EventEmitter from "node:events";
2
+
3
+ //#region src/index.d.ts
4
+ declare const dir: () => string;
5
+ declare const run: (argv: string[]) => EventEmitter;
6
+ type CompareInput = {
7
+ actualDir: string;
8
+ expectedDir: string;
9
+ diffDir: string;
10
+ report?: string;
11
+ junitReport?: string;
12
+ json?: string; /** Re-render HTML from an existing reg.json (classic `-F/--from`). */
13
+ from?: string;
14
+ /** "none" (default) | "client" — enable the report's browser-side
15
+ * second-pass detector (classic `-X/--additionalDetection`). */
16
+ additionalDetection?: 'none' | 'client';
17
+ update?: boolean;
18
+ extendedErrors?: boolean;
19
+ /** Classic reg-cli's `-I/--ignoreChange` — governs the CLI's exit code
20
+ * only. Accepted and silently dropped by `compare()` for drop-in
21
+ * compat with reg-suit's `processor.ts:107`. */
22
+ ignoreChange?: boolean;
23
+ urlPrefix?: string;
24
+ matchingThreshold?: number;
25
+ threshold?: number;
26
+ thresholdRate?: number;
27
+ thresholdPixel?: number;
28
+ concurrency?: number;
29
+ enableAntialias?: boolean;
30
+ enableClientAdditionalDetection?: boolean;
31
+ /** Classic reg-cli's CLI-side x-img-diff extra detection pass. The
32
+ * Wasm pipeline's diff already includes the equivalent classification,
33
+ * so this is a no-op but accepted for drop-in compat with reg-suit's
34
+ * `processor.ts:114`. */
35
+ enableCliAdditionalDetection?: boolean;
36
+ };
37
+ type CompareOutput = {
38
+ failedItems: string[];
39
+ newItems: string[];
40
+ deletedItems: string[];
41
+ passedItems: string[];
42
+ expectedItems: string[];
43
+ actualItems: string[];
44
+ diffItems: string[];
45
+ actualDir: string;
46
+ expectedDir: string;
47
+ diffDir: string;
48
+ };
49
+ declare const compare: (input: CompareInput) => EventEmitter;
50
+ declare function writeRegJson(path: string, data: CompareOutput): Promise<void>;
51
+ //#endregion
52
+ export { CompareInput, CompareOutput, compare, dir, run, writeRegJson };
@@ -0,0 +1,52 @@
1
+ import EventEmitter from "node:events";
2
+
3
+ //#region src/index.d.ts
4
+ declare const dir: () => string;
5
+ declare const run: (argv: string[]) => EventEmitter;
6
+ type CompareInput = {
7
+ actualDir: string;
8
+ expectedDir: string;
9
+ diffDir: string;
10
+ report?: string;
11
+ junitReport?: string;
12
+ json?: string; /** Re-render HTML from an existing reg.json (classic `-F/--from`). */
13
+ from?: string;
14
+ /** "none" (default) | "client" — enable the report's browser-side
15
+ * second-pass detector (classic `-X/--additionalDetection`). */
16
+ additionalDetection?: 'none' | 'client';
17
+ update?: boolean;
18
+ extendedErrors?: boolean;
19
+ /** Classic reg-cli's `-I/--ignoreChange` — governs the CLI's exit code
20
+ * only. Accepted and silently dropped by `compare()` for drop-in
21
+ * compat with reg-suit's `processor.ts:107`. */
22
+ ignoreChange?: boolean;
23
+ urlPrefix?: string;
24
+ matchingThreshold?: number;
25
+ threshold?: number;
26
+ thresholdRate?: number;
27
+ thresholdPixel?: number;
28
+ concurrency?: number;
29
+ enableAntialias?: boolean;
30
+ enableClientAdditionalDetection?: boolean;
31
+ /** Classic reg-cli's CLI-side x-img-diff extra detection pass. The
32
+ * Wasm pipeline's diff already includes the equivalent classification,
33
+ * so this is a no-op but accepted for drop-in compat with reg-suit's
34
+ * `processor.ts:114`. */
35
+ enableCliAdditionalDetection?: boolean;
36
+ };
37
+ type CompareOutput = {
38
+ failedItems: string[];
39
+ newItems: string[];
40
+ deletedItems: string[];
41
+ passedItems: string[];
42
+ expectedItems: string[];
43
+ actualItems: string[];
44
+ diffItems: string[];
45
+ actualDir: string;
46
+ expectedDir: string;
47
+ diffDir: string;
48
+ };
49
+ declare const compare: (input: CompareInput) => EventEmitter;
50
+ declare function writeRegJson(path: string, data: CompareOutput): Promise<void>;
51
+ //#endregion
52
+ export { CompareInput, CompareOutput, compare, dir, run, writeRegJson };