@crvy/rprtr 0.1.4 → 0.2.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.
- package/CHANGELOG.md +66 -0
- package/README.md +8 -7
- package/dist/{chunk-KLGJSEKV.js → chunk-473CWZ4V.js} +539 -130
- package/dist/{chunk-WRMHUY5A.js → chunk-HAFWYUNO.js} +181 -130
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +6 -4
- package/dist/index.css +26 -0
- package/dist/index.js +300 -79
- package/dist/offline-reports.d.ts +4 -0
- package/dist/offline-reports.d.ts.map +1 -1
- package/dist/report-state.d.ts +3 -1
- package/dist/report-state.d.ts.map +1 -1
- package/dist/reporter-utils.d.ts +0 -1
- package/dist/reporter-utils.d.ts.map +1 -1
- package/dist/reporter.cjs +186 -134
- package/dist/reporter.d.ts +1 -1
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js +8 -6
- package/dist/schemas/http.d.ts +44 -0
- package/dist/schemas/http.d.ts.map +1 -0
- package/dist/schemas.d.ts +25 -6
- package/dist/schemas.d.ts.map +1 -1
- package/dist/server/app.d.ts +8 -1
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/handlers.d.ts +6 -0
- package/dist/server/handlers.d.ts.map +1 -1
- package/dist/server/playwright-config.d.ts +2 -0
- package/dist/server/playwright-config.d.ts.map +1 -0
- package/dist/server/report-persistence.d.ts +27 -0
- package/dist/server/report-persistence.d.ts.map +1 -0
- package/dist/server/routes-context.d.ts +13 -0
- package/dist/server/routes-context.d.ts.map +1 -0
- package/dist/server/routes.d.ts +6 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/run-controller.d.ts +87 -0
- package/dist/server/run-controller.d.ts.map +1 -0
- package/dist/server/run-routes.d.ts +3 -0
- package/dist/server/run-routes.d.ts.map +1 -0
- package/dist/server/shutdown.d.ts +17 -0
- package/dist/server/shutdown.d.ts.map +1 -0
- package/dist/server.cjs +733 -276
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -2
- package/dist/types.d.ts +6 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -5
package/dist/server.cjs
CHANGED
|
@@ -35,13 +35,13 @@ __export(server_exports, {
|
|
|
35
35
|
module.exports = __toCommonJS(server_exports);
|
|
36
36
|
|
|
37
37
|
// src/server/app.ts
|
|
38
|
-
var
|
|
38
|
+
var import_path8 = require("path");
|
|
39
39
|
var import_url = require("url");
|
|
40
|
-
var import_p_limit = __toESM(require("p-limit"), 1);
|
|
41
40
|
|
|
42
41
|
// src/offline-reports.ts
|
|
43
|
-
var
|
|
44
|
-
var
|
|
42
|
+
var import_promises2 = require("fs/promises");
|
|
43
|
+
var import_path2 = require("path");
|
|
44
|
+
var import_p_limit = __toESM(require("p-limit"), 1);
|
|
45
45
|
|
|
46
46
|
// src/path-utils.ts
|
|
47
47
|
var posixAbsolutePattern = /^\//u;
|
|
@@ -186,7 +186,12 @@ function createMutableReportState(screenshotDir = "./screenshots") {
|
|
|
186
186
|
function applyTestBeginEvent(state, data) {
|
|
187
187
|
const { id, title, titlePath, browser, projectName, location } = data;
|
|
188
188
|
state.currentRunIds.add(id);
|
|
189
|
-
state.reportData.tests[id]
|
|
189
|
+
const existing = state.reportData.tests[id];
|
|
190
|
+
if (existing !== void 0) {
|
|
191
|
+
existing.status = "running";
|
|
192
|
+
return existing;
|
|
193
|
+
}
|
|
194
|
+
const created = {
|
|
190
195
|
id,
|
|
191
196
|
titlePath: titlePath ?? [],
|
|
192
197
|
browser: browser ?? "",
|
|
@@ -198,7 +203,8 @@ function applyTestBeginEvent(state, data) {
|
|
|
198
203
|
location,
|
|
199
204
|
status: "running"
|
|
200
205
|
};
|
|
201
|
-
|
|
206
|
+
state.reportData.tests[id] = created;
|
|
207
|
+
return created;
|
|
202
208
|
}
|
|
203
209
|
function applyTestEndEvent(state, data, options = {}) {
|
|
204
210
|
const test = state.reportData.tests[data.id];
|
|
@@ -236,11 +242,13 @@ function applyTestEndEvent(state, data, options = {}) {
|
|
|
236
242
|
diffCount
|
|
237
243
|
};
|
|
238
244
|
}
|
|
239
|
-
function finalizeRunEvent(state) {
|
|
245
|
+
function finalizeRunEvent(state, options = {}) {
|
|
240
246
|
state.reportData.isRunning = false;
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
247
|
+
if (options.preserveNonCurrent !== true) {
|
|
248
|
+
state.reportData.tests = Object.fromEntries(
|
|
249
|
+
Object.entries(state.reportData.tests).filter(([id]) => state.currentRunIds.has(id))
|
|
250
|
+
);
|
|
251
|
+
}
|
|
244
252
|
state.currentRunIds.clear();
|
|
245
253
|
const runTests = Object.values(state.reportData.tests).filter((test) => test !== void 0);
|
|
246
254
|
return {
|
|
@@ -251,169 +259,211 @@ function finalizeRunEvent(state) {
|
|
|
251
259
|
}
|
|
252
260
|
|
|
253
261
|
// src/schemas.ts
|
|
262
|
+
var import_zod2 = require("zod");
|
|
263
|
+
|
|
264
|
+
// src/schemas/http.ts
|
|
254
265
|
var import_zod = require("zod");
|
|
255
|
-
var
|
|
266
|
+
var ApproveRequestBodySchema = import_zod.z.object({
|
|
267
|
+
id: import_zod.z.string(),
|
|
268
|
+
retry: import_zod.z.number(),
|
|
269
|
+
image: import_zod.z.string()
|
|
270
|
+
});
|
|
271
|
+
var RunTestDescriptorSchema = import_zod.z.object({
|
|
256
272
|
file: import_zod.z.string(),
|
|
257
|
-
line: import_zod.z.number()
|
|
273
|
+
line: import_zod.z.number(),
|
|
274
|
+
column: import_zod.z.number().optional(),
|
|
275
|
+
projectName: import_zod.z.string().optional(),
|
|
276
|
+
titlePath: import_zod.z.array(import_zod.z.string())
|
|
258
277
|
});
|
|
259
|
-
var
|
|
260
|
-
|
|
261
|
-
actual: import_zod.z.string().optional(),
|
|
262
|
-
expect: import_zod.z.string().optional(),
|
|
263
|
-
diff: import_zod.z.string().optional(),
|
|
264
|
-
error: import_zod.z.string().optional(),
|
|
265
|
-
source: VisualSourceSchema.optional()
|
|
278
|
+
var RunRequestBodySchema = import_zod.z.object({
|
|
279
|
+
tests: import_zod.z.array(RunTestDescriptorSchema).optional()
|
|
266
280
|
});
|
|
267
|
-
var
|
|
281
|
+
var RunResponseSchema = import_zod.z.discriminatedUnion("ok", [
|
|
282
|
+
import_zod.z.object({ ok: import_zod.z.literal(true) }),
|
|
268
283
|
import_zod.z.object({
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}),
|
|
284
|
+
ok: import_zod.z.literal(false),
|
|
285
|
+
reason: import_zod.z.enum(["no-config", "already-running", "no-tests"])
|
|
286
|
+
})
|
|
287
|
+
]);
|
|
288
|
+
var StopResponseSchema = import_zod.z.discriminatedUnion("ok", [
|
|
289
|
+
import_zod.z.object({ ok: import_zod.z.literal(true) }),
|
|
275
290
|
import_zod.z.object({
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
291
|
+
ok: import_zod.z.literal(false),
|
|
292
|
+
reason: import_zod.z.literal("not-running")
|
|
293
|
+
})
|
|
294
|
+
]);
|
|
295
|
+
|
|
296
|
+
// src/schemas.ts
|
|
297
|
+
var LocationSchema = import_zod2.z.object({
|
|
298
|
+
file: import_zod2.z.string(),
|
|
299
|
+
line: import_zod2.z.number(),
|
|
300
|
+
column: import_zod2.z.number().optional()
|
|
301
|
+
});
|
|
302
|
+
var VisualSourceSchema = import_zod2.z.enum(["comparison", "baseline-only", "declared-only"]);
|
|
303
|
+
var ImagesSchema = import_zod2.z.object({
|
|
304
|
+
actual: import_zod2.z.string().optional(),
|
|
305
|
+
expect: import_zod2.z.string().optional(),
|
|
306
|
+
diff: import_zod2.z.string().optional(),
|
|
307
|
+
error: import_zod2.z.string().optional(),
|
|
308
|
+
source: VisualSourceSchema.optional()
|
|
309
|
+
});
|
|
310
|
+
var ScreenshotDeclarationSchema = import_zod2.z.discriminatedUnion("kind", [
|
|
311
|
+
import_zod2.z.object({
|
|
312
|
+
visualName: import_zod2.z.string(),
|
|
313
|
+
kind: import_zod2.z.literal("named"),
|
|
314
|
+
declaredName: import_zod2.z.string(),
|
|
315
|
+
snapshotBaseName: import_zod2.z.string(),
|
|
316
|
+
occurrenceIndex: import_zod2.z.number()
|
|
317
|
+
}),
|
|
318
|
+
import_zod2.z.object({
|
|
319
|
+
visualName: import_zod2.z.string(),
|
|
320
|
+
kind: import_zod2.z.literal("unnamed"),
|
|
321
|
+
occurrenceIndex: import_zod2.z.number()
|
|
279
322
|
})
|
|
280
323
|
]);
|
|
281
|
-
var AttachmentSchema =
|
|
282
|
-
name:
|
|
283
|
-
path:
|
|
284
|
-
contentType:
|
|
324
|
+
var AttachmentSchema = import_zod2.z.object({
|
|
325
|
+
name: import_zod2.z.string(),
|
|
326
|
+
path: import_zod2.z.string(),
|
|
327
|
+
contentType: import_zod2.z.string()
|
|
285
328
|
});
|
|
286
|
-
var TestStatusSchema =
|
|
287
|
-
var TestResultStatusSchema =
|
|
288
|
-
var TestResultSchema =
|
|
329
|
+
var TestStatusSchema = import_zod2.z.enum(["unknown", "pending", "running", "failed", "approved", "success", "retrying"]);
|
|
330
|
+
var TestResultStatusSchema = import_zod2.z.enum(["failed", "success", "pending"]);
|
|
331
|
+
var TestResultSchema = import_zod2.z.object({
|
|
289
332
|
status: TestResultStatusSchema,
|
|
290
|
-
retries:
|
|
291
|
-
images:
|
|
292
|
-
visualDeclarations:
|
|
293
|
-
error:
|
|
294
|
-
duration:
|
|
333
|
+
retries: import_zod2.z.number(),
|
|
334
|
+
images: import_zod2.z.record(import_zod2.z.string(), ImagesSchema).optional(),
|
|
335
|
+
visualDeclarations: import_zod2.z.array(ScreenshotDeclarationSchema).optional(),
|
|
336
|
+
error: import_zod2.z.string().optional(),
|
|
337
|
+
duration: import_zod2.z.number().optional()
|
|
295
338
|
});
|
|
296
|
-
var TestDataSchema =
|
|
297
|
-
id:
|
|
298
|
-
titlePath:
|
|
299
|
-
browser:
|
|
300
|
-
projectName:
|
|
301
|
-
title:
|
|
302
|
-
skip:
|
|
303
|
-
retries:
|
|
339
|
+
var TestDataSchema = import_zod2.z.object({
|
|
340
|
+
id: import_zod2.z.string(),
|
|
341
|
+
titlePath: import_zod2.z.array(import_zod2.z.string()),
|
|
342
|
+
browser: import_zod2.z.string(),
|
|
343
|
+
projectName: import_zod2.z.string().optional(),
|
|
344
|
+
title: import_zod2.z.string(),
|
|
345
|
+
skip: import_zod2.z.union([import_zod2.z.boolean(), import_zod2.z.string()]).optional(),
|
|
346
|
+
retries: import_zod2.z.number().optional(),
|
|
304
347
|
status: TestStatusSchema.optional(),
|
|
305
|
-
results:
|
|
306
|
-
approved:
|
|
307
|
-
attachments:
|
|
348
|
+
results: import_zod2.z.array(TestResultSchema).optional(),
|
|
349
|
+
approved: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.number()).nullable().optional(),
|
|
350
|
+
attachments: import_zod2.z.array(AttachmentSchema).optional(),
|
|
308
351
|
location: LocationSchema.optional()
|
|
309
352
|
});
|
|
310
353
|
var CrvyRprtrTestSchema = TestDataSchema.extend({
|
|
311
|
-
checked:
|
|
354
|
+
checked: import_zod2.z.boolean()
|
|
312
355
|
});
|
|
313
|
-
var CrvyRprtrSuiteSchema =
|
|
314
|
-
() =>
|
|
315
|
-
path:
|
|
316
|
-
skip:
|
|
356
|
+
var CrvyRprtrSuiteSchema = import_zod2.z.lazy(
|
|
357
|
+
() => import_zod2.z.object({
|
|
358
|
+
path: import_zod2.z.array(import_zod2.z.string()),
|
|
359
|
+
skip: import_zod2.z.boolean(),
|
|
317
360
|
status: TestStatusSchema.optional(),
|
|
318
|
-
opened:
|
|
319
|
-
checked:
|
|
320
|
-
indeterminate:
|
|
321
|
-
children:
|
|
361
|
+
opened: import_zod2.z.boolean(),
|
|
362
|
+
checked: import_zod2.z.boolean(),
|
|
363
|
+
indeterminate: import_zod2.z.boolean(),
|
|
364
|
+
children: import_zod2.z.record(import_zod2.z.string(), import_zod2.z.union([CrvyRprtrSuiteSchema, CrvyRprtrTestSchema])).optional()
|
|
322
365
|
})
|
|
323
366
|
);
|
|
324
|
-
var IncomingWebSocketMessageSchema =
|
|
325
|
-
type:
|
|
326
|
-
data:
|
|
367
|
+
var IncomingWebSocketMessageSchema = import_zod2.z.object({
|
|
368
|
+
type: import_zod2.z.enum(["test-begin", "test-end", "run-end", "approve", "sync", "register"]),
|
|
369
|
+
data: import_zod2.z.unknown()
|
|
327
370
|
});
|
|
328
|
-
var WebSocketMessageSchema =
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
type:
|
|
333
|
-
data:
|
|
334
|
-
status:
|
|
335
|
-
removedTestIds:
|
|
371
|
+
var WebSocketMessageSchema = import_zod2.z.discriminatedUnion("type", [
|
|
372
|
+
import_zod2.z.object({ type: import_zod2.z.literal("test-begin"), data: TestDataSchema }),
|
|
373
|
+
import_zod2.z.object({ type: import_zod2.z.literal("test-update"), data: TestDataSchema }),
|
|
374
|
+
import_zod2.z.object({
|
|
375
|
+
type: import_zod2.z.literal("run-end"),
|
|
376
|
+
data: import_zod2.z.object({
|
|
377
|
+
status: import_zod2.z.enum(["passed", "failed", "skipped"]),
|
|
378
|
+
removedTestIds: import_zod2.z.array(import_zod2.z.string())
|
|
336
379
|
})
|
|
337
380
|
}),
|
|
338
|
-
|
|
339
|
-
type:
|
|
340
|
-
data:
|
|
341
|
-
tests:
|
|
342
|
-
isUpdateMode:
|
|
381
|
+
import_zod2.z.object({
|
|
382
|
+
type: import_zod2.z.literal("sync"),
|
|
383
|
+
data: import_zod2.z.object({
|
|
384
|
+
tests: import_zod2.z.record(import_zod2.z.string(), TestDataSchema),
|
|
385
|
+
isUpdateMode: import_zod2.z.boolean().optional()
|
|
343
386
|
})
|
|
344
387
|
}),
|
|
345
|
-
|
|
388
|
+
import_zod2.z.object({ type: import_zod2.z.literal("approve"), data: import_zod2.z.unknown() }),
|
|
389
|
+
import_zod2.z.object({
|
|
390
|
+
type: import_zod2.z.literal("run-status"),
|
|
391
|
+
data: import_zod2.z.object({
|
|
392
|
+
running: import_zod2.z.boolean()
|
|
393
|
+
})
|
|
394
|
+
})
|
|
346
395
|
]);
|
|
347
|
-
var TestBeginDataSchema =
|
|
348
|
-
id:
|
|
349
|
-
title:
|
|
350
|
-
titlePath:
|
|
351
|
-
browser:
|
|
352
|
-
projectName:
|
|
396
|
+
var TestBeginDataSchema = import_zod2.z.object({
|
|
397
|
+
id: import_zod2.z.string(),
|
|
398
|
+
title: import_zod2.z.string(),
|
|
399
|
+
titlePath: import_zod2.z.array(import_zod2.z.string()),
|
|
400
|
+
browser: import_zod2.z.string(),
|
|
401
|
+
projectName: import_zod2.z.string().optional(),
|
|
353
402
|
location: LocationSchema
|
|
354
403
|
});
|
|
355
|
-
var TestEndDataSchema =
|
|
356
|
-
id:
|
|
357
|
-
status:
|
|
358
|
-
attachments:
|
|
359
|
-
visualNames:
|
|
360
|
-
visualDeclarations:
|
|
404
|
+
var TestEndDataSchema = import_zod2.z.object({
|
|
405
|
+
id: import_zod2.z.string(),
|
|
406
|
+
status: import_zod2.z.enum(["passed", "failed", "skipped"]),
|
|
407
|
+
attachments: import_zod2.z.array(AttachmentSchema),
|
|
408
|
+
visualNames: import_zod2.z.array(import_zod2.z.string()).default([]),
|
|
409
|
+
visualDeclarations: import_zod2.z.preprocess(
|
|
361
410
|
(value) => value === null ? void 0 : value,
|
|
362
|
-
|
|
411
|
+
import_zod2.z.array(ScreenshotDeclarationSchema).optional()
|
|
363
412
|
),
|
|
364
|
-
error:
|
|
365
|
-
duration:
|
|
413
|
+
error: import_zod2.z.string().optional(),
|
|
414
|
+
duration: import_zod2.z.number().optional()
|
|
366
415
|
});
|
|
367
|
-
var RunEndDataSchema =
|
|
368
|
-
status:
|
|
416
|
+
var RunEndDataSchema = import_zod2.z.object({
|
|
417
|
+
status: import_zod2.z.enum(["passed", "failed", "skipped"])
|
|
369
418
|
});
|
|
370
|
-
var RegisterDataSchema =
|
|
371
|
-
playwrightSnapshotDir:
|
|
372
|
-
playwrightTestDir:
|
|
373
|
-
playwrightSnapshotPathTemplate:
|
|
374
|
-
playwrightToHaveScreenshotPathTemplate:
|
|
419
|
+
var RegisterDataSchema = import_zod2.z.object({
|
|
420
|
+
playwrightSnapshotDir: import_zod2.z.string().optional(),
|
|
421
|
+
playwrightTestDir: import_zod2.z.string().optional(),
|
|
422
|
+
playwrightSnapshotPathTemplate: import_zod2.z.string().optional(),
|
|
423
|
+
playwrightToHaveScreenshotPathTemplate: import_zod2.z.string().optional(),
|
|
424
|
+
configFile: import_zod2.z.string().optional(),
|
|
425
|
+
cwd: import_zod2.z.string().optional()
|
|
375
426
|
});
|
|
376
|
-
var ReportDataSchema =
|
|
377
|
-
isRunning:
|
|
378
|
-
tests:
|
|
379
|
-
browsers:
|
|
380
|
-
isUpdateMode:
|
|
381
|
-
screenshotDir:
|
|
427
|
+
var ReportDataSchema = import_zod2.z.object({
|
|
428
|
+
isRunning: import_zod2.z.boolean(),
|
|
429
|
+
tests: import_zod2.z.record(import_zod2.z.string(), TestDataSchema),
|
|
430
|
+
browsers: import_zod2.z.array(import_zod2.z.string()),
|
|
431
|
+
isUpdateMode: import_zod2.z.boolean(),
|
|
432
|
+
screenshotDir: import_zod2.z.string()
|
|
382
433
|
});
|
|
383
|
-
var LoadedReportDataSchema =
|
|
384
|
-
tests:
|
|
385
|
-
isUpdateMode:
|
|
434
|
+
var LoadedReportDataSchema = import_zod2.z.object({
|
|
435
|
+
tests: import_zod2.z.record(import_zod2.z.string(), TestDataSchema).optional(),
|
|
436
|
+
isUpdateMode: import_zod2.z.boolean().optional()
|
|
386
437
|
});
|
|
387
|
-
var OfflineEventSchema =
|
|
388
|
-
type:
|
|
389
|
-
data:
|
|
390
|
-
timestamp:
|
|
391
|
-
workerIndex:
|
|
438
|
+
var OfflineEventSchema = import_zod2.z.object({
|
|
439
|
+
type: import_zod2.z.enum(["test-begin", "test-end", "run-end"]),
|
|
440
|
+
data: import_zod2.z.unknown(),
|
|
441
|
+
timestamp: import_zod2.z.number(),
|
|
442
|
+
workerIndex: import_zod2.z.number()
|
|
392
443
|
});
|
|
393
|
-
var OfflineReportSchema =
|
|
394
|
-
version:
|
|
395
|
-
generatedAt:
|
|
396
|
-
workers:
|
|
397
|
-
events:
|
|
444
|
+
var OfflineReportSchema = import_zod2.z.object({
|
|
445
|
+
version: import_zod2.z.number(),
|
|
446
|
+
generatedAt: import_zod2.z.string(),
|
|
447
|
+
workers: import_zod2.z.number(),
|
|
448
|
+
events: import_zod2.z.array(OfflineEventSchema)
|
|
398
449
|
});
|
|
399
|
-
var
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
var ReportApiResponseSchema = import_zod.z.object({
|
|
405
|
-
tests: import_zod.z.record(import_zod.z.string(), TestDataSchema),
|
|
406
|
-
isUpdateMode: import_zod.z.boolean().optional()
|
|
450
|
+
var ReportApiResponseSchema = import_zod2.z.object({
|
|
451
|
+
tests: import_zod2.z.record(import_zod2.z.string(), TestDataSchema),
|
|
452
|
+
isUpdateMode: import_zod2.z.boolean().optional(),
|
|
453
|
+
isRunning: import_zod2.z.boolean().optional(),
|
|
454
|
+
runEnabled: import_zod2.z.boolean().optional()
|
|
407
455
|
});
|
|
408
|
-
var ClientBootstrapDataSchema =
|
|
456
|
+
var ClientBootstrapDataSchema = import_zod2.z.object({
|
|
409
457
|
report: ReportApiResponseSchema.extend({
|
|
410
|
-
isUpdateMode:
|
|
458
|
+
isUpdateMode: import_zod2.z.boolean()
|
|
411
459
|
}),
|
|
412
|
-
liveUpdates:
|
|
413
|
-
approvalEnabled:
|
|
414
|
-
approvalMessage:
|
|
460
|
+
liveUpdates: import_zod2.z.boolean(),
|
|
461
|
+
approvalEnabled: import_zod2.z.boolean(),
|
|
462
|
+
approvalMessage: import_zod2.z.string().optional(),
|
|
463
|
+
runEnabled: import_zod2.z.boolean().optional(),
|
|
464
|
+
isRunning: import_zod2.z.boolean().optional()
|
|
415
465
|
});
|
|
416
|
-
var ImagesViewModeSchema =
|
|
466
|
+
var ImagesViewModeSchema = import_zod2.z.enum(["side-by-side", "swap", "slide", "blend"]);
|
|
417
467
|
function safeParse(schema, data) {
|
|
418
468
|
const result = schema.safeParse(data);
|
|
419
469
|
if (result.success) {
|
|
@@ -422,70 +472,15 @@ function safeParse(schema, data) {
|
|
|
422
472
|
return null;
|
|
423
473
|
}
|
|
424
474
|
|
|
425
|
-
// src/offline-reports.ts
|
|
426
|
-
var OFFLINE_REPORT_FILE_PATTERN = /^crvy-rprtr(?:-\d+)?\.json$/;
|
|
427
|
-
async function findOfflineReportPaths(searchDir) {
|
|
428
|
-
try {
|
|
429
|
-
const entries = await (0, import_promises.readdir)(searchDir);
|
|
430
|
-
return entries.filter((entry) => OFFLINE_REPORT_FILE_PATTERN.test(entry)).sort((left, right) => left.localeCompare(right)).map((entry) => (0, import_path.join)(searchDir, entry));
|
|
431
|
-
} catch (error) {
|
|
432
|
-
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
433
|
-
return [];
|
|
434
|
-
}
|
|
435
|
-
throw error;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
function parseOfflineReport(value) {
|
|
439
|
-
const parsed = safeParse(OfflineReportSchema, value);
|
|
440
|
-
if (parsed === null || parsed.version !== 1 || !Array.isArray(parsed.events)) {
|
|
441
|
-
return null;
|
|
442
|
-
}
|
|
443
|
-
return parsed;
|
|
444
|
-
}
|
|
445
|
-
function mergeOfflineReportsIntoTests(existingTests, offlineReports, options = {}) {
|
|
446
|
-
const state = createMutableReportState(options.screenshotDir);
|
|
447
|
-
let shouldFinalize = false;
|
|
448
|
-
for (const report of offlineReports) {
|
|
449
|
-
for (const event of report.events) {
|
|
450
|
-
switch (event.type) {
|
|
451
|
-
case "test-begin": {
|
|
452
|
-
const parsed = safeParse(TestBeginDataSchema, event.data);
|
|
453
|
-
if (parsed !== null) {
|
|
454
|
-
applyTestBeginEvent(state, parsed);
|
|
455
|
-
}
|
|
456
|
-
break;
|
|
457
|
-
}
|
|
458
|
-
case "test-end": {
|
|
459
|
-
const parsed = safeParse(TestEndDataSchema, event.data);
|
|
460
|
-
if (parsed !== null) {
|
|
461
|
-
applyTestEndEvent(state, parsed, { screenshotsBaseUrl: options.screenshotsBaseUrl });
|
|
462
|
-
}
|
|
463
|
-
break;
|
|
464
|
-
}
|
|
465
|
-
case "run-end":
|
|
466
|
-
shouldFinalize = true;
|
|
467
|
-
break;
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
if (shouldFinalize) {
|
|
472
|
-
finalizeRunEvent(state);
|
|
473
|
-
}
|
|
474
|
-
return {
|
|
475
|
-
...existingTests,
|
|
476
|
-
...state.reportData.tests
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
|
|
480
475
|
// src/server/file-utils.ts
|
|
481
|
-
var
|
|
482
|
-
var
|
|
476
|
+
var import_promises = require("fs/promises");
|
|
477
|
+
var import_path = require("path");
|
|
483
478
|
function isFileNotFound(error) {
|
|
484
479
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
485
480
|
}
|
|
486
481
|
async function fileExists(filePath) {
|
|
487
482
|
try {
|
|
488
|
-
await (0,
|
|
483
|
+
await (0, import_promises.access)(filePath);
|
|
489
484
|
return true;
|
|
490
485
|
} catch (error) {
|
|
491
486
|
if (isFileNotFound(error)) {
|
|
@@ -496,7 +491,7 @@ async function fileExists(filePath) {
|
|
|
496
491
|
}
|
|
497
492
|
async function isDirectory(filePath) {
|
|
498
493
|
try {
|
|
499
|
-
const stats = await (0,
|
|
494
|
+
const stats = await (0, import_promises.stat)(filePath);
|
|
500
495
|
return stats.isDirectory();
|
|
501
496
|
} catch (error) {
|
|
502
497
|
if (isFileNotFound(error)) {
|
|
@@ -507,7 +502,7 @@ async function isDirectory(filePath) {
|
|
|
507
502
|
}
|
|
508
503
|
async function readJsonFile(filePath) {
|
|
509
504
|
try {
|
|
510
|
-
const raw = await (0,
|
|
505
|
+
const raw = await (0, import_promises.readFile)(filePath, "utf8");
|
|
511
506
|
if (raw.trim() === "") {
|
|
512
507
|
return null;
|
|
513
508
|
}
|
|
@@ -520,16 +515,16 @@ async function readJsonFile(filePath) {
|
|
|
520
515
|
}
|
|
521
516
|
}
|
|
522
517
|
async function writeJsonFile(filePath, value) {
|
|
523
|
-
await (0,
|
|
524
|
-
await (0,
|
|
518
|
+
await (0, import_promises.mkdir)((0, import_path.dirname)(filePath), { recursive: true });
|
|
519
|
+
await (0, import_promises.writeFile)(filePath, `${JSON.stringify(value, null, 2)}
|
|
525
520
|
`);
|
|
526
521
|
}
|
|
527
522
|
async function copyFilePortable(sourcePath, destinationPath) {
|
|
528
|
-
await (0,
|
|
529
|
-
await (0,
|
|
523
|
+
await (0, import_promises.mkdir)((0, import_path.dirname)(destinationPath), { recursive: true });
|
|
524
|
+
await (0, import_promises.copyFile)(sourcePath, destinationPath);
|
|
530
525
|
}
|
|
531
526
|
function inferContentType(filePath) {
|
|
532
|
-
switch ((0,
|
|
527
|
+
switch ((0, import_path.extname)(filePath).toLowerCase()) {
|
|
533
528
|
case ".css":
|
|
534
529
|
return "text/css";
|
|
535
530
|
case ".gif":
|
|
@@ -557,7 +552,7 @@ function inferContentType(filePath) {
|
|
|
557
552
|
}
|
|
558
553
|
async function respondWithFile(filePath, contentType) {
|
|
559
554
|
try {
|
|
560
|
-
const file = await (0,
|
|
555
|
+
const file = await (0, import_promises.readFile)(filePath);
|
|
561
556
|
const resolvedContentType = contentType ?? inferContentType(filePath);
|
|
562
557
|
const headers = { "X-Content-Type-Options": "nosniff" };
|
|
563
558
|
if (resolvedContentType !== void 0) {
|
|
@@ -572,6 +567,93 @@ async function respondWithFile(filePath, contentType) {
|
|
|
572
567
|
}
|
|
573
568
|
}
|
|
574
569
|
|
|
570
|
+
// src/offline-reports.ts
|
|
571
|
+
var MAX_CONCURRENT_FILE_OPS = 5;
|
|
572
|
+
var OFFLINE_REPORT_FILE_PATTERN = /^crvy-rprtr(?:-\d+)?\.json$/;
|
|
573
|
+
async function findOfflineReportPaths(searchDir) {
|
|
574
|
+
try {
|
|
575
|
+
const entries = await (0, import_promises2.readdir)(searchDir);
|
|
576
|
+
return entries.filter((entry) => OFFLINE_REPORT_FILE_PATTERN.test(entry)).sort((left, right) => left.localeCompare(right)).map((entry) => (0, import_path2.join)(searchDir, entry));
|
|
577
|
+
} catch (error) {
|
|
578
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
579
|
+
return [];
|
|
580
|
+
}
|
|
581
|
+
throw error;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
function parseOfflineReport(value) {
|
|
585
|
+
const parsed = safeParse(OfflineReportSchema, value);
|
|
586
|
+
if (parsed === null || parsed.version !== 1 || !Array.isArray(parsed.events)) {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
return parsed;
|
|
590
|
+
}
|
|
591
|
+
async function readOfflineReport(filePath) {
|
|
592
|
+
try {
|
|
593
|
+
const raw = await readJsonFile(filePath);
|
|
594
|
+
if (raw === null) {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
const parsed = parseOfflineReport(raw);
|
|
598
|
+
if (parsed !== null) {
|
|
599
|
+
console.log(`[Server] Loading offline report: ${filePath}`);
|
|
600
|
+
return parsed;
|
|
601
|
+
}
|
|
602
|
+
} catch {
|
|
603
|
+
}
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
async function loadOfflineReports(reportData, offlineReportDir) {
|
|
607
|
+
const offlineReportPaths = await findOfflineReportPaths(offlineReportDir);
|
|
608
|
+
if (offlineReportPaths.length === 0) {
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
const limit = (0, import_p_limit.default)(MAX_CONCURRENT_FILE_OPS);
|
|
612
|
+
const reports = await Promise.all(offlineReportPaths.map((filePath) => limit(() => readOfflineReport(filePath))));
|
|
613
|
+
const validReports = reports.filter((report) => report !== null);
|
|
614
|
+
if (validReports.length === 0) {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
reportData.tests = mergeOfflineReportsIntoTests(reportData.tests, validReports, {
|
|
618
|
+
screenshotDir: reportData.screenshotDir,
|
|
619
|
+
screenshotsBaseUrl: "/screenshots/"
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
function mergeOfflineReportsIntoTests(existingTests, offlineReports, options = {}) {
|
|
623
|
+
const state = createMutableReportState(options.screenshotDir);
|
|
624
|
+
let shouldFinalize = false;
|
|
625
|
+
for (const report of offlineReports) {
|
|
626
|
+
for (const event of report.events) {
|
|
627
|
+
switch (event.type) {
|
|
628
|
+
case "test-begin": {
|
|
629
|
+
const parsed = safeParse(TestBeginDataSchema, event.data);
|
|
630
|
+
if (parsed !== null) {
|
|
631
|
+
applyTestBeginEvent(state, parsed);
|
|
632
|
+
}
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
case "test-end": {
|
|
636
|
+
const parsed = safeParse(TestEndDataSchema, event.data);
|
|
637
|
+
if (parsed !== null) {
|
|
638
|
+
applyTestEndEvent(state, parsed, { screenshotsBaseUrl: options.screenshotsBaseUrl });
|
|
639
|
+
}
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
case "run-end":
|
|
643
|
+
shouldFinalize = true;
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (shouldFinalize) {
|
|
649
|
+
finalizeRunEvent(state);
|
|
650
|
+
}
|
|
651
|
+
return {
|
|
652
|
+
...existingTests,
|
|
653
|
+
...state.reportData.tests
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
575
657
|
// src/server/handlers.ts
|
|
576
658
|
var import_fs2 = require("fs");
|
|
577
659
|
|
|
@@ -872,6 +954,7 @@ function handleArtifactRoute(ctx, req, pathname) {
|
|
|
872
954
|
function handleTestBegin(ctx, data) {
|
|
873
955
|
const test = applyTestBeginEvent(ctx, data);
|
|
874
956
|
ctx.reportData.isRunning = true;
|
|
957
|
+
ctx.scheduleReportSave();
|
|
875
958
|
console.log(` \u25B6 [${test.browser ?? "?"}] ${test.title}`);
|
|
876
959
|
const message = { type: "test-begin", data: test };
|
|
877
960
|
broadcastToBrowsers(ctx.wsClients, message);
|
|
@@ -910,12 +993,13 @@ function handleTestEnd(ctx, data) {
|
|
|
910
993
|
const errNote = data.error !== null && data.error !== void 0 ? `
|
|
911
994
|
Error: ${data.error}` : "";
|
|
912
995
|
console.log(` ${icon} [${test.browser}] ${test.title}${dur}${diffNote}${errNote}`);
|
|
996
|
+
ctx.scheduleReportSave();
|
|
913
997
|
const message = { type: "test-update", data: test };
|
|
914
998
|
broadcastToBrowsers(ctx.wsClients, message);
|
|
915
999
|
}
|
|
916
1000
|
async function handleRunEnd(ctx, data) {
|
|
917
|
-
const removedTestIds = Object.keys(ctx.reportData.tests).filter((id) => !ctx.currentRunIds.has(id));
|
|
918
|
-
const { passed, failed, pending } = finalizeRunEvent(ctx);
|
|
1001
|
+
const removedTestIds = ctx.isFilteredRun ? [] : Object.keys(ctx.reportData.tests).filter((id) => !ctx.currentRunIds.has(id));
|
|
1002
|
+
const { passed, failed, pending } = finalizeRunEvent(ctx, { preserveNonCurrent: ctx.isFilteredRun });
|
|
919
1003
|
await ctx.saveReport();
|
|
920
1004
|
console.log(`
|
|
921
1005
|
Run complete \u2014 ${passed} passed, ${failed} failed, ${pending} skipped`);
|
|
@@ -968,16 +1052,148 @@ function handleRegister(ctx, data) {
|
|
|
968
1052
|
ctx.routesContext.approvalRouting.playwrightToHaveScreenshotPathTemplate = data.playwrightToHaveScreenshotPathTemplate;
|
|
969
1053
|
}
|
|
970
1054
|
}
|
|
1055
|
+
if (data.configFile !== void 0 && data.cwd !== void 0) {
|
|
1056
|
+
ctx.routesContext.runContext = { configFile: data.configFile, cwd: data.cwd };
|
|
1057
|
+
}
|
|
971
1058
|
console.log("[Server] Reporter registered with config:", {
|
|
972
1059
|
playwrightSnapshotDir: data.playwrightSnapshotDir,
|
|
973
1060
|
playwrightTestDir: data.playwrightTestDir
|
|
974
1061
|
});
|
|
975
1062
|
}
|
|
976
1063
|
|
|
977
|
-
// src/server/
|
|
1064
|
+
// src/server/playwright-config.ts
|
|
978
1065
|
var import_path6 = require("path");
|
|
1066
|
+
var CONFIG_FILES = [
|
|
1067
|
+
"playwright.config.ts",
|
|
1068
|
+
"playwright.config.mts",
|
|
1069
|
+
"playwright.config.cts",
|
|
1070
|
+
"playwright.config.js",
|
|
1071
|
+
"playwright.config.mjs",
|
|
1072
|
+
"playwright.config.cjs"
|
|
1073
|
+
];
|
|
1074
|
+
async function resolvePlaywrightConfig(cwd) {
|
|
1075
|
+
const matches = await Promise.all(
|
|
1076
|
+
CONFIG_FILES.map(async (file) => {
|
|
1077
|
+
const candidate = (0, import_path6.join)(cwd, file);
|
|
1078
|
+
return await fileExists(candidate) ? candidate : null;
|
|
1079
|
+
})
|
|
1080
|
+
);
|
|
1081
|
+
return matches.find((path) => path !== null) ?? null;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// src/server/report-persistence.ts
|
|
1085
|
+
function createDebouncedSaver(save, delayMs, setTimeoutFn = (fn, ms) => setTimeout(fn, ms), clearTimeoutFn = (handle) => {
|
|
1086
|
+
if (handle !== null) clearTimeout(handle);
|
|
1087
|
+
}) {
|
|
1088
|
+
let timer = null;
|
|
1089
|
+
let dirty = false;
|
|
1090
|
+
let inFlight = null;
|
|
1091
|
+
const runSave = () => {
|
|
1092
|
+
dirty = false;
|
|
1093
|
+
inFlight = save();
|
|
1094
|
+
return inFlight.finally(() => {
|
|
1095
|
+
inFlight = null;
|
|
1096
|
+
});
|
|
1097
|
+
};
|
|
1098
|
+
return {
|
|
1099
|
+
schedule() {
|
|
1100
|
+
dirty = true;
|
|
1101
|
+
if (timer !== null) clearTimeoutFn(timer);
|
|
1102
|
+
timer = setTimeoutFn(() => {
|
|
1103
|
+
timer = null;
|
|
1104
|
+
void runSave();
|
|
1105
|
+
}, delayMs);
|
|
1106
|
+
},
|
|
1107
|
+
async flush() {
|
|
1108
|
+
if (timer !== null) {
|
|
1109
|
+
clearTimeoutFn(timer);
|
|
1110
|
+
timer = null;
|
|
1111
|
+
}
|
|
1112
|
+
if (inFlight !== null) {
|
|
1113
|
+
await inFlight;
|
|
1114
|
+
}
|
|
1115
|
+
if (dirty) {
|
|
1116
|
+
await runSave();
|
|
1117
|
+
}
|
|
1118
|
+
},
|
|
1119
|
+
cancel() {
|
|
1120
|
+
if (timer !== null) {
|
|
1121
|
+
clearTimeoutFn(timer);
|
|
1122
|
+
timer = null;
|
|
1123
|
+
}
|
|
1124
|
+
dirty = false;
|
|
1125
|
+
}
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
function createReportPersistence(reportFile, reportData) {
|
|
1129
|
+
const reportSaver = createDebouncedSaver(() => writeJsonFile(reportFile, reportData), 250);
|
|
1130
|
+
return {
|
|
1131
|
+
saveReport: () => reportSaver.flush(),
|
|
1132
|
+
scheduleReportSave: () => {
|
|
1133
|
+
reportSaver.schedule();
|
|
1134
|
+
},
|
|
1135
|
+
dispose: () => reportSaver.flush()
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// src/server/routes-context.ts
|
|
1140
|
+
function createRoutesContext(reportData, staticDir, saveReport, options) {
|
|
1141
|
+
const artifactRoots = [reportData.screenshotDir, options.outputDir, options.playwrightSnapshotDir].filter(
|
|
1142
|
+
(root) => root !== void 0 && root !== ""
|
|
1143
|
+
);
|
|
1144
|
+
return {
|
|
1145
|
+
reportData,
|
|
1146
|
+
staticDir,
|
|
1147
|
+
saveReport,
|
|
1148
|
+
artifactRoots,
|
|
1149
|
+
approvalRouting: {
|
|
1150
|
+
configDir: options.configDir ?? process.cwd(),
|
|
1151
|
+
playwrightTestDir: options.playwrightTestDir,
|
|
1152
|
+
playwrightSnapshotDir: options.playwrightSnapshotDir,
|
|
1153
|
+
playwrightSnapshotPathTemplate: options.playwrightSnapshotPathTemplate,
|
|
1154
|
+
playwrightToHaveScreenshotPathTemplate: options.playwrightToHaveScreenshotPathTemplate
|
|
1155
|
+
},
|
|
1156
|
+
runContext: void 0
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// src/server/routes.ts
|
|
1161
|
+
var import_path7 = require("path");
|
|
1162
|
+
|
|
1163
|
+
// src/server/run-routes.ts
|
|
1164
|
+
function handleRunRoutes(pathname, method, runController, req) {
|
|
1165
|
+
if (pathname === "/api/run" && method === "POST") {
|
|
1166
|
+
return handleApiRun(runController, req);
|
|
1167
|
+
}
|
|
1168
|
+
if (pathname === "/api/stop" && method === "POST") {
|
|
1169
|
+
return Promise.resolve(handleApiStop(runController));
|
|
1170
|
+
}
|
|
1171
|
+
return null;
|
|
1172
|
+
}
|
|
1173
|
+
async function handleApiRun(runController, req) {
|
|
1174
|
+
let body = {};
|
|
1175
|
+
try {
|
|
1176
|
+
body = await req.json();
|
|
1177
|
+
} catch {
|
|
1178
|
+
}
|
|
1179
|
+
const parsed = safeParse(RunRequestBodySchema, body);
|
|
1180
|
+
if (parsed === null) {
|
|
1181
|
+
return Response.json({ ok: false, error: "Invalid request body" }, { status: 400 });
|
|
1182
|
+
}
|
|
1183
|
+
const result = runController.start(parsed);
|
|
1184
|
+
if (result.ok) return Response.json(result);
|
|
1185
|
+
const status = result.reason === "no-tests" ? 400 : 409;
|
|
1186
|
+
return Response.json(result, { status });
|
|
1187
|
+
}
|
|
1188
|
+
function handleApiStop(runController) {
|
|
1189
|
+
const result = runController.stop();
|
|
1190
|
+
if (result.ok) return Response.json(result);
|
|
1191
|
+
return Response.json(result, { status: 409 });
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// src/server/routes.ts
|
|
979
1195
|
async function handleRoot(ctx) {
|
|
980
|
-
const html = await respondWithFile((0,
|
|
1196
|
+
const html = await respondWithFile((0, import_path7.join)(ctx.staticDir, "index.html"), "text/html");
|
|
981
1197
|
return html ?? new Response("Not Found", { status: 404 });
|
|
982
1198
|
}
|
|
983
1199
|
async function handleAppCss() {
|
|
@@ -992,12 +1208,15 @@ async function handleSrcFiles(req) {
|
|
|
992
1208
|
return file ?? new Response("Not Found", { status: 404 });
|
|
993
1209
|
}
|
|
994
1210
|
function handleApiReport(ctx) {
|
|
995
|
-
return Response.json(
|
|
1211
|
+
return Response.json({
|
|
1212
|
+
...ctx.reportData,
|
|
1213
|
+
runEnabled: ctx.runContext !== void 0
|
|
1214
|
+
});
|
|
996
1215
|
}
|
|
997
1216
|
var APPROVAL_TARGET_ERROR = "Could not resolve approval target";
|
|
998
1217
|
function actualPathFromUrl(ctx, actualUrl) {
|
|
999
1218
|
if (actualUrl.startsWith("/screenshots/")) {
|
|
1000
|
-
return (0,
|
|
1219
|
+
return (0, import_path7.join)(ctx.reportData.screenshotDir, actualUrl.slice("/screenshots/".length));
|
|
1001
1220
|
}
|
|
1002
1221
|
if (actualUrl.startsWith("/file/")) {
|
|
1003
1222
|
return decodeURIComponent(actualUrl.slice("/file/".length));
|
|
@@ -1119,12 +1338,12 @@ async function handleScreenshots(ctx, req) {
|
|
|
1119
1338
|
}
|
|
1120
1339
|
async function handleDist(ctx, req) {
|
|
1121
1340
|
const path = new URL(req.url).pathname.slice("/dist/".length);
|
|
1122
|
-
const filePath = (0,
|
|
1341
|
+
const filePath = (0, import_path7.join)(ctx.staticDir, path);
|
|
1123
1342
|
const contentType = filePath.endsWith(".css") ? "text/css" : filePath.endsWith(".js") ? "application/javascript" : filePath.endsWith(".svelte") ? "text/plain" : "application/octet-stream";
|
|
1124
1343
|
const file = await respondWithFile(filePath, contentType);
|
|
1125
1344
|
return file ?? new Response("Not Found", { status: 404 });
|
|
1126
1345
|
}
|
|
1127
|
-
function handleHttpRequest(ctx, req) {
|
|
1346
|
+
function handleHttpRequest(ctx, req, runController) {
|
|
1128
1347
|
const pathname = new URL(req.url).pathname;
|
|
1129
1348
|
if (pathname === "/") {
|
|
1130
1349
|
return handleRoot(ctx);
|
|
@@ -1144,6 +1363,10 @@ function handleHttpRequest(ctx, req) {
|
|
|
1144
1363
|
if (pathname === "/api/approve-all" && req.method === "POST") {
|
|
1145
1364
|
return handleApiApproveAll(ctx);
|
|
1146
1365
|
}
|
|
1366
|
+
const runResponse = handleRunRoutes(pathname, req.method, runController, req);
|
|
1367
|
+
if (runResponse !== null) {
|
|
1368
|
+
return runResponse;
|
|
1369
|
+
}
|
|
1147
1370
|
if (pathname.startsWith("/api/images/")) {
|
|
1148
1371
|
return handleApiImages(req);
|
|
1149
1372
|
}
|
|
@@ -1160,9 +1383,221 @@ function handleHttpRequest(ctx, req) {
|
|
|
1160
1383
|
return Promise.resolve(new Response("Not Found", { status: 404 }));
|
|
1161
1384
|
}
|
|
1162
1385
|
|
|
1163
|
-
// src/server/
|
|
1386
|
+
// src/server/run-controller.ts
|
|
1387
|
+
var import_child_process = require("child_process");
|
|
1388
|
+
var import_node_fs = require("node:fs");
|
|
1389
|
+
var import_node_module = require("node:module");
|
|
1390
|
+
var import_node_os = require("node:os");
|
|
1391
|
+
var import_node_path = require("node:path");
|
|
1392
|
+
var import_commands = require("package-manager-detector/commands");
|
|
1393
|
+
var import_detect = require("package-manager-detector/detect");
|
|
1164
1394
|
var import_meta = { url: require("url").pathToFileURL(__filename).href };
|
|
1165
|
-
var
|
|
1395
|
+
var STOP_GRACE_MS = 5e3;
|
|
1396
|
+
var KNOWN_SIGNALS = {
|
|
1397
|
+
SIGTERM: "SIGTERM",
|
|
1398
|
+
SIGKILL: "SIGKILL"
|
|
1399
|
+
};
|
|
1400
|
+
function resolvePlaywrightLaunch(cwd, playwrightArgs) {
|
|
1401
|
+
const agent = (0, import_detect.getUserAgent)();
|
|
1402
|
+
const resolved = agent === null ? null : (0, import_commands.resolveCommand)(agent, "execute-local", ["playwright", ...playwrightArgs]);
|
|
1403
|
+
if (resolved !== null) return { cmd: resolved.command, args: resolved.args };
|
|
1404
|
+
return { cmd: "npx", args: ["playwright", ...playwrightArgs] };
|
|
1405
|
+
}
|
|
1406
|
+
function descriptorLocation(d) {
|
|
1407
|
+
return d.column === void 0 ? `${d.file}:${d.line}` : `${d.file}:${d.line}:${d.column}`;
|
|
1408
|
+
}
|
|
1409
|
+
function sharedProject(tests) {
|
|
1410
|
+
const names = new Set(tests.map((t) => t.projectName ?? ""));
|
|
1411
|
+
if (names.size === 1) {
|
|
1412
|
+
const name = [...names][0];
|
|
1413
|
+
return name === "" ? void 0 : name;
|
|
1414
|
+
}
|
|
1415
|
+
return void 0;
|
|
1416
|
+
}
|
|
1417
|
+
function gteMinor(version, major, minor) {
|
|
1418
|
+
const match = /^(\d+)\.(\d+)/.exec(version.trim());
|
|
1419
|
+
if (match === null) return false;
|
|
1420
|
+
const maj = parseInt(match[1], 10);
|
|
1421
|
+
const min = parseInt(match[2], 10);
|
|
1422
|
+
if (maj !== major) return maj > major;
|
|
1423
|
+
return min >= minor;
|
|
1424
|
+
}
|
|
1425
|
+
function resolvePlaywrightVersion(cwd) {
|
|
1426
|
+
try {
|
|
1427
|
+
const req = (0, import_node_module.createRequire)((0, import_node_path.join)(cwd, "package.json"));
|
|
1428
|
+
const pkgPath = req.resolve("@playwright/test/package.json");
|
|
1429
|
+
const pkg = JSON.parse((0, import_node_fs.readFileSync)(pkgPath, "utf8"));
|
|
1430
|
+
return typeof pkg === "object" && pkg !== null && "version" in pkg && typeof pkg.version === "string" ? pkg.version : null;
|
|
1431
|
+
} catch {
|
|
1432
|
+
return null;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
function buildTestListEntries(tests) {
|
|
1436
|
+
return tests.map((d) => {
|
|
1437
|
+
const loc = descriptorLocation(d);
|
|
1438
|
+
const title = d.titlePath.join(" \u203A ");
|
|
1439
|
+
const prefix = d.projectName !== void 0 && d.projectName !== "" ? `[${d.projectName}] \u203A ` : "";
|
|
1440
|
+
return `${prefix}${loc} \u203A ${title}`;
|
|
1441
|
+
});
|
|
1442
|
+
}
|
|
1443
|
+
function defaultWriteTempFile(content) {
|
|
1444
|
+
const path = (0, import_node_path.join)((0, import_node_os.tmpdir)(), `crvy-rprtr-test-list-${process.pid}-${Date.now()}.txt`);
|
|
1445
|
+
(0, import_node_fs.writeFileSync)(path, content, "utf8");
|
|
1446
|
+
return path;
|
|
1447
|
+
}
|
|
1448
|
+
function defaultDeleteTempFile(path) {
|
|
1449
|
+
try {
|
|
1450
|
+
(0, import_node_fs.unlinkSync)(path);
|
|
1451
|
+
} catch {
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
function resolveReporterDefault(cwd) {
|
|
1455
|
+
try {
|
|
1456
|
+
return (0, import_node_module.createRequire)((0, import_node_path.join)(cwd, "package.json")).resolve("@crvy/rprtr");
|
|
1457
|
+
} catch {
|
|
1458
|
+
}
|
|
1459
|
+
try {
|
|
1460
|
+
return (0, import_node_module.createRequire)(import_meta.url).resolve("@crvy/rprtr");
|
|
1461
|
+
} catch {
|
|
1462
|
+
return null;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
function buildSpawnEnv(port) {
|
|
1466
|
+
const env = {};
|
|
1467
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
1468
|
+
if (key === "CI") continue;
|
|
1469
|
+
env[key] = value;
|
|
1470
|
+
}
|
|
1471
|
+
env.CRVY_RPRTR_SERVER_URL = `ws://localhost:${port}`;
|
|
1472
|
+
env.PLAYWRIGHT_HTML_OPEN = "never";
|
|
1473
|
+
return env;
|
|
1474
|
+
}
|
|
1475
|
+
var RunController = class {
|
|
1476
|
+
constructor(deps) {
|
|
1477
|
+
this.deps = deps;
|
|
1478
|
+
}
|
|
1479
|
+
child = null;
|
|
1480
|
+
sigkillTimer = null;
|
|
1481
|
+
testListPath = null;
|
|
1482
|
+
get isRunning() {
|
|
1483
|
+
return this.child !== null;
|
|
1484
|
+
}
|
|
1485
|
+
supportsTestList(cwd) {
|
|
1486
|
+
const getVersion = this.deps.getPlaywrightVersion ?? resolvePlaywrightVersion;
|
|
1487
|
+
const version = getVersion(cwd);
|
|
1488
|
+
return version !== null && gteMinor(version, 1, 56);
|
|
1489
|
+
}
|
|
1490
|
+
cleanupTempFile() {
|
|
1491
|
+
if (this.testListPath !== null) {
|
|
1492
|
+
const del = this.deps.deleteTempFile ?? defaultDeleteTempFile;
|
|
1493
|
+
del(this.testListPath);
|
|
1494
|
+
this.testListPath = null;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
start(filters) {
|
|
1498
|
+
const ctx = this.deps.getRunContext();
|
|
1499
|
+
if (ctx === null) return { ok: false, reason: "no-config" };
|
|
1500
|
+
if (this.child !== null) return { ok: false, reason: "already-running" };
|
|
1501
|
+
if (filters.tests !== void 0 && filters.tests.length === 0) {
|
|
1502
|
+
return { ok: false, reason: "no-tests" };
|
|
1503
|
+
}
|
|
1504
|
+
const resolveReporter = this.deps.resolveReporter ?? resolveReporterDefault;
|
|
1505
|
+
const reporterModule = resolveReporter(ctx.cwd);
|
|
1506
|
+
const tests = filters.tests;
|
|
1507
|
+
const useTestList = tests !== void 0 && tests.length > 1 && this.supportsTestList(ctx.cwd);
|
|
1508
|
+
const args = ["test", "--config", ctx.configFile];
|
|
1509
|
+
if (reporterModule !== null) args.push("--reporter", reporterModule);
|
|
1510
|
+
if (useTestList && tests !== void 0) {
|
|
1511
|
+
const content = buildTestListEntries(tests).join("\n");
|
|
1512
|
+
const writeTemp = this.deps.writeTempFile ?? defaultWriteTempFile;
|
|
1513
|
+
this.testListPath = writeTemp(content);
|
|
1514
|
+
args.push("--test-list", this.testListPath);
|
|
1515
|
+
} else if (tests !== void 0 && tests.length > 0) {
|
|
1516
|
+
const project = sharedProject(tests);
|
|
1517
|
+
if (project !== void 0) args.push("--project", project);
|
|
1518
|
+
for (const d of tests) args.push(descriptorLocation(d));
|
|
1519
|
+
}
|
|
1520
|
+
const resolveLaunch = this.deps.resolveLaunch ?? resolvePlaywrightLaunch;
|
|
1521
|
+
const { cmd, args: launchArgs } = resolveLaunch(ctx.cwd, args);
|
|
1522
|
+
let child;
|
|
1523
|
+
try {
|
|
1524
|
+
child = this.deps.spawn(cmd, launchArgs, { cwd: ctx.cwd, env: buildSpawnEnv(this.deps.port), stdio: "inherit" });
|
|
1525
|
+
} catch (err) {
|
|
1526
|
+
this.cleanupTempFile();
|
|
1527
|
+
throw err;
|
|
1528
|
+
}
|
|
1529
|
+
this.child = child;
|
|
1530
|
+
child.on("exit", (code) => {
|
|
1531
|
+
this.handleChildExit(code);
|
|
1532
|
+
});
|
|
1533
|
+
child.on("error", () => {
|
|
1534
|
+
this.handleChildExit(null);
|
|
1535
|
+
});
|
|
1536
|
+
this.deps.setReportRunning(true);
|
|
1537
|
+
this.deps.setRunFiltered?.(filters.tests !== void 0);
|
|
1538
|
+
this.deps.broadcast({ type: "run-status", data: { running: true } });
|
|
1539
|
+
return { ok: true };
|
|
1540
|
+
}
|
|
1541
|
+
stop() {
|
|
1542
|
+
if (this.child === null) return { ok: false, reason: "not-running" };
|
|
1543
|
+
if (this.sigkillTimer !== null) this.deps.timers.clearTimeout(this.sigkillTimer);
|
|
1544
|
+
this.child.kill("SIGTERM");
|
|
1545
|
+
this.sigkillTimer = this.deps.timers.setTimeout(() => {
|
|
1546
|
+
if (this.child !== null) this.child.kill("SIGKILL");
|
|
1547
|
+
}, STOP_GRACE_MS);
|
|
1548
|
+
return { ok: true };
|
|
1549
|
+
}
|
|
1550
|
+
dispose() {
|
|
1551
|
+
if (this.child === null) return;
|
|
1552
|
+
if (this.sigkillTimer !== null) this.deps.timers.clearTimeout(this.sigkillTimer);
|
|
1553
|
+
this.sigkillTimer = null;
|
|
1554
|
+
this.child.kill("SIGKILL");
|
|
1555
|
+
this.cleanupTempFile();
|
|
1556
|
+
}
|
|
1557
|
+
handleChildExit(code) {
|
|
1558
|
+
if (this.child === null) return;
|
|
1559
|
+
if (this.sigkillTimer !== null) {
|
|
1560
|
+
this.deps.timers.clearTimeout(this.sigkillTimer);
|
|
1561
|
+
this.sigkillTimer = null;
|
|
1562
|
+
}
|
|
1563
|
+
this.child = null;
|
|
1564
|
+
this.cleanupTempFile();
|
|
1565
|
+
if (code !== null && code !== 0) {
|
|
1566
|
+
console.warn(`[RunController] playwright test exited with code ${code}`);
|
|
1567
|
+
}
|
|
1568
|
+
this.deps.setReportRunning(false);
|
|
1569
|
+
this.deps.broadcast({ type: "run-status", data: { running: false } });
|
|
1570
|
+
void this.deps.saveReport?.();
|
|
1571
|
+
}
|
|
1572
|
+
};
|
|
1573
|
+
function createRealSpawn() {
|
|
1574
|
+
return (cmd, args, opts) => {
|
|
1575
|
+
const cp = (0, import_child_process.spawn)(cmd, args, opts);
|
|
1576
|
+
return {
|
|
1577
|
+
on: (event, cb) => cp.on(event, cb),
|
|
1578
|
+
kill: (signal) => {
|
|
1579
|
+
const sig = KNOWN_SIGNALS[signal];
|
|
1580
|
+
if (sig !== void 0) cp.kill(sig);
|
|
1581
|
+
}
|
|
1582
|
+
};
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
function createRealTimers() {
|
|
1586
|
+
const pending = [];
|
|
1587
|
+
return {
|
|
1588
|
+
setTimeout: (fn, ms) => {
|
|
1589
|
+
const id = setTimeout(fn, ms);
|
|
1590
|
+
pending.push(id);
|
|
1591
|
+
return id;
|
|
1592
|
+
},
|
|
1593
|
+
clearTimeout: () => {
|
|
1594
|
+
for (const h of pending.splice(0)) clearTimeout(h);
|
|
1595
|
+
}
|
|
1596
|
+
};
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
// src/server/app.ts
|
|
1600
|
+
var import_meta2 = {};
|
|
1166
1601
|
function createReportData(options) {
|
|
1167
1602
|
return {
|
|
1168
1603
|
isRunning: false,
|
|
@@ -1188,37 +1623,6 @@ async function loadReport(reportPath, reportData) {
|
|
|
1188
1623
|
console.log("No report.json found, using empty state");
|
|
1189
1624
|
}
|
|
1190
1625
|
}
|
|
1191
|
-
async function readOfflineReport(filePath) {
|
|
1192
|
-
try {
|
|
1193
|
-
const raw = await readJsonFile(filePath);
|
|
1194
|
-
if (raw === null) {
|
|
1195
|
-
return null;
|
|
1196
|
-
}
|
|
1197
|
-
const parsed = parseOfflineReport(raw);
|
|
1198
|
-
if (parsed !== null) {
|
|
1199
|
-
console.log(`[Server] Loading offline report: ${filePath}`);
|
|
1200
|
-
return parsed;
|
|
1201
|
-
}
|
|
1202
|
-
} catch {
|
|
1203
|
-
}
|
|
1204
|
-
return null;
|
|
1205
|
-
}
|
|
1206
|
-
async function loadOfflineReports(offlineReportDir, reportData) {
|
|
1207
|
-
const offlineReportPaths = await findOfflineReportPaths(offlineReportDir);
|
|
1208
|
-
if (offlineReportPaths.length === 0) {
|
|
1209
|
-
return;
|
|
1210
|
-
}
|
|
1211
|
-
const limit = (0, import_p_limit.default)(MAX_CONCURRENT_FILE_OPS);
|
|
1212
|
-
const reports = await Promise.all(offlineReportPaths.map((filePath) => limit(() => readOfflineReport(filePath))));
|
|
1213
|
-
const validReports = reports.filter((report) => report !== null);
|
|
1214
|
-
if (validReports.length === 0) {
|
|
1215
|
-
return;
|
|
1216
|
-
}
|
|
1217
|
-
reportData.tests = mergeOfflineReportsIntoTests(reportData.tests, validReports, {
|
|
1218
|
-
screenshotDir: reportData.screenshotDir,
|
|
1219
|
-
screenshotsBaseUrl: "/screenshots/"
|
|
1220
|
-
});
|
|
1221
|
-
}
|
|
1222
1626
|
async function handleParsedWebSocketMessage(ctx, msg) {
|
|
1223
1627
|
switch (msg.type) {
|
|
1224
1628
|
case "test-begin": {
|
|
@@ -1282,19 +1686,19 @@ function createWebSocketMessageHandler(getHandlerContext) {
|
|
|
1282
1686
|
};
|
|
1283
1687
|
}
|
|
1284
1688
|
async function resolveStaticDir(staticDir) {
|
|
1285
|
-
const currentDir = (0,
|
|
1689
|
+
const currentDir = (0, import_path8.dirname)((0, import_url.fileURLToPath)(import_meta2.url));
|
|
1286
1690
|
const candidates = staticDir === void 0 ? [
|
|
1287
1691
|
currentDir,
|
|
1288
|
-
(0,
|
|
1289
|
-
(0,
|
|
1290
|
-
(0,
|
|
1291
|
-
(0,
|
|
1292
|
-
(0,
|
|
1293
|
-
] : [staticDir, (0,
|
|
1692
|
+
(0, import_path8.join)(currentDir, "dist"),
|
|
1693
|
+
(0, import_path8.join)(currentDir, "..", "dist"),
|
|
1694
|
+
(0, import_path8.join)(currentDir, "..", "..", "dist"),
|
|
1695
|
+
(0, import_path8.join)(currentDir, ".."),
|
|
1696
|
+
(0, import_path8.join)(currentDir, "..", "..")
|
|
1697
|
+
] : [staticDir, (0, import_path8.join)(staticDir, "dist")];
|
|
1294
1698
|
const resolvedCandidates = await Promise.all(
|
|
1295
1699
|
candidates.map(async (candidate) => ({
|
|
1296
1700
|
candidate,
|
|
1297
|
-
exists: await fileExists((0,
|
|
1701
|
+
exists: await fileExists((0, import_path8.join)(candidate, "index.html"))
|
|
1298
1702
|
}))
|
|
1299
1703
|
);
|
|
1300
1704
|
const resolved = resolvedCandidates.find(({ exists }) => exists);
|
|
@@ -1305,27 +1709,34 @@ async function resolveStaticDir(staticDir) {
|
|
|
1305
1709
|
}
|
|
1306
1710
|
async function resolveReportPath(reportPath) {
|
|
1307
1711
|
if (await isDirectory(reportPath)) {
|
|
1308
|
-
return { reportFile: (0,
|
|
1712
|
+
return { reportFile: (0, import_path8.join)(reportPath, "report.json"), offlineReportDir: reportPath };
|
|
1309
1713
|
}
|
|
1310
|
-
return { reportFile: reportPath, offlineReportDir: (0,
|
|
1714
|
+
return { reportFile: reportPath, offlineReportDir: (0, import_path8.dirname)(reportPath) };
|
|
1311
1715
|
}
|
|
1312
|
-
function
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1716
|
+
async function seedRunContext(routesContext, options) {
|
|
1717
|
+
if (routesContext.runContext !== void 0) {
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
const configFile = options.playwrightConfig ?? await resolvePlaywrightConfig(process.cwd());
|
|
1721
|
+
if (configFile !== null) {
|
|
1722
|
+
routesContext.runContext = { configFile, cwd: process.cwd() };
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
function createServerRunController(routesContext, wsClients, reportData, port, setRunFiltered, saveReport) {
|
|
1726
|
+
return new RunController({
|
|
1727
|
+
getRunContext: () => routesContext.runContext ?? null,
|
|
1728
|
+
port,
|
|
1729
|
+
broadcast: (message) => {
|
|
1730
|
+
broadcastToBrowsers(wsClients, message);
|
|
1731
|
+
},
|
|
1732
|
+
setReportRunning: (running) => {
|
|
1733
|
+
reportData.isRunning = running;
|
|
1734
|
+
},
|
|
1735
|
+
setRunFiltered,
|
|
1319
1736
|
saveReport,
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
playwrightTestDir: options.playwrightTestDir,
|
|
1324
|
-
playwrightSnapshotDir: options.playwrightSnapshotDir,
|
|
1325
|
-
playwrightSnapshotPathTemplate: options.playwrightSnapshotPathTemplate,
|
|
1326
|
-
playwrightToHaveScreenshotPathTemplate: options.playwrightToHaveScreenshotPathTemplate
|
|
1327
|
-
}
|
|
1328
|
-
};
|
|
1737
|
+
spawn: createRealSpawn(),
|
|
1738
|
+
timers: createRealTimers()
|
|
1739
|
+
});
|
|
1329
1740
|
}
|
|
1330
1741
|
async function createServerApp(options = {}) {
|
|
1331
1742
|
const port = options.port ?? 3e3;
|
|
@@ -1335,31 +1746,49 @@ async function createServerApp(options = {}) {
|
|
|
1335
1746
|
const staticDir = await resolveStaticDir(options.staticDir);
|
|
1336
1747
|
const wsClients = /* @__PURE__ */ new Set();
|
|
1337
1748
|
const currentRunIds = /* @__PURE__ */ new Set();
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1749
|
+
const persistence = createReportPersistence(reportFile, reportData);
|
|
1750
|
+
const routesContext = createRoutesContext(reportData, staticDir, persistence.saveReport, options);
|
|
1751
|
+
await seedRunContext(routesContext, options);
|
|
1752
|
+
let isFilteredRun = false;
|
|
1753
|
+
const runController = createServerRunController(
|
|
1754
|
+
routesContext,
|
|
1755
|
+
wsClients,
|
|
1756
|
+
reportData,
|
|
1757
|
+
port,
|
|
1758
|
+
(filtered) => {
|
|
1759
|
+
isFilteredRun = filtered;
|
|
1760
|
+
},
|
|
1761
|
+
persistence.saveReport
|
|
1762
|
+
);
|
|
1342
1763
|
const getHandlerContext = () => ({
|
|
1343
1764
|
reportData,
|
|
1344
1765
|
wsClients,
|
|
1345
1766
|
currentRunIds,
|
|
1346
|
-
|
|
1767
|
+
isFilteredRun,
|
|
1768
|
+
saveReport: persistence.saveReport,
|
|
1769
|
+
scheduleReportSave: persistence.scheduleReportSave,
|
|
1347
1770
|
approvalRouting: routesContext.approvalRouting,
|
|
1348
|
-
routesContext
|
|
1771
|
+
routesContext,
|
|
1772
|
+
runController
|
|
1349
1773
|
});
|
|
1350
|
-
const handleRequest = (req) => handleHttpRequest(routesContext, req);
|
|
1774
|
+
const handleRequest = (req) => handleHttpRequest(routesContext, req, runController);
|
|
1351
1775
|
const handleWebSocketMessage = createWebSocketMessageHandler(getHandlerContext);
|
|
1352
1776
|
await loadReport(reportFile, reportData);
|
|
1353
|
-
await loadOfflineReports(
|
|
1777
|
+
await loadOfflineReports(reportData, offlineReportDir);
|
|
1354
1778
|
return {
|
|
1355
1779
|
port,
|
|
1356
1780
|
wsClients,
|
|
1357
|
-
close: ()
|
|
1358
|
-
},
|
|
1781
|
+
close: createCloseHandler(persistence, runController),
|
|
1359
1782
|
handleRequest,
|
|
1360
1783
|
handleWebSocketMessage
|
|
1361
1784
|
};
|
|
1362
1785
|
}
|
|
1786
|
+
function createCloseHandler(persistence, runController) {
|
|
1787
|
+
return async () => {
|
|
1788
|
+
await persistence.dispose();
|
|
1789
|
+
runController.dispose();
|
|
1790
|
+
};
|
|
1791
|
+
}
|
|
1363
1792
|
|
|
1364
1793
|
// src/server/bun-adapter.ts
|
|
1365
1794
|
function logWebSocketError(prefix, error) {
|
|
@@ -1557,9 +1986,37 @@ async function startNodeServer(app) {
|
|
|
1557
1986
|
console.log(`Crvy Rprtr started at http://localhost:${app.port}`);
|
|
1558
1987
|
}
|
|
1559
1988
|
|
|
1989
|
+
// src/server/shutdown.ts
|
|
1990
|
+
var EXIT_CODES = {
|
|
1991
|
+
SIGINT: 130,
|
|
1992
|
+
SIGTERM: 143,
|
|
1993
|
+
SIGHUP: 129
|
|
1994
|
+
};
|
|
1995
|
+
function createSignalShutdown(deps) {
|
|
1996
|
+
let shuttingDown = false;
|
|
1997
|
+
return async function shutdown(signal) {
|
|
1998
|
+
if (shuttingDown) {
|
|
1999
|
+
deps.exit(EXIT_CODES[signal] ?? 1);
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
shuttingDown = true;
|
|
2003
|
+
try {
|
|
2004
|
+
await deps.close();
|
|
2005
|
+
} catch {
|
|
2006
|
+
}
|
|
2007
|
+
deps.exit(EXIT_CODES[signal] ?? 1);
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
|
|
1560
2011
|
// src/server.ts
|
|
1561
2012
|
async function startServer(options = {}) {
|
|
1562
2013
|
const app = await createServerApp(options);
|
|
2014
|
+
const shutdown = createSignalShutdown({ close: () => app.close(), exit: (code) => process.exit(code) });
|
|
2015
|
+
const onSignal = (signal) => {
|
|
2016
|
+
void shutdown(signal);
|
|
2017
|
+
};
|
|
2018
|
+
process.on("SIGINT", onSignal);
|
|
2019
|
+
process.on("SIGTERM", onSignal);
|
|
1563
2020
|
if (typeof Bun !== "undefined" && typeof Bun.serve === "function") {
|
|
1564
2021
|
startBunServer(app);
|
|
1565
2022
|
return;
|