@crvy/rprtr 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.4] - 2026-06-19
9
+
10
+ ### Added
11
+
12
+ - **path-utils:** Add cross-platform absolute path helpers
13
+ - **server:** Log diagnostic when /file URL cannot resolve on host OS
14
+
15
+ ### Documentation
16
+
17
+ - **plans:** Cross-OS path handling and stale actual URL on passing rerun
18
+ - Document cross-OS artifact loading limitations
19
+
20
+ ### Fixed
21
+
22
+ - **report-utils:** Route Windows-style absolute paths to /file on any host
23
+ - **server:** Check pre-resolve path for foreign-OS diagnostic
24
+ - **report-state:** Drop stale comparison actual URL on passed reruns
25
+ - **report-state:** Strip stale actual/diff when preserving baseline-only images
26
+
27
+ ### Testing
28
+
29
+ - **path-utils:** Cover forward-slash UNC form
30
+ - **server-routes:** Cover baseline enrichment after failed→passed progression
8
31
  ## [0.1.3] - 2026-06-18
9
32
 
10
33
  ### Fixed
package/README.md CHANGED
@@ -113,6 +113,14 @@ Approval routing uses the same resolver, but it reads its resolver settings from
113
113
 
114
114
  When the server is running, Crvy Rprtr also refreshes the UI after report JSON or screenshot artifacts change on disk.
115
115
 
116
+ ## Cross-OS Artifact Loading
117
+
118
+ Crvy Rprtr stores image URLs exactly as the reporter that produced them saw them. In live mode (server running during the test run), failure artifacts are referenced by absolute path under `/file/<encoded>`; in CI/offline mode, attachments are copied into `screenshots/` and referenced by relative path under `/screenshots/`.
119
+
120
+ If you generate a report on one operating system and then open it on another (for example, downloading a Windows CI runner's `report.json` onto a macOS laptop), absolute-path `/file/...` URLs cannot resolve: the file is not on your filesystem. Crvy Rprtr logs a single diagnostic line per such request and returns 404. The `/screenshots/...` and `/baseline/...` URLs remain portable because they resolve through the server's `screenshotDir` or snapshot resolver.
121
+
122
+ For fully portable artifact loading across operating systems, run the reporter in CI mode (`ci: true`) and ship the `screenshots/` directory alongside the report JSON.
123
+
116
124
  ## Programmatic API
117
125
 
118
126
  ```ts
@@ -11,9 +11,10 @@ import {
11
11
  applyTestEndEvent,
12
12
  createMutableReportState,
13
13
  finalizeRunEvent,
14
+ isForeignAbsolutePath,
14
15
  resolveBaselineTargets,
15
16
  safeParse
16
- } from "./chunk-X2TCDD54.js";
17
+ } from "./chunk-WRMHUY5A.js";
17
18
 
18
19
  // src/server/app.ts
19
20
  import { dirname as dirname3, join as join3 } from "path";
@@ -210,14 +211,21 @@ async function realpathOrNull(path) {
210
211
  }
211
212
  async function handleFile(ctx, req) {
212
213
  const notFound = () => new Response("Not Found", { status: 404 });
214
+ let rawDecoded;
213
215
  let decodedPath;
214
216
  try {
215
- decodedPath = resolve2(decodeURIComponent(new URL(req.url).pathname.slice("/file/".length)));
217
+ rawDecoded = decodeURIComponent(new URL(req.url).pathname.slice("/file/".length));
218
+ decodedPath = resolve2(rawDecoded);
216
219
  } catch {
217
220
  return notFound();
218
221
  }
219
222
  const realTarget = await realpathOrNull(decodedPath);
220
223
  if (realTarget === null) {
224
+ if (isForeignAbsolutePath(rawDecoded, process.platform)) {
225
+ console.warn(
226
+ `[Crvy Rprtr] /file request resolved to a foreign-OS absolute path that cannot be served on ${process.platform}: ${rawDecoded}`
227
+ );
228
+ }
221
229
  return notFound();
222
230
  }
223
231
  const realRoots = (await Promise.all((ctx.artifactRoots ?? []).map((root) => realpathOrNull(resolve2(root))))).filter(
@@ -341,8 +341,26 @@ function safeParse(schema, data) {
341
341
  return null;
342
342
  }
343
343
 
344
+ // src/path-utils.ts
345
+ var posixAbsolutePattern = /^\//u;
346
+ var windowsDrivePattern = /^[A-Za-z]:[\\/]/u;
347
+ var windowsUncPattern = /^[\\/][\\/]/u;
348
+ function isPosixAbsolutePath(p) {
349
+ return posixAbsolutePattern.test(p);
350
+ }
351
+ function isWindowsAbsolutePath(p) {
352
+ return windowsDrivePattern.test(p) || windowsUncPattern.test(p);
353
+ }
354
+ function isAnyAbsolutePath(p) {
355
+ return isPosixAbsolutePath(p) || isWindowsAbsolutePath(p);
356
+ }
357
+ function isForeignAbsolutePath(p, hostPlatform) {
358
+ if (!isAnyAbsolutePath(p)) return false;
359
+ if (hostPlatform === "win32") return !isWindowsAbsolutePath(p);
360
+ return !isPosixAbsolutePath(p);
361
+ }
362
+
344
363
  // src/report-utils.ts
345
- import { isAbsolute } from "path";
346
364
  function normalizeScreenshotsBaseUrl(baseUrl) {
347
365
  return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
348
366
  }
@@ -387,7 +405,7 @@ function attachmentsToImages(attachments, screenshotsBaseUrl = "/screenshots/")
387
405
  const role = match[2];
388
406
  if (baseName === null || baseName === void 0 || role === null || role === void 0) continue;
389
407
  images[baseName] ??= {};
390
- const url = isAbsolute(attachment.path) ? `/file/${encodeURIComponent(attachment.path)}` : `${baseUrl}${attachment.path}`;
408
+ const url = isAnyAbsolutePath(attachment.path) ? `/file/${encodeURIComponent(attachment.path)}` : `${baseUrl}${attachment.path}`;
391
409
  const img = images[baseName];
392
410
  if (img !== null && img !== void 0) {
393
411
  if (role === "actual") img.actual = url;
@@ -422,9 +440,6 @@ function isCurrentArtifact(image) {
422
440
  }
423
441
  function isReusablePassingImage(image) {
424
442
  const source = image.source ?? classifyImage(image);
425
- if (source === "comparison") {
426
- return image.actual !== void 0 && image.diff === void 0;
427
- }
428
443
  return source === "baseline-only";
429
444
  }
430
445
  function hasReusablePassingImages(images) {
@@ -445,8 +460,8 @@ function preservePreviousPassingImages(test, status, images) {
445
460
  return {
446
461
  ...currentImages,
447
462
  [name]: {
448
- ...previousImage,
449
- source: previousImage.source ?? classifyImage(previousImage)
463
+ expect: previousImage.expect,
464
+ source: "baseline-only"
450
465
  }
451
466
  };
452
467
  }, images);
@@ -534,6 +549,7 @@ function finalizeRunEvent(state) {
534
549
  }
535
550
 
536
551
  export {
552
+ isForeignAbsolutePath,
537
553
  createMutableReportState,
538
554
  applyTestBeginEvent,
539
555
  applyTestEndEvent,
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  startServer
4
- } from "./chunk-F7NSIHCV.js";
5
- import "./chunk-X2TCDD54.js";
4
+ } from "./chunk-KLGJSEKV.js";
5
+ import "./chunk-WRMHUY5A.js";
6
6
 
7
7
  // src/cli.ts
8
8
  import { join } from "path";
package/dist/index.css CHANGED
@@ -221,6 +221,9 @@
221
221
  .absolute {
222
222
  position: absolute;
223
223
  }
224
+ .fixed {
225
+ position: fixed;
226
+ }
224
227
  .relative {
225
228
  position: relative;
226
229
  }
@@ -0,0 +1,3 @@
1
+ export declare function isAnyAbsolutePath(p: string): boolean;
2
+ export declare function isForeignAbsolutePath(p: string, hostPlatform: NodeJS.Platform): boolean;
3
+ //# sourceMappingURL=path-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"path-utils.d.ts","sourceRoot":"","sources":["../src/path-utils.ts"],"names":[],"mappings":"AAYA,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,CAAC,QAAQ,GAAG,OAAO,CAIvF"}
@@ -1 +1 @@
1
- {"version":3,"file":"report-state.d.ts","sourceRoot":"","sources":["../src/report-state.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AAChE,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC9D,OAAO,KAAK,EAAU,QAAQ,EAAc,MAAM,YAAY,CAAA;AAE9D,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,OAAO,CAAA;IAClB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;IAC/B,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB,YAAY,EAAE,OAAO,CAAA;IACrB,aAAa,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,iBAAiB,CAAA;IAC7B,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAC3B;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,QAAQ,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,KAAK,sBAAsB,GAAG,WAAW,GAAG;IAC1C,kBAAkB,CAAC,EAAE,SAAS,qBAAqB,EAAE,CAAA;CACtD,CAAA;AA0DD,wBAAgB,wBAAwB,CAAC,aAAa,SAAkB,GAAG,kBAAkB,CAW5F;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,kBAAkB,EAAE,IAAI,EAAE,aAAa,GAAG,QAAQ,CAiB5F;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,kBAAkB,EACzB,IAAI,EAAE,sBAAsB,EAC5B,OAAO,GAAE;IAAE,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAAO,GAC5C,kBAAkB,GAAG,IAAI,CAyC3B;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,kBAAkB,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAa/G"}
1
+ {"version":3,"file":"report-state.d.ts","sourceRoot":"","sources":["../src/report-state.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AAChE,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAC9D,OAAO,KAAK,EAAU,QAAQ,EAAc,MAAM,YAAY,CAAA;AAE9D,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,OAAO,CAAA;IAClB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAA;IAC/B,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB,YAAY,EAAE,OAAO,CAAA;IACrB,aAAa,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,iBAAiB,CAAA;IAC7B,aAAa,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAC3B;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,QAAQ,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,KAAK,sBAAsB,GAAG,WAAW,GAAG;IAC1C,kBAAkB,CAAC,EAAE,SAAS,qBAAqB,EAAE,CAAA;CACtD,CAAA;AAqDD,wBAAgB,wBAAwB,CAAC,aAAa,SAAkB,GAAG,kBAAkB,CAW5F;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,kBAAkB,EAAE,IAAI,EAAE,aAAa,GAAG,QAAQ,CAiB5F;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,kBAAkB,EACzB,IAAI,EAAE,sBAAsB,EAC5B,OAAO,GAAE;IAAE,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAAO,GAC5C,kBAAkB,GAAG,IAAI,CAyC3B;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,kBAAkB,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAa/G"}
@@ -1 +1 @@
1
- {"version":3,"file":"report-utils.d.ts","sourceRoot":"","sources":["../src/report-utils.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AAChE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAMhE,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,CAUzD;AASD,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,SAAS,MAAM,EAAE,EAC9B,kBAAkB,EAAE,SAAS,qBAAqB,EAAE,GAAG,SAAS,GAC/D,SAAS,MAAM,EAAE,CAEnB;AAED,wBAAgB,sBAAsB,CACpC,kBAAkB,EAAE,SAAS,qBAAqB,EAAE,GAAG,SAAS,GAC/D,SAAS,qBAAqB,EAAE,GAAG,SAAS,CAE9C;AAED,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EACvC,WAAW,EAAE,SAAS,MAAM,EAAE,GAC7B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CASjC;AAED,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,UAAU,EAAE,EACzB,kBAAkB,SAAkB,GACnC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAkCjC;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAWrF"}
1
+ {"version":3,"file":"report-utils.d.ts","sourceRoot":"","sources":["../src/report-utils.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AAChE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAMhE,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,CAUzD;AASD,wBAAgB,sBAAsB,CACpC,WAAW,EAAE,SAAS,MAAM,EAAE,EAC9B,kBAAkB,EAAE,SAAS,qBAAqB,EAAE,GAAG,SAAS,GAC/D,SAAS,MAAM,EAAE,CAEnB;AAED,wBAAgB,sBAAsB,CACpC,kBAAkB,EAAE,SAAS,qBAAqB,EAAE,GAAG,SAAS,GAC/D,SAAS,qBAAqB,EAAE,GAAG,SAAS,CAE9C;AAED,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EACvC,WAAW,EAAE,SAAS,MAAM,EAAE,GAC7B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CASjC;AAED,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,UAAU,EAAE,EACzB,kBAAkB,SAAkB,GACnC,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAkCjC;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAWrF"}
package/dist/reporter.cjs CHANGED
@@ -36,7 +36,7 @@ __export(reporter_exports, {
36
36
  module.exports = __toCommonJS(reporter_exports);
37
37
  var import_fs = require("fs");
38
38
  var import_promises3 = require("fs/promises");
39
- var import_path5 = require("path");
39
+ var import_path4 = require("path");
40
40
  var import_p_limit2 = __toESM(require("p-limit"), 1);
41
41
 
42
42
  // src/ci.ts
@@ -56,16 +56,29 @@ var logError = (...args) => {
56
56
 
57
57
  // src/reporter-artifact-ops.ts
58
58
  var import_promises2 = require("fs/promises");
59
- var import_path3 = require("path");
59
+ var import_path2 = require("path");
60
60
  var import_p_limit = __toESM(require("p-limit"), 1);
61
61
 
62
62
  // src/report-artifact.ts
63
63
  var import_promises = require("fs/promises");
64
- var import_path2 = require("path");
64
+ var import_path = require("path");
65
65
  var import_url = require("url");
66
66
 
67
+ // src/path-utils.ts
68
+ var posixAbsolutePattern = /^\//u;
69
+ var windowsDrivePattern = /^[A-Za-z]:[\\/]/u;
70
+ var windowsUncPattern = /^[\\/][\\/]/u;
71
+ function isPosixAbsolutePath(p) {
72
+ return posixAbsolutePattern.test(p);
73
+ }
74
+ function isWindowsAbsolutePath(p) {
75
+ return windowsDrivePattern.test(p) || windowsUncPattern.test(p);
76
+ }
77
+ function isAnyAbsolutePath(p) {
78
+ return isPosixAbsolutePath(p) || isWindowsAbsolutePath(p);
79
+ }
80
+
67
81
  // src/report-utils.ts
68
- var import_path = require("path");
69
82
  function normalizeScreenshotsBaseUrl(baseUrl) {
70
83
  return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
71
84
  }
@@ -110,7 +123,7 @@ function attachmentsToImages(attachments, screenshotsBaseUrl = "/screenshots/")
110
123
  const role = match[2];
111
124
  if (baseName === null || baseName === void 0 || role === null || role === void 0) continue;
112
125
  images[baseName] ??= {};
113
- const url = (0, import_path.isAbsolute)(attachment.path) ? `/file/${encodeURIComponent(attachment.path)}` : `${baseUrl}${attachment.path}`;
126
+ const url = isAnyAbsolutePath(attachment.path) ? `/file/${encodeURIComponent(attachment.path)}` : `${baseUrl}${attachment.path}`;
114
127
  const img = images[baseName];
115
128
  if (img !== null && img !== void 0) {
116
129
  if (role === "actual") img.actual = url;
@@ -145,9 +158,6 @@ function isCurrentArtifact(image) {
145
158
  }
146
159
  function isReusablePassingImage(image) {
147
160
  const source = image.source ?? classifyImage(image);
148
- if (source === "comparison") {
149
- return image.actual !== void 0 && image.diff === void 0;
150
- }
151
161
  return source === "baseline-only";
152
162
  }
153
163
  function hasReusablePassingImages(images) {
@@ -168,8 +178,8 @@ function preservePreviousPassingImages(test, status, images) {
168
178
  return {
169
179
  ...currentImages,
170
180
  [name]: {
171
- ...previousImage,
172
- source: previousImage.source ?? classifyImage(previousImage)
181
+ expect: previousImage.expect,
182
+ source: "baseline-only"
173
183
  }
174
184
  };
175
185
  }, images);
@@ -433,7 +443,7 @@ var import_meta = { url: require("url").pathToFileURL(__filename).href };
433
443
  var DEFAULT_REPORT_HTML_PATH = "./crvy-rprtr.html";
434
444
  var STATIC_ARTIFACT_APPROVAL_MESSAGE = "This artifact is read-only. Open it with the Crvy Rprtr server to approve screenshots.";
435
445
  function toBrowserPath(pathValue) {
436
- const normalized = pathValue.split(import_path2.sep).join("/");
446
+ const normalized = pathValue.split(import_path.sep).join("/");
437
447
  if (normalized === "") return ".";
438
448
  if (normalized.startsWith(".")) return normalized;
439
449
  return `./${normalized}`;
@@ -469,11 +479,11 @@ function renderArtifactHtml(template, bootstrapData, stylesheet, script) {
469
479
  );
470
480
  }
471
481
  async function getPackagedAssetPath(fileName) {
472
- const currentDir = (0, import_path2.dirname)((0, import_url.fileURLToPath)(import_meta.url));
473
- const candidates = [currentDir, (0, import_path2.resolve)(currentDir, "../dist")];
482
+ const currentDir = (0, import_path.dirname)((0, import_url.fileURLToPath)(import_meta.url));
483
+ const candidates = [currentDir, (0, import_path.resolve)(currentDir, "../dist")];
474
484
  const resolvedCandidates = await Promise.all(
475
485
  candidates.map(async (candidate) => {
476
- const assetPath = (0, import_path2.resolve)(candidate, fileName);
486
+ const assetPath = (0, import_path.resolve)(candidate, fileName);
477
487
  try {
478
488
  await (0, import_promises.access)(assetPath);
479
489
  return assetPath;
@@ -491,7 +501,7 @@ async function getPackagedAssetPath(fileName) {
491
501
  );
492
502
  }
493
503
  function buildStaticBootstrapData(events, screenshotDir, htmlPath) {
494
- const screenshotBaseUrl = toBrowserDirPath((0, import_path2.relative)((0, import_path2.dirname)(htmlPath), (0, import_path2.resolve)(screenshotDir)));
504
+ const screenshotBaseUrl = toBrowserDirPath((0, import_path.relative)((0, import_path.dirname)(htmlPath), (0, import_path.resolve)(screenshotDir)));
495
505
  const state = createMutableReportState(screenshotDir);
496
506
  for (const event of events) {
497
507
  switch (event.type) {
@@ -530,13 +540,13 @@ function buildStaticBootstrapData(events, screenshotDir, htmlPath) {
530
540
  return parsedBootstrapData;
531
541
  }
532
542
  async function writeReportArtifact(options) {
533
- const reportHtmlPath = (0, import_path2.resolve)(options.reportHtmlPath ?? DEFAULT_REPORT_HTML_PATH);
543
+ const reportHtmlPath = (0, import_path.resolve)(options.reportHtmlPath ?? DEFAULT_REPORT_HTML_PATH);
534
544
  const [indexHtmlPath, indexJsPath, indexCssPath] = await Promise.all([
535
545
  getPackagedAssetPath("index.html"),
536
546
  getPackagedAssetPath("index.js"),
537
547
  getPackagedAssetPath("index.css")
538
548
  ]);
539
- await (0, import_promises.mkdir)((0, import_path2.dirname)(reportHtmlPath), { recursive: true });
549
+ await (0, import_promises.mkdir)((0, import_path.dirname)(reportHtmlPath), { recursive: true });
540
550
  const [template, script, stylesheet] = await Promise.all([
541
551
  (0, import_promises.readFile)(indexHtmlPath, "utf8"),
542
552
  (0, import_promises.readFile)(indexJsPath, "utf8"),
@@ -569,9 +579,9 @@ function sanitizeId(id) {
569
579
  async function copyResolvedBaseline(safeTestId, testScreenshotDir, target, savedAttachments) {
570
580
  const attachmentName = `${target.attachmentBaseName}-expected.png`;
571
581
  const artifactPath = safeArtifactPath(attachmentName);
572
- const destPath = (0, import_path3.join)(testScreenshotDir, artifactPath);
582
+ const destPath = (0, import_path2.join)(testScreenshotDir, artifactPath);
573
583
  try {
574
- await (0, import_promises2.mkdir)((0, import_path3.dirname)(destPath), { recursive: true });
584
+ await (0, import_promises2.mkdir)((0, import_path2.dirname)(destPath), { recursive: true });
575
585
  await (0, import_promises2.copyFile)(target.snapshotPath, destPath);
576
586
  savedAttachments.push({ name: attachmentName, path: `${safeTestId}/${artifactPath}`, contentType: "image/png" });
577
587
  log(`[CrvyRprtr] Attached baseline: ${target.snapshotPath}`);
@@ -581,7 +591,7 @@ async function copyResolvedBaseline(safeTestId, testScreenshotDir, target, saved
581
591
  async function saveAttachments(screenshotDir, testId, result) {
582
592
  const savedAttachments = [];
583
593
  const safeTestId = sanitizeId(testId);
584
- const testScreenshotDir = (0, import_path3.join)(screenshotDir, safeTestId);
594
+ const testScreenshotDir = (0, import_path2.join)(screenshotDir, safeTestId);
585
595
  const limit = (0, import_p_limit.default)(MAX_CONCURRENT_FILE_OPS);
586
596
  await Promise.all(
587
597
  result.attachments.filter(
@@ -590,8 +600,8 @@ async function saveAttachments(screenshotDir, testId, result) {
590
600
  (attachment) => limit(async () => {
591
601
  try {
592
602
  const artifactPath = safeArtifactPath(attachment.name);
593
- const destPath = (0, import_path3.join)(testScreenshotDir, artifactPath);
594
- await (0, import_promises2.mkdir)((0, import_path3.dirname)(destPath), { recursive: true });
603
+ const destPath = (0, import_path2.join)(testScreenshotDir, artifactPath);
604
+ await (0, import_promises2.mkdir)((0, import_path2.dirname)(destPath), { recursive: true });
595
605
  await (0, import_promises2.copyFile)(attachment.path, destPath);
596
606
  savedAttachments.push({
597
607
  name: attachment.name,
@@ -721,7 +731,7 @@ function extractScreenshotDeclarations(steps) {
721
731
 
722
732
  // src/snapshot-path-resolver.ts
723
733
  var import_crypto = require("crypto");
724
- var import_path4 = require("path");
734
+ var import_path3 = require("path");
725
735
  var DEFAULT_SCREENSHOT_TEMPLATE = "{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}";
726
736
  var WINDOWS_FILESYSTEM_FRIENDLY_LENGTH = 60;
727
737
  function isUnsafeFilePathCharacter(character) {
@@ -756,16 +766,16 @@ function trimLongString(value, length = WINDOWS_FILESYSTEM_FRIENDLY_LENGTH) {
756
766
  const end = length - middle.length - start;
757
767
  return value.slice(0, start) + middle + value.slice(-end);
758
768
  }
759
- function sanitizeFilePathBeforeExtension(filePath, extension = (0, import_path4.extname)(filePath)) {
769
+ function sanitizeFilePathBeforeExtension(filePath, extension = (0, import_path3.extname)(filePath)) {
760
770
  const base = filePath.slice(0, filePath.length - extension.length);
761
771
  return sanitizeForFilePath(base) + extension;
762
772
  }
763
773
  function addSuffixToFilePath(filePath, suffix) {
764
- const extension = (0, import_path4.extname)(filePath);
774
+ const extension = (0, import_path3.extname)(filePath);
765
775
  return filePath.slice(0, filePath.length - extension.length) + suffix + extension;
766
776
  }
767
777
  function normalizedSnapshotDir(config) {
768
- return (0, import_path4.resolve)(config.configDir, config.snapshotDir);
778
+ return (0, import_path3.resolve)(config.configDir, config.snapshotDir);
769
779
  }
770
780
  function templateValue(template, token, value) {
771
781
  return template.replace(
@@ -775,8 +785,8 @@ function templateValue(template, token, value) {
775
785
  }
776
786
  function applyTemplate(input, nameArgument, extension) {
777
787
  const template = input.config.toHaveScreenshotPathTemplate ?? input.config.snapshotPathTemplate ?? DEFAULT_SCREENSHOT_TEMPLATE;
778
- const relativeTestFilePath = (0, import_path4.relative)(input.config.testDir, input.testFile);
779
- const parsed = (0, import_path4.parse)(relativeTestFilePath);
788
+ const relativeTestFilePath = (0, import_path3.relative)(input.config.testDir, input.testFile);
789
+ const parsed = (0, import_path3.parse)(relativeTestFilePath);
780
790
  const tokens = [
781
791
  ["testDir", input.config.testDir],
782
792
  ["snapshotDir", normalizedSnapshotDir(input.config)],
@@ -794,16 +804,16 @@ function applyTemplate(input, nameArgument, extension) {
794
804
  (currentTemplate, [token, value]) => templateValue(currentTemplate, token, value),
795
805
  template
796
806
  );
797
- return (0, import_path4.resolve)(input.config.configDir, snapshotPath);
807
+ return (0, import_path3.resolve)(input.config.configDir, snapshotPath);
798
808
  }
799
- function removeExtension(filePath, extension = (0, import_path4.extname)(filePath)) {
809
+ function removeExtension(filePath, extension = (0, import_path3.extname)(filePath)) {
800
810
  return filePath.slice(0, filePath.length - extension.length);
801
811
  }
802
812
  function snapshotNameParts(declaredName) {
803
- const extension = (0, import_path4.extname)(declaredName) || ".png";
813
+ const extension = (0, import_path3.extname)(declaredName) || ".png";
804
814
  return {
805
815
  extension,
806
- filePath: (0, import_path4.extname)(declaredName) === "" ? `${declaredName}${extension}` : declaredName
816
+ filePath: (0, import_path3.extname)(declaredName) === "" ? `${declaredName}${extension}` : declaredName
807
817
  };
808
818
  }
809
819
  function filePathForOccurrence(filePath, occurrenceIndex) {
@@ -827,7 +837,7 @@ function resolveStringCallTarget(input, declaration) {
827
837
  function resolveArrayCallTarget(input, declaration) {
828
838
  const { extension, filePath } = snapshotNameParts(declaration.declaredName);
829
839
  const occurrenceFilePath = filePathForOccurrence(filePath, declaration.occurrenceIndex);
830
- const nameArgument = (0, import_path4.join)((0, import_path4.dirname)(occurrenceFilePath), (0, import_path4.basename)(occurrenceFilePath, extension));
840
+ const nameArgument = (0, import_path3.join)((0, import_path3.dirname)(occurrenceFilePath), (0, import_path3.basename)(occurrenceFilePath, extension));
831
841
  return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
832
842
  }
833
843
  function resolveNamedTarget(input, declaration) {
@@ -869,7 +879,7 @@ function resolveTarget(input, declaration) {
869
879
  case "unnamed": {
870
880
  const anonymousFileName = anonymousName(input.reporterTitlePath, declaration.occurrenceIndex);
871
881
  const extension = ".png";
872
- const nameArgument = (0, import_path4.join)((0, import_path4.dirname)(anonymousFileName), (0, import_path4.basename)(anonymousFileName, extension));
882
+ const nameArgument = (0, import_path3.join)((0, import_path3.dirname)(anonymousFileName), (0, import_path3.basename)(anonymousFileName, extension));
873
883
  return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
874
884
  }
875
885
  }
@@ -921,7 +931,7 @@ var CrvyRprtr = class {
921
931
  if (this.ci) this.isOfflineMode = true;
922
932
  }
923
933
  async onBegin(config, suite) {
924
- this.configDir = config.configFile === void 0 ? config.rootDir : (0, import_path5.dirname)(config.configFile);
934
+ this.configDir = config.configFile === void 0 ? config.rootDir : (0, import_path4.dirname)(config.configFile);
925
935
  log(`[CrvyRprtr] Starting run with ${suite.allTests().length} tests`);
926
936
  await (0, import_promises3.mkdir)(this.screenshotDir, { recursive: true });
927
937
  if (!this.ci) {
@@ -1070,7 +1080,7 @@ var CrvyRprtr = class {
1070
1080
  async copyBaselinesForTargets(testId, status, resolvedTargets, savedAttachments) {
1071
1081
  if (status !== "passed" || resolvedTargets.length === 0) return;
1072
1082
  const safeTestId = sanitizeId(testId);
1073
- const testScreenshotDir = (0, import_path5.join)(this.screenshotDir, safeTestId);
1083
+ const testScreenshotDir = (0, import_path4.join)(this.screenshotDir, safeTestId);
1074
1084
  const limit = (0, import_p_limit2.default)(5);
1075
1085
  await Promise.all(
1076
1086
  resolvedTargets.map(
package/dist/reporter.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  resolveBaselineTargets,
10
10
  safeParse,
11
11
  withResolvedVisualNames
12
- } from "./chunk-X2TCDD54.js";
12
+ } from "./chunk-WRMHUY5A.js";
13
13
 
14
14
  // src/reporter.ts
15
15
  import { existsSync } from "fs";
@@ -1 +1 @@
1
- {"version":3,"file":"artifact-routes.d.ts","sourceRoot":"","sources":["../../src/server/artifact-routes.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAE3C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAWhD,wBAAsB,UAAU,CAAC,GAAG,EAAE,aAAa,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAgCpF;AAYD,MAAM,MAAM,eAAe,GAAG,aAAa,CAAC,iBAAiB,CAAC,CAAA;AAE9D,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,eAAe,EACxB,IAAI,EAAE,QAAQ,EACd,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GAChB,MAAM,GAAG,IAAI,CAyBf;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,aAAa,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAmCxF;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,aAAa,EAAE,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,IAAI,CAQhH"}
1
+ {"version":3,"file":"artifact-routes.d.ts","sourceRoot":"","sources":["../../src/server/artifact-routes.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAE3C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAWhD,wBAAsB,UAAU,CAAC,GAAG,EAAE,aAAa,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAyCpF;AAYD,MAAM,MAAM,eAAe,GAAG,aAAa,CAAC,iBAAiB,CAAC,CAAA;AAE9D,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,eAAe,EACxB,IAAI,EAAE,QAAQ,EACd,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GAChB,MAAM,GAAG,IAAI,CAyBf;AAED,wBAAsB,cAAc,CAAC,GAAG,EAAE,aAAa,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAmCxF;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,aAAa,EAAE,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,GAAG,IAAI,CAQhH"}
package/dist/server.cjs CHANGED
@@ -35,16 +35,34 @@ __export(server_exports, {
35
35
  module.exports = __toCommonJS(server_exports);
36
36
 
37
37
  // src/server/app.ts
38
- var import_path8 = require("path");
38
+ var import_path7 = require("path");
39
39
  var import_url = require("url");
40
40
  var import_p_limit = __toESM(require("p-limit"), 1);
41
41
 
42
42
  // src/offline-reports.ts
43
43
  var import_promises = require("fs/promises");
44
- var import_path2 = require("path");
44
+ var import_path = require("path");
45
+
46
+ // src/path-utils.ts
47
+ var posixAbsolutePattern = /^\//u;
48
+ var windowsDrivePattern = /^[A-Za-z]:[\\/]/u;
49
+ var windowsUncPattern = /^[\\/][\\/]/u;
50
+ function isPosixAbsolutePath(p) {
51
+ return posixAbsolutePattern.test(p);
52
+ }
53
+ function isWindowsAbsolutePath(p) {
54
+ return windowsDrivePattern.test(p) || windowsUncPattern.test(p);
55
+ }
56
+ function isAnyAbsolutePath(p) {
57
+ return isPosixAbsolutePath(p) || isWindowsAbsolutePath(p);
58
+ }
59
+ function isForeignAbsolutePath(p, hostPlatform) {
60
+ if (!isAnyAbsolutePath(p)) return false;
61
+ if (hostPlatform === "win32") return !isWindowsAbsolutePath(p);
62
+ return !isPosixAbsolutePath(p);
63
+ }
45
64
 
46
65
  // src/report-utils.ts
47
- var import_path = require("path");
48
66
  function normalizeScreenshotsBaseUrl(baseUrl) {
49
67
  return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
50
68
  }
@@ -89,7 +107,7 @@ function attachmentsToImages(attachments, screenshotsBaseUrl = "/screenshots/")
89
107
  const role = match[2];
90
108
  if (baseName === null || baseName === void 0 || role === null || role === void 0) continue;
91
109
  images[baseName] ??= {};
92
- const url = (0, import_path.isAbsolute)(attachment.path) ? `/file/${encodeURIComponent(attachment.path)}` : `${baseUrl}${attachment.path}`;
110
+ const url = isAnyAbsolutePath(attachment.path) ? `/file/${encodeURIComponent(attachment.path)}` : `${baseUrl}${attachment.path}`;
93
111
  const img = images[baseName];
94
112
  if (img !== null && img !== void 0) {
95
113
  if (role === "actual") img.actual = url;
@@ -124,9 +142,6 @@ function isCurrentArtifact(image) {
124
142
  }
125
143
  function isReusablePassingImage(image) {
126
144
  const source = image.source ?? classifyImage(image);
127
- if (source === "comparison") {
128
- return image.actual !== void 0 && image.diff === void 0;
129
- }
130
145
  return source === "baseline-only";
131
146
  }
132
147
  function hasReusablePassingImages(images) {
@@ -147,8 +162,8 @@ function preservePreviousPassingImages(test, status, images) {
147
162
  return {
148
163
  ...currentImages,
149
164
  [name]: {
150
- ...previousImage,
151
- source: previousImage.source ?? classifyImage(previousImage)
165
+ expect: previousImage.expect,
166
+ source: "baseline-only"
152
167
  }
153
168
  };
154
169
  }, images);
@@ -412,7 +427,7 @@ var OFFLINE_REPORT_FILE_PATTERN = /^crvy-rprtr(?:-\d+)?\.json$/;
412
427
  async function findOfflineReportPaths(searchDir) {
413
428
  try {
414
429
  const entries = await (0, import_promises.readdir)(searchDir);
415
- return entries.filter((entry) => OFFLINE_REPORT_FILE_PATTERN.test(entry)).sort((left, right) => left.localeCompare(right)).map((entry) => (0, import_path2.join)(searchDir, entry));
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));
416
431
  } catch (error) {
417
432
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
418
433
  return [];
@@ -464,7 +479,7 @@ function mergeOfflineReportsIntoTests(existingTests, offlineReports, options = {
464
479
 
465
480
  // src/server/file-utils.ts
466
481
  var import_promises2 = require("fs/promises");
467
- var import_path3 = require("path");
482
+ var import_path2 = require("path");
468
483
  function isFileNotFound(error) {
469
484
  return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
470
485
  }
@@ -505,16 +520,16 @@ async function readJsonFile(filePath) {
505
520
  }
506
521
  }
507
522
  async function writeJsonFile(filePath, value) {
508
- await (0, import_promises2.mkdir)((0, import_path3.dirname)(filePath), { recursive: true });
523
+ await (0, import_promises2.mkdir)((0, import_path2.dirname)(filePath), { recursive: true });
509
524
  await (0, import_promises2.writeFile)(filePath, `${JSON.stringify(value, null, 2)}
510
525
  `);
511
526
  }
512
527
  async function copyFilePortable(sourcePath, destinationPath) {
513
- await (0, import_promises2.mkdir)((0, import_path3.dirname)(destinationPath), { recursive: true });
528
+ await (0, import_promises2.mkdir)((0, import_path2.dirname)(destinationPath), { recursive: true });
514
529
  await (0, import_promises2.copyFile)(sourcePath, destinationPath);
515
530
  }
516
531
  function inferContentType(filePath) {
517
- switch ((0, import_path3.extname)(filePath).toLowerCase()) {
532
+ switch ((0, import_path2.extname)(filePath).toLowerCase()) {
518
533
  case ".css":
519
534
  return "text/css";
520
535
  case ".gif":
@@ -563,11 +578,11 @@ var import_fs2 = require("fs");
563
578
  // src/server/artifact-routes.ts
564
579
  var import_fs = require("fs");
565
580
  var import_promises3 = require("fs/promises");
566
- var import_path6 = require("path");
581
+ var import_path5 = require("path");
567
582
 
568
583
  // src/snapshot-path-resolver.ts
569
584
  var import_crypto = require("crypto");
570
- var import_path4 = require("path");
585
+ var import_path3 = require("path");
571
586
  var DEFAULT_SCREENSHOT_TEMPLATE = "{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}";
572
587
  var WINDOWS_FILESYSTEM_FRIENDLY_LENGTH = 60;
573
588
  function isUnsafeFilePathCharacter(character) {
@@ -602,16 +617,16 @@ function trimLongString(value, length = WINDOWS_FILESYSTEM_FRIENDLY_LENGTH) {
602
617
  const end = length - middle.length - start;
603
618
  return value.slice(0, start) + middle + value.slice(-end);
604
619
  }
605
- function sanitizeFilePathBeforeExtension(filePath, extension = (0, import_path4.extname)(filePath)) {
620
+ function sanitizeFilePathBeforeExtension(filePath, extension = (0, import_path3.extname)(filePath)) {
606
621
  const base = filePath.slice(0, filePath.length - extension.length);
607
622
  return sanitizeForFilePath(base) + extension;
608
623
  }
609
624
  function addSuffixToFilePath(filePath, suffix) {
610
- const extension = (0, import_path4.extname)(filePath);
625
+ const extension = (0, import_path3.extname)(filePath);
611
626
  return filePath.slice(0, filePath.length - extension.length) + suffix + extension;
612
627
  }
613
628
  function normalizedSnapshotDir(config) {
614
- return (0, import_path4.resolve)(config.configDir, config.snapshotDir);
629
+ return (0, import_path3.resolve)(config.configDir, config.snapshotDir);
615
630
  }
616
631
  function templateValue(template, token, value) {
617
632
  return template.replace(
@@ -621,8 +636,8 @@ function templateValue(template, token, value) {
621
636
  }
622
637
  function applyTemplate(input, nameArgument, extension) {
623
638
  const template = input.config.toHaveScreenshotPathTemplate ?? input.config.snapshotPathTemplate ?? DEFAULT_SCREENSHOT_TEMPLATE;
624
- const relativeTestFilePath = (0, import_path4.relative)(input.config.testDir, input.testFile);
625
- const parsed = (0, import_path4.parse)(relativeTestFilePath);
639
+ const relativeTestFilePath = (0, import_path3.relative)(input.config.testDir, input.testFile);
640
+ const parsed = (0, import_path3.parse)(relativeTestFilePath);
626
641
  const tokens = [
627
642
  ["testDir", input.config.testDir],
628
643
  ["snapshotDir", normalizedSnapshotDir(input.config)],
@@ -640,16 +655,16 @@ function applyTemplate(input, nameArgument, extension) {
640
655
  (currentTemplate, [token, value]) => templateValue(currentTemplate, token, value),
641
656
  template
642
657
  );
643
- return (0, import_path4.resolve)(input.config.configDir, snapshotPath);
658
+ return (0, import_path3.resolve)(input.config.configDir, snapshotPath);
644
659
  }
645
- function removeExtension(filePath, extension = (0, import_path4.extname)(filePath)) {
660
+ function removeExtension(filePath, extension = (0, import_path3.extname)(filePath)) {
646
661
  return filePath.slice(0, filePath.length - extension.length);
647
662
  }
648
663
  function snapshotNameParts(declaredName) {
649
- const extension = (0, import_path4.extname)(declaredName) || ".png";
664
+ const extension = (0, import_path3.extname)(declaredName) || ".png";
650
665
  return {
651
666
  extension,
652
- filePath: (0, import_path4.extname)(declaredName) === "" ? `${declaredName}${extension}` : declaredName
667
+ filePath: (0, import_path3.extname)(declaredName) === "" ? `${declaredName}${extension}` : declaredName
653
668
  };
654
669
  }
655
670
  function filePathForOccurrence(filePath, occurrenceIndex) {
@@ -673,7 +688,7 @@ function resolveStringCallTarget(input, declaration) {
673
688
  function resolveArrayCallTarget(input, declaration) {
674
689
  const { extension, filePath } = snapshotNameParts(declaration.declaredName);
675
690
  const occurrenceFilePath = filePathForOccurrence(filePath, declaration.occurrenceIndex);
676
- const nameArgument = (0, import_path4.join)((0, import_path4.dirname)(occurrenceFilePath), (0, import_path4.basename)(occurrenceFilePath, extension));
691
+ const nameArgument = (0, import_path3.join)((0, import_path3.dirname)(occurrenceFilePath), (0, import_path3.basename)(occurrenceFilePath, extension));
677
692
  return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
678
693
  }
679
694
  function resolveNamedTarget(input, declaration) {
@@ -711,7 +726,7 @@ function resolveTarget(input, declaration) {
711
726
  case "unnamed": {
712
727
  const anonymousFileName = anonymousName(input.reporterTitlePath, declaration.occurrenceIndex);
713
728
  const extension = ".png";
714
- const nameArgument = (0, import_path4.join)((0, import_path4.dirname)(anonymousFileName), (0, import_path4.basename)(anonymousFileName, extension));
729
+ const nameArgument = (0, import_path3.join)((0, import_path3.dirname)(anonymousFileName), (0, import_path3.basename)(anonymousFileName, extension));
715
730
  return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
716
731
  }
717
732
  }
@@ -724,7 +739,7 @@ function resolveBaselineTargets(input) {
724
739
  }
725
740
 
726
741
  // src/server/utils.ts
727
- var import_path5 = require("path");
742
+ var import_path4 = require("path");
728
743
  var LIVE_UPDATES_WEBSOCKET_PATH = "/";
729
744
  function broadcastToBrowsers(wsClients, msg) {
730
745
  const payload = JSON.stringify(msg);
@@ -736,10 +751,10 @@ function isWebSocketUpgradeRequest(req) {
736
751
  return new URL(req.url).pathname === LIVE_UPDATES_WEBSOCKET_PATH && req.headers.get("upgrade")?.toLowerCase() === "websocket";
737
752
  }
738
753
  function isPathWithinRoots(target, roots) {
739
- const resolvedTarget = (0, import_path5.resolve)(target);
754
+ const resolvedTarget = (0, import_path4.resolve)(target);
740
755
  return roots.some((root) => {
741
- const rel = (0, import_path5.relative)((0, import_path5.resolve)(root), resolvedTarget);
742
- return rel === "" || !rel.startsWith(`..${import_path5.sep}`) && rel !== ".." && !(0, import_path5.isAbsolute)(rel);
756
+ const rel = (0, import_path4.relative)((0, import_path4.resolve)(root), resolvedTarget);
757
+ return rel === "" || !rel.startsWith(`..${import_path4.sep}`) && rel !== ".." && !(0, import_path4.isAbsolute)(rel);
743
758
  });
744
759
  }
745
760
 
@@ -753,17 +768,24 @@ async function realpathOrNull(path) {
753
768
  }
754
769
  async function handleFile(ctx, req) {
755
770
  const notFound = () => new Response("Not Found", { status: 404 });
771
+ let rawDecoded;
756
772
  let decodedPath;
757
773
  try {
758
- decodedPath = (0, import_path6.resolve)(decodeURIComponent(new URL(req.url).pathname.slice("/file/".length)));
774
+ rawDecoded = decodeURIComponent(new URL(req.url).pathname.slice("/file/".length));
775
+ decodedPath = (0, import_path5.resolve)(rawDecoded);
759
776
  } catch {
760
777
  return notFound();
761
778
  }
762
779
  const realTarget = await realpathOrNull(decodedPath);
763
780
  if (realTarget === null) {
781
+ if (isForeignAbsolutePath(rawDecoded, process.platform)) {
782
+ console.warn(
783
+ `[Crvy Rprtr] /file request resolved to a foreign-OS absolute path that cannot be served on ${process.platform}: ${rawDecoded}`
784
+ );
785
+ }
764
786
  return notFound();
765
787
  }
766
- const realRoots = (await Promise.all((ctx.artifactRoots ?? []).map((root) => realpathOrNull((0, import_path6.resolve)(root))))).filter(
788
+ const realRoots = (await Promise.all((ctx.artifactRoots ?? []).map((root) => realpathOrNull((0, import_path5.resolve)(root))))).filter(
767
789
  (root) => root !== null
768
790
  );
769
791
  if (!isPathWithinRoots(realTarget, realRoots)) {
@@ -793,8 +815,8 @@ function resolveBaselineSnapshotPath(routing, test, retry, imageName) {
793
815
  declarations: [declaration],
794
816
  config: {
795
817
  configDir: routing.configDir,
796
- testDir: routing.playwrightTestDir ?? (0, import_path6.dirname)(testFile),
797
- snapshotDir: routing.playwrightSnapshotDir ?? (0, import_path6.dirname)(testFile),
818
+ testDir: routing.playwrightTestDir ?? (0, import_path5.dirname)(testFile),
819
+ snapshotDir: routing.playwrightSnapshotDir ?? (0, import_path5.dirname)(testFile),
798
820
  projectName: test.projectName ?? test.browser,
799
821
  snapshotSuffix: process.platform,
800
822
  snapshotPathTemplate: routing.playwrightSnapshotPathTemplate,
@@ -953,9 +975,9 @@ function handleRegister(ctx, data) {
953
975
  }
954
976
 
955
977
  // src/server/routes.ts
956
- var import_path7 = require("path");
978
+ var import_path6 = require("path");
957
979
  async function handleRoot(ctx) {
958
- const html = await respondWithFile((0, import_path7.join)(ctx.staticDir, "index.html"), "text/html");
980
+ const html = await respondWithFile((0, import_path6.join)(ctx.staticDir, "index.html"), "text/html");
959
981
  return html ?? new Response("Not Found", { status: 404 });
960
982
  }
961
983
  async function handleAppCss() {
@@ -975,7 +997,7 @@ function handleApiReport(ctx) {
975
997
  var APPROVAL_TARGET_ERROR = "Could not resolve approval target";
976
998
  function actualPathFromUrl(ctx, actualUrl) {
977
999
  if (actualUrl.startsWith("/screenshots/")) {
978
- return (0, import_path7.join)(ctx.reportData.screenshotDir, actualUrl.slice("/screenshots/".length));
1000
+ return (0, import_path6.join)(ctx.reportData.screenshotDir, actualUrl.slice("/screenshots/".length));
979
1001
  }
980
1002
  if (actualUrl.startsWith("/file/")) {
981
1003
  return decodeURIComponent(actualUrl.slice("/file/".length));
@@ -1097,7 +1119,7 @@ async function handleScreenshots(ctx, req) {
1097
1119
  }
1098
1120
  async function handleDist(ctx, req) {
1099
1121
  const path = new URL(req.url).pathname.slice("/dist/".length);
1100
- const filePath = (0, import_path7.join)(ctx.staticDir, path);
1122
+ const filePath = (0, import_path6.join)(ctx.staticDir, path);
1101
1123
  const contentType = filePath.endsWith(".css") ? "text/css" : filePath.endsWith(".js") ? "application/javascript" : filePath.endsWith(".svelte") ? "text/plain" : "application/octet-stream";
1102
1124
  const file = await respondWithFile(filePath, contentType);
1103
1125
  return file ?? new Response("Not Found", { status: 404 });
@@ -1260,19 +1282,19 @@ function createWebSocketMessageHandler(getHandlerContext) {
1260
1282
  };
1261
1283
  }
1262
1284
  async function resolveStaticDir(staticDir) {
1263
- const currentDir = (0, import_path8.dirname)((0, import_url.fileURLToPath)(import_meta.url));
1285
+ const currentDir = (0, import_path7.dirname)((0, import_url.fileURLToPath)(import_meta.url));
1264
1286
  const candidates = staticDir === void 0 ? [
1265
1287
  currentDir,
1266
- (0, import_path8.join)(currentDir, "dist"),
1267
- (0, import_path8.join)(currentDir, "..", "dist"),
1268
- (0, import_path8.join)(currentDir, "..", "..", "dist"),
1269
- (0, import_path8.join)(currentDir, ".."),
1270
- (0, import_path8.join)(currentDir, "..", "..")
1271
- ] : [staticDir, (0, import_path8.join)(staticDir, "dist")];
1288
+ (0, import_path7.join)(currentDir, "dist"),
1289
+ (0, import_path7.join)(currentDir, "..", "dist"),
1290
+ (0, import_path7.join)(currentDir, "..", "..", "dist"),
1291
+ (0, import_path7.join)(currentDir, ".."),
1292
+ (0, import_path7.join)(currentDir, "..", "..")
1293
+ ] : [staticDir, (0, import_path7.join)(staticDir, "dist")];
1272
1294
  const resolvedCandidates = await Promise.all(
1273
1295
  candidates.map(async (candidate) => ({
1274
1296
  candidate,
1275
- exists: await fileExists((0, import_path8.join)(candidate, "index.html"))
1297
+ exists: await fileExists((0, import_path7.join)(candidate, "index.html"))
1276
1298
  }))
1277
1299
  );
1278
1300
  const resolved = resolvedCandidates.find(({ exists }) => exists);
@@ -1283,9 +1305,9 @@ async function resolveStaticDir(staticDir) {
1283
1305
  }
1284
1306
  async function resolveReportPath(reportPath) {
1285
1307
  if (await isDirectory(reportPath)) {
1286
- return { reportFile: (0, import_path8.join)(reportPath, "report.json"), offlineReportDir: reportPath };
1308
+ return { reportFile: (0, import_path7.join)(reportPath, "report.json"), offlineReportDir: reportPath };
1287
1309
  }
1288
- return { reportFile: reportPath, offlineReportDir: (0, import_path8.dirname)(reportPath) };
1310
+ return { reportFile: reportPath, offlineReportDir: (0, import_path7.dirname)(reportPath) };
1289
1311
  }
1290
1312
  function createRoutesContext(reportData, staticDir, saveReport, options) {
1291
1313
  const artifactRoots = [reportData.screenshotDir, options.outputDir, options.playwrightSnapshotDir].filter(
package/dist/server.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  startServer
3
- } from "./chunk-F7NSIHCV.js";
4
- import "./chunk-X2TCDD54.js";
3
+ } from "./chunk-KLGJSEKV.js";
4
+ import "./chunk-WRMHUY5A.js";
5
5
  export {
6
6
  startServer
7
7
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crvy/rprtr",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Playwright reporter with visual regression UI for comparing and approving screenshot tests",
5
5
  "keywords": [
6
6
  "crvy",