@crvy/rprtr 0.3.2 → 0.3.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/dist/server.cjs CHANGED
@@ -35,7 +35,7 @@ __export(server_exports, {
35
35
  module.exports = __toCommonJS(server_exports);
36
36
 
37
37
  // src/server/app.ts
38
- var import_path9 = require("path");
38
+ var import_path11 = require("path");
39
39
  var import_url = require("url");
40
40
 
41
41
  // src/offline-reports.ts
@@ -659,6 +659,47 @@ function mergeOfflineReportsIntoTests(existingTests, offlineReports, options = {
659
659
  };
660
660
  }
661
661
 
662
+ // src/server/docker-env.ts
663
+ var ENV_DENYLIST = /* @__PURE__ */ new Set([
664
+ "CI",
665
+ "PLAYWRIGHT_BROWSERS_PATH",
666
+ "CRVY_RPRTR_SERVER_URL",
667
+ "CRVY_RPRTR_PORTABLE_ARTIFACTS",
668
+ "TZ",
669
+ // The container must use its own home: a forwarded host HOME makes NSS (browser cert
670
+ // store init) and npm resolve their state under it, and a Windows-style value like
671
+ // `C:\Users\dev` is a *relative* path on Linux, so those tools would create a literal
672
+ // `C:\Users\dev/.local/share/pki/nssdb` tree under the container CWD — i.e. inside the
673
+ // bind-mounted project.
674
+ "HOME",
675
+ "NPM_CONFIG_CACHE",
676
+ "LANG",
677
+ "LC_ALL",
678
+ "PLAYWRIGHT_HTML_OPEN",
679
+ "PATH",
680
+ // Host paths that do not exist in the container: fontconfig resolves them to nothing and
681
+ // the browser ends up with no font directories at all (blank text). The container gets its
682
+ // AA from the mounted drop-in instead.
683
+ "FONTCONFIG_FILE",
684
+ "FONTCONFIG_PATH"
685
+ ]);
686
+ var WINDOWS_ENV_NOISE = new Set(
687
+ "SYSTEMROOT COMSPEC WINDIR PATHEXT OS PROGRAMFILES PROGRAMFILES(X86) PROGRAMW6432 PROGRAMDATA ALLUSERSPROFILE PUBLIC APPDATA LOCALAPPDATA TEMP TMP USERPROFILE HOMEDRIVE HOMEPATH USERNAME PSMODULEPATH DRIVERDATA NUMBER_OF_PROCESSORS PROCESSOR_ARCHITECTURE PROCESSOR_LEVEL PROCESSOR_REVISION".split(
688
+ " "
689
+ )
690
+ );
691
+ var WINDOWS_PATH_VALUE = /^(?:[A-Za-z]:[\\/]|\\\\)/;
692
+ function collectForwardedEnvNames(env, platform) {
693
+ const names = [];
694
+ for (const [key, value] of Object.entries(env)) {
695
+ const upper = key.toUpperCase();
696
+ if (ENV_DENYLIST.has(upper) || WINDOWS_ENV_NOISE.has(upper) || value === void 0) continue;
697
+ if (platform === "win32" && WINDOWS_PATH_VALUE.test(value)) continue;
698
+ names.push(key);
699
+ }
700
+ return names;
701
+ }
702
+
662
703
  // src/server/docker-support.ts
663
704
  var import_child_process = require("child_process");
664
705
  var import_node_fs = require("node:fs");
@@ -822,6 +863,81 @@ function buildTestListEntries(tests, rootDir, cwd, pathStyle = "host") {
822
863
  });
823
864
  }
824
865
 
866
+ // src/server/fontconfig.ts
867
+ var import_fs2 = require("fs");
868
+ var import_os2 = require("os");
869
+ var import_path4 = require("path");
870
+
871
+ // src/fontconfig.ts
872
+ var import_fs = require("fs");
873
+ var import_os = require("os");
874
+ var import_path3 = require("path");
875
+ var GRAYSCALE_MATCH = ` <match target="font">
876
+ <edit name="rgba" mode="assign"><const>none</const></edit>
877
+ </match>`;
878
+ var HEADER = `<?xml version="1.0"?>
879
+ <!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">
880
+ <!-- Written by crvy-rprtr: pins grayscale text antialiasing for screenshot determinism. -->`;
881
+ var GRAYSCALE_FONTCONFIG_XML = `${HEADER}
882
+ <fontconfig>
883
+ ${GRAYSCALE_MATCH}
884
+ </fontconfig>
885
+ `;
886
+ function grayscaleRootFontconfigXml(systemConfigPath) {
887
+ return `${HEADER}
888
+ <fontconfig>
889
+ <include ignore_missing="no">${escapeXml(systemConfigPath)}</include>
890
+ ${GRAYSCALE_MATCH}
891
+ </fontconfig>
892
+ `;
893
+ }
894
+ function escapeXml(value) {
895
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
896
+ }
897
+ var SYSTEM_FONTCONFIG_PATHS = ["/etc/fonts/fonts.conf", "/usr/local/etc/fonts/fonts.conf"];
898
+ function rootFontconfigPath() {
899
+ return (0, import_path3.join)((0, import_os.tmpdir)(), "crvy-rprtr", "fonts.conf");
900
+ }
901
+ function resolveSystemFontconfig(env = process.env, exists = import_fs.existsSync) {
902
+ const ours = rootFontconfigPath();
903
+ const candidates = [
904
+ env.FONTCONFIG_FILE,
905
+ env.FONTCONFIG_PATH === void 0 ? void 0 : (0, import_path3.join)(env.FONTCONFIG_PATH, "fonts.conf"),
906
+ ...SYSTEM_FONTCONFIG_PATHS
907
+ ];
908
+ for (const candidate of candidates) {
909
+ if (candidate === void 0 || candidate === ours) continue;
910
+ if (exists(candidate)) return candidate;
911
+ }
912
+ return null;
913
+ }
914
+ function ensureRootFontconfig(systemConfigPath) {
915
+ const path = rootFontconfigPath();
916
+ (0, import_fs.mkdirSync)((0, import_path3.join)((0, import_os.tmpdir)(), "crvy-rprtr"), { recursive: true });
917
+ const staging = `${path}.${process.pid}.tmp`;
918
+ (0, import_fs.writeFileSync)(staging, grayscaleRootFontconfigXml(systemConfigPath), "utf8");
919
+ (0, import_fs.renameSync)(staging, path);
920
+ return path;
921
+ }
922
+ function grayscaleFontconfigEnv(baseEnv = process.env, options = {}) {
923
+ if ((options.platform ?? process.platform) !== "linux") return null;
924
+ const systemConfig = resolveSystemFontconfig(baseEnv, options.exists);
925
+ if (systemConfig === null) return null;
926
+ return { FONTCONFIG_FILE: (options.writeConfig ?? ensureRootFontconfig)(systemConfig) };
927
+ }
928
+
929
+ // src/server/fontconfig.ts
930
+ var CONTAINER_FONTCONFIG_PATH = "/etc/fonts/conf.d/99-crvy-rprtr-grayscale.conf";
931
+ function hostFontconfigPath() {
932
+ return (0, import_path4.join)((0, import_os2.tmpdir)(), "crvy-rprtr", "99-crvy-rprtr-grayscale.conf");
933
+ }
934
+ function ensureGrayscaleFontconfig() {
935
+ const path = hostFontconfigPath();
936
+ (0, import_fs2.mkdirSync)((0, import_path4.join)((0, import_os2.tmpdir)(), "crvy-rprtr"), { recursive: true });
937
+ (0, import_fs2.writeFileSync)(path, GRAYSCALE_FONTCONFIG_XML, "utf8");
938
+ return path;
939
+ }
940
+
825
941
  // src/server/run-controller.ts
826
942
  var import_child_process2 = require("child_process");
827
943
  var import_node_fs2 = require("node:fs");
@@ -838,7 +954,7 @@ function resolvePlaywrightLaunch(cwd, playwrightArgs) {
838
954
  if (resolved !== null) return { cmd: resolved.command, args: resolved.args };
839
955
  return { cmd: "npx", args: ["playwright", ...playwrightArgs] };
840
956
  }
841
- function buildSpawnEnv(port, baseEnv = process.env) {
957
+ function buildSpawnEnv(port, baseEnv = process.env, options = {}) {
842
958
  const env = {};
843
959
  for (const [key, value] of Object.entries(baseEnv)) {
844
960
  if (key === "CI") continue;
@@ -846,6 +962,10 @@ function buildSpawnEnv(port, baseEnv = process.env) {
846
962
  }
847
963
  env.CRVY_RPRTR_SERVER_URL = `ws://localhost:${port}`;
848
964
  env.PLAYWRIGHT_HTML_OPEN = "never";
965
+ const { fontRendering = "grayscale", ...fontconfigOptions } = options;
966
+ if (fontRendering !== "inherit") {
967
+ Object.assign(env, grayscaleFontconfigEnv(baseEnv, fontconfigOptions) ?? {});
968
+ }
849
969
  return env;
850
970
  }
851
971
  function createLocalLauncher(options) {
@@ -854,7 +974,8 @@ function createLocalLauncher(options) {
854
974
  launch({ ctx, playwrightArgs }) {
855
975
  const resolve6 = options.resolveLaunch ?? resolvePlaywrightLaunch;
856
976
  const { cmd, args } = resolve6(ctx.cwd, playwrightArgs);
857
- return { cmd, args, env: buildSpawnEnv(options.port, options.env) };
977
+ const { port, resolveLaunch: _resolveLaunch, env: baseEnv, ...spawnEnvOptions } = options;
978
+ return { cmd, args, env: buildSpawnEnv(port, baseEnv, spawnEnvOptions) };
858
979
  }
859
980
  };
860
981
  }
@@ -1061,22 +1182,6 @@ function createRealTimers() {
1061
1182
  // src/server/docker-launcher.ts
1062
1183
  var DOCKER_WORK_DIR = "/work";
1063
1184
  var DOCKER_HOST_GATEWAY = "host.docker.internal";
1064
- var ENV_DENYLIST = /* @__PURE__ */ new Set([
1065
- "CI",
1066
- "PLAYWRIGHT_BROWSERS_PATH",
1067
- "CRVY_RPRTR_SERVER_URL",
1068
- "CRVY_RPRTR_PORTABLE_ARTIFACTS",
1069
- "TZ",
1070
- "LANG",
1071
- "LC_ALL",
1072
- "PLAYWRIGHT_HTML_OPEN",
1073
- "PATH"
1074
- ]);
1075
- var WINDOWS_ENV_NOISE = new Set(
1076
- "SYSTEMROOT COMSPEC WINDIR PATHEXT OS PROGRAMFILES PROGRAMFILES(X86) PROGRAMW6432 PROGRAMDATA ALLUSERSPROFILE PUBLIC APPDATA LOCALAPPDATA TEMP TMP USERPROFILE HOMEDRIVE HOMEPATH USERNAME PSMODULEPATH DRIVERDATA NUMBER_OF_PROCESSORS PROCESSOR_ARCHITECTURE PROCESSOR_IDENTIFIER PROCESSOR_LEVEL PROCESSOR_REVISION".split(
1077
- " "
1078
- )
1079
- );
1080
1185
  var DockerUnavailableError = class extends Error {
1081
1186
  constructor() {
1082
1187
  super("Docker daemon is not available");
@@ -1142,9 +1247,10 @@ function buildDockerRunArgs(ctx, playwrightArgs, deps) {
1142
1247
  args.push("-e", `CRVY_RPRTR_SERVER_URL=ws://${DOCKER_HOST_GATEWAY}:${deps.port}`);
1143
1248
  args.push("-e", "CRVY_RPRTR_PORTABLE_ARTIFACTS=1", "-e", "TZ=UTC", "-e", "LANG=C.UTF-8", "-e", "LC_ALL=C.UTF-8");
1144
1249
  args.push("-e", "PLAYWRIGHT_HTML_OPEN=never");
1145
- for (const [key, value] of Object.entries(deps.env)) {
1146
- const upper = key.toUpperCase();
1147
- if (ENV_DENYLIST.has(upper) || WINDOWS_ENV_NOISE.has(upper) || value === void 0) continue;
1250
+ if (deps.docker?.fontRendering !== "inherit") {
1251
+ args.push("-v", `${ensureGrayscaleFontconfig()}:${CONTAINER_FONTCONFIG_PATH}:ro`);
1252
+ }
1253
+ for (const key of collectForwardedEnvNames(deps.env, deps.platform)) {
1148
1254
  args.push("-e", key);
1149
1255
  }
1150
1256
  if (deps.docker?.extraArgs !== void 0) args.push(...deps.docker.extraArgs);
@@ -1207,7 +1313,8 @@ function buildLauncher(state, deps) {
1207
1313
  env: deps.baseEnv,
1208
1314
  image,
1209
1315
  command: state.command,
1210
- warn: deps.warn
1316
+ warn: deps.warn,
1317
+ platform: deps.platform
1211
1318
  });
1212
1319
  return { cmd: "docker", args, env: stripCi(deps.baseEnv) };
1213
1320
  },
@@ -1232,17 +1339,17 @@ function createDockerLauncher(options) {
1232
1339
  }
1233
1340
 
1234
1341
  // src/server/handlers.ts
1235
- var import_fs2 = require("fs");
1236
- var import_path6 = require("path");
1342
+ var import_fs4 = require("fs");
1343
+ var import_path8 = require("path");
1237
1344
 
1238
1345
  // src/server/artifact-routes.ts
1239
- var import_fs = require("fs");
1346
+ var import_fs3 = require("fs");
1240
1347
  var import_promises3 = require("fs/promises");
1241
- var import_path5 = require("path");
1348
+ var import_path7 = require("path");
1242
1349
 
1243
1350
  // src/snapshot-path-resolver.ts
1244
1351
  var import_crypto = require("crypto");
1245
- var import_path3 = require("path");
1352
+ var import_path5 = require("path");
1246
1353
  var DEFAULT_SCREENSHOT_TEMPLATE = "{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{-projectName}{-snapshotSuffix}{ext}";
1247
1354
  var WINDOWS_FILESYSTEM_FRIENDLY_LENGTH = 60;
1248
1355
  var PLAYWRIGHT_SNAPSHOT_NAME_LIMIT = 100;
@@ -1278,16 +1385,16 @@ function trimLongString(value, length = WINDOWS_FILESYSTEM_FRIENDLY_LENGTH) {
1278
1385
  const end = length - middle.length - start;
1279
1386
  return value.slice(0, start) + middle + value.slice(-end);
1280
1387
  }
1281
- function sanitizeFilePathBeforeExtension(filePath, extension = (0, import_path3.extname)(filePath)) {
1388
+ function sanitizeFilePathBeforeExtension(filePath, extension = (0, import_path5.extname)(filePath)) {
1282
1389
  const base = filePath.slice(0, filePath.length - extension.length);
1283
1390
  return sanitizeForFilePath(base) + extension;
1284
1391
  }
1285
1392
  function addSuffixToFilePath(filePath, suffix) {
1286
- const extension = (0, import_path3.extname)(filePath);
1393
+ const extension = (0, import_path5.extname)(filePath);
1287
1394
  return filePath.slice(0, filePath.length - extension.length) + suffix + extension;
1288
1395
  }
1289
1396
  function normalizedSnapshotDir(config) {
1290
- return (0, import_path3.resolve)(config.configDir, config.snapshotDir);
1397
+ return (0, import_path5.resolve)(config.configDir, config.snapshotDir);
1291
1398
  }
1292
1399
  function templateValue(template, token, value) {
1293
1400
  return template.replace(
@@ -1297,8 +1404,8 @@ function templateValue(template, token, value) {
1297
1404
  }
1298
1405
  function applyTemplate(input, nameArgument, extension) {
1299
1406
  const template = input.config.toHaveScreenshotPathTemplate ?? input.config.snapshotPathTemplate ?? DEFAULT_SCREENSHOT_TEMPLATE;
1300
- const relativeTestFilePath = (0, import_path3.relative)(input.config.testDir, input.testFile);
1301
- const parsed = (0, import_path3.parse)(relativeTestFilePath);
1407
+ const relativeTestFilePath = (0, import_path5.relative)(input.config.testDir, input.testFile);
1408
+ const parsed = (0, import_path5.parse)(relativeTestFilePath);
1302
1409
  const tokens = [
1303
1410
  ["testDir", input.config.testDir],
1304
1411
  ["snapshotDir", normalizedSnapshotDir(input.config)],
@@ -1316,16 +1423,16 @@ function applyTemplate(input, nameArgument, extension) {
1316
1423
  (currentTemplate, [token, value]) => templateValue(currentTemplate, token, value),
1317
1424
  template
1318
1425
  );
1319
- return (0, import_path3.resolve)(input.config.configDir, snapshotPath);
1426
+ return (0, import_path5.resolve)(input.config.configDir, snapshotPath);
1320
1427
  }
1321
- function removeExtension(filePath, extension = (0, import_path3.extname)(filePath)) {
1428
+ function removeExtension(filePath, extension = (0, import_path5.extname)(filePath)) {
1322
1429
  return filePath.slice(0, filePath.length - extension.length);
1323
1430
  }
1324
1431
  function snapshotNameParts(declaredName) {
1325
- const extension = (0, import_path3.extname)(declaredName) || ".png";
1432
+ const extension = (0, import_path5.extname)(declaredName) || ".png";
1326
1433
  return {
1327
1434
  extension,
1328
- filePath: (0, import_path3.extname)(declaredName) === "" ? `${declaredName}${extension}` : declaredName
1435
+ filePath: (0, import_path5.extname)(declaredName) === "" ? `${declaredName}${extension}` : declaredName
1329
1436
  };
1330
1437
  }
1331
1438
  function filePathForOccurrence(filePath, occurrenceIndex) {
@@ -1349,7 +1456,7 @@ function resolveStringCallTarget(input, declaration) {
1349
1456
  function resolveArrayCallTarget(input, declaration) {
1350
1457
  const { extension, filePath } = snapshotNameParts(declaration.declaredName);
1351
1458
  const occurrenceFilePath = filePathForOccurrence(filePath, declaration.occurrenceIndex);
1352
- const nameArgument = (0, import_path3.join)((0, import_path3.dirname)(occurrenceFilePath), (0, import_path3.basename)(occurrenceFilePath, extension));
1459
+ const nameArgument = (0, import_path5.join)((0, import_path5.dirname)(occurrenceFilePath), (0, import_path5.basename)(occurrenceFilePath, extension));
1353
1460
  return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
1354
1461
  }
1355
1462
  function resolveNamedTarget(input, declaration) {
@@ -1391,7 +1498,7 @@ function resolveTarget(input, declaration) {
1391
1498
  case "unnamed": {
1392
1499
  const anonymousFileName = anonymousName(input.reporterTitlePath, declaration.occurrenceIndex);
1393
1500
  const extension = ".png";
1394
- const nameArgument = (0, import_path3.join)((0, import_path3.dirname)(anonymousFileName), (0, import_path3.basename)(anonymousFileName, extension));
1501
+ const nameArgument = (0, import_path5.join)((0, import_path5.dirname)(anonymousFileName), (0, import_path5.basename)(anonymousFileName, extension));
1395
1502
  return createResolvedBaselineTarget(input, declaration, nameArgument, extension);
1396
1503
  }
1397
1504
  }
@@ -1404,7 +1511,7 @@ function resolveBaselineTargets(input) {
1404
1511
  }
1405
1512
 
1406
1513
  // src/server/utils.ts
1407
- var import_path4 = require("path");
1514
+ var import_path6 = require("path");
1408
1515
  var LIVE_UPDATES_WEBSOCKET_PATH = "/";
1409
1516
  function broadcastToBrowsers(wsClients, msg) {
1410
1517
  const payload = JSON.stringify(msg);
@@ -1416,10 +1523,10 @@ function isWebSocketUpgradeRequest(req) {
1416
1523
  return new URL(req.url).pathname === LIVE_UPDATES_WEBSOCKET_PATH && req.headers.get("upgrade")?.toLowerCase() === "websocket";
1417
1524
  }
1418
1525
  function isPathWithinRoots(target, roots) {
1419
- const resolvedTarget = (0, import_path4.resolve)(target);
1526
+ const resolvedTarget = (0, import_path6.resolve)(target);
1420
1527
  return roots.some((root) => {
1421
- const rel = (0, import_path4.relative)((0, import_path4.resolve)(root), resolvedTarget);
1422
- return rel === "" || !rel.startsWith(`..${import_path4.sep}`) && rel !== ".." && !(0, import_path4.isAbsolute)(rel);
1528
+ const rel = (0, import_path6.relative)((0, import_path6.resolve)(root), resolvedTarget);
1529
+ return rel === "" || !rel.startsWith(`..${import_path6.sep}`) && rel !== ".." && !(0, import_path6.isAbsolute)(rel);
1423
1530
  });
1424
1531
  }
1425
1532
 
@@ -1437,7 +1544,7 @@ async function handleFile(ctx, req) {
1437
1544
  let decodedPath;
1438
1545
  try {
1439
1546
  rawDecoded = decodeURIComponent(new URL(req.url).pathname.slice("/file/".length));
1440
- decodedPath = (0, import_path5.resolve)(rawDecoded);
1547
+ decodedPath = (0, import_path7.resolve)(rawDecoded);
1441
1548
  } catch {
1442
1549
  return notFound();
1443
1550
  }
@@ -1450,7 +1557,7 @@ async function handleFile(ctx, req) {
1450
1557
  }
1451
1558
  return notFound();
1452
1559
  }
1453
- const realRoots = (await Promise.all((ctx.artifactRoots ?? []).map((root) => realpathOrNull((0, import_path5.resolve)(root))))).filter(
1560
+ const realRoots = (await Promise.all((ctx.artifactRoots ?? []).map((root) => realpathOrNull((0, import_path7.resolve)(root))))).filter(
1454
1561
  (root) => root !== null
1455
1562
  );
1456
1563
  if (!isPathWithinRoots(realTarget, realRoots)) {
@@ -1483,14 +1590,14 @@ function resolveBaselineSnapshotPath(routing, test, retry, imageName) {
1483
1590
  declarations: [declaration],
1484
1591
  config: {
1485
1592
  configDir: routing.configDir,
1486
- testDir: routing.playwrightTestDir ?? (0, import_path5.dirname)(testFile),
1487
- snapshotDir: routing.playwrightSnapshotDir ?? (0, import_path5.dirname)(testFile),
1593
+ testDir: routing.playwrightTestDir ?? (0, import_path7.dirname)(testFile),
1594
+ snapshotDir: routing.playwrightSnapshotDir ?? (0, import_path7.dirname)(testFile),
1488
1595
  projectName: test.projectName ?? test.browser,
1489
1596
  snapshotSuffix: isContainerPath ? "linux" : process.platform,
1490
1597
  snapshotPathTemplate: routing.playwrightSnapshotPathTemplate,
1491
1598
  toHaveScreenshotPathTemplate: routing.playwrightToHaveScreenshotPathTemplate
1492
1599
  },
1493
- snapshotPathExists: import_fs.existsSync
1600
+ snapshotPathExists: import_fs3.existsSync
1494
1601
  });
1495
1602
  return targets.length === 1 ? targets[0]?.snapshotPath ?? null : null;
1496
1603
  }
@@ -1556,7 +1663,7 @@ function enrichDeclaredBaselines(ctx, test) {
1556
1663
  continue;
1557
1664
  }
1558
1665
  const snapshotPath = resolveBaselineSnapshotPath(ctx.approvalRouting, test, retry, visualName);
1559
- if (snapshotPath === null || !(0, import_fs2.existsSync)(snapshotPath)) {
1666
+ if (snapshotPath === null || !(0, import_fs4.existsSync)(snapshotPath)) {
1560
1667
  continue;
1561
1668
  }
1562
1669
  image.expect = `/baseline/${encodeURIComponent(test.id)}/${retry}/${encodeURIComponent(visualName)}`;
@@ -1661,11 +1768,11 @@ function handleRegister(ctx, rawData) {
1661
1768
  });
1662
1769
  }
1663
1770
  function buildRunContext(configFile, data) {
1664
- const configDir = (0, import_path6.dirname)(configFile);
1771
+ const configDir = (0, import_path8.dirname)(configFile);
1665
1772
  return {
1666
1773
  configFile,
1667
1774
  cwd: configDir,
1668
- rootDir: data.playwrightRootDir ?? (data.playwrightTestDir === void 0 ? configDir : (0, import_path6.resolve)(configDir, data.playwrightTestDir))
1775
+ rootDir: data.playwrightRootDir ?? (data.playwrightTestDir === void 0 ? configDir : (0, import_path8.resolve)(configDir, data.playwrightTestDir))
1669
1776
  };
1670
1777
  }
1671
1778
 
@@ -1698,7 +1805,11 @@ async function resolveRunBackend(options) {
1698
1805
  console.warn(`[crvy-rprtr] ${message}`);
1699
1806
  }
1700
1807
  });
1701
- const launcher = resolvedRunMode === "docker" ? createDockerLauncher({ port: options.port, docker: options.docker, exec: dockerExec }) : createLocalLauncher({ port: options.port });
1808
+ const launcher = resolvedRunMode === "docker" ? createDockerLauncher({
1809
+ port: options.port,
1810
+ docker: { fontRendering: options.fontRendering, ...options.docker },
1811
+ exec: dockerExec
1812
+ }) : createLocalLauncher({ port: options.port, fontRendering: options.fontRendering });
1702
1813
  return {
1703
1814
  launcher,
1704
1815
  routesContextOptions: {
@@ -1709,7 +1820,7 @@ async function resolveRunBackend(options) {
1709
1820
  }
1710
1821
 
1711
1822
  // src/server/playwright-config.ts
1712
- var import_path7 = require("path");
1823
+ var import_path9 = require("path");
1713
1824
  var CONFIG_FILES = [
1714
1825
  "playwright.config.ts",
1715
1826
  "playwright.config.mts",
@@ -1721,14 +1832,14 @@ var CONFIG_FILES = [
1721
1832
  async function resolvePlaywrightConfig(cwd) {
1722
1833
  const matches = await Promise.all(
1723
1834
  CONFIG_FILES.map(async (file) => {
1724
- const candidate = (0, import_path7.join)(cwd, file);
1835
+ const candidate = (0, import_path9.join)(cwd, file);
1725
1836
  return await fileExists(candidate) ? candidate : null;
1726
1837
  })
1727
1838
  );
1728
1839
  return matches.find((path) => path !== null) ?? null;
1729
1840
  }
1730
1841
  function resolveSeedConfigFile(option, cwd) {
1731
- return option === void 0 ? resolvePlaywrightConfig(cwd) : Promise.resolve((0, import_path7.resolve)(cwd, option));
1842
+ return option === void 0 ? resolvePlaywrightConfig(cwd) : Promise.resolve((0, import_path9.resolve)(cwd, option));
1732
1843
  }
1733
1844
 
1734
1845
  // src/server/report-persistence.ts
@@ -1811,7 +1922,7 @@ function createRoutesContext(reportData, staticDir, saveReport, options) {
1811
1922
  }
1812
1923
 
1813
1924
  // src/server/routes.ts
1814
- var import_path8 = require("path");
1925
+ var import_path10 = require("path");
1815
1926
 
1816
1927
  // src/server/run-routes.ts
1817
1928
  function handleRunRoutes(pathname, method, runController, req) {
@@ -1850,7 +1961,7 @@ function handleApiStop(runController) {
1850
1961
 
1851
1962
  // src/server/routes.ts
1852
1963
  async function handleRoot(ctx) {
1853
- const html = await respondWithFile((0, import_path8.join)(ctx.staticDir, "index.html"), "text/html");
1964
+ const html = await respondWithFile((0, import_path10.join)(ctx.staticDir, "index.html"), "text/html");
1854
1965
  return html ?? new Response("Not Found", { status: 404 });
1855
1966
  }
1856
1967
  async function handleAppCss() {
@@ -1874,7 +1985,7 @@ function handleApiReport(ctx) {
1874
1985
  var APPROVAL_TARGET_ERROR = "Could not resolve approval target";
1875
1986
  function actualPathFromUrl(ctx, actualUrl) {
1876
1987
  if (actualUrl.startsWith("/screenshots/")) {
1877
- return (0, import_path8.join)(ctx.reportData.screenshotDir, actualUrl.slice("/screenshots/".length));
1988
+ return (0, import_path10.join)(ctx.reportData.screenshotDir, actualUrl.slice("/screenshots/".length));
1878
1989
  }
1879
1990
  if (actualUrl.startsWith("/file/")) {
1880
1991
  return decodeURIComponent(actualUrl.slice("/file/".length));
@@ -1996,7 +2107,7 @@ async function handleScreenshots(ctx, req) {
1996
2107
  }
1997
2108
  async function handleDist(ctx, req) {
1998
2109
  const path = new URL(req.url).pathname.slice("/dist/".length);
1999
- const filePath = (0, import_path8.join)(ctx.staticDir, path);
2110
+ const filePath = (0, import_path10.join)(ctx.staticDir, path);
2000
2111
  const contentType = filePath.endsWith(".css") ? "text/css" : filePath.endsWith(".js") ? "application/javascript" : filePath.endsWith(".svelte") ? "text/plain" : "application/octet-stream";
2001
2112
  const file = await respondWithFile(filePath, contentType);
2002
2113
  return file ?? new Response("Not Found", { status: 404 });
@@ -2183,19 +2294,19 @@ function createWebSocketMessageHandler(getHandlerContext) {
2183
2294
  };
2184
2295
  }
2185
2296
  async function resolveStaticDir(staticDir) {
2186
- const currentDir = (0, import_path9.dirname)((0, import_url.fileURLToPath)(import_meta2.url));
2297
+ const currentDir = (0, import_path11.dirname)((0, import_url.fileURLToPath)(import_meta2.url));
2187
2298
  const candidates = staticDir === void 0 ? [
2188
2299
  currentDir,
2189
- (0, import_path9.join)(currentDir, "dist"),
2190
- (0, import_path9.join)(currentDir, "..", "dist"),
2191
- (0, import_path9.join)(currentDir, "..", "..", "dist"),
2192
- (0, import_path9.join)(currentDir, ".."),
2193
- (0, import_path9.join)(currentDir, "..", "..")
2194
- ] : [staticDir, (0, import_path9.join)(staticDir, "dist")];
2300
+ (0, import_path11.join)(currentDir, "dist"),
2301
+ (0, import_path11.join)(currentDir, "..", "dist"),
2302
+ (0, import_path11.join)(currentDir, "..", "..", "dist"),
2303
+ (0, import_path11.join)(currentDir, ".."),
2304
+ (0, import_path11.join)(currentDir, "..", "..")
2305
+ ] : [staticDir, (0, import_path11.join)(staticDir, "dist")];
2195
2306
  const resolvedCandidates = await Promise.all(
2196
2307
  candidates.map(async (candidate) => ({
2197
2308
  candidate,
2198
- exists: await fileExists((0, import_path9.join)(candidate, "index.html"))
2309
+ exists: await fileExists((0, import_path11.join)(candidate, "index.html"))
2199
2310
  }))
2200
2311
  );
2201
2312
  const resolved = resolvedCandidates.find(({ exists }) => exists);
@@ -2206,9 +2317,9 @@ async function resolveStaticDir(staticDir) {
2206
2317
  }
2207
2318
  async function resolveReportPath(reportPath) {
2208
2319
  if (await isDirectory(reportPath)) {
2209
- return { reportFile: (0, import_path9.join)(reportPath, "report.json"), offlineReportDir: reportPath };
2320
+ return { reportFile: (0, import_path11.join)(reportPath, "report.json"), offlineReportDir: reportPath };
2210
2321
  }
2211
- return { reportFile: reportPath, offlineReportDir: (0, import_path9.dirname)(reportPath) };
2322
+ return { reportFile: reportPath, offlineReportDir: (0, import_path11.dirname)(reportPath) };
2212
2323
  }
2213
2324
  async function seedRunContext(routesContext, options) {
2214
2325
  if (routesContext.runContext !== void 0) {
@@ -2223,6 +2334,7 @@ async function setupRoutesContext(options, reportData, staticDir, saveReport, po
2223
2334
  const { launcher, routesContextOptions } = await resolveRunBackend({
2224
2335
  runMode: options.runMode,
2225
2336
  docker: options.docker,
2337
+ fontRendering: options.fontRendering,
2226
2338
  port
2227
2339
  });
2228
2340
  const routesContext = createRoutesContext(reportData, staticDir, saveReport, {
package/dist/server.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  startServer
3
- } from "./chunk-C5SVPK2O.js";
3
+ } from "./chunk-NXW25MU3.js";
4
4
  import "./chunk-7YLKL3SL.js";
5
+ import "./chunk-7JUICMVX.js";
5
6
  export {
6
7
  startServer
7
8
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crvy/rprtr",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Playwright reporter with visual regression UI for comparing and approving screenshot tests",
5
5
  "keywords": [
6
6
  "crvy",
@@ -44,6 +44,12 @@
44
44
  "require": "./dist/server.cjs",
45
45
  "default": "./dist/server.js"
46
46
  },
47
+ "./rendering": {
48
+ "types": "./dist/rendering.d.ts",
49
+ "import": "./dist/rendering.js",
50
+ "require": "./dist/rendering.cjs",
51
+ "default": "./dist/rendering.js"
52
+ },
47
53
  "./types": {
48
54
  "types": "./dist/types.d.ts"
49
55
  }
@@ -69,6 +75,7 @@
69
75
  "check": "./scripts/check.sh",
70
76
  "check:staged": "./scripts/check.sh --staged",
71
77
  "example": "cd examples/playwright && bunx playwright test",
78
+ "example:ct": "cd examples/component-testing && bunx playwright test",
72
79
  "changelog:preview": "git-cliff --dry-run",
73
80
  "changelog:generate": "git-cliff -o CHANGELOG.md",
74
81
  "prepublishOnly": "bun run build && bunx publint",
@@ -83,7 +90,7 @@
83
90
  "devDependencies": {
84
91
  "@playwright/test": "1.59.0",
85
92
  "@tailwindcss/postcss": "^4.2.2",
86
- "@types/bun": "latest",
93
+ "@types/bun": "^1.4.0",
87
94
  "@types/ws": "^8.18.1",
88
95
  "esbuild": "^0.27.4",
89
96
  "esbuild-svelte": "^0.9.4",
@@ -102,7 +109,7 @@
102
109
  "@playwright/test": ">=1.40"
103
110
  },
104
111
  "engines": {
105
- "bun": ">=1.0.0",
112
+ "bun": ">=1.4.0",
106
113
  "node": ">=22"
107
114
  }
108
115
  }