reg-cli 0.18.16 → 0.19.0-rc0

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.
Files changed (46) hide show
  1. package/README.md +155 -46
  2. package/dist/cli.cjs +221 -0
  3. package/dist/cli.d.cts +1 -0
  4. package/dist/cli.d.mts +1 -0
  5. package/dist/cli.mjs +221 -0
  6. package/dist/entry.cjs +119 -0
  7. package/dist/entry.d.cts +15 -0
  8. package/dist/entry.d.mts +15 -0
  9. package/dist/entry.mjs +118 -0
  10. package/dist/index.cjs +212 -0
  11. package/dist/index.d.cts +52 -0
  12. package/dist/index.d.mts +52 -0
  13. package/dist/index.mjs +206 -0
  14. package/dist/progress-BB7D-xke.mjs +28 -0
  15. package/dist/progress-UOiKHaTY.cjs +33 -0
  16. package/dist/reg.wasm +0 -0
  17. package/dist/rolldown-runtime-D_mwlA32.cjs +43 -0
  18. package/{report/ui/dist/worker.js → dist/shared/report-worker.js} +859 -247
  19. package/dist/tracing-7HO8hac9.mjs +378 -0
  20. package/dist/tracing-DlLL541f.cjs +445 -0
  21. package/dist/worker.cjs +143 -0
  22. package/dist/worker.d.cts +1 -0
  23. package/dist/worker.d.mts +1 -0
  24. package/dist/worker.mjs +142 -0
  25. package/dist/ximgdiff-DzkRRnkW.mjs +41 -0
  26. package/dist/ximgdiff-wmW1BH1H.cjs +41 -0
  27. package/package.json +61 -112
  28. package/dist/.keep +0 -0
  29. package/dist/cli.js +0 -214
  30. package/dist/diff.js +0 -80
  31. package/dist/icon.js +0 -12
  32. package/dist/image-finder.js +0 -25
  33. package/dist/index.js +0 -200
  34. package/dist/log.js +0 -21
  35. package/dist/process-adaptor.js +0 -40
  36. package/dist/report.js +0 -170
  37. package/dist/tracing.js +0 -169
  38. package/report/assets/favicon_failure.png +0 -0
  39. package/report/assets/favicon_success.png +0 -0
  40. package/report/sample/actual/sample.png +0 -0
  41. package/report/sample/diff/sample.png +0 -0
  42. package/report/sample/expected/sample.png +0 -0
  43. package/report/ui/dist/report.js +0 -123
  44. package/report/ui/dist/style.css +0 -1
  45. package/template/template.html +0 -20
  46. /package/{template → dist/shared}/worker_pre.js +0 -0
package/dist/cli.mjs ADDED
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env node
2
+ import { t as writeXimgdiffAssets } from "./ximgdiff-DzkRRnkW.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/entry.cjs ADDED
@@ -0,0 +1,119 @@
1
+ const require_rolldown_runtime = require("./rolldown-runtime-D_mwlA32.cjs");
2
+ const require_tracing = require("./tracing-DlLL541f.cjs");
3
+ const require_progress = require("./progress-UOiKHaTY.cjs");
4
+ let node_worker_threads = require("node:worker_threads");
5
+ let node_process = require("node:process");
6
+ let node_fs = require("node:fs");
7
+ node_fs = require_rolldown_runtime.__toESM(node_fs, 1);
8
+ let _tybys_wasm_util = require("@tybys/wasm-util");
9
+ //#region src/entry.ts
10
+ const isTracingEnabled = () => {
11
+ return node_process.env.OTEL_ENABLED === "true" || node_process.env.JAEGER_ENABLED === "true";
12
+ };
13
+ const workerSpans = [];
14
+ const tSpan = async (name, fn, attributes) => {
15
+ if (!isTracingEnabled()) return fn();
16
+ const start_ms = Date.now();
17
+ const result = await fn();
18
+ workerSpans.push({
19
+ name,
20
+ start_ms,
21
+ end_ms: Date.now(),
22
+ attributes,
23
+ worker_label: "entry"
24
+ });
25
+ return result;
26
+ };
27
+ const sandbox = require_tracing.computeWasiSandbox(node_worker_threads.workerData.argv);
28
+ const printErr = require_progress.createPrintErrHook((ev) => {
29
+ node_worker_threads.parentPort?.postMessage({
30
+ cmd: "compare-event",
31
+ event: ev
32
+ });
33
+ });
34
+ const wasi = new _tybys_wasm_util.WASI({
35
+ version: "preview1",
36
+ args: node_worker_threads.workerData.argv,
37
+ env: sandbox.env,
38
+ returnOnExit: true,
39
+ preopens: sandbox.preopens,
40
+ fs: node_fs.default,
41
+ printErr
42
+ });
43
+ const imports = wasi.getImportObject();
44
+ const file = require_tracing.readWasm();
45
+ /**
46
+ * Read a string from WASM memory using WasmOutput structure
47
+ */
48
+ const readWasmString = (exports, memory, outputPtr) => {
49
+ const view = new DataView(memory.buffer, outputPtr);
50
+ const len = view.getUint32(0, true);
51
+ const bufPtr = view.getUint32(4, true);
52
+ const stringData = new Uint8Array(memory.buffer, bufPtr, len);
53
+ const str = new TextDecoder("utf-8").decode(stringData);
54
+ exports.free_wasm_output(outputPtr);
55
+ return str;
56
+ };
57
+ (async () => {
58
+ try {
59
+ const entry_start_ms = Date.now();
60
+ const wasm = await tSpan("entry.wasm_compile", async () => WebAssembly.compile(await file));
61
+ const memory = new WebAssembly.Memory({
62
+ initial: 256,
63
+ maximum: 16384,
64
+ shared: true
65
+ });
66
+ let instance = await tSpan("entry.wasm_instantiate", async () => WebAssembly.instantiate(wasm, {
67
+ ...imports,
68
+ wasi: { "thread-spawn": (startArg) => {
69
+ const threadIdBuffer = new SharedArrayBuffer(4);
70
+ const id = new Int32Array(threadIdBuffer);
71
+ Atomics.store(id, 0, -1);
72
+ node_worker_threads.parentPort?.postMessage({
73
+ cmd: "thread-spawn",
74
+ startArg,
75
+ threadId: id,
76
+ memory
77
+ });
78
+ Atomics.wait(id, 0, -1);
79
+ return Atomics.load(id, 0);
80
+ } },
81
+ env: { memory }
82
+ }));
83
+ const exports = instance.exports;
84
+ await tSpan("entry.wasi_start", async () => {
85
+ wasi.start(instance);
86
+ });
87
+ if (isTracingEnabled() && typeof exports.init_tracing === "function") await tSpan("entry.init_tracing_rust", async () => {
88
+ exports.init_tracing();
89
+ });
90
+ const m = await tSpan("entry.wasm_main", async () => exports.wasm_main());
91
+ const reportString = await tSpan("entry.read_report_string", async () => readWasmString(exports, memory, m));
92
+ const report = JSON.parse(reportString);
93
+ let traceData = null;
94
+ if (isTracingEnabled() && typeof exports.get_trace_data === "function") await tSpan("entry.collect_rust_traces", async () => {
95
+ const traceJson = readWasmString(exports, memory, exports.get_trace_data());
96
+ try {
97
+ traceData = JSON.parse(traceJson);
98
+ } catch (e) {
99
+ console.error("[Tracing] Failed to parse trace data:", e);
100
+ }
101
+ if (typeof exports.clear_trace_data === "function") exports.clear_trace_data();
102
+ });
103
+ if (isTracingEnabled()) workerSpans.push({
104
+ name: "entry.worker_total",
105
+ start_ms: entry_start_ms,
106
+ end_ms: Date.now(),
107
+ worker_label: "entry"
108
+ });
109
+ node_worker_threads.parentPort?.postMessage({
110
+ cmd: "complete",
111
+ data: report,
112
+ traceData,
113
+ workerSpans
114
+ });
115
+ } catch (e) {
116
+ throw e;
117
+ }
118
+ })();
119
+ //#endregion
@@ -0,0 +1,15 @@
1
+ //#region src/entry.d.ts
2
+ type CompareOutput = {
3
+ failedItems: string[];
4
+ newItems: string[];
5
+ deletedItems: string[];
6
+ passedItems: string[];
7
+ expectedItems: string[];
8
+ actualItems: string[];
9
+ diffItems: string[];
10
+ actualDir: string;
11
+ expectedDir: string;
12
+ diffDir: string;
13
+ };
14
+ //#endregion
15
+ export { CompareOutput };
@@ -0,0 +1,15 @@
1
+ //#region src/entry.d.ts
2
+ type CompareOutput = {
3
+ failedItems: string[];
4
+ newItems: string[];
5
+ deletedItems: string[];
6
+ passedItems: string[];
7
+ expectedItems: string[];
8
+ actualItems: string[];
9
+ diffItems: string[];
10
+ actualDir: string;
11
+ expectedDir: string;
12
+ diffDir: string;
13
+ };
14
+ //#endregion
15
+ export { CompareOutput };
package/dist/entry.mjs ADDED
@@ -0,0 +1,118 @@
1
+ import { c as computeWasiSandbox, u as readWasm } from "./tracing-7HO8hac9.mjs";
2
+ import { t as createPrintErrHook } from "./progress-BB7D-xke.mjs";
3
+ import { parentPort, workerData } from "node:worker_threads";
4
+ import { env } from "node:process";
5
+ import fs from "node:fs";
6
+ import { WASI } from "@tybys/wasm-util";
7
+ //#region src/entry.ts
8
+ const isTracingEnabled = () => {
9
+ return env.OTEL_ENABLED === "true" || env.JAEGER_ENABLED === "true";
10
+ };
11
+ const workerSpans = [];
12
+ const tSpan = async (name, fn, attributes) => {
13
+ if (!isTracingEnabled()) return fn();
14
+ const start_ms = Date.now();
15
+ const result = await fn();
16
+ workerSpans.push({
17
+ name,
18
+ start_ms,
19
+ end_ms: Date.now(),
20
+ attributes,
21
+ worker_label: "entry"
22
+ });
23
+ return result;
24
+ };
25
+ const sandbox = computeWasiSandbox(workerData.argv);
26
+ const printErr = createPrintErrHook((ev) => {
27
+ parentPort?.postMessage({
28
+ cmd: "compare-event",
29
+ event: ev
30
+ });
31
+ });
32
+ const wasi = new WASI({
33
+ version: "preview1",
34
+ args: workerData.argv,
35
+ env: sandbox.env,
36
+ returnOnExit: true,
37
+ preopens: sandbox.preopens,
38
+ fs,
39
+ printErr
40
+ });
41
+ const imports = wasi.getImportObject();
42
+ const file = readWasm();
43
+ /**
44
+ * Read a string from WASM memory using WasmOutput structure
45
+ */
46
+ const readWasmString = (exports, memory, outputPtr) => {
47
+ const view = new DataView(memory.buffer, outputPtr);
48
+ const len = view.getUint32(0, true);
49
+ const bufPtr = view.getUint32(4, true);
50
+ const stringData = new Uint8Array(memory.buffer, bufPtr, len);
51
+ const str = new TextDecoder("utf-8").decode(stringData);
52
+ exports.free_wasm_output(outputPtr);
53
+ return str;
54
+ };
55
+ (async () => {
56
+ try {
57
+ const entry_start_ms = Date.now();
58
+ const wasm = await tSpan("entry.wasm_compile", async () => WebAssembly.compile(await file));
59
+ const memory = new WebAssembly.Memory({
60
+ initial: 256,
61
+ maximum: 16384,
62
+ shared: true
63
+ });
64
+ let instance = await tSpan("entry.wasm_instantiate", async () => WebAssembly.instantiate(wasm, {
65
+ ...imports,
66
+ wasi: { "thread-spawn": (startArg) => {
67
+ const threadIdBuffer = new SharedArrayBuffer(4);
68
+ const id = new Int32Array(threadIdBuffer);
69
+ Atomics.store(id, 0, -1);
70
+ parentPort?.postMessage({
71
+ cmd: "thread-spawn",
72
+ startArg,
73
+ threadId: id,
74
+ memory
75
+ });
76
+ Atomics.wait(id, 0, -1);
77
+ return Atomics.load(id, 0);
78
+ } },
79
+ env: { memory }
80
+ }));
81
+ const exports = instance.exports;
82
+ await tSpan("entry.wasi_start", async () => {
83
+ wasi.start(instance);
84
+ });
85
+ if (isTracingEnabled() && typeof exports.init_tracing === "function") await tSpan("entry.init_tracing_rust", async () => {
86
+ exports.init_tracing();
87
+ });
88
+ const m = await tSpan("entry.wasm_main", async () => exports.wasm_main());
89
+ const reportString = await tSpan("entry.read_report_string", async () => readWasmString(exports, memory, m));
90
+ const report = JSON.parse(reportString);
91
+ let traceData = null;
92
+ if (isTracingEnabled() && typeof exports.get_trace_data === "function") await tSpan("entry.collect_rust_traces", async () => {
93
+ const traceJson = readWasmString(exports, memory, exports.get_trace_data());
94
+ try {
95
+ traceData = JSON.parse(traceJson);
96
+ } catch (e) {
97
+ console.error("[Tracing] Failed to parse trace data:", e);
98
+ }
99
+ if (typeof exports.clear_trace_data === "function") exports.clear_trace_data();
100
+ });
101
+ if (isTracingEnabled()) workerSpans.push({
102
+ name: "entry.worker_total",
103
+ start_ms: entry_start_ms,
104
+ end_ms: Date.now(),
105
+ worker_label: "entry"
106
+ });
107
+ parentPort?.postMessage({
108
+ cmd: "complete",
109
+ data: report,
110
+ traceData,
111
+ workerSpans
112
+ });
113
+ } catch (e) {
114
+ throw e;
115
+ }
116
+ })();
117
+ //#endregion
118
+ export {};
package/dist/index.cjs ADDED
@@ -0,0 +1,212 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_rolldown_runtime = require("./rolldown-runtime-D_mwlA32.cjs");
3
+ const require_tracing = require("./tracing-DlLL541f.cjs");
4
+ let node_events = require("node:events");
5
+ node_events = require_rolldown_runtime.__toESM(node_events, 1);
6
+ let node_worker_threads = require("node:worker_threads");
7
+ let url = require("url");
8
+ let node_path = require("node:path");
9
+ //#region src/index.ts
10
+ const dir = () => {
11
+ return require_tracing.isCJS ? __dirname : (0, node_path.dirname)((0, url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
12
+ };
13
+ const mainSpans = [];
14
+ const recordMainSpan = (name, start_ms, attributes) => {
15
+ if (!require_tracing.isTracingEnabled()) return;
16
+ mainSpans.push({
17
+ name,
18
+ start_ms,
19
+ end_ms: Date.now(),
20
+ attributes,
21
+ worker_label: "main"
22
+ });
23
+ };
24
+ const run = (argv) => {
25
+ const emitter = new node_events.default();
26
+ setImmediate(() => emitter.emit("start"));
27
+ runInternal(argv, emitter);
28
+ return emitter;
29
+ };
30
+ const runInternal = async (argv, emitter) => {
31
+ const run_start_ms = Date.now();
32
+ const t_init = Date.now();
33
+ if (require_tracing.isTracingEnabled()) {
34
+ await require_tracing.initTracing();
35
+ recordMainSpan("main.init_tracing", t_init);
36
+ }
37
+ const traceContext = require_tracing.isTracingEnabled() ? require_tracing.startRootSpan("reg-cli") : null;
38
+ const t_new_entry = Date.now();
39
+ const worker = new node_worker_threads.Worker((0, node_path.join)(dir(), `./entry.${require_tracing.resolveExtention()}`), { workerData: { argv } });
40
+ recordMainSpan("main.new_entry_worker", t_new_entry);
41
+ let nextTid = 1;
42
+ const workers = [worker];
43
+ const threadWorkerSpans = [];
44
+ const spawn = (startArg, threadId, memory) => {
45
+ const t_new_worker = Date.now();
46
+ const worker = new node_worker_threads.Worker((0, node_path.join)(dir(), `./worker.${require_tracing.resolveExtention()}`), { workerData: { argv } });
47
+ const tid = nextTid++;
48
+ recordMainSpan("main.new_thread_worker", t_new_worker, { tid });
49
+ workers.push(worker);
50
+ worker.on("message", (msg) => {
51
+ const { cmd } = msg;
52
+ if (cmd === "loaded") {
53
+ if (typeof worker.unref === "function") worker.unref();
54
+ } else if (cmd === "thread-spawn") spawn(msg.startArg, msg.threadId, msg.memory);
55
+ else if (cmd === "worker-spans") {
56
+ if (Array.isArray(msg.workerSpans)) threadWorkerSpans.push(...msg.workerSpans);
57
+ } else if (cmd === "compare-event") {
58
+ if (msg.event) emitter.emit("compare", msg.event);
59
+ }
60
+ });
61
+ worker.on("error", (e) => {
62
+ workers.forEach((w) => w.terminate());
63
+ emitter.emit("error", e);
64
+ });
65
+ if (threadId) {
66
+ Atomics.store(threadId, 0, tid);
67
+ Atomics.notify(threadId, 0);
68
+ }
69
+ worker.postMessage({
70
+ startArg,
71
+ tid,
72
+ memory
73
+ });
74
+ return tid;
75
+ };
76
+ worker.on("message", async ({ cmd, startArg, threadId, memory, data, traceData, workerSpans, event }) => {
77
+ if (cmd === "compare-event") {
78
+ if (event) emitter.emit("compare", event);
79
+ return;
80
+ }
81
+ if (cmd === "complete") {
82
+ const t_post_complete = Date.now();
83
+ if (require_tracing.isTracingEnabled()) {
84
+ if (Array.isArray(workerSpans)) require_tracing.processWorkerSpans(workerSpans, "entry");
85
+ if (threadWorkerSpans.length) require_tracing.processWorkerSpans(threadWorkerSpans, "thread");
86
+ if (traceData) require_tracing.processRustTraceData(traceData);
87
+ recordMainSpan("main.process_trace_and_spans", t_post_complete);
88
+ mainSpans.push({
89
+ name: "main.run_total",
90
+ start_ms: run_start_ms,
91
+ end_ms: Date.now(),
92
+ worker_label: "main"
93
+ });
94
+ require_tracing.processWorkerSpans(mainSpans, "main");
95
+ require_tracing.endRootSpan(true);
96
+ await require_tracing.shutdownTracing();
97
+ }
98
+ workers.forEach((w) => w.terminate());
99
+ emitter.emit("complete", data);
100
+ return;
101
+ }
102
+ if (cmd === "loaded") {
103
+ if (typeof worker.unref === "function") worker.unref();
104
+ return;
105
+ }
106
+ if (cmd === "thread-spawn") spawn(startArg, threadId, memory);
107
+ });
108
+ worker.on("error", (err) => {
109
+ if (traceContext) require_tracing.endRootSpan(false);
110
+ workers.forEach((w) => w.terminate());
111
+ emitter.emit("error", err);
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-wmW1BH1H.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;