@isentinel/jest-roblox 0.1.5 → 0.2.1

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.
@@ -5,9 +5,10 @@ import { fileURLToPath } from "node:url";
5
5
 
6
6
  const sourceEntry = resolve(dirname(fileURLToPath(import.meta.url)), "../src/cli.ts");
7
7
 
8
+ const { register } = await import("node:module");
9
+ register("../loaders/luau-raw.mjs", import.meta.url);
10
+
8
11
  if (existsSync(sourceEntry)) {
9
- const { register } = await import("node:module");
10
- register("../loaders/luau-raw.mjs", import.meta.url);
11
12
  const { main } = await import("../src/cli.ts");
12
13
  main();
13
14
  } else {
package/dist/cli.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { A as hashBuffer, C as resolveNestedProjects, D as createStudioBackend, F as VALID_BACKENDS, P as ROOT_ONLY_KEYS, R as isValidBackend, S as collectPaths, _ as formatResult, a as formatAnnotations, b as formatBanner, c as execute, d as findFormatterOptions, g as formatMultiProjectResult, i as runTypecheck, k as createOpenCloudBackend, l as formatExecuteOutput, m as formatAgentMultiProject, n as parseGameOutput, o as formatJobSummary, p as writeJsonFile, r as writeGameOutput, s as resolveGitHubActionsOptions, t as formatGameOutputNotice, u as loadCoverageManifest, w as loadConfig$1, x as rojoProjectSchema, y as formatTypecheckSummary, z as LuauScriptError } from "./game-output-71ciORUU.mjs";
1
+ import { A as createOpenCloudBackend, B as LuauScriptError, C as formatTypecheckSummary, F as ROOT_ONLY_KEYS, I as VALID_BACKENDS, O as createStudioBackend, T as loadConfig$1, _ as writeJsonFile, a as formatAnnotations, b as formatMultiProjectResult, c as execute, d as resolveTsconfigDirectories, f as collectPaths, h as findFormatterOptions, i as runTypecheck, j as hashBuffer, l as formatExecuteOutput, m as rojoProjectSchema, n as parseGameOutput, o as formatJobSummary, p as resolveNestedProjects, r as writeGameOutput, s as resolveGitHubActionsOptions, t as formatGameOutputNotice, u as loadCoverageManifest, v as formatAgentMultiProject, w as formatBanner, x as formatResult, z as isValidBackend } from "./game-output-C0KykXIi.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import { type } from "arktype";
4
4
  import assert from "node:assert";
@@ -14,16 +14,16 @@ import { WebSocketServer } from "ws";
14
14
  import { loadConfig } from "c12";
15
15
  import * as os from "node:os";
16
16
  import { Buffer } from "node:buffer";
17
+ import { getTsconfig } from "get-tsconfig";
18
+ import { TraceMap, originalPositionFor } from "@jridgewell/trace-mapping";
17
19
  import * as cp from "node:child_process";
18
20
  import { RojoResolver } from "@roblox-ts/rojo-resolver";
19
- import { TraceMap, originalPositionFor } from "@jridgewell/trace-mapping";
20
- import { getTsconfig } from "get-tsconfig";
21
21
  import picomatch from "picomatch";
22
22
  import istanbulCoverage from "istanbul-lib-coverage";
23
23
  import istanbulReport from "istanbul-lib-report";
24
24
  import istanbulReports from "istanbul-reports";
25
25
  //#region package.json
26
- var version = "0.1.5";
26
+ var version = "0.2.1";
27
27
  //#endregion
28
28
  //#region src/backends/auto.ts
29
29
  var StudioWithFallback = class {
@@ -156,7 +156,7 @@ function evalTable(entries) {
156
156
  }
157
157
  //#endregion
158
158
  //#region src/luau/parse-ast.luau
159
- var parse_ast_default = "local fs = require(\"@std/fs\")\nlocal json = require(\"@std/json\")\nlocal process = require(\"@std/process\")\nlocal syntax = require(\"@std/syntax\")\n\nlocal rawArgs = process.args\nlocal userArgs: { string } = {}\nlocal pastSeparator = false\n\nfor _, arg in rawArgs do\n if pastSeparator then\n table.insert(userArgs, arg)\n elseif arg == \"--\" then\n pastSeparator = true\n end\nend\n\nlocal luauRoot = userArgs[1]\nif not luauRoot then\n error(\"Usage: lute run parse-ast.luau -- <file.luau | luau-root> [output-dir]\")\nend\n\nluauRoot = string.gsub(luauRoot, \"\\\\\", \"/\")\n\n-- Fields to keep per AST tag (beyond tag/kind/location which are always kept).\n-- Tags shared by stat/expr variants are merged — nil fields are harmless.\nlocal KEEP: { [string]: { string } } = {\n assign = { \"values\", \"variables\" },\n binary = { \"lhsoperand\", \"rhsoperand\" },\n block = { \"statements\" },\n boolean = { \"value\" },\n call = { \"arguments\", \"func\" },\n cast = { \"operand\" },\n compoundassign = { \"value\", \"variable\" },\n conditional = { \"condition\", \"thenblock\", \"elseifs\", \"elseblock\", \"thenexpr\", \"elseexpr\" },\n [\"do\"] = { \"body\" },\n expression = { \"expression\" },\n [\"for\"] = { \"body\", \"from\", \"to\", \"step\" },\n forin = { \"body\", \"values\" },\n [\"function\"] = { \"body\", \"name\", \"func\" },\n global = { \"name\" },\n group = { \"expression\" },\n index = { \"expression\", \"index\" },\n indexname = { \"expression\", \"accessor\", \"index\" },\n instantiate = { \"expr\" },\n interpolatedstring = { \"expressions\" },\n [\"local\"] = { \"values\", \"variables\" },\n localfunction = { \"name\", \"func\" },\n number = { \"value\" },\n [\"repeat\"] = { \"body\", \"condition\" },\n [\"return\"] = { \"expressions\" },\n string = { \"text\" },\n table = { \"entries\" },\n unary = { \"operand\" },\n [\"while\"] = { \"body\", \"condition\" },\n}\n\nlocal function strip(value: any): any\n if type(value) ~= \"table\" then\n return value\n end\n\n -- LuauSpan — has beginline, no tag\n if value.beginline ~= nil then\n return value\n end\n\n -- Token — has text but no tag, reduce to {text}\n if value.text ~= nil and value.tag == nil then\n return { text = value.text }\n end\n\n -- AST node — has tag, keep only allowlisted fields\n if value.tag ~= nil then\n local result = { tag = value.tag, kind = value.kind, location = value.location }\n local fields = KEEP[value.tag :: string]\n if fields then\n for _, field in fields do\n if value[field] ~= nil then\n result[field] = strip(value[field])\n end\n end\n end\n\n return result\n end\n\n -- Other tables (arrays, Pairs, ElseIf structs) — recurse all fields\n local result: any = {}\n for k, v in value do\n result[k] = strip(v)\n end\n\n return result\nend\n\n-- Single-file mode: parse one file, print stripped AST to stdout\nif string.sub(luauRoot, -5) == \".luau\" or string.sub(luauRoot, -4) == \".lua\" then\n local source = fs.readfiletostring(luauRoot)\n local parseResult = syntax.parse(source)\n print(json.serialize(strip(parseResult.root)))\n return\nend\n\nlocal outputDir = userArgs[2]\nif not outputDir then\n error(\"Usage: lute run parse-ast.luau -- <luau-root> <output-dir>\")\nend\n\noutputDir = string.gsub(outputDir, \"\\\\\", \"/\")\n\n-- Discover .luau files recursively, skipping node_modules, dot dirs, spec/test files\nlocal function discoverFiles(directory: string, relativeTo: string, results: { string })\n local entries = fs.listdirectory(directory)\n for _, entry in entries do\n local fullPath = directory .. \"/\" .. entry.name\n if entry.type == \"dir\" then\n if entry.name == \"node_modules\" or entry.name == \".jest-roblox-coverage\" then\n continue\n end\n\n if string.sub(entry.name, 1, 1) == \".\" then\n continue\n end\n\n discoverFiles(fullPath, relativeTo, results)\n elseif\n entry.type == \"file\"\n and (string.sub(entry.name, -5) == \".luau\" or string.sub(entry.name, -4) == \".lua\")\n then\n if\n string.sub(entry.name, -10) == \".spec.luau\"\n or string.sub(entry.name, -10) == \".test.luau\"\n or string.sub(entry.name, -9) == \".spec.lua\"\n or string.sub(entry.name, -9) == \".test.lua\"\n or string.sub(entry.name, -10) == \".snap.luau\"\n or string.sub(entry.name, -9) == \".snap.lua\"\n then\n continue\n end\n\n -- Compute relative path\n local relative = string.sub(fullPath, #relativeTo + 2)\n table.insert(results, relative)\n end\n end\nend\n\nlocal function dirname(filepath: string): string\n local pos = string.find(filepath, \"/[^/]*$\")\n if pos then\n return string.sub(filepath, 1, pos - 1)\n end\n\n return \"\"\nend\n\nlocal files: { string } = {}\ndiscoverFiles(luauRoot, luauRoot, files)\n\n-- Parse, strip, and write per-file AST JSON\nfor _, relativePath in files do\n local fullPath = luauRoot .. \"/\" .. relativePath\n local source = fs.readfiletostring(fullPath)\n local parseResult = syntax.parse(source)\n local stripped = strip(parseResult.root)\n\n local outPath = outputDir .. \"/\" .. relativePath .. \".json\"\n local dir = dirname(outPath)\n if dir ~= \"\" then\n fs.createdirectory(dir, { makeparents = true })\n end\n\n fs.writestringtofile(outPath, json.serialize(stripped))\nend\n\n-- Print file list to stdout (tiny — just paths)\nprint(json.serialize(files :: json.array))\n";
159
+ var parse_ast_default = "local fs = require(\"@std/fs\")\nlocal json = require(\"@std/json\")\nlocal process = require(\"@std/process\")\nlocal syntax = require(\"@std/syntax\")\n\nlocal rawArgs = process.args\nlocal userArgs: { string } = {}\nlocal pastSeparator = false\n\nfor _, arg in rawArgs do\n if pastSeparator then\n table.insert(userArgs, arg)\n elseif arg == \"--\" then\n pastSeparator = true\n end\nend\n\nlocal luauRoot = userArgs[1]\nif not luauRoot then\n error(\"Usage: lute run parse-ast.luau -- <file.luau | luau-root> [output-dir]\")\nend\n\nluauRoot = string.gsub(luauRoot, \"\\\\\", \"/\")\n\n-- Fields to keep per AST tag (beyond tag/kind/location which are always kept).\n-- Tags shared by stat/expr variants are merged — nil fields are harmless.\nlocal KEEP: { [string]: { string } } = {\n assign = { \"values\", \"variables\" },\n binary = { \"lhsOperand\", \"rhsOperand\" },\n block = { \"statements\" },\n boolean = { \"value\" },\n call = { \"arguments\", \"func\" },\n cast = { \"operand\" },\n compoundassign = { \"value\", \"variable\" },\n conditional = { \"condition\", \"thenBlock\", \"elseifs\", \"elseBlock\", \"thenExpr\", \"elseExpr\" },\n [\"do\"] = { \"body\" },\n expression = { \"expression\" },\n [\"for\"] = { \"body\", \"from\", \"to\", \"step\" },\n forin = { \"body\", \"values\" },\n [\"function\"] = { \"body\", \"name\", \"func\" },\n global = { \"name\" },\n group = { \"expression\" },\n index = { \"expression\", \"index\" },\n indexname = { \"expression\", \"accessor\", \"index\" },\n instantiate = { \"expr\" },\n interpolatedstring = { \"expressions\" },\n [\"local\"] = { \"values\", \"variables\" },\n localfunction = { \"name\", \"func\" },\n number = { \"value\" },\n [\"repeat\"] = { \"body\", \"condition\" },\n [\"return\"] = { \"expressions\" },\n string = { \"text\" },\n table = { \"entries\" },\n unary = { \"operand\" },\n [\"while\"] = { \"body\", \"condition\" },\n}\n\nlocal function strip(value: any): any\n if type(value) ~= \"table\" then\n return value\n end\n\n -- LuauSpan — has beginLine, no tag\n if value.beginLine ~= nil then\n return value\n end\n\n -- Token — has text but no tag, reduce to {text}\n if value.text ~= nil and value.tag == nil then\n return { text = value.text }\n end\n\n -- AST node — has tag, keep only allowlisted fields\n if value.tag ~= nil then\n local result = { tag = value.tag, kind = value.kind, location = value.location }\n local fields = KEEP[value.tag :: string]\n if fields then\n for _, field in fields do\n if value[field] ~= nil then\n result[field] = strip(value[field])\n end\n end\n end\n\n return result\n end\n\n -- Other tables (arrays, Pairs, ElseIf structs) — recurse all fields\n local result: any = {}\n for k, v in value do\n result[k] = strip(v)\n end\n\n return result\nend\n\n-- Single-file mode: parse one file, print stripped AST to stdout\nif string.sub(luauRoot, -5) == \".luau\" or string.sub(luauRoot, -4) == \".lua\" then\n local source = fs.readfiletostring(luauRoot)\n local parseResult = syntax.parse(source)\n print(json.serialize(strip(parseResult.root)))\n return\nend\n\nlocal outputDir = userArgs[2]\nif not outputDir then\n error(\"Usage: lute run parse-ast.luau -- <luau-root> <output-dir>\")\nend\n\noutputDir = string.gsub(outputDir, \"\\\\\", \"/\")\n\n-- Discover .luau files recursively, skipping node_modules, dot dirs, spec/test files\nlocal function discoverFiles(directory: string, relativeTo: string, results: { string })\n local entries = fs.listdirectory(directory)\n for _, entry in entries do\n local fullPath = directory .. \"/\" .. entry.name\n if entry.type == \"dir\" then\n if entry.name == \"node_modules\" or entry.name == \".jest-roblox-coverage\" then\n continue\n end\n\n if string.sub(entry.name, 1, 1) == \".\" then\n continue\n end\n\n discoverFiles(fullPath, relativeTo, results)\n elseif\n entry.type == \"file\"\n and (string.sub(entry.name, -5) == \".luau\" or string.sub(entry.name, -4) == \".lua\")\n then\n if\n string.sub(entry.name, -10) == \".spec.luau\"\n or string.sub(entry.name, -10) == \".test.luau\"\n or string.sub(entry.name, -9) == \".spec.lua\"\n or string.sub(entry.name, -9) == \".test.lua\"\n or string.sub(entry.name, -10) == \".snap.luau\"\n or string.sub(entry.name, -9) == \".snap.lua\"\n then\n continue\n end\n\n -- Compute relative path\n local relative = string.sub(fullPath, #relativeTo + 2)\n table.insert(results, relative)\n end\n end\nend\n\nlocal function dirname(filepath: string): string\n local pos = string.find(filepath, \"/[^/]*$\")\n if pos then\n return string.sub(filepath, 1, pos - 1)\n end\n\n return \"\"\nend\n\nlocal files: { string } = {}\ndiscoverFiles(luauRoot, luauRoot, files)\n\n-- Parse, strip, and write per-file AST JSON\nfor _, relativePath in files do\n local fullPath = luauRoot .. \"/\" .. relativePath\n local source = fs.readfiletostring(fullPath)\n local parseResult = syntax.parse(source)\n local stripped = strip(parseResult.root)\n\n local outPath = outputDir .. \"/\" .. relativePath .. \".json\"\n local dir = dirname(outPath)\n if dir ~= \"\" then\n fs.createdirectory(dir, { makeparents = true })\n end\n\n fs.writestringtofile(outPath, json.serialize(stripped))\nend\n\n-- Print file list to stdout (tiny — just paths)\nprint(json.serialize(files :: json.array))\n";
160
160
  //#endregion
161
161
  //#region src/config/luau-config-loader.ts
162
162
  let cachedTemporaryDirectory$1;
@@ -326,6 +326,7 @@ async function loadProjectConfigFile(filePath, cwd) {
326
326
  }
327
327
  const { config } = result;
328
328
  if ((typeof config.displayName === "string" ? config.displayName : config.displayName.name) === "") throw new Error(`Project config file "${filePath}" must have a displayName`);
329
+ deriveIncludeFromTestMatch(config, path$1.posix.dirname(filePath), resolveTsconfigDirectories(cwd));
329
330
  return config;
330
331
  }
331
332
  async function resolveAllProjects(entries, rootConfig, rojoTree, cwd) {
@@ -348,6 +349,25 @@ function mergeProjectConfig(rootConfig, project) {
348
349
  for (const [key, value] of Object.entries(project)) if (!PROJECT_ONLY_KEYS.has(key) && value !== void 0) merged[key] = value;
349
350
  return merged;
350
351
  }
352
+ /**
353
+ * When a project config provides `testMatch` but not `include`, derive
354
+ * `include` by appending `.ts` and `.tsx` extensions. This lets users
355
+ * write project configs with the standard Jest `testMatch` field without
356
+ * needing the CLI-specific `include`.
357
+ */
358
+ function deriveIncludeFromTestMatch(config, configDirectory, tsconfig) {
359
+ const raw = config;
360
+ if (raw["include"] !== void 0) return;
361
+ if (!Array.isArray(raw["testMatch"])) return;
362
+ config.include = raw["testMatch"].flatMap((pattern) => {
363
+ return (/\.(tsx?|luau?)$/.test(pattern) ? [pattern] : [`${pattern}.ts`, `${pattern}.tsx`]).map((extension) => path$1.posix.join(configDirectory, extension));
364
+ });
365
+ const { outDir, rootDir } = tsconfig;
366
+ if (raw["outDir"] === void 0 && rootDir !== void 0 && outDir !== void 0) {
367
+ const rootPrefix = `${rootDir}/`;
368
+ if (configDirectory.startsWith(rootPrefix)) config.outDir = `${outDir}/${configDirectory.slice(rootPrefix.length)}`;
369
+ }
370
+ }
351
371
  const LUAU_BOOLEAN_KEYS = [
352
372
  "automock",
353
373
  "clearMocks",
@@ -527,6 +547,28 @@ function findShadowStubs(directory) {
527
547
  return results;
528
548
  }
529
549
  //#endregion
550
+ //#region src/coverage/derive-coverage-from.ts
551
+ /**
552
+ * Derives `collectCoverageFrom` glob patterns from project `include` patterns.
553
+ *
554
+ * Extracts the static root directory from each include pattern and generates
555
+ * coverage globs that match all `.ts` source files within those roots, excluding
556
+ * test files. Returns `undefined` when no roots can be extracted (preserving
557
+ * default all-files behavior).
558
+ */
559
+ function deriveCoverageFromIncludes(projects) {
560
+ const roots = /* @__PURE__ */ new Set();
561
+ for (const project of projects) for (const pattern of project.include) try {
562
+ const { root } = extractStaticRoot(pattern);
563
+ roots.add(root);
564
+ } catch {}
565
+ if (roots.size === 0) return;
566
+ const patterns = [];
567
+ for (const root of roots) patterns.push(`${root}/**/*.ts`);
568
+ patterns.push("!**/*.spec.ts", "!**/*.test.ts");
569
+ return patterns;
570
+ }
571
+ //#endregion
530
572
  //#region src/coverage/mapper.ts
531
573
  const positionSchema = type({
532
574
  column: "number",
@@ -569,6 +611,7 @@ function mapCoverageToTypeScript(coverageData, manifest) {
569
611
  } else {
570
612
  const mapped = {
571
613
  coverageMap: resources.coverageMap,
614
+ sourceMapDirectory: resources.sourceMapDirectory,
572
615
  traceMap: resources.traceMap
573
616
  };
574
617
  mapFileFunctions(mapped, fileCoverage, pendingFunctions, mapFileStatements(mapped, fileCoverage, pendingStatements));
@@ -590,9 +633,11 @@ function loadFileResources(record) {
590
633
  try {
591
634
  traceMap = new TraceMap(fs$1.readFileSync(record.sourceMapPath, "utf-8"));
592
635
  } catch {}
636
+ const sourceMapDirectory = path$1.posix.dirname(record.sourceMapPath);
593
637
  return {
594
638
  coverageMap: parsed,
595
639
  sourceKey: record.key,
640
+ sourceMapDirectory,
596
641
  traceMap
597
642
  };
598
643
  }
@@ -689,7 +734,19 @@ function passthroughFileBranches(resources, fileCoverage, pendingBranches) {
689
734
  });
690
735
  }
691
736
  }
692
- function mapStatement(traceMap, span) {
737
+ /**
738
+ * Resolves a source path from a source map against the source map's directory.
739
+ * Source maps produce paths relative to the .map file (e.g.,
740
+ * `../../../packages/src/file.ts` from `out/packages/src/file.lua.map`).
741
+ * Joining with the map directory normalizes these to cwd-relative paths.
742
+ * Paths that are already cwd-relative (no `..` prefix) pass through unchanged.
743
+ */
744
+ function resolveSourcePath(source, sourceMapDirectory) {
745
+ const normalized = source.replaceAll("\\", "/");
746
+ if (!normalized.startsWith("..")) return normalized;
747
+ return path$1.posix.normalize(path$1.posix.join(sourceMapDirectory, normalized));
748
+ }
749
+ function mapStatement(traceMap, span, sourceMapDirectory) {
693
750
  const mappedStart = originalPositionFor(traceMap, {
694
751
  column: Math.max(0, span.start.column - 1),
695
752
  line: span.start.line
@@ -699,16 +756,17 @@ function mapStatement(traceMap, span) {
699
756
  line: span.end.line
700
757
  });
701
758
  if (mappedStart.source === null || mappedEnd.source === null || mappedStart.source !== mappedEnd.source) return;
759
+ const resolvedSource = resolveSourcePath(mappedStart.source, sourceMapDirectory);
702
760
  return {
703
761
  end: {
704
762
  column: mappedEnd.column,
705
763
  line: mappedEnd.line,
706
- source: mappedEnd.source
764
+ source: resolvedSource
707
765
  },
708
766
  start: {
709
767
  column: mappedStart.column,
710
768
  line: mappedStart.line,
711
- source: mappedStart.source
769
+ source: resolvedSource
712
770
  }
713
771
  };
714
772
  }
@@ -750,7 +808,7 @@ function mapFileStatements(resources, fileCoverage, pending) {
750
808
  const span = spanSchema(rawSpan);
751
809
  if (span instanceof type.errors) continue;
752
810
  const hitCount = fileCoverage.s[statementId] ?? 0;
753
- const mapped = mapStatement(resources.traceMap, span);
811
+ const mapped = mapStatement(resources.traceMap, span, resources.sourceMapDirectory);
754
812
  if (mapped === void 0) continue;
755
813
  resolvedTsPaths.add(mapped.start.source);
756
814
  addOrCoalesce(pending, mapped.start, mapped.end, hitCount);
@@ -763,7 +821,7 @@ function mapFileFunctions(resources, fileCoverage, pendingFunctions, resolvedTsP
763
821
  const entry = functionEntrySchema(rawEntry);
764
822
  if (entry instanceof type.errors) continue;
765
823
  const hitCount = fileCoverage.f?.[functionId] ?? 0;
766
- const mapped = mapStatement(resources.traceMap, entry.location);
824
+ const mapped = mapStatement(resources.traceMap, entry.location, resources.sourceMapDirectory);
767
825
  if (mapped !== void 0) {
768
826
  const tsPath = mapped.start.source;
769
827
  let fileFunctions = pendingFunctions.get(tsPath);
@@ -810,13 +868,13 @@ function mapFileFunctions(resources, fileCoverage, pendingFunctions, resolvedTsP
810
868
  });
811
869
  }
812
870
  }
813
- function mapBranchArmLocations(traceMap, rawLocations) {
871
+ function mapBranchArmLocations(traceMap, rawLocations, sourceMapDirectory) {
814
872
  const mappedLocations = [];
815
873
  let tsPath;
816
874
  for (const rawLocation of rawLocations) {
817
875
  const location = spanSchema(rawLocation);
818
876
  if (location instanceof type.errors) return;
819
- const mapped = mapStatement(traceMap, location);
877
+ const mapped = mapStatement(traceMap, location, sourceMapDirectory);
820
878
  if (mapped === void 0) return;
821
879
  if (tsPath === void 0) tsPath = mapped.start.source;
822
880
  else if (tsPath !== mapped.start.source) return;
@@ -843,7 +901,7 @@ function mapFileBranches(resources, fileCoverage, pendingBranches) {
843
901
  const entry = branchEntrySchema(rawEntry);
844
902
  if (entry instanceof type.errors) continue;
845
903
  const armHitCounts = fileCoverage.b?.[branchId] ?? [];
846
- const result = mapBranchArmLocations(resources.traceMap, entry.locations);
904
+ const result = mapBranchArmLocations(resources.traceMap, entry.locations, resources.sourceMapDirectory);
847
905
  if (result === void 0) continue;
848
906
  let fileBranches = pendingBranches.get(result.tsPath);
849
907
  if (fileBranches === void 0) {
@@ -1113,13 +1171,13 @@ function visitStatDo(node, visitor) {
1113
1171
  function visitStatIf(node, visitor) {
1114
1172
  if (visitor.visitStatIf?.(node) === false) return;
1115
1173
  visitExpression(node.condition, visitor);
1116
- visitStatBlock(node.thenblock, visitor);
1174
+ visitStatBlock(node.thenBlock, visitor);
1117
1175
  for (const elseif of node.elseifs) visitElseIfStat(elseif, visitor);
1118
- if (node.elseblock) visitStatBlock(node.elseblock, visitor);
1176
+ if (node.elseBlock) visitStatBlock(node.elseBlock, visitor);
1119
1177
  }
1120
1178
  function visitElseIfStat(node, visitor) {
1121
1179
  visitExpression(node.condition, visitor);
1122
- visitStatBlock(node.thenblock, visitor);
1180
+ visitStatBlock(node.thenBlock, visitor);
1123
1181
  }
1124
1182
  function visitStatWhile(node, visitor) {
1125
1183
  if (visitor.visitStatWhile?.(node) === false) return;
@@ -1190,8 +1248,8 @@ function visitExprUnary(node, visitor) {
1190
1248
  }
1191
1249
  function visitExprBinary(node, visitor) {
1192
1250
  if (visitor.visitExprBinary?.(node) === false) return;
1193
- visitExpression(node.lhsoperand, visitor);
1194
- visitExpression(node.rhsoperand, visitor);
1251
+ visitExpression(node.lhsOperand, visitor);
1252
+ visitExpression(node.rhsOperand, visitor);
1195
1253
  }
1196
1254
  function visitExprTable(node, visitor) {
1197
1255
  if (visitor.visitExprTable?.(node) === false) return;
@@ -1226,13 +1284,13 @@ function visitExprTypeAssertion(node, visitor) {
1226
1284
  function visitExprIfElse(node, visitor) {
1227
1285
  if (visitor.visitExprIfElse?.(node) === false) return;
1228
1286
  visitExpression(node.condition, visitor);
1229
- visitExpression(node.thenexpr, visitor);
1287
+ visitExpression(node.thenExpr, visitor);
1230
1288
  for (const elseif of node.elseifs) visitElseIfExpr(elseif, visitor);
1231
- visitExpression(node.elseexpr, visitor);
1289
+ visitExpression(node.elseExpr, visitor);
1232
1290
  }
1233
1291
  function visitElseIfExpr(node, visitor) {
1234
1292
  visitExpression(node.condition, visitor);
1235
- visitExpression(node.thenexpr, visitor);
1293
+ visitExpression(node.thenExpr, visitor);
1236
1294
  }
1237
1295
  function visitExprInstantiate(node, visitor) {
1238
1296
  if (visitor.visitExprInstantiate?.(node) === false) return;
@@ -1292,36 +1350,36 @@ function collectCoverage(root) {
1292
1350
  branch.arms.push({
1293
1351
  bodyFirstColumn: 0,
1294
1352
  bodyFirstLine: 0,
1295
- location: { ...node.thenexpr.location }
1353
+ location: { ...node.thenExpr.location }
1296
1354
  });
1297
1355
  exprIfProbes.push({
1298
1356
  armIndex,
1299
1357
  branchIndex,
1300
- exprLocation: { ...node.thenexpr.location }
1358
+ exprLocation: { ...node.thenExpr.location }
1301
1359
  });
1302
1360
  armIndex++;
1303
1361
  for (const elseif of node.elseifs) {
1304
1362
  branch.arms.push({
1305
1363
  bodyFirstColumn: 0,
1306
1364
  bodyFirstLine: 0,
1307
- location: { ...elseif.thenexpr.location }
1365
+ location: { ...elseif.thenExpr.location }
1308
1366
  });
1309
1367
  exprIfProbes.push({
1310
1368
  armIndex,
1311
1369
  branchIndex,
1312
- exprLocation: { ...elseif.thenexpr.location }
1370
+ exprLocation: { ...elseif.thenExpr.location }
1313
1371
  });
1314
1372
  armIndex++;
1315
1373
  }
1316
1374
  branch.arms.push({
1317
1375
  bodyFirstColumn: 0,
1318
1376
  bodyFirstLine: 0,
1319
- location: { ...node.elseexpr.location }
1377
+ location: { ...node.elseExpr.location }
1320
1378
  });
1321
1379
  exprIfProbes.push({
1322
1380
  armIndex,
1323
1381
  branchIndex,
1324
- exprLocation: { ...node.elseexpr.location }
1382
+ exprLocation: { ...node.elseExpr.location }
1325
1383
  });
1326
1384
  branches.push(branch);
1327
1385
  branchIndex++;
@@ -1357,21 +1415,21 @@ function collectCoverage(root) {
1357
1415
  branchType: "if",
1358
1416
  index: branchIndex
1359
1417
  };
1360
- const thenFirst = getBodyFirstStatement(node.thenblock);
1418
+ const thenFirst = getBodyFirstStatement(node.thenBlock);
1361
1419
  branch.arms.push({
1362
1420
  bodyFirstColumn: thenFirst.column,
1363
1421
  bodyFirstLine: thenFirst.line,
1364
- location: { ...node.thenblock.location }
1422
+ location: { ...node.thenBlock.location }
1365
1423
  });
1366
1424
  for (const elseif of node.elseifs) {
1367
- const elseifFirst = getBodyFirstStatement(elseif.thenblock);
1425
+ const elseifFirst = getBodyFirstStatement(elseif.thenBlock);
1368
1426
  branch.arms.push({
1369
1427
  bodyFirstColumn: elseifFirst.column,
1370
1428
  bodyFirstLine: elseifFirst.line,
1371
- location: { ...elseif.thenblock.location }
1429
+ location: { ...elseif.thenBlock.location }
1372
1430
  });
1373
1431
  }
1374
- const elseBlock = node.elseblock;
1432
+ const { elseBlock } = node;
1375
1433
  const hasExplicitElse = elseBlock !== void 0 && elseBlock.statements.length > 0;
1376
1434
  if (hasExplicitElse) {
1377
1435
  const elseFirst = getBodyFirstStatement(elseBlock);
@@ -1386,17 +1444,17 @@ function collectCoverage(root) {
1386
1444
  bodyFirstColumn: 0,
1387
1445
  bodyFirstLine: 0,
1388
1446
  location: {
1389
- begincolumn: node.location.begincolumn,
1390
- beginline: node.location.beginline,
1391
- endcolumn: node.location.begincolumn,
1392
- endline: node.location.beginline
1447
+ beginColumn: node.location.beginColumn,
1448
+ beginLine: node.location.beginLine,
1449
+ endColumn: node.location.beginColumn,
1450
+ endLine: node.location.beginLine
1393
1451
  }
1394
1452
  });
1395
1453
  implicitElseProbes.push({
1396
1454
  armIndex: branch.arms.length,
1397
1455
  branchIndex,
1398
- endColumn: node.location.endcolumn - END_KEYWORD_LENGTH,
1399
- endLine: node.location.endline
1456
+ endColumn: node.location.endColumn - END_KEYWORD_LENGTH,
1457
+ endLine: node.location.endLine
1400
1458
  });
1401
1459
  }
1402
1460
  branches.push(branch);
@@ -1429,12 +1487,12 @@ function collectCoverage(root) {
1429
1487
  function getBodyFirstStatement(block) {
1430
1488
  const first = block.statements[0];
1431
1489
  if (first !== void 0) return {
1432
- column: first.location.begincolumn,
1433
- line: first.location.beginline
1490
+ column: first.location.beginColumn,
1491
+ line: first.location.beginLine
1434
1492
  };
1435
1493
  return {
1436
- column: block.location.begincolumn,
1437
- line: block.location.beginline
1494
+ column: block.location.beginColumn,
1495
+ line: block.location.beginLine
1438
1496
  };
1439
1497
  }
1440
1498
  function extractExprName(expr) {
@@ -1451,12 +1509,12 @@ function buildCoverageMap$1(result) {
1451
1509
  const statementMap = {};
1452
1510
  for (const statement of result.statements) statementMap[String(statement.index)] = {
1453
1511
  end: {
1454
- column: statement.location.endcolumn,
1455
- line: statement.location.endline
1512
+ column: statement.location.endColumn,
1513
+ line: statement.location.endLine
1456
1514
  },
1457
1515
  start: {
1458
- column: statement.location.begincolumn,
1459
- line: statement.location.beginline
1516
+ column: statement.location.beginColumn,
1517
+ line: statement.location.beginLine
1460
1518
  }
1461
1519
  };
1462
1520
  const functionMap = {};
@@ -1464,12 +1522,12 @@ function buildCoverageMap$1(result) {
1464
1522
  name: func.name,
1465
1523
  location: {
1466
1524
  end: {
1467
- column: func.location.endcolumn,
1468
- line: func.location.endline
1525
+ column: func.location.endColumn,
1526
+ line: func.location.endLine
1469
1527
  },
1470
1528
  start: {
1471
- column: func.location.begincolumn,
1472
- line: func.location.beginline
1529
+ column: func.location.beginColumn,
1530
+ line: func.location.beginLine
1473
1531
  }
1474
1532
  }
1475
1533
  };
@@ -1478,12 +1536,12 @@ function buildCoverageMap$1(result) {
1478
1536
  locations: branch.arms.map((arm) => {
1479
1537
  return {
1480
1538
  end: {
1481
- column: arm.location.endcolumn,
1482
- line: arm.location.endline
1539
+ column: arm.location.endColumn,
1540
+ line: arm.location.endLine
1483
1541
  },
1484
1542
  start: {
1485
- column: arm.location.begincolumn,
1486
- line: arm.location.beginline
1543
+ column: arm.location.beginColumn,
1544
+ line: arm.location.beginLine
1487
1545
  }
1488
1546
  };
1489
1547
  }),
@@ -1505,8 +1563,8 @@ function insertProbes(source, result, fileKey) {
1505
1563
  function collectProbes(result) {
1506
1564
  const probes = [];
1507
1565
  for (const stmt of result.statements) probes.push({
1508
- column: stmt.location.begincolumn,
1509
- line: stmt.location.beginline,
1566
+ column: stmt.location.beginColumn,
1567
+ line: stmt.location.beginLine,
1510
1568
  text: `__cov_s[${stmt.index}] += 1; `
1511
1569
  });
1512
1570
  for (const func of result.functions) if (func.bodyFirstLine > 0) probes.push({
@@ -1528,12 +1586,12 @@ function collectProbes(result) {
1528
1586
  text: `else __cov_b[${probe.branchIndex}][${probe.armIndex}] += 1 `
1529
1587
  });
1530
1588
  for (const probe of result.exprIfProbes) probes.push({
1531
- column: probe.exprLocation.begincolumn,
1532
- line: probe.exprLocation.beginline,
1589
+ column: probe.exprLocation.beginColumn,
1590
+ line: probe.exprLocation.beginLine,
1533
1591
  text: `__cov_br(${probe.branchIndex}, ${probe.armIndex}, `
1534
1592
  }, {
1535
- column: probe.exprLocation.endcolumn,
1536
- line: probe.exprLocation.endline,
1593
+ column: probe.exprLocation.endColumn,
1594
+ line: probe.exprLocation.endLine,
1537
1595
  text: ")"
1538
1596
  });
1539
1597
  probes.sort((a, b) => {
@@ -1753,10 +1811,30 @@ function walkTree(node, context) {
1753
1811
  //#endregion
1754
1812
  //#region src/coverage/prepare.ts
1755
1813
  const COVERAGE_DIR = ".jest-roblox-coverage";
1814
+ /**
1815
+ * Suffixes for files that are not instrumented for coverage but still need
1816
+ * syncing to the shadow directory. Matches parse-ast.luau:131-139.
1817
+ */
1818
+ const NON_INSTRUMENTED_SUFFIXES = [
1819
+ ".spec.luau",
1820
+ ".test.luau",
1821
+ ".spec.lua",
1822
+ ".test.lua",
1823
+ ".snap.luau",
1824
+ ".snap.lua"
1825
+ ];
1826
+ function isNonInstrumentedFile(filename) {
1827
+ return NON_INSTRUMENTED_SUFFIXES.some((suffix) => filename.endsWith(suffix));
1828
+ }
1756
1829
  const previousManifestSchema = type({
1757
1830
  "files": type({ "[string]": { sourceHash: "string" } }),
1758
1831
  "instrumenterVersion": "number",
1759
1832
  "luauRoots": "string[]",
1833
+ "nonInstrumentedFiles?": type({ "[string]": {
1834
+ shadowPath: "string",
1835
+ sourceHash: "string",
1836
+ sourcePath: "string"
1837
+ } }),
1760
1838
  "placeFilePath?": "string",
1761
1839
  "shadowDir": "string",
1762
1840
  "version": "number"
@@ -1782,12 +1860,14 @@ function prepareCoverage(config, beforeBuild) {
1782
1860
  const useIncremental = canUseIncremental(previousManifest, config);
1783
1861
  if (!useIncremental && fs$1.existsSync(COVERAGE_DIR)) fs$1.rmSync(COVERAGE_DIR, { recursive: true });
1784
1862
  const allFiles = {};
1863
+ const allNonInstrumented = {};
1785
1864
  const roots = [];
1786
1865
  let hasChanges = !useIncremental;
1787
1866
  for (const luauRoot of luauRoots) {
1788
1867
  const rootResult = instrumentRootWithCache(luauRoot, useIncremental, previousManifest);
1789
1868
  if (rootResult.changed) hasChanges = true;
1790
1869
  Object.assign(allFiles, rootResult.files);
1870
+ Object.assign(allNonInstrumented, rootResult.nonInstrumentedFiles);
1791
1871
  roots.push(rootResult.rootEntry);
1792
1872
  }
1793
1873
  if (useIncremental && previousManifest !== void 0) {
@@ -1799,7 +1879,13 @@ function prepareCoverage(config, beforeBuild) {
1799
1879
  if (beforeBuild(COVERAGE_DIR)) hasChanges = true;
1800
1880
  }
1801
1881
  const placeFile = path$1.join(COVERAGE_DIR, "game.rbxl");
1802
- const manifest = writeManifest(manifestPath, allFiles, luauRoots, placeFile);
1882
+ const manifest = writeManifest({
1883
+ allFiles,
1884
+ luauRoots,
1885
+ manifestPath,
1886
+ nonInstrumentedFiles: allNonInstrumented,
1887
+ placeFile
1888
+ });
1803
1889
  if (!hasChanges && previousManifest?.placeFilePath !== void 0) return {
1804
1890
  manifest,
1805
1891
  placeFile: previousManifest.placeFilePath
@@ -1866,6 +1952,64 @@ function carryForwardRecords(luauRoot, previousManifest, allFiles, skipFiles) {
1866
1952
  Object.assign(allFiles, { [fileKey]: previousManifest.files[fileKey] });
1867
1953
  }
1868
1954
  }
1955
+ function discoverNonInstrumentedFiles(directory, relativeTo, results) {
1956
+ const entries = fs$1.readdirSync(directory, { withFileTypes: true });
1957
+ for (const entry of entries) {
1958
+ const fullPath = path$1.join(directory, entry.name).replaceAll("\\", "/");
1959
+ if (entry.isDirectory()) {
1960
+ if (entry.name === "node_modules" || entry.name === COVERAGE_DIR) continue;
1961
+ if (entry.name.startsWith(".")) continue;
1962
+ discoverNonInstrumentedFiles(fullPath, relativeTo, results);
1963
+ } else if (isNonInstrumentedFile(entry.name)) {
1964
+ const relative = fullPath.slice(relativeTo.length + 1);
1965
+ results.push(relative);
1966
+ }
1967
+ }
1968
+ }
1969
+ function pruneStaleNonInstrumented(posixRoot, previousNonInstrumented, currentFiles) {
1970
+ if (previousNonInstrumented === void 0) return false;
1971
+ let changed = false;
1972
+ for (const [fileKey, record] of Object.entries(previousNonInstrumented)) {
1973
+ if (!fileKey.startsWith(`${posixRoot}/`)) continue;
1974
+ if (fileKey in currentFiles) continue;
1975
+ try {
1976
+ if (fs$1.existsSync(record.shadowPath)) fs$1.unlinkSync(record.shadowPath);
1977
+ } catch {}
1978
+ changed = true;
1979
+ }
1980
+ return changed;
1981
+ }
1982
+ function syncNonInstrumentedFiles(luauRoot, shadowDirectory, previousNonInstrumented) {
1983
+ const posixRoot = luauRoot.replaceAll("\\", "/");
1984
+ const discovered = [];
1985
+ discoverNonInstrumentedFiles(posixRoot, posixRoot, discovered);
1986
+ const files = {};
1987
+ let changed = false;
1988
+ for (const relativePath of discovered) {
1989
+ const sourcePath = `${posixRoot}/${relativePath}`;
1990
+ const shadowPath = `${shadowDirectory}/${relativePath}`;
1991
+ const currentHash = hashBuffer(fs$1.readFileSync(path$1.resolve(sourcePath)));
1992
+ const previousRecord = previousNonInstrumented?.[sourcePath];
1993
+ if (previousRecord?.sourceHash === currentHash) {
1994
+ files[sourcePath] = previousRecord;
1995
+ continue;
1996
+ }
1997
+ const outputDirectory = path$1.dirname(shadowPath);
1998
+ fs$1.mkdirSync(outputDirectory, { recursive: true });
1999
+ fs$1.copyFileSync(path$1.resolve(sourcePath), shadowPath);
2000
+ files[sourcePath] = {
2001
+ shadowPath,
2002
+ sourceHash: currentHash,
2003
+ sourcePath
2004
+ };
2005
+ changed = true;
2006
+ }
2007
+ changed = pruneStaleNonInstrumented(posixRoot, previousNonInstrumented, files) || changed;
2008
+ return {
2009
+ changed,
2010
+ files
2011
+ };
2012
+ }
1869
2013
  function instrumentRootWithCache(luauRoot, useIncremental, previousManifest) {
1870
2014
  const shadowDirectory = path$1.join(COVERAGE_DIR, luauRoot).replaceAll("\\", "/");
1871
2015
  let changed = false;
@@ -1887,10 +2031,13 @@ function instrumentRootWithCache(luauRoot, useIncremental, previousManifest) {
1887
2031
  if (Object.keys(files).length > 0) changed = true;
1888
2032
  const allFiles = { ...files };
1889
2033
  if (useIncremental && previousManifest !== void 0 && skipFiles !== void 0) carryForwardRecords(luauRoot, previousManifest, allFiles, skipFiles);
2034
+ const syncResult = syncNonInstrumentedFiles(luauRoot, shadowDirectory, previousManifest?.nonInstrumentedFiles);
2035
+ if (syncResult.changed) changed = true;
1890
2036
  const relocatedShadowDirectory = path$1.relative(COVERAGE_DIR, shadowDirectory).replaceAll("\\", "/");
1891
2037
  return {
1892
2038
  changed,
1893
2039
  files: allFiles,
2040
+ nonInstrumentedFiles: syncResult.files,
1894
2041
  rootEntry: {
1895
2042
  luauRoot,
1896
2043
  relocatedShadowDirectory,
@@ -1898,12 +2045,14 @@ function instrumentRootWithCache(luauRoot, useIncremental, previousManifest) {
1898
2045
  }
1899
2046
  };
1900
2047
  }
1901
- function writeManifest(manifestPath, allFiles, luauRoots, placeFile) {
2048
+ function writeManifest(options) {
2049
+ const { allFiles, luauRoots, manifestPath, nonInstrumentedFiles, placeFile } = options;
1902
2050
  const manifest = {
1903
2051
  files: allFiles,
1904
2052
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1905
2053
  instrumenterVersion: 2,
1906
2054
  luauRoots,
2055
+ nonInstrumentedFiles,
1907
2056
  placeFilePath: placeFile,
1908
2057
  shadowDir: COVERAGE_DIR,
1909
2058
  version: 1
@@ -1941,6 +2090,7 @@ function canUseIncremental(previousManifest, config) {
1941
2090
  if (!config.cache) return false;
1942
2091
  if (previousManifest === void 0) return false;
1943
2092
  if (previousManifest.instrumenterVersion !== 2) return false;
2093
+ if (previousManifest.nonInstrumentedFiles === void 0) return false;
1944
2094
  return true;
1945
2095
  }
1946
2096
  function detectDeletedFiles(previousManifest, currentFiles) {
@@ -2720,7 +2870,10 @@ async function runMultiProject(cli, rootConfig, projectEntries) {
2720
2870
  return 2;
2721
2871
  }
2722
2872
  if (projectResults.length === 0) return outputResults(rootConfig, typecheckResult, void 0, preCoverageMs);
2723
- return outputMultiProjectResults(rootConfig, projectResults, typecheckResult, preCoverageMs);
2873
+ return outputMultiProjectResults({
2874
+ ...rootConfig,
2875
+ collectCoverageFrom: rootConfig.collectCoverageFrom ?? deriveCoverageFromIncludes(projects)
2876
+ }, projectResults, typecheckResult, preCoverageMs);
2724
2877
  }
2725
2878
  async function executeRuntimeTests(config, testFiles, totalFiles) {
2726
2879
  if (!config.silent && !usesAgentFormatter(config) && !hasFormatter(config, "json") && testFiles.length !== totalFiles) process.stderr.write(`Running ${String(testFiles.length)} of ${String(totalFiles)} test files\n`);