@crvy/rprtr 0.0.3

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 (50) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/LICENSE +21 -0
  3. package/README.md +130 -0
  4. package/dist/chunk-BRFA7JCQ.js +729 -0
  5. package/dist/chunk-IEYNAB6M.js +263 -0
  6. package/dist/cli.d.ts +10 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +61 -0
  9. package/dist/index.css +1335 -0
  10. package/dist/index.html +14 -0
  11. package/dist/index.js +21106 -0
  12. package/dist/is-direct-execution.d.ts +2 -0
  13. package/dist/is-direct-execution.d.ts.map +1 -0
  14. package/dist/offline-reports.d.ts +9 -0
  15. package/dist/offline-reports.d.ts.map +1 -0
  16. package/dist/report-artifact.d.ts +12 -0
  17. package/dist/report-artifact.d.ts.map +1 -0
  18. package/dist/report-state.d.ts +28 -0
  19. package/dist/report-state.d.ts.map +1 -0
  20. package/dist/report-utils.d.ts +5 -0
  21. package/dist/report-utils.d.ts.map +1 -0
  22. package/dist/reporter-utils.d.ts +3 -0
  23. package/dist/reporter-utils.d.ts.map +1 -0
  24. package/dist/reporter.d.ts +34 -0
  25. package/dist/reporter.d.ts.map +1 -0
  26. package/dist/reporter.js +393 -0
  27. package/dist/schemas.d.ts +380 -0
  28. package/dist/schemas.d.ts.map +1 -0
  29. package/dist/server/app.d.ts +16 -0
  30. package/dist/server/app.d.ts.map +1 -0
  31. package/dist/server/bun-adapter.d.ts +3 -0
  32. package/dist/server/bun-adapter.d.ts.map +1 -0
  33. package/dist/server/file-utils.d.ts +7 -0
  34. package/dist/server/file-utils.d.ts.map +1 -0
  35. package/dist/server/handlers.d.ts +21 -0
  36. package/dist/server/handlers.d.ts.map +1 -0
  37. package/dist/server/node-adapter.d.ts +3 -0
  38. package/dist/server/node-adapter.d.ts.map +1 -0
  39. package/dist/server/routes.d.ts +16 -0
  40. package/dist/server/routes.d.ts.map +1 -0
  41. package/dist/server/utils.d.ts +3 -0
  42. package/dist/server/utils.d.ts.map +1 -0
  43. package/dist/server/ws.d.ts +4 -0
  44. package/dist/server/ws.d.ts.map +1 -0
  45. package/dist/server.d.ts +4 -0
  46. package/dist/server.d.ts.map +1 -0
  47. package/dist/server.js +7 -0
  48. package/dist/types.d.ts +116 -0
  49. package/dist/types.d.ts.map +1 -0
  50. package/package.json +102 -0
@@ -0,0 +1,729 @@
1
+ import {
2
+ ApproveRequestBodySchema,
3
+ LoadedReportDataSchema,
4
+ OfflineReportSchema,
5
+ TestBeginDataSchema,
6
+ TestEndDataSchema,
7
+ WebSocketMessageSchema,
8
+ applyTestBeginEvent,
9
+ applyTestEndEvent,
10
+ createMutableReportState,
11
+ finalizeRunEvent,
12
+ safeParse
13
+ } from "./chunk-IEYNAB6M.js";
14
+
15
+ // src/server/app.ts
16
+ import { dirname as dirname2, join as join3 } from "path";
17
+ import { fileURLToPath } from "url";
18
+ import pLimit from "p-limit";
19
+
20
+ // src/offline-reports.ts
21
+ import { readdir } from "fs/promises";
22
+ import { join } from "path";
23
+ var OFFLINE_REPORT_FILE_PATTERN = /^crvy-rprtr(?:-\d+)?\.json$/;
24
+ async function findOfflineReportPaths(searchDir) {
25
+ try {
26
+ const entries = await readdir(searchDir);
27
+ return entries.filter((entry) => OFFLINE_REPORT_FILE_PATTERN.test(entry)).sort((left, right) => left.localeCompare(right)).map((entry) => join(searchDir, entry));
28
+ } catch (error) {
29
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
30
+ return [];
31
+ }
32
+ throw error;
33
+ }
34
+ }
35
+ function parseOfflineReport(value) {
36
+ const parsed = safeParse(OfflineReportSchema, value);
37
+ if (parsed === null || parsed.version !== 1 || !Array.isArray(parsed.events)) {
38
+ return null;
39
+ }
40
+ return parsed;
41
+ }
42
+ function mergeOfflineReportsIntoTests(existingTests, offlineReports, options = {}) {
43
+ const state = createMutableReportState(options.screenshotDir);
44
+ let shouldFinalize = false;
45
+ for (const report of offlineReports) {
46
+ for (const event of report.events) {
47
+ switch (event.type) {
48
+ case "test-begin": {
49
+ const parsed = safeParse(TestBeginDataSchema, event.data);
50
+ if (parsed !== null) {
51
+ applyTestBeginEvent(state, parsed);
52
+ }
53
+ break;
54
+ }
55
+ case "test-end": {
56
+ const parsed = safeParse(TestEndDataSchema, event.data);
57
+ if (parsed !== null) {
58
+ applyTestEndEvent(state, parsed, { screenshotsBaseUrl: options.screenshotsBaseUrl });
59
+ }
60
+ break;
61
+ }
62
+ case "run-end":
63
+ shouldFinalize = true;
64
+ break;
65
+ }
66
+ }
67
+ }
68
+ if (shouldFinalize) {
69
+ finalizeRunEvent(state);
70
+ }
71
+ return {
72
+ ...existingTests,
73
+ ...state.reportData.tests
74
+ };
75
+ }
76
+
77
+ // src/server/file-utils.ts
78
+ import { access, copyFile, mkdir, readFile, stat, writeFile } from "fs/promises";
79
+ import { dirname, extname } from "path";
80
+ function isFileNotFound(error) {
81
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
82
+ }
83
+ async function fileExists(filePath) {
84
+ try {
85
+ await access(filePath);
86
+ return true;
87
+ } catch (error) {
88
+ if (isFileNotFound(error)) {
89
+ return false;
90
+ }
91
+ throw error;
92
+ }
93
+ }
94
+ async function isDirectory(filePath) {
95
+ try {
96
+ const stats = await stat(filePath);
97
+ return stats.isDirectory();
98
+ } catch (error) {
99
+ if (isFileNotFound(error)) {
100
+ return false;
101
+ }
102
+ throw error;
103
+ }
104
+ }
105
+ async function readJsonFile(filePath) {
106
+ try {
107
+ const raw = await readFile(filePath, "utf8");
108
+ if (raw.trim() === "") {
109
+ return null;
110
+ }
111
+ return JSON.parse(raw);
112
+ } catch (error) {
113
+ if (isFileNotFound(error)) {
114
+ return null;
115
+ }
116
+ throw error;
117
+ }
118
+ }
119
+ async function writeJsonFile(filePath, value) {
120
+ await mkdir(dirname(filePath), { recursive: true });
121
+ await writeFile(filePath, `${JSON.stringify(value, null, 2)}
122
+ `);
123
+ }
124
+ async function copyFilePortable(sourcePath, destinationPath) {
125
+ await mkdir(dirname(destinationPath), { recursive: true });
126
+ await copyFile(sourcePath, destinationPath);
127
+ }
128
+ function inferContentType(filePath) {
129
+ switch (extname(filePath).toLowerCase()) {
130
+ case ".css":
131
+ return "text/css";
132
+ case ".gif":
133
+ return "image/gif";
134
+ case ".html":
135
+ return "text/html";
136
+ case ".jpeg":
137
+ case ".jpg":
138
+ return "image/jpeg";
139
+ case ".js":
140
+ return "application/javascript";
141
+ case ".json":
142
+ return "application/json";
143
+ case ".png":
144
+ return "image/png";
145
+ case ".svg":
146
+ return "image/svg+xml";
147
+ case ".txt":
148
+ return "text/plain";
149
+ case ".webp":
150
+ return "image/webp";
151
+ default:
152
+ return void 0;
153
+ }
154
+ }
155
+ async function respondWithFile(filePath, contentType) {
156
+ try {
157
+ const file = await readFile(filePath);
158
+ const resolvedContentType = contentType ?? inferContentType(filePath);
159
+ const headers = resolvedContentType === void 0 ? void 0 : { "Content-Type": resolvedContentType };
160
+ return new Response(file, { headers });
161
+ } catch (error) {
162
+ if (isFileNotFound(error)) {
163
+ return null;
164
+ }
165
+ throw error;
166
+ }
167
+ }
168
+
169
+ // src/server/utils.ts
170
+ function broadcastToBrowsers(wsClients, msg) {
171
+ const payload = JSON.stringify(msg);
172
+ wsClients.forEach((ws) => {
173
+ ws.send(payload);
174
+ });
175
+ }
176
+
177
+ // src/server/handlers.ts
178
+ function handleTestBegin(ctx, data) {
179
+ const test = applyTestBeginEvent(ctx, data);
180
+ console.log(` \u25B6 [${test.browser ?? "?"}] ${test.title}`);
181
+ }
182
+ function handleTestEnd(ctx, data) {
183
+ const result = applyTestEndEvent(ctx, data);
184
+ if (result !== null) {
185
+ const { test, diffCount } = result;
186
+ const icon = data.status === "passed" ? "\u2713" : data.status === "skipped" ? "\u2013" : "\u2717";
187
+ const dur = data.duration === null || data.duration === void 0 ? "" : ` (${data.duration}ms)`;
188
+ const diffNote = diffCount > 0 ? ` [${diffCount} diff(s)]` : "";
189
+ const errNote = data.error !== null && data.error !== void 0 ? `
190
+ Error: ${data.error}` : "";
191
+ console.log(` ${icon} [${test.browser}] ${test.title}${dur}${diffNote}${errNote}`);
192
+ }
193
+ broadcastToBrowsers(ctx.wsClients, { type: "test-update", data });
194
+ }
195
+ async function handleRunEnd(ctx, data) {
196
+ const { passed, failed, pending } = finalizeRunEvent(ctx);
197
+ await ctx.saveReport();
198
+ console.log(`
199
+ Run complete \u2014 ${passed} passed, ${failed} failed, ${pending} skipped`);
200
+ broadcastToBrowsers(ctx.wsClients, { type: "run-end", data });
201
+ }
202
+ function handleApprove() {
203
+ console.log("[Server] Received approve message via WebSocket (handled via HTTP API)");
204
+ }
205
+ function handleSync(ctx) {
206
+ console.log("[Server] Received sync message");
207
+ broadcastToBrowsers(ctx.wsClients, { type: "sync", data: ctx.reportData });
208
+ }
209
+
210
+ // src/server/routes.ts
211
+ import { join as join2 } from "path";
212
+ var LIVE_UPDATES_WEBSOCKET_PATH = "/";
213
+ function isWebSocketUpgradeRequest(req) {
214
+ return new URL(req.url).pathname === LIVE_UPDATES_WEBSOCKET_PATH && req.headers.get("upgrade")?.toLowerCase() === "websocket";
215
+ }
216
+ async function handleRoot(ctx) {
217
+ const html = await respondWithFile(join2(ctx.staticDir, "index.html"), "text/html");
218
+ return html ?? new Response("Not Found", { status: 404 });
219
+ }
220
+ async function handleAppCss() {
221
+ const css = await respondWithFile("./src/client/app.css", "text/css");
222
+ return css ?? new Response("Not Found", { status: 404 });
223
+ }
224
+ async function handleSrcFiles(req) {
225
+ const path = new URL(req.url).pathname.slice("/src/".length);
226
+ const filePath = `./src/${path}`;
227
+ const contentType = filePath.endsWith(".ts") || filePath.endsWith(".tsx") ? "application/javascript" : filePath.endsWith(".css") ? "text/css" : "text/plain";
228
+ const file = await respondWithFile(filePath, contentType);
229
+ return file ?? new Response("Not Found", { status: 404 });
230
+ }
231
+ function handleApiReport(ctx) {
232
+ return Response.json(ctx.reportData);
233
+ }
234
+ async function handleApiApprove(ctx, req) {
235
+ try {
236
+ const rawBody = await req.json();
237
+ const parsed = safeParse(ApproveRequestBodySchema, rawBody);
238
+ if (!parsed) {
239
+ console.error("Invalid approve request body", rawBody);
240
+ return Response.json({ success: false, error: "Invalid request body" }, { status: 400 });
241
+ }
242
+ const { id, retry, image } = parsed;
243
+ const test = ctx.reportData.tests[id];
244
+ if (test !== null && test !== void 0) {
245
+ test.approved ??= {};
246
+ test.approved[image] = retry;
247
+ await ctx.saveReport();
248
+ const actualUrl = test.results?.[retry]?.images?.[image]?.actual;
249
+ if (actualUrl !== null && actualUrl !== void 0 && test.location?.file !== null && test.location?.file !== void 0) {
250
+ const actualPath = actualUrl.replace("/screenshots/", `${ctx.reportData.screenshotDir}/`);
251
+ const snapshotPath = `${test.location.file}-snapshots/${image}-${test.browser}-${process.platform}.png`;
252
+ try {
253
+ await copyFilePortable(actualPath, snapshotPath);
254
+ console.log(` \u2714 Updated baseline: ${snapshotPath}`);
255
+ } catch (err) {
256
+ const errorMsg = err instanceof Error ? err.message : String(err);
257
+ console.error(` \u2717 Failed to update baseline: ${errorMsg}`);
258
+ }
259
+ }
260
+ console.log(` \u2714 Approved [${test.browser}] ${test.title} \u2014 ${image}`);
261
+ }
262
+ return Response.json({ success: true });
263
+ } catch {
264
+ return Response.json({ success: false, error: "Invalid request" }, { status: 400 });
265
+ }
266
+ }
267
+ async function handleApiApproveAll(ctx) {
268
+ let approvedCount = 0;
269
+ const baselineUpdates = [];
270
+ Object.values(ctx.reportData.tests).forEach((test) => {
271
+ if (!test?.results) return;
272
+ const approved = {};
273
+ const lastRetry = test.results.length - 1;
274
+ const lastResult = test.results[lastRetry];
275
+ if (!lastResult?.images) return;
276
+ Object.keys(lastResult.images).forEach((imageName) => {
277
+ approved[imageName] = lastRetry;
278
+ approvedCount++;
279
+ const actualUrl = lastResult.images?.[imageName]?.actual;
280
+ if (actualUrl !== null && actualUrl !== void 0 && test.location?.file !== null && test.location?.file !== void 0) {
281
+ const actualPath = actualUrl.replace("/screenshots/", `${ctx.reportData.screenshotDir}/`);
282
+ const snapshotPath = `${test.location.file}-snapshots/${imageName}-${test.browser}-${process.platform}.png`;
283
+ baselineUpdates.push(
284
+ copyFilePortable(actualPath, snapshotPath).then(() => {
285
+ console.log(` \u2714 Updated baseline: ${snapshotPath}`);
286
+ }).catch((err) => {
287
+ const errorMsg = err instanceof Error ? err.message : String(err);
288
+ console.error(` \u2717 Failed to update baseline: ${errorMsg}`);
289
+ })
290
+ );
291
+ }
292
+ });
293
+ test.approved = approved;
294
+ });
295
+ await ctx.saveReport();
296
+ await Promise.all(baselineUpdates);
297
+ console.log(` \u2714 Approved all \u2014 ${approvedCount} image(s)`);
298
+ return Response.json({ success: true });
299
+ }
300
+ async function handleApiImages(req) {
301
+ const path = new URL(req.url).pathname.slice("/api/images/".length);
302
+ const imagePath = `./images/${path}`;
303
+ const file = await respondWithFile(imagePath);
304
+ return file ?? Response.json({ error: "Image not found" }, { status: 404 });
305
+ }
306
+ async function handleScreenshots(ctx, req) {
307
+ const path = new URL(req.url).pathname.slice("/screenshots/".length);
308
+ const screenshotPath = `${ctx.reportData.screenshotDir}/${path}`;
309
+ const file = await respondWithFile(screenshotPath);
310
+ return file ?? Response.json({ error: "Screenshot not found" }, { status: 404 });
311
+ }
312
+ async function handleDist(ctx, req) {
313
+ const path = new URL(req.url).pathname.slice("/dist/".length);
314
+ const filePath = join2(ctx.staticDir, path);
315
+ const contentType = filePath.endsWith(".css") ? "text/css" : filePath.endsWith(".js") ? "application/javascript" : filePath.endsWith(".svelte") ? "text/plain" : "application/octet-stream";
316
+ const file = await respondWithFile(filePath, contentType);
317
+ return file ?? new Response("Not Found", { status: 404 });
318
+ }
319
+ function handleHttpRequest(ctx, req) {
320
+ const pathname = new URL(req.url).pathname;
321
+ if (pathname === "/") {
322
+ return handleRoot(ctx);
323
+ }
324
+ if (pathname === "/src/client/app.css") {
325
+ return handleAppCss();
326
+ }
327
+ if (pathname.startsWith("/src/")) {
328
+ return handleSrcFiles(req);
329
+ }
330
+ if (pathname === "/api/report") {
331
+ return Promise.resolve(handleApiReport(ctx));
332
+ }
333
+ if (pathname === "/api/approve") {
334
+ return handleApiApprove(ctx, req);
335
+ }
336
+ if (pathname === "/api/approve-all") {
337
+ return handleApiApproveAll(ctx);
338
+ }
339
+ if (pathname.startsWith("/api/images/")) {
340
+ return handleApiImages(req);
341
+ }
342
+ if (pathname.startsWith("/screenshots/")) {
343
+ return handleScreenshots(ctx, req);
344
+ }
345
+ if (pathname.startsWith("/dist/")) {
346
+ return handleDist(ctx, req);
347
+ }
348
+ return Promise.resolve(new Response("Not Found", { status: 404 }));
349
+ }
350
+
351
+ // src/server/app.ts
352
+ var MAX_CONCURRENT_FILE_OPS = 5;
353
+ function createReportData(options) {
354
+ return {
355
+ isRunning: false,
356
+ tests: {},
357
+ browsers: ["chromium"],
358
+ isUpdateMode: false,
359
+ screenshotDir: options.screenshotDir ?? "./screenshots"
360
+ };
361
+ }
362
+ function createHandlerContext(reportData, wsClients, currentRunIds, saveReport) {
363
+ return {
364
+ reportData,
365
+ wsClients,
366
+ currentRunIds,
367
+ saveReport
368
+ };
369
+ }
370
+ async function loadReport(reportPath, reportData) {
371
+ try {
372
+ const raw = await readJsonFile(reportPath);
373
+ if (raw === null) {
374
+ console.log("No report.json found, using empty state");
375
+ return;
376
+ }
377
+ const parsed = safeParse(LoadedReportDataSchema, raw);
378
+ if (parsed !== null) {
379
+ reportData.tests = parsed.tests ?? {};
380
+ reportData.isUpdateMode = parsed.isUpdateMode ?? false;
381
+ }
382
+ } catch {
383
+ console.log("No report.json found, using empty state");
384
+ }
385
+ }
386
+ async function readOfflineReport(filePath) {
387
+ try {
388
+ const raw = await readJsonFile(filePath);
389
+ if (raw === null) {
390
+ return null;
391
+ }
392
+ const parsed = parseOfflineReport(raw);
393
+ if (parsed !== null) {
394
+ console.log(`[Server] Loading offline report: ${filePath}`);
395
+ return parsed;
396
+ }
397
+ } catch {
398
+ }
399
+ return null;
400
+ }
401
+ async function loadOfflineReports(offlineReportDir, reportData) {
402
+ const offlineReportPaths = await findOfflineReportPaths(offlineReportDir);
403
+ if (offlineReportPaths.length === 0) {
404
+ return;
405
+ }
406
+ const limit = pLimit(MAX_CONCURRENT_FILE_OPS);
407
+ const reports = await Promise.all(offlineReportPaths.map((filePath) => limit(() => readOfflineReport(filePath))));
408
+ const validReports = reports.filter((report) => report !== null);
409
+ if (validReports.length === 0) {
410
+ return;
411
+ }
412
+ reportData.tests = mergeOfflineReportsIntoTests(reportData.tests, validReports, {
413
+ screenshotDir: reportData.screenshotDir,
414
+ screenshotsBaseUrl: "/screenshots/"
415
+ });
416
+ }
417
+ async function handleParsedWebSocketMessage(ctx, msg) {
418
+ switch (msg.type) {
419
+ case "test-begin": {
420
+ const parsed = safeParse(TestBeginDataSchema, msg.data);
421
+ if (parsed === null) {
422
+ console.error("Invalid test-begin message data", msg.data);
423
+ break;
424
+ }
425
+ handleTestBegin(ctx, parsed);
426
+ break;
427
+ }
428
+ case "test-end": {
429
+ const parsed = safeParse(TestEndDataSchema, msg.data);
430
+ if (parsed === null) {
431
+ console.error("Invalid test-end message data", msg.data);
432
+ break;
433
+ }
434
+ handleTestEnd(ctx, parsed);
435
+ break;
436
+ }
437
+ case "run-end":
438
+ await handleRunEnd(ctx, msg.data);
439
+ break;
440
+ case "approve":
441
+ handleApprove();
442
+ break;
443
+ case "sync":
444
+ handleSync(ctx);
445
+ break;
446
+ }
447
+ }
448
+ function createWebSocketMessageHandler(getHandlerContext) {
449
+ return async function handleWebSocketMessage(message) {
450
+ try {
451
+ const parsed = JSON.parse(message);
452
+ const wsMessage = safeParse(WebSocketMessageSchema, parsed);
453
+ if (wsMessage === null) {
454
+ console.error("Invalid WebSocket message: missing or invalid type", parsed);
455
+ return;
456
+ }
457
+ await handleParsedWebSocketMessage(getHandlerContext(), wsMessage);
458
+ } catch (error) {
459
+ const errorMsg = error instanceof Error ? error.message : String(error);
460
+ console.error("Invalid WebSocket message:", errorMsg);
461
+ }
462
+ };
463
+ }
464
+ async function resolveStaticDir(staticDir) {
465
+ const currentDir = dirname2(fileURLToPath(import.meta.url));
466
+ const candidates = staticDir === void 0 ? [
467
+ currentDir,
468
+ join3(currentDir, "dist"),
469
+ join3(currentDir, "..", "dist"),
470
+ join3(currentDir, "..", "..", "dist"),
471
+ join3(currentDir, ".."),
472
+ join3(currentDir, "..", "..")
473
+ ] : [staticDir, join3(staticDir, "dist")];
474
+ const resolvedCandidates = await Promise.all(
475
+ candidates.map(async (candidate) => ({
476
+ candidate,
477
+ exists: await fileExists(join3(candidate, "index.html"))
478
+ }))
479
+ );
480
+ const resolved = resolvedCandidates.find(({ exists }) => exists);
481
+ if (resolved !== void 0) {
482
+ return resolved.candidate;
483
+ }
484
+ return candidates[0];
485
+ }
486
+ async function resolveReportPath(reportPath) {
487
+ if (await isDirectory(reportPath)) {
488
+ return { reportFile: join3(reportPath, "report.json"), offlineReportDir: reportPath };
489
+ }
490
+ return { reportFile: reportPath, offlineReportDir: dirname2(reportPath) };
491
+ }
492
+ async function createServerApp(options = {}) {
493
+ const port = options.port ?? 3e3;
494
+ const reportData = createReportData(options);
495
+ const reportPathOption = options.reportPath ?? "./report.json";
496
+ const { reportFile, offlineReportDir } = await resolveReportPath(reportPathOption);
497
+ const staticDir = await resolveStaticDir(options.staticDir);
498
+ const wsClients = /* @__PURE__ */ new Set();
499
+ const currentRunIds = /* @__PURE__ */ new Set();
500
+ async function saveReport() {
501
+ await writeJsonFile(reportFile, reportData);
502
+ }
503
+ const routesContext = {
504
+ reportData,
505
+ staticDir,
506
+ saveReport
507
+ };
508
+ const getHandlerContext = () => createHandlerContext(reportData, wsClients, currentRunIds, saveReport);
509
+ const handleRequest = (req) => handleHttpRequest(routesContext, req);
510
+ const handleWebSocketMessage = createWebSocketMessageHandler(getHandlerContext);
511
+ await loadReport(reportFile, reportData);
512
+ await loadOfflineReports(offlineReportDir, reportData);
513
+ return {
514
+ port,
515
+ wsClients,
516
+ handleRequest,
517
+ handleWebSocketMessage
518
+ };
519
+ }
520
+
521
+ // src/server/bun-adapter.ts
522
+ function logWebSocketError(prefix, error) {
523
+ const errorMsg = error instanceof Error ? error.message : String(error);
524
+ console.error(`${prefix}:`, errorMsg);
525
+ }
526
+ function toMessageString(message) {
527
+ if (typeof message === "string") {
528
+ return message;
529
+ }
530
+ if (message instanceof ArrayBuffer) {
531
+ return Buffer.from(message).toString();
532
+ }
533
+ return Buffer.from(message.buffer, message.byteOffset, message.byteLength).toString();
534
+ }
535
+ function startBunServer(app) {
536
+ Bun.serve({
537
+ port: app.port,
538
+ fetch(req, server) {
539
+ if (isWebSocketUpgradeRequest(req)) {
540
+ if (server.upgrade(req)) {
541
+ return;
542
+ }
543
+ return new Response("WebSocket upgrade failed", { status: 400 });
544
+ }
545
+ return app.handleRequest(req);
546
+ },
547
+ websocket: {
548
+ open(ws) {
549
+ app.wsClients.add(ws);
550
+ },
551
+ message(_ws, message) {
552
+ app.handleWebSocketMessage(toMessageString(message)).catch((error) => {
553
+ logWebSocketError("Error handling WebSocket message", error);
554
+ });
555
+ },
556
+ close(ws) {
557
+ app.wsClients.delete(ws);
558
+ }
559
+ },
560
+ development: {
561
+ hmr: true,
562
+ console: true
563
+ }
564
+ });
565
+ console.log(`Crvy Rprtr started at http://localhost:${app.port}`);
566
+ }
567
+
568
+ // src/server/node-adapter.ts
569
+ import { createServer } from "http";
570
+ import { WebSocketServer } from "ws";
571
+ function toHeaders(headersObject) {
572
+ const headers = new Headers();
573
+ for (const [key, value] of Object.entries(headersObject)) {
574
+ if (value === void 0) {
575
+ continue;
576
+ }
577
+ if (Array.isArray(value)) {
578
+ value.forEach((entry) => {
579
+ headers.append(key, entry);
580
+ });
581
+ } else {
582
+ headers.set(key, value);
583
+ }
584
+ }
585
+ return headers;
586
+ }
587
+ async function readRequestBody(req) {
588
+ const chunks = [];
589
+ for await (const chunk of req) {
590
+ const chunkValue = chunk;
591
+ if (typeof chunkValue === "string") {
592
+ chunks.push(Buffer.from(chunkValue));
593
+ continue;
594
+ }
595
+ if (chunkValue instanceof Uint8Array) {
596
+ chunks.push(chunkValue);
597
+ continue;
598
+ }
599
+ throw new TypeError("Unexpected request body chunk type");
600
+ }
601
+ return Buffer.concat(chunks);
602
+ }
603
+ async function toRequest(req) {
604
+ const headers = toHeaders(req.headers);
605
+ const protocol = headers.get("x-forwarded-proto") ?? "http";
606
+ const host = headers.get("host") ?? "localhost";
607
+ const url = new URL(req.url ?? "/", `${protocol}://${host}`);
608
+ const method = req.method ?? "GET";
609
+ if (method === "GET" || method === "HEAD") {
610
+ return new Request(url, { method, headers });
611
+ }
612
+ const body = await readRequestBody(req);
613
+ return new Request(url, {
614
+ method,
615
+ headers,
616
+ body: Buffer.from(body)
617
+ });
618
+ }
619
+ async function writeResponse(res, response) {
620
+ res.statusCode = response.status;
621
+ res.statusMessage = response.statusText;
622
+ response.headers.forEach((value, key) => {
623
+ res.setHeader(key, value);
624
+ });
625
+ if (response.body === null) {
626
+ res.end();
627
+ return;
628
+ }
629
+ const body = Buffer.from(await response.arrayBuffer());
630
+ res.end(body);
631
+ }
632
+ function logRequestError(error) {
633
+ const errorMsg = error instanceof Error ? error.message : String(error);
634
+ console.error("Error handling HTTP request:", errorMsg);
635
+ }
636
+ function logWebSocketError2(error) {
637
+ const errorMsg = error instanceof Error ? error.message : String(error);
638
+ console.error("Error handling WebSocket message:", errorMsg);
639
+ }
640
+ async function handleNodeRequest(app, req, res) {
641
+ try {
642
+ const request = await toRequest(req);
643
+ const response = await app.handleRequest(request);
644
+ await writeResponse(res, response);
645
+ } catch (error) {
646
+ logRequestError(error);
647
+ if (res.headersSent) {
648
+ res.end();
649
+ return;
650
+ }
651
+ res.statusCode = 500;
652
+ res.end("Internal Server Error");
653
+ }
654
+ }
655
+ function rawDataToString(message) {
656
+ if (typeof message === "string") {
657
+ return message;
658
+ }
659
+ if (message instanceof ArrayBuffer) {
660
+ return Buffer.from(message).toString();
661
+ }
662
+ if (Array.isArray(message)) {
663
+ return Buffer.concat(message.map((part) => Buffer.from(part))).toString();
664
+ }
665
+ return Buffer.from(message).toString();
666
+ }
667
+ function attachWebSocketServer(server, app) {
668
+ const wsServer = new WebSocketServer({ noServer: true });
669
+ wsServer.on("error", (error) => {
670
+ logWebSocketError2(error);
671
+ });
672
+ server.on("upgrade", (req, socket, head) => {
673
+ const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
674
+ if (pathname !== LIVE_UPDATES_WEBSOCKET_PATH) {
675
+ socket.destroy();
676
+ return;
677
+ }
678
+ wsServer.handleUpgrade(req, socket, head, (ws) => {
679
+ app.wsClients.add(ws);
680
+ ws.on("message", (message) => {
681
+ app.handleWebSocketMessage(rawDataToString(message)).catch((error) => {
682
+ logWebSocketError2(error);
683
+ });
684
+ });
685
+ ws.on("error", (error) => {
686
+ app.wsClients.delete(ws);
687
+ logWebSocketError2(error);
688
+ });
689
+ ws.on("close", () => {
690
+ app.wsClients.delete(ws);
691
+ });
692
+ });
693
+ });
694
+ }
695
+ async function listen(server, port) {
696
+ await new Promise((resolve, reject) => {
697
+ const onError = (error) => {
698
+ server.off("error", onError);
699
+ reject(error);
700
+ };
701
+ server.on("error", onError);
702
+ server.listen(port, () => {
703
+ server.off("error", onError);
704
+ resolve();
705
+ });
706
+ });
707
+ }
708
+ async function startNodeServer(app) {
709
+ const server = createServer((req, res) => {
710
+ void handleNodeRequest(app, req, res);
711
+ });
712
+ attachWebSocketServer(server, app);
713
+ await listen(server, app.port);
714
+ console.log(`Crvy Rprtr started at http://localhost:${app.port}`);
715
+ }
716
+
717
+ // src/server.ts
718
+ async function startServer(options = {}) {
719
+ const app = await createServerApp(options);
720
+ if (typeof Bun !== "undefined" && typeof Bun.serve === "function") {
721
+ startBunServer(app);
722
+ return;
723
+ }
724
+ await startNodeServer(app);
725
+ }
726
+
727
+ export {
728
+ startServer
729
+ };