@crvy/rprtr 0.0.3 → 0.0.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.
@@ -0,0 +1,1003 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/server.ts
31
+ var server_exports = {};
32
+ __export(server_exports, {
33
+ startServer: () => startServer
34
+ });
35
+ module.exports = __toCommonJS(server_exports);
36
+
37
+ // src/server/app.ts
38
+ var import_path4 = require("path");
39
+ var import_url = require("url");
40
+ var import_p_limit = __toESM(require("p-limit"), 1);
41
+
42
+ // src/offline-reports.ts
43
+ var import_promises = require("fs/promises");
44
+ var import_path = require("path");
45
+
46
+ // src/report-utils.ts
47
+ function normalizeScreenshotsBaseUrl(baseUrl) {
48
+ return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
49
+ }
50
+ function attachmentsToImages(attachments, screenshotsBaseUrl = "/screenshots/") {
51
+ const images = {};
52
+ const baseUrl = normalizeScreenshotsBaseUrl(screenshotsBaseUrl);
53
+ for (const attachment of attachments) {
54
+ if (attachment.contentType !== "image/png") continue;
55
+ const match = attachment.name.match(/^(.+?)-(actual|expected|diff)(?:\.png)?$/);
56
+ if (match === null) continue;
57
+ const baseName = match[1];
58
+ const role = match[2];
59
+ if (baseName === null || baseName === void 0 || role === null || role === void 0) continue;
60
+ images[baseName] ??= { actual: "" };
61
+ const url = `${baseUrl}${attachment.path}`;
62
+ const img = images[baseName];
63
+ if (img !== null && img !== void 0) {
64
+ if (role === "actual") img.actual = url;
65
+ else if (role === "expected") img.expect = url;
66
+ else if (role === "diff") img.diff = url;
67
+ }
68
+ }
69
+ for (const key of Object.keys(images)) {
70
+ const img = images[key];
71
+ if (img?.actual !== null && img?.actual !== void 0 && img?.expect !== null && img?.expect !== void 0 && img?.diff === void 0)
72
+ delete img.expect;
73
+ }
74
+ return images;
75
+ }
76
+ function mapStatus(status) {
77
+ switch (status) {
78
+ case "passed":
79
+ return "success";
80
+ case "failed":
81
+ return "failed";
82
+ case "skipped":
83
+ return "pending";
84
+ default:
85
+ return "unknown";
86
+ }
87
+ }
88
+
89
+ // src/report-state.ts
90
+ function hasReviewablePassingImages(images) {
91
+ return Object.values(images).some(
92
+ (img) => img !== null && img !== void 0 && img.actual !== null && img.actual !== void 0 && img.diff === void 0
93
+ );
94
+ }
95
+ function preservePreviousPassingImages(test, status, images) {
96
+ if (status !== "passed" || Object.keys(images).length > 0) {
97
+ return images;
98
+ }
99
+ const previousImages = test.results?.[0]?.images ?? {};
100
+ return hasReviewablePassingImages(previousImages) ? previousImages : images;
101
+ }
102
+ function countDiffImages(images) {
103
+ return Object.values(images).filter((img) => img?.diff !== null && img?.diff !== void 0).length;
104
+ }
105
+ function createMutableReportState(screenshotDir = "./screenshots") {
106
+ return {
107
+ reportData: {
108
+ isRunning: false,
109
+ tests: {},
110
+ browsers: ["chromium"],
111
+ isUpdateMode: false,
112
+ screenshotDir
113
+ },
114
+ currentRunIds: /* @__PURE__ */ new Set()
115
+ };
116
+ }
117
+ function applyTestBeginEvent(state, data) {
118
+ const { id, title, titlePath, browser, location } = data;
119
+ state.currentRunIds.add(id);
120
+ state.reportData.tests[id] ??= {
121
+ id,
122
+ titlePath: titlePath ?? [],
123
+ browser: browser ?? "",
124
+ title: title ?? "",
125
+ location,
126
+ status: "running"
127
+ };
128
+ return state.reportData.tests[id];
129
+ }
130
+ function applyTestEndEvent(state, data, options = {}) {
131
+ const test = state.reportData.tests[data.id];
132
+ if (test === void 0) {
133
+ return null;
134
+ }
135
+ test.status = mapStatus(data.status);
136
+ const images = preservePreviousPassingImages(
137
+ test,
138
+ data.status,
139
+ attachmentsToImages(data.attachments, options.screenshotsBaseUrl)
140
+ );
141
+ const diffCount = countDiffImages(images);
142
+ const hasDiffs = diffCount > 0;
143
+ if (hasDiffs) {
144
+ test.approved = null;
145
+ }
146
+ test.results = [
147
+ {
148
+ status: data.status === "passed" ? "success" : "failed",
149
+ retries: 0,
150
+ images,
151
+ error: data.error,
152
+ duration: data.duration
153
+ }
154
+ ];
155
+ return {
156
+ test,
157
+ diffCount
158
+ };
159
+ }
160
+ function finalizeRunEvent(state) {
161
+ state.reportData.isRunning = false;
162
+ state.reportData.tests = Object.fromEntries(
163
+ Object.entries(state.reportData.tests).filter(([id]) => state.currentRunIds.has(id))
164
+ );
165
+ state.currentRunIds.clear();
166
+ const runTests = Object.values(state.reportData.tests).filter((test) => test !== void 0);
167
+ return {
168
+ passed: runTests.filter((test) => test.status === "success").length,
169
+ failed: runTests.filter((test) => test.status === "failed").length,
170
+ pending: runTests.filter((test) => test.status === "pending").length
171
+ };
172
+ }
173
+
174
+ // src/schemas.ts
175
+ var import_zod = require("zod");
176
+ var LocationSchema = import_zod.z.object({
177
+ file: import_zod.z.string(),
178
+ line: import_zod.z.number()
179
+ });
180
+ var ImagesSchema = import_zod.z.object({
181
+ actual: import_zod.z.string(),
182
+ expect: import_zod.z.string().optional(),
183
+ diff: import_zod.z.string().optional(),
184
+ error: import_zod.z.string().optional()
185
+ });
186
+ var AttachmentSchema = import_zod.z.object({
187
+ name: import_zod.z.string(),
188
+ path: import_zod.z.string(),
189
+ contentType: import_zod.z.string()
190
+ });
191
+ var TestStatusSchema = import_zod.z.enum(["unknown", "pending", "running", "failed", "approved", "success", "retrying"]);
192
+ var TestResultSchema = import_zod.z.object({
193
+ status: import_zod.z.enum(["failed", "success"]),
194
+ retries: import_zod.z.number(),
195
+ // eslint-disable-next-line typescript/no-unsafe-type-assertion
196
+ images: import_zod.z.record(import_zod.z.string(), ImagesSchema).optional(),
197
+ error: import_zod.z.string().optional(),
198
+ duration: import_zod.z.number().optional()
199
+ });
200
+ var TestDataSchema = import_zod.z.object({
201
+ id: import_zod.z.string(),
202
+ titlePath: import_zod.z.array(import_zod.z.string()),
203
+ browser: import_zod.z.string(),
204
+ title: import_zod.z.string(),
205
+ skip: import_zod.z.union([import_zod.z.boolean(), import_zod.z.string()]).optional(),
206
+ retries: import_zod.z.number().optional(),
207
+ status: TestStatusSchema.optional(),
208
+ results: import_zod.z.array(TestResultSchema).optional(),
209
+ approved: import_zod.z.record(import_zod.z.string(), import_zod.z.number()).nullable().optional(),
210
+ attachments: import_zod.z.array(AttachmentSchema).optional(),
211
+ location: LocationSchema.optional()
212
+ });
213
+ var CrvyRprtrTestSchema = TestDataSchema.extend({
214
+ checked: import_zod.z.boolean()
215
+ });
216
+ var CrvyRprtrSuiteSchema = import_zod.z.lazy(
217
+ () => import_zod.z.object({
218
+ path: import_zod.z.array(import_zod.z.string()),
219
+ skip: import_zod.z.boolean(),
220
+ status: TestStatusSchema.optional(),
221
+ opened: import_zod.z.boolean(),
222
+ checked: import_zod.z.boolean(),
223
+ indeterminate: import_zod.z.boolean(),
224
+ // eslint-disable-next-line typescript/no-unsafe-type-assertion
225
+ children: import_zod.z.record(import_zod.z.string(), import_zod.z.union([CrvyRprtrSuiteSchema, CrvyRprtrTestSchema])).optional()
226
+ })
227
+ );
228
+ var WebSocketMessageSchema = import_zod.z.object({
229
+ type: import_zod.z.enum(["test-begin", "test-end", "run-end", "approve", "sync"]),
230
+ data: import_zod.z.unknown()
231
+ });
232
+ var TestBeginDataSchema = import_zod.z.object({
233
+ id: import_zod.z.string(),
234
+ title: import_zod.z.string(),
235
+ titlePath: import_zod.z.array(import_zod.z.string()),
236
+ browser: import_zod.z.string(),
237
+ location: LocationSchema
238
+ });
239
+ var TestEndDataSchema = import_zod.z.object({
240
+ id: import_zod.z.string(),
241
+ status: import_zod.z.enum(["passed", "failed", "skipped"]),
242
+ attachments: import_zod.z.array(AttachmentSchema),
243
+ error: import_zod.z.string().optional(),
244
+ duration: import_zod.z.number().optional()
245
+ });
246
+ var ReportDataSchema = import_zod.z.object({
247
+ isRunning: import_zod.z.boolean(),
248
+ tests: import_zod.z.record(import_zod.z.string(), TestDataSchema),
249
+ browsers: import_zod.z.array(import_zod.z.string()),
250
+ isUpdateMode: import_zod.z.boolean(),
251
+ screenshotDir: import_zod.z.string()
252
+ });
253
+ var LoadedReportDataSchema = import_zod.z.object({
254
+ tests: import_zod.z.record(import_zod.z.string(), TestDataSchema).optional(),
255
+ isUpdateMode: import_zod.z.boolean().optional()
256
+ });
257
+ var OfflineEventSchema = import_zod.z.object({
258
+ type: import_zod.z.enum(["test-begin", "test-end", "run-end"]),
259
+ data: import_zod.z.unknown(),
260
+ timestamp: import_zod.z.number(),
261
+ workerIndex: import_zod.z.number()
262
+ });
263
+ var OfflineReportSchema = import_zod.z.object({
264
+ version: import_zod.z.number(),
265
+ generatedAt: import_zod.z.string(),
266
+ workers: import_zod.z.number(),
267
+ events: import_zod.z.array(OfflineEventSchema)
268
+ });
269
+ var ApproveRequestBodySchema = import_zod.z.object({
270
+ id: import_zod.z.string(),
271
+ retry: import_zod.z.number(),
272
+ image: import_zod.z.string()
273
+ });
274
+ var ReportApiResponseSchema = import_zod.z.object({
275
+ tests: import_zod.z.record(import_zod.z.string(), TestDataSchema),
276
+ isUpdateMode: import_zod.z.boolean().optional()
277
+ });
278
+ var ClientBootstrapDataSchema = import_zod.z.object({
279
+ report: ReportApiResponseSchema.extend({
280
+ isUpdateMode: import_zod.z.boolean()
281
+ }),
282
+ liveUpdates: import_zod.z.boolean(),
283
+ approvalEnabled: import_zod.z.boolean(),
284
+ approvalMessage: import_zod.z.string().optional()
285
+ });
286
+ var ImagesViewModeSchema = import_zod.z.enum(["side-by-side", "swap", "slide", "blend"]);
287
+ function safeParse(schema, data) {
288
+ const result = schema.safeParse(data);
289
+ if (result.success) {
290
+ return result.data;
291
+ }
292
+ return null;
293
+ }
294
+
295
+ // src/offline-reports.ts
296
+ var OFFLINE_REPORT_FILE_PATTERN = /^crvy-rprtr(?:-\d+)?\.json$/;
297
+ async function findOfflineReportPaths(searchDir) {
298
+ try {
299
+ const entries = await (0, import_promises.readdir)(searchDir);
300
+ return entries.filter((entry) => OFFLINE_REPORT_FILE_PATTERN.test(entry)).sort((left, right) => left.localeCompare(right)).map((entry) => (0, import_path.join)(searchDir, entry));
301
+ } catch (error) {
302
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
303
+ return [];
304
+ }
305
+ throw error;
306
+ }
307
+ }
308
+ function parseOfflineReport(value) {
309
+ const parsed = safeParse(OfflineReportSchema, value);
310
+ if (parsed === null || parsed.version !== 1 || !Array.isArray(parsed.events)) {
311
+ return null;
312
+ }
313
+ return parsed;
314
+ }
315
+ function mergeOfflineReportsIntoTests(existingTests, offlineReports, options = {}) {
316
+ const state = createMutableReportState(options.screenshotDir);
317
+ let shouldFinalize = false;
318
+ for (const report of offlineReports) {
319
+ for (const event of report.events) {
320
+ switch (event.type) {
321
+ case "test-begin": {
322
+ const parsed = safeParse(TestBeginDataSchema, event.data);
323
+ if (parsed !== null) {
324
+ applyTestBeginEvent(state, parsed);
325
+ }
326
+ break;
327
+ }
328
+ case "test-end": {
329
+ const parsed = safeParse(TestEndDataSchema, event.data);
330
+ if (parsed !== null) {
331
+ applyTestEndEvent(state, parsed, { screenshotsBaseUrl: options.screenshotsBaseUrl });
332
+ }
333
+ break;
334
+ }
335
+ case "run-end":
336
+ shouldFinalize = true;
337
+ break;
338
+ }
339
+ }
340
+ }
341
+ if (shouldFinalize) {
342
+ finalizeRunEvent(state);
343
+ }
344
+ return {
345
+ ...existingTests,
346
+ ...state.reportData.tests
347
+ };
348
+ }
349
+
350
+ // src/server/file-utils.ts
351
+ var import_promises2 = require("fs/promises");
352
+ var import_path2 = require("path");
353
+ function isFileNotFound(error) {
354
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
355
+ }
356
+ async function fileExists(filePath) {
357
+ try {
358
+ await (0, import_promises2.access)(filePath);
359
+ return true;
360
+ } catch (error) {
361
+ if (isFileNotFound(error)) {
362
+ return false;
363
+ }
364
+ throw error;
365
+ }
366
+ }
367
+ async function isDirectory(filePath) {
368
+ try {
369
+ const stats = await (0, import_promises2.stat)(filePath);
370
+ return stats.isDirectory();
371
+ } catch (error) {
372
+ if (isFileNotFound(error)) {
373
+ return false;
374
+ }
375
+ throw error;
376
+ }
377
+ }
378
+ async function readJsonFile(filePath) {
379
+ try {
380
+ const raw = await (0, import_promises2.readFile)(filePath, "utf8");
381
+ if (raw.trim() === "") {
382
+ return null;
383
+ }
384
+ return JSON.parse(raw);
385
+ } catch (error) {
386
+ if (isFileNotFound(error)) {
387
+ return null;
388
+ }
389
+ throw error;
390
+ }
391
+ }
392
+ async function writeJsonFile(filePath, value) {
393
+ await (0, import_promises2.mkdir)((0, import_path2.dirname)(filePath), { recursive: true });
394
+ await (0, import_promises2.writeFile)(filePath, `${JSON.stringify(value, null, 2)}
395
+ `);
396
+ }
397
+ async function copyFilePortable(sourcePath, destinationPath) {
398
+ await (0, import_promises2.mkdir)((0, import_path2.dirname)(destinationPath), { recursive: true });
399
+ await (0, import_promises2.copyFile)(sourcePath, destinationPath);
400
+ }
401
+ function inferContentType(filePath) {
402
+ switch ((0, import_path2.extname)(filePath).toLowerCase()) {
403
+ case ".css":
404
+ return "text/css";
405
+ case ".gif":
406
+ return "image/gif";
407
+ case ".html":
408
+ return "text/html";
409
+ case ".jpeg":
410
+ case ".jpg":
411
+ return "image/jpeg";
412
+ case ".js":
413
+ return "application/javascript";
414
+ case ".json":
415
+ return "application/json";
416
+ case ".png":
417
+ return "image/png";
418
+ case ".svg":
419
+ return "image/svg+xml";
420
+ case ".txt":
421
+ return "text/plain";
422
+ case ".webp":
423
+ return "image/webp";
424
+ default:
425
+ return void 0;
426
+ }
427
+ }
428
+ async function respondWithFile(filePath, contentType) {
429
+ try {
430
+ const file = await (0, import_promises2.readFile)(filePath);
431
+ const resolvedContentType = contentType ?? inferContentType(filePath);
432
+ const headers = resolvedContentType === void 0 ? void 0 : { "Content-Type": resolvedContentType };
433
+ return new Response(file, { headers });
434
+ } catch (error) {
435
+ if (isFileNotFound(error)) {
436
+ return null;
437
+ }
438
+ throw error;
439
+ }
440
+ }
441
+
442
+ // src/server/utils.ts
443
+ function broadcastToBrowsers(wsClients, msg) {
444
+ const payload = JSON.stringify(msg);
445
+ wsClients.forEach((ws) => {
446
+ ws.send(payload);
447
+ });
448
+ }
449
+
450
+ // src/server/handlers.ts
451
+ function handleTestBegin(ctx, data) {
452
+ const test = applyTestBeginEvent(ctx, data);
453
+ console.log(` \u25B6 [${test.browser ?? "?"}] ${test.title}`);
454
+ }
455
+ function handleTestEnd(ctx, data) {
456
+ const result = applyTestEndEvent(ctx, data);
457
+ if (result !== null) {
458
+ const { test, diffCount } = result;
459
+ const icon = data.status === "passed" ? "\u2713" : data.status === "skipped" ? "\u2013" : "\u2717";
460
+ const dur = data.duration === null || data.duration === void 0 ? "" : ` (${data.duration}ms)`;
461
+ const diffNote = diffCount > 0 ? ` [${diffCount} diff(s)]` : "";
462
+ const errNote = data.error !== null && data.error !== void 0 ? `
463
+ Error: ${data.error}` : "";
464
+ console.log(` ${icon} [${test.browser}] ${test.title}${dur}${diffNote}${errNote}`);
465
+ }
466
+ broadcastToBrowsers(ctx.wsClients, { type: "test-update", data });
467
+ }
468
+ async function handleRunEnd(ctx, data) {
469
+ const { passed, failed, pending } = finalizeRunEvent(ctx);
470
+ await ctx.saveReport();
471
+ console.log(`
472
+ Run complete \u2014 ${passed} passed, ${failed} failed, ${pending} skipped`);
473
+ broadcastToBrowsers(ctx.wsClients, { type: "run-end", data });
474
+ }
475
+ function handleApprove() {
476
+ console.log("[Server] Received approve message via WebSocket (handled via HTTP API)");
477
+ }
478
+ function handleSync(ctx) {
479
+ console.log("[Server] Received sync message");
480
+ broadcastToBrowsers(ctx.wsClients, { type: "sync", data: ctx.reportData });
481
+ }
482
+
483
+ // src/server/routes.ts
484
+ var import_path3 = require("path");
485
+ var LIVE_UPDATES_WEBSOCKET_PATH = "/";
486
+ function isWebSocketUpgradeRequest(req) {
487
+ return new URL(req.url).pathname === LIVE_UPDATES_WEBSOCKET_PATH && req.headers.get("upgrade")?.toLowerCase() === "websocket";
488
+ }
489
+ async function handleRoot(ctx) {
490
+ const html = await respondWithFile((0, import_path3.join)(ctx.staticDir, "index.html"), "text/html");
491
+ return html ?? new Response("Not Found", { status: 404 });
492
+ }
493
+ async function handleAppCss() {
494
+ const css = await respondWithFile("./src/client/app.css", "text/css");
495
+ return css ?? new Response("Not Found", { status: 404 });
496
+ }
497
+ async function handleSrcFiles(req) {
498
+ const path = new URL(req.url).pathname.slice("/src/".length);
499
+ const filePath = `./src/${path}`;
500
+ const contentType = filePath.endsWith(".ts") || filePath.endsWith(".tsx") ? "application/javascript" : filePath.endsWith(".css") ? "text/css" : "text/plain";
501
+ const file = await respondWithFile(filePath, contentType);
502
+ return file ?? new Response("Not Found", { status: 404 });
503
+ }
504
+ function handleApiReport(ctx) {
505
+ return Response.json(ctx.reportData);
506
+ }
507
+ async function handleApiApprove(ctx, req) {
508
+ try {
509
+ const rawBody = await req.json();
510
+ const parsed = safeParse(ApproveRequestBodySchema, rawBody);
511
+ if (!parsed) {
512
+ console.error("Invalid approve request body", rawBody);
513
+ return Response.json({ success: false, error: "Invalid request body" }, { status: 400 });
514
+ }
515
+ const { id, retry, image } = parsed;
516
+ const test = ctx.reportData.tests[id];
517
+ if (test !== null && test !== void 0) {
518
+ test.approved ??= {};
519
+ test.approved[image] = retry;
520
+ await ctx.saveReport();
521
+ const actualUrl = test.results?.[retry]?.images?.[image]?.actual;
522
+ if (actualUrl !== null && actualUrl !== void 0 && test.location?.file !== null && test.location?.file !== void 0) {
523
+ const actualPath = actualUrl.replace("/screenshots/", `${ctx.reportData.screenshotDir}/`);
524
+ const snapshotPath = `${test.location.file}-snapshots/${image}-${test.browser}-${process.platform}.png`;
525
+ try {
526
+ await copyFilePortable(actualPath, snapshotPath);
527
+ console.log(` \u2714 Updated baseline: ${snapshotPath}`);
528
+ } catch (err) {
529
+ const errorMsg = err instanceof Error ? err.message : String(err);
530
+ console.error(` \u2717 Failed to update baseline: ${errorMsg}`);
531
+ }
532
+ }
533
+ console.log(` \u2714 Approved [${test.browser}] ${test.title} \u2014 ${image}`);
534
+ }
535
+ return Response.json({ success: true });
536
+ } catch {
537
+ return Response.json({ success: false, error: "Invalid request" }, { status: 400 });
538
+ }
539
+ }
540
+ async function handleApiApproveAll(ctx) {
541
+ let approvedCount = 0;
542
+ const baselineUpdates = [];
543
+ Object.values(ctx.reportData.tests).forEach((test) => {
544
+ if (!test?.results) return;
545
+ const approved = {};
546
+ const lastRetry = test.results.length - 1;
547
+ const lastResult = test.results[lastRetry];
548
+ if (!lastResult?.images) return;
549
+ Object.keys(lastResult.images).forEach((imageName) => {
550
+ approved[imageName] = lastRetry;
551
+ approvedCount++;
552
+ const actualUrl = lastResult.images?.[imageName]?.actual;
553
+ if (actualUrl !== null && actualUrl !== void 0 && test.location?.file !== null && test.location?.file !== void 0) {
554
+ const actualPath = actualUrl.replace("/screenshots/", `${ctx.reportData.screenshotDir}/`);
555
+ const snapshotPath = `${test.location.file}-snapshots/${imageName}-${test.browser}-${process.platform}.png`;
556
+ baselineUpdates.push(
557
+ copyFilePortable(actualPath, snapshotPath).then(() => {
558
+ console.log(` \u2714 Updated baseline: ${snapshotPath}`);
559
+ }).catch((err) => {
560
+ const errorMsg = err instanceof Error ? err.message : String(err);
561
+ console.error(` \u2717 Failed to update baseline: ${errorMsg}`);
562
+ })
563
+ );
564
+ }
565
+ });
566
+ test.approved = approved;
567
+ });
568
+ await ctx.saveReport();
569
+ await Promise.all(baselineUpdates);
570
+ console.log(` \u2714 Approved all \u2014 ${approvedCount} image(s)`);
571
+ return Response.json({ success: true });
572
+ }
573
+ async function handleApiImages(req) {
574
+ const path = new URL(req.url).pathname.slice("/api/images/".length);
575
+ const imagePath = `./images/${path}`;
576
+ const file = await respondWithFile(imagePath);
577
+ return file ?? Response.json({ error: "Image not found" }, { status: 404 });
578
+ }
579
+ async function handleScreenshots(ctx, req) {
580
+ const path = new URL(req.url).pathname.slice("/screenshots/".length);
581
+ const screenshotPath = `${ctx.reportData.screenshotDir}/${path}`;
582
+ const file = await respondWithFile(screenshotPath);
583
+ return file ?? Response.json({ error: "Screenshot not found" }, { status: 404 });
584
+ }
585
+ async function handleDist(ctx, req) {
586
+ const path = new URL(req.url).pathname.slice("/dist/".length);
587
+ const filePath = (0, import_path3.join)(ctx.staticDir, path);
588
+ const contentType = filePath.endsWith(".css") ? "text/css" : filePath.endsWith(".js") ? "application/javascript" : filePath.endsWith(".svelte") ? "text/plain" : "application/octet-stream";
589
+ const file = await respondWithFile(filePath, contentType);
590
+ return file ?? new Response("Not Found", { status: 404 });
591
+ }
592
+ function handleHttpRequest(ctx, req) {
593
+ const pathname = new URL(req.url).pathname;
594
+ if (pathname === "/") {
595
+ return handleRoot(ctx);
596
+ }
597
+ if (pathname === "/src/client/app.css") {
598
+ return handleAppCss();
599
+ }
600
+ if (pathname.startsWith("/src/")) {
601
+ return handleSrcFiles(req);
602
+ }
603
+ if (pathname === "/api/report") {
604
+ return Promise.resolve(handleApiReport(ctx));
605
+ }
606
+ if (pathname === "/api/approve") {
607
+ return handleApiApprove(ctx, req);
608
+ }
609
+ if (pathname === "/api/approve-all") {
610
+ return handleApiApproveAll(ctx);
611
+ }
612
+ if (pathname.startsWith("/api/images/")) {
613
+ return handleApiImages(req);
614
+ }
615
+ if (pathname.startsWith("/screenshots/")) {
616
+ return handleScreenshots(ctx, req);
617
+ }
618
+ if (pathname.startsWith("/dist/")) {
619
+ return handleDist(ctx, req);
620
+ }
621
+ return Promise.resolve(new Response("Not Found", { status: 404 }));
622
+ }
623
+
624
+ // src/server/app.ts
625
+ var import_meta = { url: require("url").pathToFileURL(__filename).href };
626
+ var MAX_CONCURRENT_FILE_OPS = 5;
627
+ function createReportData(options) {
628
+ return {
629
+ isRunning: false,
630
+ tests: {},
631
+ browsers: ["chromium"],
632
+ isUpdateMode: false,
633
+ screenshotDir: options.screenshotDir ?? "./screenshots"
634
+ };
635
+ }
636
+ function createHandlerContext(reportData, wsClients, currentRunIds, saveReport) {
637
+ return {
638
+ reportData,
639
+ wsClients,
640
+ currentRunIds,
641
+ saveReport
642
+ };
643
+ }
644
+ async function loadReport(reportPath, reportData) {
645
+ try {
646
+ const raw = await readJsonFile(reportPath);
647
+ if (raw === null) {
648
+ console.log("No report.json found, using empty state");
649
+ return;
650
+ }
651
+ const parsed = safeParse(LoadedReportDataSchema, raw);
652
+ if (parsed !== null) {
653
+ reportData.tests = parsed.tests ?? {};
654
+ reportData.isUpdateMode = parsed.isUpdateMode ?? false;
655
+ }
656
+ } catch {
657
+ console.log("No report.json found, using empty state");
658
+ }
659
+ }
660
+ async function readOfflineReport(filePath) {
661
+ try {
662
+ const raw = await readJsonFile(filePath);
663
+ if (raw === null) {
664
+ return null;
665
+ }
666
+ const parsed = parseOfflineReport(raw);
667
+ if (parsed !== null) {
668
+ console.log(`[Server] Loading offline report: ${filePath}`);
669
+ return parsed;
670
+ }
671
+ } catch {
672
+ }
673
+ return null;
674
+ }
675
+ async function loadOfflineReports(offlineReportDir, reportData) {
676
+ const offlineReportPaths = await findOfflineReportPaths(offlineReportDir);
677
+ if (offlineReportPaths.length === 0) {
678
+ return;
679
+ }
680
+ const limit = (0, import_p_limit.default)(MAX_CONCURRENT_FILE_OPS);
681
+ const reports = await Promise.all(offlineReportPaths.map((filePath) => limit(() => readOfflineReport(filePath))));
682
+ const validReports = reports.filter((report) => report !== null);
683
+ if (validReports.length === 0) {
684
+ return;
685
+ }
686
+ reportData.tests = mergeOfflineReportsIntoTests(reportData.tests, validReports, {
687
+ screenshotDir: reportData.screenshotDir,
688
+ screenshotsBaseUrl: "/screenshots/"
689
+ });
690
+ }
691
+ async function handleParsedWebSocketMessage(ctx, msg) {
692
+ switch (msg.type) {
693
+ case "test-begin": {
694
+ const parsed = safeParse(TestBeginDataSchema, msg.data);
695
+ if (parsed === null) {
696
+ console.error("Invalid test-begin message data", msg.data);
697
+ break;
698
+ }
699
+ handleTestBegin(ctx, parsed);
700
+ break;
701
+ }
702
+ case "test-end": {
703
+ const parsed = safeParse(TestEndDataSchema, msg.data);
704
+ if (parsed === null) {
705
+ console.error("Invalid test-end message data", msg.data);
706
+ break;
707
+ }
708
+ handleTestEnd(ctx, parsed);
709
+ break;
710
+ }
711
+ case "run-end":
712
+ await handleRunEnd(ctx, msg.data);
713
+ break;
714
+ case "approve":
715
+ handleApprove();
716
+ break;
717
+ case "sync":
718
+ handleSync(ctx);
719
+ break;
720
+ }
721
+ }
722
+ function createWebSocketMessageHandler(getHandlerContext) {
723
+ return async function handleWebSocketMessage(message) {
724
+ try {
725
+ const parsed = JSON.parse(message);
726
+ const wsMessage = safeParse(WebSocketMessageSchema, parsed);
727
+ if (wsMessage === null) {
728
+ console.error("Invalid WebSocket message: missing or invalid type", parsed);
729
+ return;
730
+ }
731
+ await handleParsedWebSocketMessage(getHandlerContext(), wsMessage);
732
+ } catch (error) {
733
+ const errorMsg = error instanceof Error ? error.message : String(error);
734
+ console.error("Invalid WebSocket message:", errorMsg);
735
+ }
736
+ };
737
+ }
738
+ async function resolveStaticDir(staticDir) {
739
+ const currentDir = (0, import_path4.dirname)((0, import_url.fileURLToPath)(import_meta.url));
740
+ const candidates = staticDir === void 0 ? [
741
+ currentDir,
742
+ (0, import_path4.join)(currentDir, "dist"),
743
+ (0, import_path4.join)(currentDir, "..", "dist"),
744
+ (0, import_path4.join)(currentDir, "..", "..", "dist"),
745
+ (0, import_path4.join)(currentDir, ".."),
746
+ (0, import_path4.join)(currentDir, "..", "..")
747
+ ] : [staticDir, (0, import_path4.join)(staticDir, "dist")];
748
+ const resolvedCandidates = await Promise.all(
749
+ candidates.map(async (candidate) => ({
750
+ candidate,
751
+ exists: await fileExists((0, import_path4.join)(candidate, "index.html"))
752
+ }))
753
+ );
754
+ const resolved = resolvedCandidates.find(({ exists }) => exists);
755
+ if (resolved !== void 0) {
756
+ return resolved.candidate;
757
+ }
758
+ return candidates[0];
759
+ }
760
+ async function resolveReportPath(reportPath) {
761
+ if (await isDirectory(reportPath)) {
762
+ return { reportFile: (0, import_path4.join)(reportPath, "report.json"), offlineReportDir: reportPath };
763
+ }
764
+ return { reportFile: reportPath, offlineReportDir: (0, import_path4.dirname)(reportPath) };
765
+ }
766
+ async function createServerApp(options = {}) {
767
+ const port = options.port ?? 3e3;
768
+ const reportData = createReportData(options);
769
+ const reportPathOption = options.reportPath ?? "./report.json";
770
+ const { reportFile, offlineReportDir } = await resolveReportPath(reportPathOption);
771
+ const staticDir = await resolveStaticDir(options.staticDir);
772
+ const wsClients = /* @__PURE__ */ new Set();
773
+ const currentRunIds = /* @__PURE__ */ new Set();
774
+ async function saveReport() {
775
+ await writeJsonFile(reportFile, reportData);
776
+ }
777
+ const routesContext = {
778
+ reportData,
779
+ staticDir,
780
+ saveReport
781
+ };
782
+ const getHandlerContext = () => createHandlerContext(reportData, wsClients, currentRunIds, saveReport);
783
+ const handleRequest = (req) => handleHttpRequest(routesContext, req);
784
+ const handleWebSocketMessage = createWebSocketMessageHandler(getHandlerContext);
785
+ await loadReport(reportFile, reportData);
786
+ await loadOfflineReports(offlineReportDir, reportData);
787
+ return {
788
+ port,
789
+ wsClients,
790
+ handleRequest,
791
+ handleWebSocketMessage
792
+ };
793
+ }
794
+
795
+ // src/server/bun-adapter.ts
796
+ function logWebSocketError(prefix, error) {
797
+ const errorMsg = error instanceof Error ? error.message : String(error);
798
+ console.error(`${prefix}:`, errorMsg);
799
+ }
800
+ function toMessageString(message) {
801
+ if (typeof message === "string") {
802
+ return message;
803
+ }
804
+ if (message instanceof ArrayBuffer) {
805
+ return Buffer.from(message).toString();
806
+ }
807
+ return Buffer.from(message.buffer, message.byteOffset, message.byteLength).toString();
808
+ }
809
+ function startBunServer(app) {
810
+ Bun.serve({
811
+ port: app.port,
812
+ fetch(req, server) {
813
+ if (isWebSocketUpgradeRequest(req)) {
814
+ if (server.upgrade(req)) {
815
+ return;
816
+ }
817
+ return new Response("WebSocket upgrade failed", { status: 400 });
818
+ }
819
+ return app.handleRequest(req);
820
+ },
821
+ websocket: {
822
+ open(ws) {
823
+ app.wsClients.add(ws);
824
+ },
825
+ message(_ws, message) {
826
+ app.handleWebSocketMessage(toMessageString(message)).catch((error) => {
827
+ logWebSocketError("Error handling WebSocket message", error);
828
+ });
829
+ },
830
+ close(ws) {
831
+ app.wsClients.delete(ws);
832
+ }
833
+ },
834
+ development: {
835
+ hmr: true,
836
+ console: true
837
+ }
838
+ });
839
+ console.log(`Crvy Rprtr started at http://localhost:${app.port}`);
840
+ }
841
+
842
+ // src/server/node-adapter.ts
843
+ var import_http = require("http");
844
+ var import_ws = require("ws");
845
+ function toHeaders(headersObject) {
846
+ const headers = new Headers();
847
+ for (const [key, value] of Object.entries(headersObject)) {
848
+ if (value === void 0) {
849
+ continue;
850
+ }
851
+ if (Array.isArray(value)) {
852
+ value.forEach((entry) => {
853
+ headers.append(key, entry);
854
+ });
855
+ } else {
856
+ headers.set(key, value);
857
+ }
858
+ }
859
+ return headers;
860
+ }
861
+ async function readRequestBody(req) {
862
+ const chunks = [];
863
+ for await (const chunk of req) {
864
+ const chunkValue = chunk;
865
+ if (typeof chunkValue === "string") {
866
+ chunks.push(Buffer.from(chunkValue));
867
+ continue;
868
+ }
869
+ if (chunkValue instanceof Uint8Array) {
870
+ chunks.push(chunkValue);
871
+ continue;
872
+ }
873
+ throw new TypeError("Unexpected request body chunk type");
874
+ }
875
+ return Buffer.concat(chunks);
876
+ }
877
+ async function toRequest(req) {
878
+ const headers = toHeaders(req.headers);
879
+ const protocol = headers.get("x-forwarded-proto") ?? "http";
880
+ const host = headers.get("host") ?? "localhost";
881
+ const url = new URL(req.url ?? "/", `${protocol}://${host}`);
882
+ const method = req.method ?? "GET";
883
+ if (method === "GET" || method === "HEAD") {
884
+ return new Request(url, { method, headers });
885
+ }
886
+ const body = await readRequestBody(req);
887
+ return new Request(url, {
888
+ method,
889
+ headers,
890
+ body: Buffer.from(body)
891
+ });
892
+ }
893
+ async function writeResponse(res, response) {
894
+ res.statusCode = response.status;
895
+ res.statusMessage = response.statusText;
896
+ response.headers.forEach((value, key) => {
897
+ res.setHeader(key, value);
898
+ });
899
+ if (response.body === null) {
900
+ res.end();
901
+ return;
902
+ }
903
+ const body = Buffer.from(await response.arrayBuffer());
904
+ res.end(body);
905
+ }
906
+ function logRequestError(error) {
907
+ const errorMsg = error instanceof Error ? error.message : String(error);
908
+ console.error("Error handling HTTP request:", errorMsg);
909
+ }
910
+ function logWebSocketError2(error) {
911
+ const errorMsg = error instanceof Error ? error.message : String(error);
912
+ console.error("Error handling WebSocket message:", errorMsg);
913
+ }
914
+ async function handleNodeRequest(app, req, res) {
915
+ try {
916
+ const request = await toRequest(req);
917
+ const response = await app.handleRequest(request);
918
+ await writeResponse(res, response);
919
+ } catch (error) {
920
+ logRequestError(error);
921
+ if (res.headersSent) {
922
+ res.end();
923
+ return;
924
+ }
925
+ res.statusCode = 500;
926
+ res.end("Internal Server Error");
927
+ }
928
+ }
929
+ function rawDataToString(message) {
930
+ if (typeof message === "string") {
931
+ return message;
932
+ }
933
+ if (message instanceof ArrayBuffer) {
934
+ return Buffer.from(message).toString();
935
+ }
936
+ if (Array.isArray(message)) {
937
+ return Buffer.concat(message.map((part) => Buffer.from(part))).toString();
938
+ }
939
+ return Buffer.from(message).toString();
940
+ }
941
+ function attachWebSocketServer(server, app) {
942
+ const wsServer = new import_ws.WebSocketServer({ noServer: true });
943
+ wsServer.on("error", (error) => {
944
+ logWebSocketError2(error);
945
+ });
946
+ server.on("upgrade", (req, socket, head) => {
947
+ const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
948
+ if (pathname !== LIVE_UPDATES_WEBSOCKET_PATH) {
949
+ socket.destroy();
950
+ return;
951
+ }
952
+ wsServer.handleUpgrade(req, socket, head, (ws) => {
953
+ app.wsClients.add(ws);
954
+ ws.on("message", (message) => {
955
+ app.handleWebSocketMessage(rawDataToString(message)).catch((error) => {
956
+ logWebSocketError2(error);
957
+ });
958
+ });
959
+ ws.on("error", (error) => {
960
+ app.wsClients.delete(ws);
961
+ logWebSocketError2(error);
962
+ });
963
+ ws.on("close", () => {
964
+ app.wsClients.delete(ws);
965
+ });
966
+ });
967
+ });
968
+ }
969
+ async function listen(server, port) {
970
+ await new Promise((resolve, reject) => {
971
+ const onError = (error) => {
972
+ server.off("error", onError);
973
+ reject(error);
974
+ };
975
+ server.on("error", onError);
976
+ server.listen(port, () => {
977
+ server.off("error", onError);
978
+ resolve();
979
+ });
980
+ });
981
+ }
982
+ async function startNodeServer(app) {
983
+ const server = (0, import_http.createServer)((req, res) => {
984
+ void handleNodeRequest(app, req, res);
985
+ });
986
+ attachWebSocketServer(server, app);
987
+ await listen(server, app.port);
988
+ console.log(`Crvy Rprtr started at http://localhost:${app.port}`);
989
+ }
990
+
991
+ // src/server.ts
992
+ async function startServer(options = {}) {
993
+ const app = await createServerApp(options);
994
+ if (typeof Bun !== "undefined" && typeof Bun.serve === "function") {
995
+ startBunServer(app);
996
+ return;
997
+ }
998
+ await startNodeServer(app);
999
+ }
1000
+ // Annotate the CommonJS export names for ESM import in node:
1001
+ 0 && (module.exports = {
1002
+ startServer
1003
+ });