@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.
- package/bin/jest-roblox.js +3 -2
- package/dist/cli.mjs +216 -63
- package/dist/{game-output-71ciORUU.mjs → game-output-C0KykXIi.mjs} +72 -57
- package/dist/index.mjs +1 -1
- package/dist/sea/jest-roblox +0 -0
- package/dist/sea-entry.cjs +18392 -18224
- package/loaders/luau-raw.d.mts +34 -0
- package/loaders/luau-raw.mjs +33 -0
- package/package.json +9 -8
package/bin/jest-roblox.js
CHANGED
|
@@ -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
|
|
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
|
|
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 = { \"
|
|
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
|
-
|
|
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:
|
|
764
|
+
source: resolvedSource
|
|
707
765
|
},
|
|
708
766
|
start: {
|
|
709
767
|
column: mappedStart.column,
|
|
710
768
|
line: mappedStart.line,
|
|
711
|
-
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.
|
|
1174
|
+
visitStatBlock(node.thenBlock, visitor);
|
|
1117
1175
|
for (const elseif of node.elseifs) visitElseIfStat(elseif, visitor);
|
|
1118
|
-
if (node.
|
|
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.
|
|
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.
|
|
1194
|
-
visitExpression(node.
|
|
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.
|
|
1287
|
+
visitExpression(node.thenExpr, visitor);
|
|
1230
1288
|
for (const elseif of node.elseifs) visitElseIfExpr(elseif, visitor);
|
|
1231
|
-
visitExpression(node.
|
|
1289
|
+
visitExpression(node.elseExpr, visitor);
|
|
1232
1290
|
}
|
|
1233
1291
|
function visitElseIfExpr(node, visitor) {
|
|
1234
1292
|
visitExpression(node.condition, visitor);
|
|
1235
|
-
visitExpression(node.
|
|
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.
|
|
1353
|
+
location: { ...node.thenExpr.location }
|
|
1296
1354
|
});
|
|
1297
1355
|
exprIfProbes.push({
|
|
1298
1356
|
armIndex,
|
|
1299
1357
|
branchIndex,
|
|
1300
|
-
exprLocation: { ...node.
|
|
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.
|
|
1365
|
+
location: { ...elseif.thenExpr.location }
|
|
1308
1366
|
});
|
|
1309
1367
|
exprIfProbes.push({
|
|
1310
1368
|
armIndex,
|
|
1311
1369
|
branchIndex,
|
|
1312
|
-
exprLocation: { ...elseif.
|
|
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.
|
|
1377
|
+
location: { ...node.elseExpr.location }
|
|
1320
1378
|
});
|
|
1321
1379
|
exprIfProbes.push({
|
|
1322
1380
|
armIndex,
|
|
1323
1381
|
branchIndex,
|
|
1324
|
-
exprLocation: { ...node.
|
|
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.
|
|
1418
|
+
const thenFirst = getBodyFirstStatement(node.thenBlock);
|
|
1361
1419
|
branch.arms.push({
|
|
1362
1420
|
bodyFirstColumn: thenFirst.column,
|
|
1363
1421
|
bodyFirstLine: thenFirst.line,
|
|
1364
|
-
location: { ...node.
|
|
1422
|
+
location: { ...node.thenBlock.location }
|
|
1365
1423
|
});
|
|
1366
1424
|
for (const elseif of node.elseifs) {
|
|
1367
|
-
const elseifFirst = getBodyFirstStatement(elseif.
|
|
1425
|
+
const elseifFirst = getBodyFirstStatement(elseif.thenBlock);
|
|
1368
1426
|
branch.arms.push({
|
|
1369
1427
|
bodyFirstColumn: elseifFirst.column,
|
|
1370
1428
|
bodyFirstLine: elseifFirst.line,
|
|
1371
|
-
location: { ...elseif.
|
|
1429
|
+
location: { ...elseif.thenBlock.location }
|
|
1372
1430
|
});
|
|
1373
1431
|
}
|
|
1374
|
-
const elseBlock = node
|
|
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
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
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.
|
|
1399
|
-
endLine: node.location.
|
|
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.
|
|
1433
|
-
line: first.location.
|
|
1490
|
+
column: first.location.beginColumn,
|
|
1491
|
+
line: first.location.beginLine
|
|
1434
1492
|
};
|
|
1435
1493
|
return {
|
|
1436
|
-
column: block.location.
|
|
1437
|
-
line: block.location.
|
|
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.
|
|
1455
|
-
line: statement.location.
|
|
1512
|
+
column: statement.location.endColumn,
|
|
1513
|
+
line: statement.location.endLine
|
|
1456
1514
|
},
|
|
1457
1515
|
start: {
|
|
1458
|
-
column: statement.location.
|
|
1459
|
-
line: statement.location.
|
|
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.
|
|
1468
|
-
line: func.location.
|
|
1525
|
+
column: func.location.endColumn,
|
|
1526
|
+
line: func.location.endLine
|
|
1469
1527
|
},
|
|
1470
1528
|
start: {
|
|
1471
|
-
column: func.location.
|
|
1472
|
-
line: func.location.
|
|
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.
|
|
1482
|
-
line: arm.location.
|
|
1539
|
+
column: arm.location.endColumn,
|
|
1540
|
+
line: arm.location.endLine
|
|
1483
1541
|
},
|
|
1484
1542
|
start: {
|
|
1485
|
-
column: arm.location.
|
|
1486
|
-
line: arm.location.
|
|
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.
|
|
1509
|
-
line: stmt.location.
|
|
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.
|
|
1532
|
-
line: probe.exprLocation.
|
|
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.
|
|
1536
|
-
line: probe.exprLocation.
|
|
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(
|
|
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(
|
|
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(
|
|
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`);
|