@crvy/rprtr 0.0.3 → 0.0.5

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