@isentinel/jest-roblox 0.0.6 → 0.0.8

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/cli.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { t as CliOptions } from "./schema-CG-f4OUB.mjs";
1
+ import { t as CliOptions } from "./schema-DcDQmTyn.mjs";
2
2
 
3
3
  //#region src/cli.d.ts
4
4
  declare function parseArgs(args: Array<string>): CliOptions;
package/dist/cli.mjs CHANGED
@@ -1,12 +1,12 @@
1
- import { _ as isValidBackend, a as execute, c as writeJsonFile, f as loadConfig, h as VALID_BACKENDS, i as runTypecheck, n as parseGameOutput, o as loadCoverageManifest, r as writeGameOutput, t as formatGameOutputNotice, w as LuauScriptError, x as createOpenCloudBackend, y as createStudioBackend } from "./game-output-lpY5mxJ_.mjs";
1
+ import { C as createStudioBackend, O as LuauScriptError, T as createOpenCloudBackend, a as formatAnnotations, c as execute, d as writeJsonFile, g as loadConfig, h as rojoProjectSchema, i as runTypecheck, l as loadCoverageManifest, n as parseGameOutput, o as formatJobSummary, r as writeGameOutput, s as resolveGitHubActionsOptions, t as formatGameOutputNotice, x as isValidBackend, y as VALID_BACKENDS } from "./game-output-M8du29nj.mjs";
2
2
  import assert from "node:assert";
3
+ import * as fs from "node:fs";
3
4
  import * as path$1 from "node:path";
4
5
  import process from "node:process";
5
6
  import { parseArgs as parseArgs$1 } from "node:util";
6
7
  import color from "tinyrainbow";
7
8
  import { WebSocketServer } from "ws";
8
9
  import { type } from "arktype";
9
- import * as fs from "node:fs";
10
10
  import * as os from "node:os";
11
11
  import { Buffer } from "node:buffer";
12
12
  import { TraceMap, originalPositionFor } from "@jridgewell/trace-mapping";
@@ -18,7 +18,7 @@ import istanbulReport from "istanbul-lib-report";
18
18
  import istanbulReports from "istanbul-reports";
19
19
 
20
20
  //#region package.json
21
- var version = "0.0.6";
21
+ var version = "0.0.8";
22
22
 
23
23
  //#endregion
24
24
  //#region src/backends/auto.ts
@@ -41,7 +41,7 @@ var StudioWithFallback = class {
41
41
  };
42
42
  function isStudioBusyError(error) {
43
43
  if (error instanceof LuauScriptError) return /previous call to start play session/i.test(error.message);
44
- return error.code === "EADDRINUSE";
44
+ return typeof error === "object" && error !== null && "code" in error && error.code === "EADDRINUSE";
45
45
  }
46
46
  async function probeStudioPlugin(port, timeoutMs, createServer = (wsPort) => {
47
47
  return new WebSocketServer({ port: wsPort });
@@ -139,15 +139,15 @@ function mapCoverageToTypeScript(coverageData, manifest) {
139
139
  return buildResult(pendingStatements, pendingFunctions, pendingBranches);
140
140
  }
141
141
  function loadFileResources(record) {
142
- let covMapRaw;
142
+ let coverageMapRaw;
143
143
  let sourceMapRaw;
144
144
  try {
145
- covMapRaw = fs.readFileSync(record.coverageMapPath, "utf-8");
145
+ coverageMapRaw = fs.readFileSync(record.coverageMapPath, "utf-8");
146
146
  sourceMapRaw = fs.readFileSync(record.sourceMapPath, "utf-8");
147
147
  } catch {
148
148
  return;
149
149
  }
150
- const parsed = coverageMapSchema(JSON.parse(covMapRaw));
150
+ const parsed = coverageMapSchema(JSON.parse(coverageMapRaw));
151
151
  if (parsed instanceof type.errors) return;
152
152
  return {
153
153
  coverageMap: parsed,
@@ -278,10 +278,10 @@ function mapFileFunctions(resources, fileCoverage, pendingFunctions, resolvedTsP
278
278
  function mapBranchArmLocations(traceMap, rawLocations) {
279
279
  const mappedLocations = [];
280
280
  let tsPath;
281
- for (const rawLoc of rawLocations) {
282
- const loc = spanSchema(rawLoc);
283
- if (loc instanceof type.errors) return;
284
- const mapped = mapStatement(traceMap, loc);
281
+ for (const rawLocation of rawLocations) {
282
+ const location = spanSchema(rawLocation);
283
+ if (location instanceof type.errors) return;
284
+ const mapped = mapStatement(traceMap, location);
285
285
  if (mapped === void 0) return;
286
286
  if (tsPath === void 0) tsPath = mapped.start.source;
287
287
  else if (tsPath !== mapped.start.source) return;
@@ -315,14 +315,14 @@ function mapFileBranches(resources, fileCoverage, pendingBranches) {
315
315
  fileBranches = [];
316
316
  pendingBranches.set(result.tsPath, fileBranches);
317
317
  }
318
- const firstLoc = result.locations[0];
319
- const lastLoc = result.locations[result.locations.length - 1];
320
- assert(firstLoc !== void 0 && lastLoc !== void 0, "Branch locations must not be empty after successful mapping");
318
+ const firstLocation = result.locations[0];
319
+ const lastLocation = result.locations[result.locations.length - 1];
320
+ assert(firstLocation !== void 0 && lastLocation !== void 0, "Branch locations must not be empty after successful mapping");
321
321
  fileBranches.push({
322
322
  armHitCounts: entry.locations.map((_, index) => armHitCounts[index] ?? 0),
323
323
  loc: {
324
- end: lastLoc.end,
325
- start: firstLoc.start
324
+ end: lastLocation.end,
325
+ start: firstLocation.start
326
326
  },
327
327
  locations: result.locations,
328
328
  type: entry.type
@@ -877,17 +877,17 @@ function extractFunctionName(node) {
877
877
  }
878
878
 
879
879
  //#endregion
880
- //#region src/coverage/covmap-builder.ts
880
+ //#region src/coverage/coverage-map-builder.ts
881
881
  function buildCoverageMap$1(result) {
882
882
  const statementMap = {};
883
- for (const stmt of result.statements) statementMap[String(stmt.index)] = {
883
+ for (const statement of result.statements) statementMap[String(statement.index)] = {
884
884
  end: {
885
- column: stmt.location.endcolumn,
886
- line: stmt.location.endline
885
+ column: statement.location.endcolumn,
886
+ line: statement.location.endline
887
887
  },
888
888
  start: {
889
- column: stmt.location.begincolumn,
890
- line: stmt.location.beginline
889
+ column: statement.location.begincolumn,
890
+ line: statement.location.beginline
891
891
  }
892
892
  };
893
893
  const functionMap = {};
@@ -980,14 +980,13 @@ function collectProbes(result) {
980
980
  }
981
981
  /** Mutates `mutableLines` in place, inserting probe text at each probe's position. */
982
982
  function applyProbes(mutableLines, probes) {
983
- for (const probe of probes) {
984
- const lineIndex = probe.line - 1;
983
+ for (const { column, line: probeLine, text } of probes) {
984
+ const lineIndex = probeLine - 1;
985
985
  const line = mutableLines[lineIndex];
986
- assert(line !== void 0, `Invalid probe line number: ${probe.line}`);
987
- const col = probe.column;
988
- const before = line.slice(0, col - 1);
989
- const after = line.slice(col - 1);
990
- mutableLines[lineIndex] = before + probe.text + after;
986
+ assert(line !== void 0, `Invalid probe line number: ${probeLine}`);
987
+ const before = line.slice(0, column - 1);
988
+ const after = line.slice(column - 1);
989
+ mutableLines[lineIndex] = before + text + after;
991
990
  }
992
991
  }
993
992
  function extractModeDirective(lines) {
@@ -1100,9 +1099,9 @@ function instrumentRoot(options) {
1100
1099
  const source = fs.readFileSync(path$1.resolve(originalLuauPath), "utf-8");
1101
1100
  const collectorResult = collectCoverage(ast);
1102
1101
  const instrumentedSource = insertProbes(source, collectorResult, fileKey);
1103
- const covmap = buildCoverageMap$1(collectorResult);
1102
+ const coverageMap = buildCoverageMap$1(collectorResult);
1104
1103
  fs.writeFileSync(path$1.join(shadowDir, relativePath), instrumentedSource);
1105
- fs.writeFileSync(coverageMapOutputPath, JSON.stringify(covmap, void 0, " "));
1104
+ fs.writeFileSync(coverageMapOutputPath, JSON.stringify(coverageMap, void 0, " "));
1106
1105
  files[fileKey] = {
1107
1106
  key: fileKey,
1108
1107
  branchCount: collectorResult.branches.length,
@@ -1232,7 +1231,9 @@ function prepareCoverage(config) {
1232
1231
  };
1233
1232
  fs.mkdirSync(path$1.dirname(manifestPath), { recursive: true });
1234
1233
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, void 0, " "));
1235
- const rewritten = rewriteRojoProject(JSON.parse(fs.readFileSync(rojoProjectPath, "utf-8")), {
1234
+ const rojoProjectRaw = rojoProjectSchema(JSON.parse(fs.readFileSync(rojoProjectPath, "utf-8")));
1235
+ if (rojoProjectRaw instanceof type.errors) throw new Error(`Malformed Rojo project JSON: ${rojoProjectRaw.toString()}`);
1236
+ const rewritten = rewriteRojoProject(rojoProjectRaw, {
1236
1237
  projectRelocation: path$1.relative(COVERAGE_DIR, path$1.dirname(rojoProjectPath)).replaceAll("\\", "/"),
1237
1238
  roots
1238
1239
  });
@@ -1433,6 +1434,7 @@ Options:
1433
1434
  --coverage Enable coverage collection
1434
1435
  --coverageDirectory <path> Directory for coverage output (default: coverage)
1435
1436
  --coverageReporters <r...> Coverage reporters (default: text, lcov)
1437
+ --formatters <name...> Output formatters (default: default; auto: github-actions)
1436
1438
  --no-cache Force re-upload place file (skip cache)
1437
1439
  --pollInterval <ms> Open Cloud poll interval in ms (default: 500)
1438
1440
  --projects <path...> DataModel paths to search for tests
@@ -1475,6 +1477,10 @@ function parseArgs(args) {
1475
1477
  multiple: true,
1476
1478
  type: "string"
1477
1479
  },
1480
+ "formatters": {
1481
+ multiple: true,
1482
+ type: "string"
1483
+ },
1478
1484
  "gameOutput": { type: "string" },
1479
1485
  "help": {
1480
1486
  default: false,
@@ -1539,6 +1545,7 @@ function parseArgs(args) {
1539
1545
  coverageDirectory: values.coverageDirectory,
1540
1546
  coverageReporters: values.coverageReporters,
1541
1547
  files: positionals.length > 0 ? positionals : void 0,
1548
+ formatters: values.formatters,
1542
1549
  gameOutput: values.gameOutput,
1543
1550
  help: values.help,
1544
1551
  json: values.json,
@@ -1630,6 +1637,30 @@ function processCoverage(config, coverageData) {
1630
1637
  }
1631
1638
  return true;
1632
1639
  }
1640
+ function findFormatterOptions(formatters, name) {
1641
+ for (const entry of formatters) {
1642
+ if (entry === name) return {};
1643
+ if (Array.isArray(entry) && entry[0] === name) return entry[1];
1644
+ }
1645
+ }
1646
+ function runGitHubActionsFormatter(config, result, sourceMapper) {
1647
+ const userOptions = findFormatterOptions(config.formatters, "github-actions");
1648
+ if (userOptions === void 0) return;
1649
+ const typedOptions = userOptions;
1650
+ const options = resolveGitHubActionsOptions(typedOptions, sourceMapper);
1651
+ if (typedOptions.displayAnnotations !== false) {
1652
+ const annotations = formatAnnotations(result, options);
1653
+ if (annotations !== "") process.stderr.write(`${annotations}\n`);
1654
+ }
1655
+ const { jobSummary } = typedOptions;
1656
+ if (jobSummary?.enabled !== false) {
1657
+ const outputPath = jobSummary?.outputPath ?? process.env["GITHUB_STEP_SUMMARY"];
1658
+ if (outputPath !== void 0) {
1659
+ const summary = formatJobSummary(result, options);
1660
+ fs.appendFileSync(outputPath, summary);
1661
+ }
1662
+ }
1663
+ }
1633
1664
  async function outputResults(config, typecheckResult, runtimeResult) {
1634
1665
  const mergedResult = mergeResults(typecheckResult, runtimeResult?.result);
1635
1666
  if (runtimeResult !== void 0 && runtimeResult.output !== "") console.log(runtimeResult.output);
@@ -1637,6 +1668,7 @@ async function outputResults(config, typecheckResult, runtimeResult) {
1637
1668
  if (typecheckResult !== void 0 && !config.silent) printTypecheckSummary(typecheckResult);
1638
1669
  if (config.outputFile !== void 0) await writeJsonFile(mergedResult, config.outputFile);
1639
1670
  if (runtimeResult !== void 0) writeGameOutputIfConfigured(config, runtimeResult.gameOutput, { hintsShown: !mergedResult.success });
1671
+ runGitHubActionsFormatter(config, mergedResult, runtimeResult?.sourceMapper);
1640
1672
  const passed = mergedResult.success && coveragePassed;
1641
1673
  if (!config.silent && config.collectCoverage) printFinalStatus(passed);
1642
1674
  return passed ? 0 : 1;
@@ -1725,6 +1757,11 @@ function validateBackend(value) {
1725
1757
  function getLuauErrorHint(message) {
1726
1758
  for (const [pattern, hint] of LUAU_ERROR_HINTS) if (pattern.test(message)) return hint;
1727
1759
  }
1760
+ function resolveFormatters(cli, config) {
1761
+ const explicit = cli.formatters ?? config.formatters;
1762
+ if (explicit !== void 0) return explicit;
1763
+ return process.env["GITHUB_ACTIONS"] === "true" ? ["default", "github-actions"] : ["default"];
1764
+ }
1728
1765
  function mergeCliWithConfig(cli, config) {
1729
1766
  return {
1730
1767
  ...config,
@@ -1736,6 +1773,7 @@ function mergeCliWithConfig(cli, config) {
1736
1773
  compactMaxFailures: cli.compactMaxFailures ?? config.compactMaxFailures,
1737
1774
  coverageDirectory: cli.coverageDirectory ?? config.coverageDirectory,
1738
1775
  coverageReporters: cli.coverageReporters ?? config.coverageReporters,
1776
+ formatters: resolveFormatters(cli, config),
1739
1777
  gameOutput: cli.gameOutput ?? config.gameOutput,
1740
1778
  json: cli.json ?? config.json,
1741
1779
  outputFile: cli.outputFile ?? config.outputFile,
@@ -1763,6 +1801,7 @@ function mergeResults(typecheck, runtime) {
1763
1801
  numFailedTests: typecheck.numFailedTests + runtime.numFailedTests,
1764
1802
  numPassedTests: typecheck.numPassedTests + runtime.numPassedTests,
1765
1803
  numPendingTests: typecheck.numPendingTests + runtime.numPendingTests,
1804
+ numTodoTests: (typecheck.numTodoTests ?? 0) + (runtime.numTodoTests ?? 0),
1766
1805
  numTotalTests: typecheck.numTotalTests + runtime.numTotalTests,
1767
1806
  startTime: Math.min(typecheck.startTime, runtime.startTime),
1768
1807
  success: typecheck.success && runtime.success,
@@ -1,12 +1,12 @@
1
1
  import assert from "node:assert";
2
+ import * as fs from "node:fs";
3
+ import { existsSync } from "node:fs";
2
4
  import * as path$1 from "node:path";
3
5
  import path from "node:path";
4
6
  import process from "node:process";
5
7
  import color from "tinyrainbow";
6
8
  import { WebSocketServer } from "ws";
7
9
  import { type } from "arktype";
8
- import * as fs from "node:fs";
9
- import { existsSync } from "node:fs";
10
10
  import { homedir, tmpdir } from "node:os";
11
11
  import * as crypto from "node:crypto";
12
12
  import { randomUUID } from "node:crypto";
@@ -82,15 +82,15 @@ function parseJestOutput(output) {
82
82
  };
83
83
  } catch {}
84
84
  }
85
- const jsonStr = extractJsonFromOutput(output);
86
- if (jsonStr === void 0) throw new Error(`No valid Jest result JSON found in output, output was:\n${output}`);
87
- return { result: validateJestResult(JSON.parse(jsonStr)) };
85
+ const jsonString = extractJsonFromOutput(output);
86
+ if (jsonString === void 0) throw new Error(`No valid Jest result JSON found in output, output was:\n${output}`);
87
+ return { result: validateJestResult(JSON.parse(jsonString)) };
88
88
  }
89
89
  function countBraces(line) {
90
90
  let count = 0;
91
- for (const char of line) {
92
- if (char === "{") count++;
93
- if (char === "}") count--;
91
+ for (const character of line) {
92
+ if (character === "{") count++;
93
+ if (character === "}") count--;
94
94
  }
95
95
  return count;
96
96
  }
@@ -311,6 +311,12 @@ function createFetchClient(defaultHeaders) {
311
311
  const OPEN_CLOUD_BASE_URL = "https://apis.roblox.com";
312
312
  const RATE_LIMIT_DEFAULT_WAIT_MS = 5e3;
313
313
  const MAX_RATE_LIMIT_RETRIES = 5;
314
+ const taskResponse = type({ path: "string" });
315
+ const taskStatusResponse = type({
316
+ "error?": { "message?": "string" },
317
+ "output?": { "results?": "string[]" },
318
+ "state": "'CANCELLED' | 'COMPLETE' | 'FAILED' | 'PROCESSING'"
319
+ });
314
320
  var OpenCloudBackend = class {
315
321
  credentials;
316
322
  http;
@@ -319,7 +325,7 @@ var OpenCloudBackend = class {
319
325
  constructor(credentials, options) {
320
326
  this.credentials = credentials;
321
327
  this.http = options?.http ?? createFetchClient({ "x-api-key": credentials.apiKey });
322
- this.readFile = options?.readFile ?? ((fp) => fs.readFileSync(fp));
328
+ this.readFile = options?.readFile ?? ((filePath) => fs.readFileSync(filePath));
323
329
  this.sleepFn = options?.sleep ?? (async (ms) => {
324
330
  return new Promise((resolve) => {
325
331
  setTimeout(resolve, ms);
@@ -334,14 +340,15 @@ var OpenCloudBackend = class {
334
340
  const placeData = this.readFile(placeFilePath);
335
341
  const fileHash = hashBuffer(placeData);
336
342
  const cacheKey = getCacheKey(this.credentials.universeId, this.credentials.placeId);
337
- let uploadCached = false;
338
343
  const cache = readCache(cacheFilePath);
339
- if (options.config.cache && isUploaded(cache, cacheKey, fileHash)) uploadCached = true;
340
- else {
341
- await this.uploadPlaceData(placeData);
342
- markUploaded(cache, cacheKey, fileHash);
343
- writeCache(cacheFilePath, cache);
344
- }
344
+ const uploadCached = await this.uploadOrReuseCached({
345
+ cache,
346
+ cacheEnabled: options.config.cache,
347
+ cacheFilePath,
348
+ cacheKey,
349
+ fileHash,
350
+ placeData
351
+ });
345
352
  const uploadMs = Date.now() - uploadStart;
346
353
  const executionStart = Date.now();
347
354
  const taskPath = await this.createExecutionTask(options);
@@ -369,7 +376,7 @@ var OpenCloudBackend = class {
369
376
  timeout: `${Math.floor(options.config.timeout / 1e3)}s`
370
377
  } });
371
378
  if (!response.ok) throw new Error(`Failed to create execution task: ${response.status}`);
372
- return response.body.path;
379
+ return taskResponse.assert(response.body).path;
373
380
  }
374
381
  async pollForCompletion(taskPath, timeoutMs, pollIntervalMs) {
375
382
  const url = `${OPEN_CLOUD_BASE_URL}/cloud/v2/${taskPath}`;
@@ -385,7 +392,7 @@ var OpenCloudBackend = class {
385
392
  continue;
386
393
  }
387
394
  if (!response.ok) throw new Error(`Failed to poll task: ${response.status}`);
388
- const body = response.body;
395
+ const body = taskStatusResponse.assert(response.body);
389
396
  switch (body.state) {
390
397
  case "COMPLETE": {
391
398
  const value = body.output?.results?.[0];
@@ -404,6 +411,13 @@ var OpenCloudBackend = class {
404
411
  }
405
412
  throw new Error("Execution timed out");
406
413
  }
414
+ async uploadOrReuseCached({ cache, cacheEnabled, cacheFilePath, cacheKey, fileHash, placeData }) {
415
+ if (cacheEnabled && isUploaded(cache, cacheKey, fileHash)) return true;
416
+ await this.uploadPlaceData(placeData);
417
+ markUploaded(cache, cacheKey, fileHash);
418
+ writeCache(cacheFilePath, cache);
419
+ return false;
420
+ }
407
421
  async uploadPlaceData(placeData) {
408
422
  const url = `${OPEN_CLOUD_BASE_URL}/universes/v1/${this.credentials.universeId}/places/${this.credentials.placeId}/versions?versionType=Saved`;
409
423
  const response = await this.http.request("POST", url, {
@@ -415,10 +429,10 @@ var OpenCloudBackend = class {
415
429
  };
416
430
  function createOpenCloudBackend() {
417
431
  const apiKey = process.env["ROBLOX_OPEN_CLOUD_API_KEY"];
418
- const universeId = process.env["ROBLOX_UNIVERSE_ID"];
419
- const placeId = process.env["ROBLOX_PLACE_ID"];
420
432
  if (apiKey === void 0) throw new Error("ROBLOX_OPEN_CLOUD_API_KEY environment variable is required");
433
+ const universeId = process.env["ROBLOX_UNIVERSE_ID"];
421
434
  if (universeId === void 0) throw new Error("ROBLOX_UNIVERSE_ID environment variable is required");
435
+ const placeId = process.env["ROBLOX_PLACE_ID"];
422
436
  if (placeId === void 0) throw new Error("ROBLOX_PLACE_ID environment variable is required");
423
437
  return new OpenCloudBackend({
424
438
  apiKey,
@@ -436,6 +450,7 @@ function parseRetryAfter(headers) {
436
450
 
437
451
  //#endregion
438
452
  //#region src/backends/studio.ts
453
+ const DEFAULT_STUDIO_TIMEOUT = 3e5;
439
454
  const pluginMessageSchema = type({
440
455
  "gameOutput?": "string",
441
456
  "jestOutput": "string",
@@ -449,7 +464,7 @@ var StudioBackend = class {
449
464
  preConnected;
450
465
  constructor(options) {
451
466
  this.port = options.port;
452
- this.timeout = options.timeout ?? 3e5;
467
+ this.timeout = options.timeout ?? DEFAULT_STUDIO_TIMEOUT;
453
468
  this.createServer = options.createServer ?? ((port) => new WebSocketServer({ port }));
454
469
  this.preConnected = options.preConnected;
455
470
  }
@@ -573,7 +588,11 @@ const DEFAULT_CONFIG = {
573
588
  "**/*.test.lua",
574
589
  "**/*.test.luau"
575
590
  ],
576
- testPathIgnorePatterns: ["/node_modules/", "/dist/"],
591
+ testPathIgnorePatterns: [
592
+ "/node_modules/",
593
+ "/dist/",
594
+ "/out/"
595
+ ],
577
596
  timeout: 3e5,
578
597
  typecheck: false,
579
598
  typecheckOnly: false,
@@ -630,6 +649,14 @@ async function loadConfig$1(configPath, cwd = process.cwd()) {
630
649
  return resolveConfig(result.config);
631
650
  }
632
651
 
652
+ //#endregion
653
+ //#region src/types/rojo.ts
654
+ const rojoProjectSchema = type({
655
+ "name": "string",
656
+ "servePort?": "number.integer",
657
+ "tree": "object"
658
+ }).as();
659
+
633
660
  //#endregion
634
661
  //#region src/utils/normalize-windows-path.ts
635
662
  const DRIVE_LETTER_START_REGEX = /^[A-Za-z]:\//;
@@ -666,7 +693,7 @@ function createPathResolver(rojoProject, config) {
666
693
  if (key.startsWith("$") || typeof value !== "object") continue;
667
694
  const dataModelPath = prefix ? `${prefix}.${key}` : key;
668
695
  const node = value;
669
- if (typeof node["$path"] === "string") mappings.set(dataModelPath, node["$path"]);
696
+ if (typeof node.$path === "string") mappings.set(dataModelPath, node.$path);
670
697
  walkTree(node, dataModelPath);
671
698
  }
672
699
  }
@@ -876,7 +903,7 @@ function hasExecError(file) {
876
903
 
877
904
  //#endregion
878
905
  //#region src/utils/banner.ts
879
- const SEPARATOR = "⎯";
906
+ const SEPARATOR$1 = "⎯";
880
907
  const levelStyles = {
881
908
  error: {
882
909
  badge: (text) => color.bgRed(color.white(color.bold(text))),
@@ -895,7 +922,7 @@ function formatBannerBar({ level, termWidth, title }) {
895
922
  const remaining = width - badgeText.length;
896
923
  const leftWidth = Math.max(1, Math.floor(remaining / 2));
897
924
  const rightWidth = Math.max(1, remaining - leftWidth);
898
- return `${styles.separator(SEPARATOR.repeat(leftWidth))}${badge}${styles.separator(SEPARATOR.repeat(rightWidth))}`;
925
+ return `${styles.separator(SEPARATOR$1.repeat(leftWidth))}${badge}${styles.separator(SEPARATOR$1.repeat(rightWidth))}`;
899
926
  }
900
927
  function formatBanner({ body, level, termWidth, title }) {
901
928
  const width = termWidth ?? getDefaultWidth();
@@ -905,7 +932,7 @@ function formatBanner({ body, level, termWidth, title }) {
905
932
  termWidth: width,
906
933
  title
907
934
  });
908
- const closing = styles.separator(SEPARATOR.repeat(width));
935
+ const closing = styles.separator(SEPARATOR$1.repeat(width));
909
936
  return `\n${header}\n${body.length > 0 ? `\n${body.join("\n")}\n` : ""}\n${closing}\n\n`;
910
937
  }
911
938
  function getDefaultWidth() {
@@ -1106,27 +1133,27 @@ function cleanExecErrorMessage(raw) {
1106
1133
  }
1107
1134
  function formatSourceSnippet(snippet, filePath, options) {
1108
1135
  const useColor = options?.useColor ?? true;
1109
- const st = options?.styles ?? createStyles(useColor);
1136
+ const styles = options?.styles ?? createStyles(useColor);
1110
1137
  const language = options?.language;
1111
1138
  const lines = [];
1112
1139
  const indent = " ";
1113
1140
  const location = snippet.column !== void 0 ? `${filePath}:${snippet.failureLine}:${snippet.column}` : `${filePath}:${snippet.failureLine}`;
1114
- const langSuffix = language !== void 0 ? st.dim(` (${language})`) : "";
1115
- lines.push(st.location(` ❯ ${location}`) + langSuffix);
1141
+ const langSuffix = language !== void 0 ? styles.dim(` (${language})`) : "";
1142
+ lines.push(styles.location(` ❯ ${location}`) + langSuffix);
1116
1143
  const maxLineNumber = Math.max(...snippet.lines.map((line) => line.num));
1117
1144
  const padding = String(maxLineNumber).length;
1118
1145
  for (const line of snippet.lines) {
1119
1146
  const prefix = `${String(line.num).padStart(padding)}|`;
1120
1147
  const highlighted = highlightSyntax(filePath, expandTabs(line.content), useColor);
1121
1148
  if (line.num === snippet.failureLine) {
1122
- lines.push(`${indent}${st.lineNumber(prefix)} ${highlighted}`);
1149
+ lines.push(`${indent}${styles.lineNumber(prefix)} ${highlighted}`);
1123
1150
  if (snippet.column !== void 0) {
1124
1151
  const beforeColumn = expandTabs(line.content.slice(0, snippet.column - 1));
1125
1152
  const caretGutter = `${" ".repeat(padding)}|`;
1126
- const gutterPrefix = st.lineNumber(caretGutter);
1127
- lines.push(`${indent}${gutterPrefix} ${" ".repeat(beforeColumn.length)}${st.status.fail("^")}`);
1153
+ const gutterPrefix = styles.lineNumber(caretGutter);
1154
+ lines.push(`${indent}${gutterPrefix} ${" ".repeat(beforeColumn.length)}${styles.status.fail("^")}`);
1128
1155
  }
1129
- } else lines.push(`${indent}${st.lineNumber(prefix)} ${highlighted}`);
1156
+ } else lines.push(`${indent}${styles.lineNumber(prefix)} ${highlighted}`);
1130
1157
  }
1131
1158
  return lines.join("\n");
1132
1159
  }
@@ -1305,27 +1332,27 @@ function highlightSyntax(filePath, code, useColor) {
1305
1332
  if (!useColor) return code;
1306
1333
  return highlightCode(filePath, code);
1307
1334
  }
1308
- function formatDiffBlock(parsed, st) {
1335
+ function formatDiffBlock(parsed, styles) {
1309
1336
  if (parsed.snapshotDiff !== void 0) {
1310
1337
  const lines = [""];
1311
- for (const diffLine of parsed.snapshotDiff.split("\n")) if (diffLine.startsWith("- ")) lines.push(st.diff.expected(diffLine));
1312
- else if (diffLine.startsWith("+ ")) lines.push(st.diff.received(diffLine));
1313
- else lines.push(st.dim(diffLine));
1338
+ for (const diffLine of parsed.snapshotDiff.split("\n")) if (diffLine.startsWith("- ")) lines.push(styles.diff.expected(diffLine));
1339
+ else if (diffLine.startsWith("+ ")) lines.push(styles.diff.received(diffLine));
1340
+ else lines.push(styles.dim(diffLine));
1314
1341
  return lines;
1315
1342
  }
1316
1343
  if (parsed.expected !== void 0 && parsed.received !== void 0) return [
1317
1344
  "",
1318
- st.diff.expected("- Expected"),
1319
- st.diff.received("+ Received"),
1345
+ styles.diff.expected("- Expected"),
1346
+ styles.diff.received("+ Received"),
1320
1347
  "",
1321
- st.diff.expected(`- ${parsed.expected}`),
1322
- st.diff.received(`+ ${parsed.received}`)
1348
+ styles.diff.expected(`- ${parsed.expected}`),
1349
+ styles.diff.received(`+ ${parsed.received}`)
1323
1350
  ];
1324
1351
  return [];
1325
1352
  }
1326
- function formatErrorLine(parsed, st, useColor) {
1327
- if (useColor && parsed.message.startsWith("Error:")) return st.status.fail(color.bold("Error:") + parsed.message.slice(6));
1328
- return st.status.fail(parsed.message);
1353
+ function formatErrorLine(parsed, styles, useColor) {
1354
+ if (useColor && parsed.message.startsWith("Error:")) return styles.status.fail(color.bold("Error:") + parsed.message.slice(6));
1355
+ return styles.status.fail(parsed.message);
1329
1356
  }
1330
1357
  function formatFallbackSnippet(message, styles, useColor) {
1331
1358
  const location = parseSourceLocation(message);
@@ -1342,7 +1369,7 @@ function formatFallbackSnippet(message, styles, useColor) {
1342
1369
  useColor
1343
1370
  })];
1344
1371
  }
1345
- function formatMappedLocationSnippets(loc, showLuau, st, useColor) {
1372
+ function formatMappedLocationSnippets(loc, showLuau, styles, useColor) {
1346
1373
  const snippets = [];
1347
1374
  if (loc.tsPath !== void 0 && loc.tsLine !== void 0) {
1348
1375
  const tsSnippet = getSourceSnippet({
@@ -1356,7 +1383,7 @@ function formatMappedLocationSnippets(loc, showLuau, st, useColor) {
1356
1383
  const label = showLuau ? "TypeScript" : void 0;
1357
1384
  snippets.push("", formatSourceSnippet(tsSnippet, loc.tsPath, {
1358
1385
  language: label,
1359
- styles: st,
1386
+ styles,
1360
1387
  useColor
1361
1388
  }));
1362
1389
  }
@@ -1368,7 +1395,7 @@ function formatMappedLocationSnippets(loc, showLuau, st, useColor) {
1368
1395
  });
1369
1396
  if (luauSnippet !== void 0) snippets.push("", formatSourceSnippet(luauSnippet, loc.luauPath, {
1370
1397
  language: "Luau",
1371
- styles: st,
1398
+ styles,
1372
1399
  useColor
1373
1400
  }));
1374
1401
  }
@@ -1379,7 +1406,7 @@ function formatMappedLocationSnippets(loc, showLuau, st, useColor) {
1379
1406
  line: loc.luauLine
1380
1407
  });
1381
1408
  if (luauSnippet !== void 0) snippets.push("", formatSourceSnippet(luauSnippet, loc.luauPath, {
1382
- styles: st,
1409
+ styles,
1383
1410
  useColor
1384
1411
  }));
1385
1412
  }
@@ -1406,22 +1433,24 @@ function formatSnapshotCallSnippet(filePath, styles, useColor) {
1406
1433
  })];
1407
1434
  }
1408
1435
  function resolveSourceSnippets(options) {
1409
- const { filePath, hasSnapshotDiff, mappedLocations, message, showLuau, sourceMapper, styles: st, useColor } = options;
1410
- if (mappedLocations.length > 0) return mappedLocations.flatMap((loc) => formatMappedLocationSnippets(loc, showLuau, st, useColor));
1411
- const fallback = formatFallbackSnippet(message, st, useColor);
1436
+ const { filePath, hasSnapshotDiff, mappedLocations, message, showLuau, sourceMapper, styles, useColor } = options;
1437
+ if (mappedLocations.length > 0) return mappedLocations.flatMap((loc) => {
1438
+ return formatMappedLocationSnippets(loc, showLuau, styles, useColor);
1439
+ });
1440
+ const fallback = formatFallbackSnippet(message, styles, useColor);
1412
1441
  if (fallback.length > 0) return fallback;
1413
- if (hasSnapshotDiff && filePath !== void 0) return formatSnapshotCallSnippet(resolveDisplayPath(filePath, sourceMapper), st, useColor);
1442
+ if (hasSnapshotDiff && filePath !== void 0) return formatSnapshotCallSnippet(resolveDisplayPath(filePath, sourceMapper), styles, useColor);
1414
1443
  return [];
1415
1444
  }
1416
1445
  function formatFailureMessage(originalMessage, options) {
1417
- const { filePath, showLuau, sourceMapper, styles: st, useColor } = options;
1446
+ const { filePath, showLuau, sourceMapper, styles, useColor } = options;
1418
1447
  let mappedLocations = [];
1419
1448
  let message = originalMessage;
1420
1449
  if (sourceMapper !== void 0) ({locations: mappedLocations, message} = sourceMapper.mapFailureWithLocations(originalMessage));
1421
1450
  const parsed = parseErrorMessage(originalMessage);
1422
1451
  return [
1423
- formatErrorLine(parsed, st, useColor),
1424
- ...formatDiffBlock(parsed, st),
1452
+ formatErrorLine(parsed, styles, useColor),
1453
+ ...formatDiffBlock(parsed, styles),
1425
1454
  ...resolveSourceSnippets({
1426
1455
  filePath,
1427
1456
  hasSnapshotDiff: parsed.snapshotDiff !== void 0,
@@ -1429,19 +1458,19 @@ function formatFailureMessage(originalMessage, options) {
1429
1458
  message,
1430
1459
  showLuau,
1431
1460
  sourceMapper,
1432
- styles: st,
1461
+ styles,
1433
1462
  useColor
1434
1463
  })
1435
1464
  ];
1436
1465
  }
1437
- function formatSnapshotLine(snapshot, st) {
1466
+ function formatSnapshotLine(snapshot, styles) {
1438
1467
  if (snapshot === void 0 || snapshot.unmatched === 0) return;
1439
- return `${st.dim(" Snapshots")} ${st.summary.failed(`${snapshot.unmatched} failed`)}`;
1468
+ return `${styles.dim(" Snapshots")} ${styles.summary.failed(`${snapshot.unmatched} failed`)}`;
1440
1469
  }
1441
1470
  function formatFileFailures(file, options, styles, failureCtx) {
1442
1471
  const lines = [];
1443
1472
  const displayPath = resolveDisplayPath(file.testFilePath, options.sourceMapper);
1444
- for (const tc of file.testResults) if (tc.status === "failed") {
1473
+ for (const testCase of file.testResults) if (testCase.status === "failed") {
1445
1474
  const index = failureCtx.currentIndex;
1446
1475
  failureCtx.currentIndex++;
1447
1476
  lines.push(formatFailure({
@@ -1450,7 +1479,7 @@ function formatFileFailures(file, options, styles, failureCtx) {
1450
1479
  showLuau: options.showLuau,
1451
1480
  sourceMapper: options.sourceMapper,
1452
1481
  styles,
1453
- test: tc,
1482
+ test: testCase,
1454
1483
  totalFailures: failureCtx.totalFailures,
1455
1484
  useColor: options.color
1456
1485
  }));
@@ -1480,24 +1509,24 @@ function formatLogHints(options, styles) {
1480
1509
  if (options.gameOutput !== void 0) lines.push(styles.dim(` View ${options.gameOutput} for Roblox game logs`));
1481
1510
  return lines.join("\n");
1482
1511
  }
1483
- function formatTestInGroup(tc, styles) {
1484
- const duration = tc.duration !== void 0 ? styles.lineNumber(` ${tc.duration}ms`) : "";
1485
- if (tc.status === "passed") return `${styles.status.pass(" ✓")}${styles.status.fail(` ${tc.title}`)}${duration}`;
1486
- const failedText = ` × ${tc.title}`;
1512
+ function formatTestInGroup(testCase, styles) {
1513
+ const duration = testCase.duration !== void 0 ? styles.lineNumber(` ${testCase.duration}ms`) : "";
1514
+ if (testCase.status === "passed") return `${styles.status.pass(" ✓")}${styles.status.fail(` ${testCase.title}`)}${duration}`;
1515
+ const failedText = ` × ${testCase.title}`;
1487
1516
  return `${styles.status.fail(failedText)}${duration}`;
1488
1517
  }
1489
1518
  function formatDescribeGroup(describeName, tests, styles) {
1490
1519
  const lines = [];
1491
- const groupHasFailure = tests.some((tc) => tc.status === "failed");
1520
+ const groupHasFailure = tests.some((testCase) => testCase.status === "failed");
1492
1521
  const groupTestCount = tests.length;
1493
- const groupDuration = tests.reduce((sum, tc) => sum + (tc.duration ?? 0), 0);
1522
+ const groupDuration = tests.reduce((sum, testCase) => sum + (testCase.duration ?? 0), 0);
1494
1523
  const groupDurationStr = styles.lineNumber(` ${groupDuration}ms`);
1495
1524
  if (groupHasFailure) {
1496
- const failedCount = tests.filter((tc) => tc.status === "failed").length;
1525
+ const failedCount = tests.filter((testCase) => testCase.status === "failed").length;
1497
1526
  const groupMeta = styles.dim(`(${groupTestCount} tests | `) + styles.summary.failed(`${failedCount} failed`) + styles.dim(")");
1498
1527
  const header = styles.status.fail(` ❯ ${describeName}`);
1499
1528
  lines.push(`${header} ${groupMeta}${groupDurationStr}`);
1500
- for (const tc of tests) lines.push(formatTestInGroup(tc, styles));
1529
+ for (const testCase of tests) lines.push(formatTestInGroup(testCase, styles));
1501
1530
  } else {
1502
1531
  const groupMeta = styles.dim(`(${groupTestCount} tests)`);
1503
1532
  const marker = styles.status.pass(" ✓");
@@ -1556,7 +1585,7 @@ function formatPassedFileSummary(file, ctx) {
1556
1585
  const meta = ctx.styles.dim(`(${ctx.testCount} tests${duration})`);
1557
1586
  lines.push(` ${symbol} ${ctx.formattedPath} ${meta}`);
1558
1587
  if (ctx.verbose) {
1559
- for (const tc of file.testResults) if (tc.status === "passed") lines.push(formatPass(tc, ctx.styles));
1588
+ for (const testCase of file.testResults) if (testCase.status === "passed") lines.push(formatPass(testCase, ctx.styles));
1560
1589
  }
1561
1590
  return lines;
1562
1591
  }
@@ -1596,7 +1625,7 @@ function formatCompact(result, options) {
1596
1625
  lines.push(...failureLines);
1597
1626
  }
1598
1627
  for (const file of execErrors) {
1599
- const relativePath = makeRelative(resolveDisplayPath(file.testFilePath, options.sourceMapper), options.rootDir);
1628
+ const relativePath = makeRelative$1(resolveDisplayPath(file.testFilePath, options.sourceMapper), options.rootDir);
1600
1629
  assert(file.failureMessage !== void 0, "exec error files have failureMessage");
1601
1630
  const errorMessage = cleanExecErrorMessage(file.failureMessage);
1602
1631
  lines.push(`[FAIL] ${relativePath} - suite failed to run`, errorMessage);
@@ -1615,7 +1644,7 @@ function formatCompactLogHints(options) {
1615
1644
  if (options.gameOutput !== void 0) lines.push(`View ${options.gameOutput} for Roblox game logs`);
1616
1645
  return lines.join("\n");
1617
1646
  }
1618
- function makeRelative(filePath, rootDirectory) {
1647
+ function makeRelative$1(filePath, rootDirectory) {
1619
1648
  if (filePath.startsWith(rootDirectory)) return path.relative(rootDirectory, filePath);
1620
1649
  return filePath;
1621
1650
  }
@@ -1683,7 +1712,7 @@ function formatCompactFailure(test, filePath, options) {
1683
1712
  if (options.sourceMapper !== void 0) ({locations: mappedLocations, message} = options.sourceMapper.mapFailureWithLocations(originalMessage));
1684
1713
  const parsed = parseErrorMessage(originalMessage);
1685
1714
  const location = findFailureLocation(mappedLocations, message);
1686
- const relativePath = makeRelative(location?.path ?? filePath, options.rootDir);
1715
+ const relativePath = makeRelative$1(location?.path ?? filePath, options.rootDir);
1687
1716
  const lineInfo = location?.line !== void 0 ? `:${location.line}` : "";
1688
1717
  lines.push(`[FAIL] ${relativePath}${lineInfo} - ${test.title}`);
1689
1718
  if (parsed.snapshotDiff !== void 0) lines.push(parsed.snapshotDiff);
@@ -1739,7 +1768,7 @@ function buildMappings(tree, prefix) {
1739
1768
  if (key.startsWith("$") || typeof value !== "object") continue;
1740
1769
  const dataModelPath = prefix ? `${prefix}/${key}` : key;
1741
1770
  const node = value;
1742
- if (typeof node["$path"] === "string") mappings.push([dataModelPath, node["$path"]]);
1771
+ if (typeof node.$path === "string") mappings.push([dataModelPath, node.$path]);
1743
1772
  mappings.push(...buildMappings(node, dataModelPath));
1744
1773
  }
1745
1774
  mappings.sort((a, b) => b[0].length - a[0].length);
@@ -1748,7 +1777,6 @@ function buildMappings(tree, prefix) {
1748
1777
 
1749
1778
  //#endregion
1750
1779
  //#region src/executor.ts
1751
- const rojoProjectSchema = type({ tree: "object" });
1752
1780
  function isLuauProject(testFiles, tsconfigDirectories) {
1753
1781
  if (tsconfigDirectories.outDir !== void 0) return false;
1754
1782
  if (testFiles.some((file) => /\.tsx?$/.test(file))) return false;
@@ -1820,7 +1848,8 @@ async function execute(options) {
1820
1848
  exitCode: result.success ? 0 : 1,
1821
1849
  gameOutput,
1822
1850
  output,
1823
- result
1851
+ result,
1852
+ sourceMapper
1824
1853
  };
1825
1854
  }
1826
1855
  function normalizeDirectoryPath(directory) {
@@ -1878,10 +1907,13 @@ const instrumentedFileRecordSchema = type({
1878
1907
  const coverageManifestSchema = type({
1879
1908
  files: type("Record<string, unknown>").pipe((files) => {
1880
1909
  const validated = {};
1910
+ const skipped = [];
1881
1911
  for (const [key, value] of Object.entries(files)) {
1882
1912
  const parsed = instrumentedFileRecordSchema(value);
1883
- if (!(parsed instanceof type.errors)) validated[key] = parsed;
1913
+ if (parsed instanceof type.errors) skipped.push(key);
1914
+ else validated[key] = parsed;
1884
1915
  }
1916
+ if (skipped.length > 0) process.stderr.write(`Warning: ${skipped.length} file record(s) in coverage manifest failed validation and were skipped: ${skipped.join(", ")}\n`);
1885
1917
  return validated;
1886
1918
  }),
1887
1919
  generatedAt: "string",
@@ -1951,6 +1983,154 @@ function writeSnapshots(snapshotWrites, config, tsconfigDirectories) {
1951
1983
  }
1952
1984
  }
1953
1985
 
1986
+ //#endregion
1987
+ //#region src/formatters/github-actions.ts
1988
+ const SEPARATOR = " · ";
1989
+ function escapeData(value) {
1990
+ return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
1991
+ }
1992
+ function escapeProperty(value) {
1993
+ return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A").replace(/:/g, "%3A").replace(/,/g, "%2C");
1994
+ }
1995
+ function formatAnnotation(annotation) {
1996
+ const properties = [`file=${escapeProperty(annotation.file)}`];
1997
+ if (annotation.line !== void 0) properties.push(`line=${String(annotation.line)}`);
1998
+ if (annotation.col !== void 0) properties.push(`col=${String(annotation.col)}`);
1999
+ if (annotation.title !== void 0) properties.push(`title=${escapeProperty(annotation.title)}`);
2000
+ return `::error ${properties.join(",")}::${escapeData(annotation.message)}`;
2001
+ }
2002
+ function collectAnnotations(result, options) {
2003
+ const annotations = [];
2004
+ for (const file of result.testResults) {
2005
+ if (hasExecError(file)) {
2006
+ collectExecErrorAnnotation(annotations, file, options);
2007
+ continue;
2008
+ }
2009
+ collectTestFailureAnnotations(annotations, file, options);
2010
+ }
2011
+ return annotations;
2012
+ }
2013
+ function formatAnnotations(result, options) {
2014
+ const annotations = collectAnnotations(result, options);
2015
+ if (annotations.length === 0) return "";
2016
+ return annotations.map(formatAnnotation).join("\n");
2017
+ }
2018
+ function formatJobSummary(result, options) {
2019
+ const fileLink = createFileLink(options);
2020
+ const lines = ["## Test Results\n", renderStats(result)];
2021
+ const failures = [];
2022
+ for (const file of result.testResults) {
2023
+ if (hasExecError(file)) {
2024
+ failures.push({
2025
+ file: makeRelative(file.testFilePath, options.workspace),
2026
+ title: "Test suite failed to run"
2027
+ });
2028
+ continue;
2029
+ }
2030
+ for (const test of file.testResults) {
2031
+ if (test.status !== "failed") continue;
2032
+ failures.push({
2033
+ file: makeRelative(file.testFilePath, options.workspace),
2034
+ title: test.fullName
2035
+ });
2036
+ }
2037
+ }
2038
+ if (failures.length > 0) {
2039
+ lines.push("### Failures\n");
2040
+ for (const failure of failures) {
2041
+ const link = fileLink(failure.file);
2042
+ const fileRef = link !== void 0 ? `[${failure.file}](${link})` : failure.file;
2043
+ lines.push(`- **${failure.title}** in ${fileRef}`);
2044
+ }
2045
+ lines.push("");
2046
+ }
2047
+ return lines.join("\n");
2048
+ }
2049
+ function resolveGitHubActionsOptions(userOptions, sourceMapper, environment = process.env) {
2050
+ return {
2051
+ repository: userOptions.jobSummary?.fileLinks?.repository ?? environment["GITHUB_REPOSITORY"],
2052
+ serverUrl: environment["GITHUB_SERVER_URL"],
2053
+ sha: userOptions.jobSummary?.fileLinks?.commitHash ?? environment["GITHUB_SHA"],
2054
+ sourceMapper,
2055
+ workspace: userOptions.jobSummary?.fileLinks?.workspacePath ?? environment["GITHUB_WORKSPACE"]
2056
+ };
2057
+ }
2058
+ function makeRelative(filePath, workspace) {
2059
+ if (workspace === void 0) return filePath;
2060
+ const normalized = filePath.replace(/\\/g, "/");
2061
+ const normalizedWorkspace = workspace.replace(/\\/g, "/").replace(/\/$/, "");
2062
+ if (normalized.startsWith(`${normalizedWorkspace}/`)) return normalized.slice(normalizedWorkspace.length + 1);
2063
+ return filePath;
2064
+ }
2065
+ function collectExecErrorAnnotation(annotations, file, options) {
2066
+ annotations.push({
2067
+ file: makeRelative(file.testFilePath, options.workspace),
2068
+ message: file.failureMessage,
2069
+ title: "Test suite failed to run"
2070
+ });
2071
+ }
2072
+ function collectTestFailureAnnotations(annotations, file, options) {
2073
+ for (const test of file.testResults) {
2074
+ if (test.status !== "failed") continue;
2075
+ const firstFailure = test.failureMessages[0] ?? "";
2076
+ let annotationFile = file.testFilePath;
2077
+ let line;
2078
+ let column;
2079
+ if (options.sourceMapper !== void 0 && firstFailure !== "") {
2080
+ const location = options.sourceMapper.mapFailureWithLocations(firstFailure).locations[0];
2081
+ if (location?.tsPath !== void 0) {
2082
+ annotationFile = location.tsPath;
2083
+ line = location.tsLine;
2084
+ column = location.tsColumn;
2085
+ } else if (location !== void 0) {
2086
+ annotationFile = location.luauPath;
2087
+ line = location.luauLine;
2088
+ }
2089
+ }
2090
+ annotations.push({
2091
+ col: column,
2092
+ file: makeRelative(annotationFile, options.workspace),
2093
+ line,
2094
+ message: firstFailure,
2095
+ title: test.fullName
2096
+ });
2097
+ }
2098
+ }
2099
+ function noun(count, singular, plural) {
2100
+ return count === 1 ? singular : plural;
2101
+ }
2102
+ function renderStats(result) {
2103
+ const failedFiles = result.testResults.filter((file) => file.numFailingTests > 0 || hasExecError(file)).length;
2104
+ const passedFiles = result.testResults.filter((file) => file.numFailingTests === 0 && !hasExecError(file)).length;
2105
+ const totalFiles = failedFiles + passedFiles;
2106
+ const fileInfo = [];
2107
+ if (failedFiles > 0) fileInfo.push(`❌ **${String(failedFiles)} ${noun(failedFiles, "failure", "failures")}**`);
2108
+ if (passedFiles > 0) fileInfo.push(`✅ **${String(passedFiles)} ${noun(passedFiles, "pass", "passes")}**`);
2109
+ fileInfo.push(`${String(totalFiles)} total`);
2110
+ const testInfo = [];
2111
+ if (result.numFailedTests > 0) testInfo.push(`❌ **${String(result.numFailedTests)} ${noun(result.numFailedTests, "failure", "failures")}**`);
2112
+ if (result.numPassedTests > 0) testInfo.push(`✅ **${String(result.numPassedTests)} ${noun(result.numPassedTests, "pass", "passes")}**`);
2113
+ const primaryTotal = result.numFailedTests + result.numPassedTests;
2114
+ testInfo.push(`${String(primaryTotal)} total`);
2115
+ let output = "### Summary\n\n";
2116
+ output += `- **Test Files**: ${fileInfo.join(SEPARATOR)}\n`;
2117
+ output += `- **Test Results**: ${testInfo.join(SEPARATOR)}\n`;
2118
+ const otherInfo = [];
2119
+ if (result.numPendingTests > 0) otherInfo.push(`${String(result.numPendingTests)} ${noun(result.numPendingTests, "skip", "skips")}`);
2120
+ if (result.numTodoTests !== void 0 && result.numTodoTests > 0) otherInfo.push(`${String(result.numTodoTests)} ${noun(result.numTodoTests, "todo", "todos")}`);
2121
+ if (otherInfo.length > 0) {
2122
+ const otherTotal = result.numPendingTests + (result.numTodoTests ?? 0);
2123
+ otherInfo.push(`${String(otherTotal)} total`);
2124
+ output += `- **Other**: ${otherInfo.join(SEPARATOR)}\n`;
2125
+ }
2126
+ return output;
2127
+ }
2128
+ function createFileLink(options) {
2129
+ const { repository, serverUrl, sha } = options;
2130
+ if (serverUrl === void 0 || repository === void 0 || sha === void 0) return (_filePath) => {};
2131
+ return (filePath) => `${serverUrl}/${repository}/blob/${sha}/${filePath}`;
2132
+ }
2133
+
1954
2134
  //#endregion
1955
2135
  //#region src/typecheck/collect.ts
1956
2136
  const TEST_FUNCTIONS = new Set(["it", "test"]);
@@ -2021,28 +2201,28 @@ function extractDefinition(node, source) {
2021
2201
 
2022
2202
  //#endregion
2023
2203
  //#region src/typecheck/parse.ts
2024
- const errCodeRegExp = /error TS(?<errCode>\d+)/;
2204
+ const errorCodeRegExp = /error TS(?<errorCode>\d+)/;
2025
2205
  function parseTscErrorLine(line) {
2026
2206
  const parenIndex = line.lastIndexOf("(", line.indexOf("): error TS"));
2027
2207
  if (parenIndex === -1) return ["", null];
2028
2208
  const filePath = line.slice(0, parenIndex);
2029
2209
  const rest = line.slice(parenIndex);
2030
2210
  const closeParenIndex = rest.indexOf(")");
2031
- const [lineStr, colStr] = rest.slice(1, closeParenIndex).split(",");
2032
- if (lineStr === void 0 || lineStr === "" || colStr === void 0 || colStr === "") return [filePath, null];
2211
+ const [lineString, columnString] = rest.slice(1, closeParenIndex).split(",");
2212
+ if (lineString === void 0 || lineString === "" || columnString === void 0 || columnString === "") return [filePath, null];
2033
2213
  const afterParen = rest.slice(closeParenIndex + 1);
2034
- const errCodeStr = errCodeRegExp.exec(afterParen)?.groups?.["errCode"];
2035
- if (errCodeStr === void 0) return [filePath, null];
2036
- const errCode = Number(errCodeStr);
2037
- const marker = `error TS${String(errCode)}: `;
2214
+ const errorCodeString = errorCodeRegExp.exec(afterParen)?.groups?.["errorCode"];
2215
+ if (errorCodeString === void 0) return [filePath, null];
2216
+ const errorCode = Number(errorCodeString);
2217
+ const marker = `error TS${String(errorCode)}: `;
2038
2218
  const markerIndex = afterParen.indexOf(marker);
2039
- const errMessage = afterParen.slice(markerIndex + marker.length).trim();
2219
+ const errorMessage = afterParen.slice(markerIndex + marker.length).trim();
2040
2220
  return [filePath, {
2041
- column: Number(colStr),
2042
- errCode,
2043
- errMsg: errMessage,
2221
+ column: Number(columnString),
2222
+ errorCode,
2223
+ errorMessage,
2044
2224
  filePath,
2045
- line: Number(lineStr)
2225
+ line: Number(lineString)
2046
2226
  }];
2047
2227
  }
2048
2228
  function parseTscOutput(stdout) {
@@ -2128,7 +2308,7 @@ function buildFileResult(filePath, fileInfo, errors) {
2128
2308
  for (const error of errors) {
2129
2309
  const charIndex = indexMap.get(`${String(error.line)}:${String(error.column)}`);
2130
2310
  const definition = charIndex !== void 0 ? sortedDefinitions.find((td) => td.start <= charIndex && td.end >= charIndex) : void 0;
2131
- const message = `TS${String(error.errCode)}: ${error.errMsg}`;
2311
+ const message = `TS${String(error.errorCode)}: ${error.errorMessage}`;
2132
2312
  if (definition) {
2133
2313
  const existing = errorsByTest.get(definition.name) ?? [];
2134
2314
  existing.push(message);
@@ -2152,7 +2332,7 @@ function buildFileResult(filePath, fileInfo, errors) {
2152
2332
  status: "failed",
2153
2333
  title: "<file-level type error>"
2154
2334
  });
2155
- const numberFailing = testCases.filter((tc) => tc.status === "failed").length;
2335
+ const numberFailing = testCases.filter((testCase) => testCase.status === "failed").length;
2156
2336
  return {
2157
2337
  numFailingTests: numberFailing,
2158
2338
  numPassingTests: testCases.length - numberFailing,
@@ -2209,4 +2389,4 @@ function writeGameOutput(filePath, entries) {
2209
2389
  }
2210
2390
 
2211
2391
  //#endregion
2212
- export { generateTestScript as C, parseJestOutput as E, buildJestArgv as S, extractJsonFromOutput as T, isValidBackend as _, execute as a, OpenCloudBackend as b, writeJsonFile as c, formatTestSummary as d, loadConfig$1 as f, defineConfig as g, VALID_BACKENDS as h, runTypecheck as i, formatFailure as l, DEFAULT_CONFIG as m, parseGameOutput as n, loadCoverageManifest as o, resolveConfig as p, writeGameOutput as r, formatJson as s, formatGameOutputNotice as t, formatResult as u, StudioBackend as v, LuauScriptError as w, createOpenCloudBackend as x, createStudioBackend as y };
2392
+ export { parseJestOutput as A, createStudioBackend as C, generateTestScript as D, buildJestArgv as E, LuauScriptError as O, StudioBackend as S, createOpenCloudBackend as T, resolveConfig as _, formatAnnotations as a, defineConfig as b, execute as c, writeJsonFile as d, formatFailure as f, loadConfig$1 as g, rojoProjectSchema as h, runTypecheck as i, extractJsonFromOutput as k, loadCoverageManifest as l, formatTestSummary as m, parseGameOutput as n, formatJobSummary as o, formatResult as p, writeGameOutput as r, resolveGitHubActionsOptions as s, formatGameOutputNotice as t, formatJson as u, DEFAULT_CONFIG as v, OpenCloudBackend as w, isValidBackend as x, VALID_BACKENDS as y };
package/dist/index.d.mts CHANGED
@@ -1,5 +1,6 @@
1
- import { a as SnapshotFormatOptions, i as ResolvedConfig, n as Config, o as defineConfig, r as DEFAULT_CONFIG, s as Argv, t as CliOptions } from "./schema-CG-f4OUB.mjs";
1
+ import { a as ResolvedConfig, c as Argv, i as FormatterEntry, n as Config, o as SnapshotFormatOptions, r as DEFAULT_CONFIG, s as defineConfig, t as CliOptions } from "./schema-DcDQmTyn.mjs";
2
2
  import { WebSocket, WebSocketServer } from "ws";
3
+ import "arktype";
3
4
  import buffer from "node:buffer";
4
5
 
5
6
  //#region src/coverage/types.d.ts
@@ -17,12 +18,18 @@ interface RawFileCoverage {
17
18
  type RawCoverageData = Record<string, RawFileCoverage>;
18
19
  //#endregion
19
20
  //#region src/types/jest-result.d.ts
20
- type TestStatus = "failed" | "passed" | "pending" | "skipped";
21
+ type TestStatus = "disabled" | "failed" | "passed" | "pending" | "skipped" | "todo";
21
22
  interface TestCaseResult {
22
23
  ancestorTitles: Array<string>;
23
24
  duration?: number;
24
25
  failureMessages: Array<string>;
25
26
  fullName: string;
27
+ location?: {
28
+ column: number;
29
+ line: number;
30
+ };
31
+ numPassingAsserts?: number;
32
+ retryReasons?: Array<string>;
26
33
  status: TestStatus;
27
34
  title: string;
28
35
  }
@@ -45,6 +52,7 @@ interface JestResult {
45
52
  numFailedTests: number;
46
53
  numPassedTests: number;
47
54
  numPendingTests: number;
55
+ numTodoTests?: number;
48
56
  numTotalTests: number;
49
57
  snapshot?: SnapshotSummary;
50
58
  startTime: number;
@@ -121,6 +129,7 @@ declare class OpenCloudBackend implements Backend {
121
129
  runTests(options: BackendOptions): Promise<BackendResult>;
122
130
  private createExecutionTask;
123
131
  private pollForCompletion;
132
+ private uploadOrReuseCached;
124
133
  private uploadPlaceData;
125
134
  }
126
135
  declare function createOpenCloudBackend(): OpenCloudBackend;
@@ -157,22 +166,6 @@ declare function createStudioBackend(options: StudioOptions): StudioBackend;
157
166
  declare function resolveConfig(config: Config): ResolvedConfig;
158
167
  declare function loadConfig(configPath?: string, cwd?: string): Promise<ResolvedConfig>;
159
168
  //#endregion
160
- //#region src/executor.d.ts
161
- interface ExecuteOptions {
162
- backend: Backend;
163
- config: ResolvedConfig;
164
- testFiles: Array<string>;
165
- version: string;
166
- }
167
- interface ExecuteResult {
168
- coverageData?: RawCoverageData;
169
- exitCode: number;
170
- gameOutput?: string;
171
- output: string;
172
- result: JestResult;
173
- }
174
- declare function execute(options: ExecuteOptions): Promise<ExecuteResult>;
175
- //#endregion
176
169
  //#region src/source-mapper/index.d.ts
177
170
  interface MappedLocation {
178
171
  luauLine: number;
@@ -192,6 +185,23 @@ interface SourceMapper {
192
185
  resolveTestFilePath(testFilePath: string): string | undefined;
193
186
  }
194
187
  //#endregion
188
+ //#region src/executor.d.ts
189
+ interface ExecuteOptions {
190
+ backend: Backend;
191
+ config: ResolvedConfig;
192
+ testFiles: Array<string>;
193
+ version: string;
194
+ }
195
+ interface ExecuteResult {
196
+ coverageData?: RawCoverageData;
197
+ exitCode: number;
198
+ gameOutput?: string;
199
+ output: string;
200
+ result: JestResult;
201
+ sourceMapper?: SourceMapper;
202
+ }
203
+ declare function execute(options: ExecuteOptions): Promise<ExecuteResult>;
204
+ //#endregion
195
205
  //#region src/types/timing.d.ts
196
206
  interface TimingResult {
197
207
  executionMs: number;
@@ -263,6 +273,76 @@ declare function formatFailure({
263
273
  declare function formatTestSummary(result: JestResult, timing: TimingResult, styles?: Styles): string;
264
274
  declare function formatResult(result: JestResult, timing: TimingResult, options: FormatOptions): string;
265
275
  //#endregion
276
+ //#region src/formatters/github-actions.d.ts
277
+ interface GitHubActionsOptions {
278
+ repository?: string;
279
+ serverUrl?: string;
280
+ sha?: string;
281
+ sourceMapper?: SourceMapper;
282
+ workspace?: string;
283
+ }
284
+ interface GitHubActionsFormatterOptions {
285
+ /**
286
+ * Whether to emit `::error` workflow commands for test failures.
287
+ *
288
+ * @default true
289
+ */
290
+ displayAnnotations?: boolean;
291
+ /**
292
+ * Configuration for the GitHub Actions Job Summary.
293
+ *
294
+ * When enabled, a markdown summary of test results is written to the path
295
+ * specified by `outputPath`.
296
+ */
297
+ jobSummary?: Partial<JobSummaryOptions>;
298
+ }
299
+ interface JobSummaryOptions {
300
+ /**
301
+ * Whether to generate the summary.
302
+ *
303
+ * @default true
304
+ */
305
+ enabled: boolean;
306
+ /**
307
+ * Configuration for generating permalink URLs to source files in the
308
+ * GitHub repository.
309
+ *
310
+ * When all three values are available (either from this config or the
311
+ * defaults picked from environment variables), test names in the summary
312
+ * will link to the relevant source lines.
313
+ */
314
+ fileLinks: {
315
+ /**
316
+ * The commit SHA to use in permalink URLs.
317
+ *
318
+ * @default process.env.GITHUB_SHA
319
+ */
320
+ commitHash?: string;
321
+ /**
322
+ * The GitHub repository in `owner/repo` format.
323
+ *
324
+ * @default process.env.GITHUB_REPOSITORY
325
+ */
326
+ repository?: string;
327
+ /**
328
+ * The absolute path to the root of the repository on disk.
329
+ *
330
+ * Used to compute relative file paths for the permalink URLs.
331
+ *
332
+ * @default process.env.GITHUB_WORKSPACE
333
+ */
334
+ workspacePath?: string;
335
+ };
336
+ /**
337
+ * File path to write the summary to.
338
+ *
339
+ * @default process.env.GITHUB_STEP_SUMMARY
340
+ */
341
+ outputPath: string | undefined;
342
+ }
343
+ declare function formatAnnotations(result: JestResult, options: GitHubActionsOptions): string;
344
+ declare function formatJobSummary(result: JestResult, options: GitHubActionsOptions): string;
345
+ //#endregion
266
346
  //#region src/formatters/json.d.ts
267
347
  declare function formatJson(result: JestResult): string;
268
348
  declare function writeJsonFile(result: JestResult, filePath: string): Promise<void>;
@@ -278,8 +358,8 @@ declare function generateTestScript(options: BackendOptions): string;
278
358
  //#region src/typecheck/types.d.ts
279
359
  interface TscErrorInfo {
280
360
  column: number;
281
- errCode: number;
282
- errMsg: string;
361
+ errorCode: number;
362
+ errorMessage: string;
283
363
  filePath: string;
284
364
  line: number;
285
365
  }
@@ -311,4 +391,4 @@ declare function formatGameOutputNotice(filePath: string, entryCount: number): s
311
391
  declare function parseGameOutput(raw: string | undefined): Array<GameOutputEntry>;
312
392
  declare function writeGameOutput(filePath: string, entries: Array<GameOutputEntry>): void;
313
393
  //#endregion
314
- export { type Backend, type BackendOptions, type CliOptions, type Config, DEFAULT_CONFIG, type ExecuteOptions, type ExecuteResult, type GameOutputEntry, type JestArgv, type JestResult, OpenCloudBackend, type ResolvedConfig, StudioBackend, type TestCaseResult, type TestDefinition, type TestFileResult, type TestStatus, type TscErrorInfo, type TypecheckOptions, buildJestArgv, createOpenCloudBackend, createStudioBackend, defineConfig, execute, extractJsonFromOutput, formatFailure, formatGameOutputNotice, formatJson, formatResult, formatTestSummary, generateTestScript, loadConfig, parseGameOutput, parseJestOutput, resolveConfig, runTypecheck, writeGameOutput, writeJsonFile };
394
+ export { type Backend, type BackendOptions, type CliOptions, type Config, DEFAULT_CONFIG, type ExecuteOptions, type ExecuteResult, type FormatterEntry, type GameOutputEntry, type GitHubActionsFormatterOptions, type JestArgv, type JestResult, OpenCloudBackend, type ResolvedConfig, StudioBackend, type TestCaseResult, type TestDefinition, type TestFileResult, type TestStatus, type TscErrorInfo, type TypecheckOptions, buildJestArgv, createOpenCloudBackend, createStudioBackend, defineConfig, execute, extractJsonFromOutput, formatAnnotations, formatFailure, formatGameOutputNotice, formatJobSummary, formatJson, formatResult, formatTestSummary, generateTestScript, loadConfig, parseGameOutput, parseJestOutput, resolveConfig, runTypecheck, writeGameOutput, writeJsonFile };
package/dist/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
- import { C as generateTestScript, E as parseJestOutput, S as buildJestArgv, T as extractJsonFromOutput, a as execute, b as OpenCloudBackend, c as writeJsonFile, d as formatTestSummary, f as loadConfig, g as defineConfig, i as runTypecheck, l as formatFailure, m as DEFAULT_CONFIG, n as parseGameOutput, p as resolveConfig, r as writeGameOutput, s as formatJson, t as formatGameOutputNotice, u as formatResult, v as StudioBackend, x as createOpenCloudBackend, y as createStudioBackend } from "./game-output-lpY5mxJ_.mjs";
1
+ import { A as parseJestOutput, C as createStudioBackend, D as generateTestScript, E as buildJestArgv, S as StudioBackend, T as createOpenCloudBackend, _ as resolveConfig, a as formatAnnotations, b as defineConfig, c as execute, d as writeJsonFile, f as formatFailure, g as loadConfig, i as runTypecheck, k as extractJsonFromOutput, m as formatTestSummary, n as parseGameOutput, o as formatJobSummary, p as formatResult, r as writeGameOutput, t as formatGameOutputNotice, u as formatJson, v as DEFAULT_CONFIG, w as OpenCloudBackend } from "./game-output-M8du29nj.mjs";
2
2
 
3
- export { DEFAULT_CONFIG, OpenCloudBackend, StudioBackend, buildJestArgv, createOpenCloudBackend, createStudioBackend, defineConfig, execute, extractJsonFromOutput, formatFailure, formatGameOutputNotice, formatJson, formatResult, formatTestSummary, generateTestScript, loadConfig, parseGameOutput, parseJestOutput, resolveConfig, runTypecheck, writeGameOutput, writeJsonFile };
3
+ export { DEFAULT_CONFIG, OpenCloudBackend, StudioBackend, buildJestArgv, createOpenCloudBackend, createStudioBackend, defineConfig, execute, extractJsonFromOutput, formatAnnotations, formatFailure, formatGameOutputNotice, formatJobSummary, formatJson, formatResult, formatTestSummary, generateTestScript, loadConfig, parseGameOutput, parseJestOutput, resolveConfig, runTypecheck, writeGameOutput, writeJsonFile };
@@ -1,4 +1,4 @@
1
- import { DefineConfig } from "c12";
1
+ import { ReportOptions } from "istanbul-reports";
2
2
 
3
3
  //#region ../../node_modules/.pnpm/@rbxts+jest@3.13.3-ts.1/node_modules/@rbxts/jest/src/config.d.ts
4
4
  interface ReporterConfig {
@@ -760,6 +760,8 @@ type _Except<ObjectType, KeysType extends keyof ObjectType, Options extends Requ
760
760
  //#endregion
761
761
  //#region src/config/schema.d.ts
762
762
  type Backend = "auto" | "open-cloud" | "studio";
763
+ type CoverageReporter = keyof ReportOptions;
764
+ type FormatterEntry = [string, Record<string, unknown>] | string;
763
765
  interface SnapshotFormatOptions {
764
766
  callToJSON?: boolean;
765
767
  escapeRegex?: boolean;
@@ -779,19 +781,21 @@ interface Config extends Except<Argv, "rootDir" | "setupFiles" | "setupFilesAfte
779
781
  compactMaxFailures?: number;
780
782
  coverageDirectory?: string;
781
783
  coveragePathIgnorePatterns?: Array<string>;
782
- coverageReporters?: Array<string>;
784
+ coverageReporters?: Array<CoverageReporter>;
783
785
  coverageThreshold?: {
784
786
  branches?: number;
785
787
  functions?: number;
786
788
  lines?: number;
787
789
  statements?: number;
788
790
  };
791
+ formatters?: Array<FormatterEntry>;
789
792
  gameOutput?: string;
790
793
  jestPath?: string;
791
794
  luauRoots?: Array<string>;
792
795
  placeFile?: string;
793
796
  pollInterval?: number;
794
797
  port?: number;
798
+ reporters?: Array<string>;
795
799
  rojoProject?: string;
796
800
  rootDir?: string;
797
801
  setupFiles?: Array<string>;
@@ -815,7 +819,7 @@ interface ResolvedConfig extends Config {
815
819
  compactMaxFailures: number;
816
820
  coverageDirectory: string;
817
821
  coveragePathIgnorePatterns: Array<string>;
818
- coverageReporters: Array<string>;
822
+ coverageReporters: Array<CoverageReporter>;
819
823
  json: boolean;
820
824
  placeFile: string;
821
825
  pollInterval: number;
@@ -843,8 +847,9 @@ interface CliOptions {
843
847
  compactMaxFailures?: number;
844
848
  config?: string;
845
849
  coverageDirectory?: string;
846
- coverageReporters?: Array<string>;
850
+ coverageReporters?: Array<CoverageReporter>;
847
851
  files?: Array<string>;
852
+ formatters?: Array<string>;
848
853
  gameOutput?: string;
849
854
  help?: boolean;
850
855
  json?: boolean;
@@ -852,6 +857,7 @@ interface CliOptions {
852
857
  pollInterval?: number;
853
858
  port?: number;
854
859
  projects?: Array<string>;
860
+ reporters?: Array<string>;
855
861
  rojoProject?: string;
856
862
  setupFiles?: Array<string>;
857
863
  setupFilesAfterEnv?: Array<string>;
@@ -868,6 +874,6 @@ interface CliOptions {
868
874
  verbose?: boolean;
869
875
  version?: boolean;
870
876
  }
871
- declare const defineConfig: DefineConfig<Config>;
877
+ declare const defineConfig: (input: Config) => Config;
872
878
  //#endregion
873
- export { SnapshotFormatOptions as a, ResolvedConfig as i, Config as n, defineConfig as o, DEFAULT_CONFIG as r, Argv as s, CliOptions as t };
879
+ export { ResolvedConfig as a, Argv as c, FormatterEntry as i, Config as n, SnapshotFormatOptions as o, DEFAULT_CONFIG as r, defineConfig as s, CliOptions as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@isentinel/jest-roblox",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "description": "Jest-compatible CLI for running roblox-ts tests via Roblox Open Cloud",
5
5
  "keywords": [
6
6
  "jest",
@@ -52,7 +52,7 @@
52
52
  "ws": "8.18.0"
53
53
  },
54
54
  "devDependencies": {
55
- "@isentinel/eslint-config": "5.0.0-beta.8",
55
+ "@isentinel/eslint-config": "5.0.0-beta.9",
56
56
  "@oxc-project/types": "0.120.0",
57
57
  "@rbxts/jest": "3.13.3-ts.1",
58
58
  "@rbxts/types": "1.0.911",
@@ -68,6 +68,8 @@
68
68
  "better-typescript-lib": "2.12.0",
69
69
  "bumpp": "10.4.1",
70
70
  "eslint": "9.39.2",
71
+ "eslint-plugin-jest-extended": "3.0.1",
72
+ "jest-extended": "7.0.0",
71
73
  "memfs": "4.56.11",
72
74
  "publint": "0.3.15",
73
75
  "tsdown": "0.20.1",