@isentinel/jest-roblox 0.2.0 → 0.2.2
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/README.md +11 -2
- package/dist/cli.d.mts +1 -1
- package/dist/cli.mjs +295 -398
- package/dist/{executor-CNz6_04-.d.mts → executor-B2IDh6bH.d.mts} +176 -89
- package/dist/{game-output-C0KykXIi.mjs → game-output-CwmtpYhn.mjs} +531 -172
- package/dist/index.d.mts +301 -19
- package/dist/index.mjs +2 -2
- package/dist/sea/jest-roblox +0 -0
- package/dist/sea-entry.cjs +5157 -20039
- package/package.json +18 -14
- package/plugin/JestRobloxRunner.rbxm +0 -0
- package/plugin/out/shared/entry.luau +3 -2
- package/plugin/out/shared/promise.luau +2006 -0
- package/plugin/out/shared/runner.luau +69 -1
- package/plugin/out/shared/setup-timing.luau +89 -0
- package/plugin/src/init.server.luau +1 -1
- package/plugin/src/test-in-run-mode.server.luau +14 -4
package/dist/cli.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as formatBanner, C as writeJsonFile, D as formatResult, E as formatMultiProjectResult, F as createStudioBackend, G as isValidBackend, H as VALID_BACKENDS, K as LuauScriptError, L as createOpenCloudBackend, M as loadConfig$1, V as ROOT_ONLY_KEYS, _ as resolveTsconfigDirectories, a as formatAnnotations, b as rojoProjectSchema, c as visitBlock, d as buildProjectJob, f as execute, g as processProjectResult, h as loadCoverageManifest, i as runTypecheck, j as combineSourceMappers, k as formatTypecheckSummary, m as formatExecuteOutput, n as parseGameOutput, o as formatJobSummary, p as executeBackend, r as writeGameOutput, s as resolveGitHubActionsOptions, t as formatGameOutputNotice, v as collectPaths$1, w as formatAgentMultiProject, x as findFormatterOptions, y as resolveNestedProjects } from "./game-output-CwmtpYhn.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { type } from "arktype";
|
|
4
4
|
import assert from "node:assert";
|
|
@@ -11,26 +11,32 @@ import { parseArgs as parseArgs$1 } from "node:util";
|
|
|
11
11
|
import { isAgent } from "std-env";
|
|
12
12
|
import color from "tinyrainbow";
|
|
13
13
|
import { WebSocketServer } from "ws";
|
|
14
|
+
import { hashBuffer as hashBuffer$1 } from "@isentinel/roblox-runner";
|
|
14
15
|
import { loadConfig } from "c12";
|
|
15
|
-
import
|
|
16
|
-
import { Buffer } from "node:buffer";
|
|
16
|
+
import { collectPaths, findInTree } from "@isentinel/rojo-utils";
|
|
17
17
|
import { getTsconfig } from "get-tsconfig";
|
|
18
18
|
import { TraceMap, originalPositionFor } from "@jridgewell/trace-mapping";
|
|
19
19
|
import * as cp from "node:child_process";
|
|
20
|
+
import * as os from "node:os";
|
|
20
21
|
import { RojoResolver } from "@roblox-ts/rojo-resolver";
|
|
22
|
+
import { Buffer } from "node:buffer";
|
|
21
23
|
import picomatch from "picomatch";
|
|
22
24
|
import istanbulCoverage from "istanbul-lib-coverage";
|
|
23
25
|
import istanbulReport from "istanbul-lib-report";
|
|
24
26
|
import istanbulReports from "istanbul-reports";
|
|
25
27
|
//#region package.json
|
|
26
|
-
var version = "0.2.
|
|
28
|
+
var version = "0.2.2";
|
|
27
29
|
//#endregion
|
|
28
30
|
//#region src/backends/auto.ts
|
|
29
31
|
var StudioWithFallback = class {
|
|
30
32
|
studio;
|
|
33
|
+
kind = "studio";
|
|
31
34
|
constructor(studio) {
|
|
32
35
|
this.studio = studio;
|
|
33
36
|
}
|
|
37
|
+
async close() {
|
|
38
|
+
await this.studio.close?.();
|
|
39
|
+
}
|
|
34
40
|
async runTests(options) {
|
|
35
41
|
try {
|
|
36
42
|
return await this.studio.runTests(options);
|
|
@@ -156,7 +162,7 @@ function evalTable(entries) {
|
|
|
156
162
|
}
|
|
157
163
|
//#endregion
|
|
158
164
|
//#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 = { \"
|
|
165
|
+
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\n-- Optional 3rd arg: path to skip list JSON file.\n-- Skipped files are still included in the file list but not parsed.\nlocal skipListPath = userArgs[3]\nlocal skipSet: { [string]: boolean } = {}\nif skipListPath then\n local skipJson = fs.readfiletostring(skipListPath)\n local skipList = json.deserialize(skipJson) :: { string }\n for _, entry in skipList do\n skipSet[entry] = true\n end\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 if skipSet[relativePath] then\n continue\n end\n\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
166
|
//#endregion
|
|
161
167
|
//#region src/config/luau-config-loader.ts
|
|
162
168
|
let cachedTemporaryDirectory$1;
|
|
@@ -400,24 +406,6 @@ function buildProjectConfigFromLuau(luauConfigPath, directoryPath) {
|
|
|
400
406
|
copyLuauOptionalFields(raw, config);
|
|
401
407
|
return config;
|
|
402
408
|
}
|
|
403
|
-
function matchNodePath(childNode, targetPath, childDataModelPath) {
|
|
404
|
-
const nodePath = childNode.$path;
|
|
405
|
-
if (typeof nodePath !== "string") return;
|
|
406
|
-
const normalizedNodePath = nodePath.replace(/\/$/, "");
|
|
407
|
-
if (normalizedNodePath === targetPath) return childDataModelPath;
|
|
408
|
-
if (targetPath.startsWith(`${normalizedNodePath}/`)) return `${childDataModelPath}/${targetPath.slice(normalizedNodePath.length + 1)}`;
|
|
409
|
-
}
|
|
410
|
-
function findInTree(node, targetPath, currentDataModelPath) {
|
|
411
|
-
for (const [key, value] of Object.entries(node)) {
|
|
412
|
-
if (key.startsWith("$") || typeof value !== "object") continue;
|
|
413
|
-
const childNode = value;
|
|
414
|
-
const childDataModelPath = currentDataModelPath === "" ? key : `${currentDataModelPath}/${key}`;
|
|
415
|
-
const pathMatch = matchNodePath(childNode, targetPath, childDataModelPath);
|
|
416
|
-
if (pathMatch !== void 0) return pathMatch;
|
|
417
|
-
const found = findInTree(childNode, targetPath, childDataModelPath);
|
|
418
|
-
if (found !== void 0) return found;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
409
|
//#endregion
|
|
422
410
|
//#region src/config/setup-resolver.ts
|
|
423
411
|
const PROBE_EXTENSIONS = [
|
|
@@ -552,22 +540,39 @@ function findShadowStubs(directory) {
|
|
|
552
540
|
* Derives `collectCoverageFrom` glob patterns from project `include` patterns.
|
|
553
541
|
*
|
|
554
542
|
* Extracts the static root directory from each include pattern and generates
|
|
555
|
-
* coverage globs that match
|
|
556
|
-
*
|
|
557
|
-
*
|
|
543
|
+
* coverage globs that match source files within those roots, excluding test
|
|
544
|
+
* files. The source extension (`.ts`, `.tsx`, `.luau`, `.lua`) is inferred from
|
|
545
|
+
* each include pattern. Returns `undefined` when no roots can be extracted
|
|
546
|
+
* (preserving default all-files behavior).
|
|
558
547
|
*/
|
|
559
548
|
function deriveCoverageFromIncludes(projects) {
|
|
560
|
-
const
|
|
561
|
-
for (const project of projects) for (const pattern of project.include)
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
549
|
+
const rootsByExtension = /* @__PURE__ */ new Map();
|
|
550
|
+
for (const project of projects) for (const pattern of project.include) {
|
|
551
|
+
const extension = inferSourceExtension(pattern);
|
|
552
|
+
try {
|
|
553
|
+
const { root } = extractStaticRoot(pattern);
|
|
554
|
+
const roots = rootsByExtension.get(extension) ?? /* @__PURE__ */ new Set();
|
|
555
|
+
roots.add(root);
|
|
556
|
+
rootsByExtension.set(extension, roots);
|
|
557
|
+
} catch {}
|
|
558
|
+
}
|
|
559
|
+
if (rootsByExtension.size === 0) return;
|
|
566
560
|
const patterns = [];
|
|
567
|
-
for (const root of roots) patterns.push(`${root}
|
|
568
|
-
patterns.push(
|
|
561
|
+
for (const [extension, roots] of rootsByExtension) for (const root of roots) patterns.push(`${root}/**/*${extension}`);
|
|
562
|
+
for (const extension of rootsByExtension.keys()) patterns.push(`!**/*.spec${extension}`, `!**/*.test${extension}`);
|
|
569
563
|
return patterns;
|
|
570
564
|
}
|
|
565
|
+
/**
|
|
566
|
+
* Infers the source file extension from an include pattern by stripping the
|
|
567
|
+
* `.spec` or `.test` suffix. Throws when the pattern has no recognizable test
|
|
568
|
+
* extension so that misconfigured globs fail loudly.
|
|
569
|
+
*/
|
|
570
|
+
function inferSourceExtension(pattern) {
|
|
571
|
+
const match = pattern.match(/\.(?:spec|test)(\.\w+)$/);
|
|
572
|
+
if (!match) throw new Error(`Cannot infer source extension from include pattern "${pattern}". Patterns must end with .spec.<ext> or .test.<ext> (e.g. **/*.spec.ts, **/*.test.luau).`);
|
|
573
|
+
const [, extension] = match;
|
|
574
|
+
return extension;
|
|
575
|
+
}
|
|
571
576
|
//#endregion
|
|
572
577
|
//#region src/coverage/mapper.ts
|
|
573
578
|
const positionSchema = type({
|
|
@@ -1027,276 +1032,6 @@ function mergeFileCoverage(a, b) {
|
|
|
1027
1032
|
return merged;
|
|
1028
1033
|
}
|
|
1029
1034
|
//#endregion
|
|
1030
|
-
//#region src/luau/visitor.ts
|
|
1031
|
-
function visitExpression(expression, visitor) {
|
|
1032
|
-
if (visitor.visitExpr?.(expression) === false) return;
|
|
1033
|
-
const { tag } = expression;
|
|
1034
|
-
switch (tag) {
|
|
1035
|
-
case "binary":
|
|
1036
|
-
visitExprBinary(expression, visitor);
|
|
1037
|
-
break;
|
|
1038
|
-
case "boolean":
|
|
1039
|
-
visitor.visitExprConstantBool?.(expression);
|
|
1040
|
-
break;
|
|
1041
|
-
case "call":
|
|
1042
|
-
visitExprCall(expression, visitor);
|
|
1043
|
-
break;
|
|
1044
|
-
case "cast":
|
|
1045
|
-
visitExprTypeAssertion(expression, visitor);
|
|
1046
|
-
break;
|
|
1047
|
-
case "conditional":
|
|
1048
|
-
visitExprIfElse(expression, visitor);
|
|
1049
|
-
break;
|
|
1050
|
-
case "function":
|
|
1051
|
-
visitExprFunction(expression, visitor);
|
|
1052
|
-
break;
|
|
1053
|
-
case "global":
|
|
1054
|
-
visitor.visitExprGlobal?.(expression);
|
|
1055
|
-
break;
|
|
1056
|
-
case "group":
|
|
1057
|
-
visitExprGroup(expression, visitor);
|
|
1058
|
-
break;
|
|
1059
|
-
case "index":
|
|
1060
|
-
visitExprIndexExpr(expression, visitor);
|
|
1061
|
-
break;
|
|
1062
|
-
case "indexname":
|
|
1063
|
-
visitExprIndexName(expression, visitor);
|
|
1064
|
-
break;
|
|
1065
|
-
case "instantiate":
|
|
1066
|
-
visitExprInstantiate(expression, visitor);
|
|
1067
|
-
break;
|
|
1068
|
-
case "interpolatedstring":
|
|
1069
|
-
visitExprInterpString(expression, visitor);
|
|
1070
|
-
break;
|
|
1071
|
-
case "local":
|
|
1072
|
-
visitor.visitExprLocal?.(expression);
|
|
1073
|
-
break;
|
|
1074
|
-
case "nil":
|
|
1075
|
-
visitor.visitExprConstantNil?.(expression);
|
|
1076
|
-
break;
|
|
1077
|
-
case "number":
|
|
1078
|
-
visitor.visitExprConstantNumber?.(expression);
|
|
1079
|
-
break;
|
|
1080
|
-
case "string":
|
|
1081
|
-
visitor.visitExprConstantString?.(expression);
|
|
1082
|
-
break;
|
|
1083
|
-
case "table":
|
|
1084
|
-
visitExprTable(expression, visitor);
|
|
1085
|
-
break;
|
|
1086
|
-
case "unary":
|
|
1087
|
-
visitExprUnary(expression, visitor);
|
|
1088
|
-
break;
|
|
1089
|
-
case "vararg":
|
|
1090
|
-
visitor.visitExprVarargs?.(expression);
|
|
1091
|
-
break;
|
|
1092
|
-
default: break;
|
|
1093
|
-
}
|
|
1094
|
-
visitor.visitExprEnd?.(expression);
|
|
1095
|
-
}
|
|
1096
|
-
function visitStatement(statement, visitor) {
|
|
1097
|
-
const { tag } = statement;
|
|
1098
|
-
switch (tag) {
|
|
1099
|
-
case "assign":
|
|
1100
|
-
visitStatAssign(statement, visitor);
|
|
1101
|
-
break;
|
|
1102
|
-
case "block":
|
|
1103
|
-
visitStatBlock(statement, visitor);
|
|
1104
|
-
break;
|
|
1105
|
-
case "break":
|
|
1106
|
-
visitor.visitStatBreak?.(statement);
|
|
1107
|
-
break;
|
|
1108
|
-
case "compoundassign":
|
|
1109
|
-
visitStatCompoundAssign(statement, visitor);
|
|
1110
|
-
break;
|
|
1111
|
-
case "conditional":
|
|
1112
|
-
visitStatIf(statement, visitor);
|
|
1113
|
-
break;
|
|
1114
|
-
case "continue":
|
|
1115
|
-
visitor.visitStatContinue?.(statement);
|
|
1116
|
-
break;
|
|
1117
|
-
case "do":
|
|
1118
|
-
visitStatDo(statement, visitor);
|
|
1119
|
-
break;
|
|
1120
|
-
case "expression":
|
|
1121
|
-
visitStatExpr(statement, visitor);
|
|
1122
|
-
break;
|
|
1123
|
-
case "for":
|
|
1124
|
-
visitStatFor(statement, visitor);
|
|
1125
|
-
break;
|
|
1126
|
-
case "forin":
|
|
1127
|
-
visitStatForIn(statement, visitor);
|
|
1128
|
-
break;
|
|
1129
|
-
case "function":
|
|
1130
|
-
visitStatFunction(statement, visitor);
|
|
1131
|
-
break;
|
|
1132
|
-
case "local":
|
|
1133
|
-
visitStatLocal(statement, visitor);
|
|
1134
|
-
break;
|
|
1135
|
-
case "localfunction":
|
|
1136
|
-
visitStatLocalFunction(statement, visitor);
|
|
1137
|
-
break;
|
|
1138
|
-
case "repeat":
|
|
1139
|
-
visitStatRepeat(statement, visitor);
|
|
1140
|
-
break;
|
|
1141
|
-
case "return":
|
|
1142
|
-
visitStatReturn(statement, visitor);
|
|
1143
|
-
break;
|
|
1144
|
-
case "typealias":
|
|
1145
|
-
visitor.visitStatTypeAlias?.(statement);
|
|
1146
|
-
break;
|
|
1147
|
-
case "typefunction":
|
|
1148
|
-
visitor.visitStatTypeFunction?.(statement);
|
|
1149
|
-
break;
|
|
1150
|
-
case "while":
|
|
1151
|
-
visitStatWhile(statement, visitor);
|
|
1152
|
-
break;
|
|
1153
|
-
default: break;
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
function visitBlock(block, visitor) {
|
|
1157
|
-
visitStatBlock(block, visitor);
|
|
1158
|
-
}
|
|
1159
|
-
function visitPunctuated(list, visitor, apply) {
|
|
1160
|
-
for (const item of list) apply(item.node, visitor);
|
|
1161
|
-
}
|
|
1162
|
-
function visitStatBlock(block, visitor) {
|
|
1163
|
-
if (visitor.visitStatBlock?.(block) === false) return;
|
|
1164
|
-
for (const statement of block.statements) visitStatement(statement, visitor);
|
|
1165
|
-
visitor.visitStatBlockEnd?.(block);
|
|
1166
|
-
}
|
|
1167
|
-
function visitStatDo(node, visitor) {
|
|
1168
|
-
if (visitor.visitStatDo?.(node) === false) return;
|
|
1169
|
-
visitStatBlock(node.body, visitor);
|
|
1170
|
-
}
|
|
1171
|
-
function visitStatIf(node, visitor) {
|
|
1172
|
-
if (visitor.visitStatIf?.(node) === false) return;
|
|
1173
|
-
visitExpression(node.condition, visitor);
|
|
1174
|
-
visitStatBlock(node.thenblock, visitor);
|
|
1175
|
-
for (const elseif of node.elseifs) visitElseIfStat(elseif, visitor);
|
|
1176
|
-
if (node.elseblock) visitStatBlock(node.elseblock, visitor);
|
|
1177
|
-
}
|
|
1178
|
-
function visitElseIfStat(node, visitor) {
|
|
1179
|
-
visitExpression(node.condition, visitor);
|
|
1180
|
-
visitStatBlock(node.thenblock, visitor);
|
|
1181
|
-
}
|
|
1182
|
-
function visitStatWhile(node, visitor) {
|
|
1183
|
-
if (visitor.visitStatWhile?.(node) === false) return;
|
|
1184
|
-
visitExpression(node.condition, visitor);
|
|
1185
|
-
visitStatBlock(node.body, visitor);
|
|
1186
|
-
}
|
|
1187
|
-
function visitStatRepeat(node, visitor) {
|
|
1188
|
-
if (visitor.visitStatRepeat?.(node) === false) return;
|
|
1189
|
-
visitStatBlock(node.body, visitor);
|
|
1190
|
-
visitExpression(node.condition, visitor);
|
|
1191
|
-
}
|
|
1192
|
-
function visitStatReturn(node, visitor) {
|
|
1193
|
-
if (visitor.visitStatReturn?.(node) === false) return;
|
|
1194
|
-
visitPunctuated(node.expressions, visitor, visitExpression);
|
|
1195
|
-
}
|
|
1196
|
-
function visitStatLocal(node, visitor) {
|
|
1197
|
-
if (visitor.visitStatLocal?.(node) === false) return;
|
|
1198
|
-
visitPunctuated(node.values, visitor, visitExpression);
|
|
1199
|
-
}
|
|
1200
|
-
function visitStatFor(node, visitor) {
|
|
1201
|
-
if (visitor.visitStatFor?.(node) === false) return;
|
|
1202
|
-
visitExpression(node.from, visitor);
|
|
1203
|
-
visitExpression(node.to, visitor);
|
|
1204
|
-
if (node.step) visitExpression(node.step, visitor);
|
|
1205
|
-
visitStatBlock(node.body, visitor);
|
|
1206
|
-
}
|
|
1207
|
-
function visitStatForIn(node, visitor) {
|
|
1208
|
-
if (visitor.visitStatForIn?.(node) === false) return;
|
|
1209
|
-
visitPunctuated(node.values, visitor, visitExpression);
|
|
1210
|
-
visitStatBlock(node.body, visitor);
|
|
1211
|
-
}
|
|
1212
|
-
function visitStatAssign(node, visitor) {
|
|
1213
|
-
if (visitor.visitStatAssign?.(node) === false) return;
|
|
1214
|
-
visitPunctuated(node.variables, visitor, visitExpression);
|
|
1215
|
-
visitPunctuated(node.values, visitor, visitExpression);
|
|
1216
|
-
}
|
|
1217
|
-
function visitStatCompoundAssign(node, visitor) {
|
|
1218
|
-
if (visitor.visitStatCompoundAssign?.(node) === false) return;
|
|
1219
|
-
visitExpression(node.variable, visitor);
|
|
1220
|
-
visitExpression(node.value, visitor);
|
|
1221
|
-
}
|
|
1222
|
-
function visitStatExpr(node, visitor) {
|
|
1223
|
-
if (visitor.visitStatExpr?.(node) === false) return;
|
|
1224
|
-
visitExpression(node.expression, visitor);
|
|
1225
|
-
}
|
|
1226
|
-
function visitStatFunction(node, visitor) {
|
|
1227
|
-
if (visitor.visitStatFunction?.(node) === false) return;
|
|
1228
|
-
visitExpression(node.name, visitor);
|
|
1229
|
-
visitExprFunction(node.func, visitor);
|
|
1230
|
-
}
|
|
1231
|
-
function visitStatLocalFunction(node, visitor) {
|
|
1232
|
-
if (visitor.visitStatLocalFunction?.(node) === false) return;
|
|
1233
|
-
visitExprFunction(node.func, visitor);
|
|
1234
|
-
}
|
|
1235
|
-
function visitExprFunction(node, visitor) {
|
|
1236
|
-
if (visitor.visitExprFunction?.(node) === false) return;
|
|
1237
|
-
visitStatBlock(node.body, visitor);
|
|
1238
|
-
visitor.visitExprFunctionEnd?.(node);
|
|
1239
|
-
}
|
|
1240
|
-
function visitExprCall(node, visitor) {
|
|
1241
|
-
if (visitor.visitExprCall?.(node) === false) return;
|
|
1242
|
-
visitExpression(node.func, visitor);
|
|
1243
|
-
visitPunctuated(node.arguments, visitor, visitExpression);
|
|
1244
|
-
}
|
|
1245
|
-
function visitExprUnary(node, visitor) {
|
|
1246
|
-
if (visitor.visitExprUnary?.(node) === false) return;
|
|
1247
|
-
visitExpression(node.operand, visitor);
|
|
1248
|
-
}
|
|
1249
|
-
function visitExprBinary(node, visitor) {
|
|
1250
|
-
if (visitor.visitExprBinary?.(node) === false) return;
|
|
1251
|
-
visitExpression(node.lhsoperand, visitor);
|
|
1252
|
-
visitExpression(node.rhsoperand, visitor);
|
|
1253
|
-
}
|
|
1254
|
-
function visitExprTable(node, visitor) {
|
|
1255
|
-
if (visitor.visitExprTable?.(node) === false) return;
|
|
1256
|
-
for (const item of node.entries) visitTableExprItem(item, visitor);
|
|
1257
|
-
}
|
|
1258
|
-
function visitTableExprItem(node, visitor) {
|
|
1259
|
-
if (visitor.visitTableExprItem?.(node) === false) return;
|
|
1260
|
-
visitExpression(node.value, visitor);
|
|
1261
|
-
if (node.kind === "general") visitExpression(node.key, visitor);
|
|
1262
|
-
}
|
|
1263
|
-
function visitExprIndexName(node, visitor) {
|
|
1264
|
-
if (visitor.visitExprIndexName?.(node) === false) return;
|
|
1265
|
-
visitExpression(node.expression, visitor);
|
|
1266
|
-
}
|
|
1267
|
-
function visitExprIndexExpr(node, visitor) {
|
|
1268
|
-
if (visitor.visitExprIndexExpr?.(node) === false) return;
|
|
1269
|
-
visitExpression(node.expression, visitor);
|
|
1270
|
-
visitExpression(node.index, visitor);
|
|
1271
|
-
}
|
|
1272
|
-
function visitExprGroup(node, visitor) {
|
|
1273
|
-
if (visitor.visitExprGroup?.(node) === false) return;
|
|
1274
|
-
visitExpression(node.expression, visitor);
|
|
1275
|
-
}
|
|
1276
|
-
function visitExprInterpString(node, visitor) {
|
|
1277
|
-
if (visitor.visitExprInterpString?.(node) === false) return;
|
|
1278
|
-
for (const expr of node.expressions) visitExpression(expr, visitor);
|
|
1279
|
-
}
|
|
1280
|
-
function visitExprTypeAssertion(node, visitor) {
|
|
1281
|
-
if (visitor.visitExprTypeAssertion?.(node) === false) return;
|
|
1282
|
-
visitExpression(node.operand, visitor);
|
|
1283
|
-
}
|
|
1284
|
-
function visitExprIfElse(node, visitor) {
|
|
1285
|
-
if (visitor.visitExprIfElse?.(node) === false) return;
|
|
1286
|
-
visitExpression(node.condition, visitor);
|
|
1287
|
-
visitExpression(node.thenexpr, visitor);
|
|
1288
|
-
for (const elseif of node.elseifs) visitElseIfExpr(elseif, visitor);
|
|
1289
|
-
visitExpression(node.elseexpr, visitor);
|
|
1290
|
-
}
|
|
1291
|
-
function visitElseIfExpr(node, visitor) {
|
|
1292
|
-
visitExpression(node.condition, visitor);
|
|
1293
|
-
visitExpression(node.thenexpr, visitor);
|
|
1294
|
-
}
|
|
1295
|
-
function visitExprInstantiate(node, visitor) {
|
|
1296
|
-
if (visitor.visitExprInstantiate?.(node) === false) return;
|
|
1297
|
-
visitExpression(node.expr, visitor);
|
|
1298
|
-
}
|
|
1299
|
-
//#endregion
|
|
1300
1035
|
//#region src/coverage/coverage-collector.ts
|
|
1301
1036
|
const INSTRUMENTABLE_STATEMENT_TAGS = new Set([
|
|
1302
1037
|
"assign",
|
|
@@ -1350,36 +1085,36 @@ function collectCoverage(root) {
|
|
|
1350
1085
|
branch.arms.push({
|
|
1351
1086
|
bodyFirstColumn: 0,
|
|
1352
1087
|
bodyFirstLine: 0,
|
|
1353
|
-
location: { ...node.
|
|
1088
|
+
location: { ...node.thenExpr.location }
|
|
1354
1089
|
});
|
|
1355
1090
|
exprIfProbes.push({
|
|
1356
1091
|
armIndex,
|
|
1357
1092
|
branchIndex,
|
|
1358
|
-
exprLocation: { ...node.
|
|
1093
|
+
exprLocation: { ...node.thenExpr.location }
|
|
1359
1094
|
});
|
|
1360
1095
|
armIndex++;
|
|
1361
1096
|
for (const elseif of node.elseifs) {
|
|
1362
1097
|
branch.arms.push({
|
|
1363
1098
|
bodyFirstColumn: 0,
|
|
1364
1099
|
bodyFirstLine: 0,
|
|
1365
|
-
location: { ...elseif.
|
|
1100
|
+
location: { ...elseif.thenExpr.location }
|
|
1366
1101
|
});
|
|
1367
1102
|
exprIfProbes.push({
|
|
1368
1103
|
armIndex,
|
|
1369
1104
|
branchIndex,
|
|
1370
|
-
exprLocation: { ...elseif.
|
|
1105
|
+
exprLocation: { ...elseif.thenExpr.location }
|
|
1371
1106
|
});
|
|
1372
1107
|
armIndex++;
|
|
1373
1108
|
}
|
|
1374
1109
|
branch.arms.push({
|
|
1375
1110
|
bodyFirstColumn: 0,
|
|
1376
1111
|
bodyFirstLine: 0,
|
|
1377
|
-
location: { ...node.
|
|
1112
|
+
location: { ...node.elseExpr.location }
|
|
1378
1113
|
});
|
|
1379
1114
|
exprIfProbes.push({
|
|
1380
1115
|
armIndex,
|
|
1381
1116
|
branchIndex,
|
|
1382
|
-
exprLocation: { ...node.
|
|
1117
|
+
exprLocation: { ...node.elseExpr.location }
|
|
1383
1118
|
});
|
|
1384
1119
|
branches.push(branch);
|
|
1385
1120
|
branchIndex++;
|
|
@@ -1415,21 +1150,21 @@ function collectCoverage(root) {
|
|
|
1415
1150
|
branchType: "if",
|
|
1416
1151
|
index: branchIndex
|
|
1417
1152
|
};
|
|
1418
|
-
const thenFirst = getBodyFirstStatement(node.
|
|
1153
|
+
const thenFirst = getBodyFirstStatement(node.thenBlock);
|
|
1419
1154
|
branch.arms.push({
|
|
1420
1155
|
bodyFirstColumn: thenFirst.column,
|
|
1421
1156
|
bodyFirstLine: thenFirst.line,
|
|
1422
|
-
location: { ...node.
|
|
1157
|
+
location: { ...node.thenBlock.location }
|
|
1423
1158
|
});
|
|
1424
1159
|
for (const elseif of node.elseifs) {
|
|
1425
|
-
const elseifFirst = getBodyFirstStatement(elseif.
|
|
1160
|
+
const elseifFirst = getBodyFirstStatement(elseif.thenBlock);
|
|
1426
1161
|
branch.arms.push({
|
|
1427
1162
|
bodyFirstColumn: elseifFirst.column,
|
|
1428
1163
|
bodyFirstLine: elseifFirst.line,
|
|
1429
|
-
location: { ...elseif.
|
|
1164
|
+
location: { ...elseif.thenBlock.location }
|
|
1430
1165
|
});
|
|
1431
1166
|
}
|
|
1432
|
-
const elseBlock = node
|
|
1167
|
+
const { elseBlock } = node;
|
|
1433
1168
|
const hasExplicitElse = elseBlock !== void 0 && elseBlock.statements.length > 0;
|
|
1434
1169
|
if (hasExplicitElse) {
|
|
1435
1170
|
const elseFirst = getBodyFirstStatement(elseBlock);
|
|
@@ -1676,15 +1411,21 @@ function instrumentRoot(options) {
|
|
|
1676
1411
|
const astOutputDirectory = astOutputDirectoryOption ?? path$1.join(lazyTemporaryDirectory, "asts");
|
|
1677
1412
|
if (parseScript === void 0) fs$1.writeFileSync(scriptPath, parse_ast_default);
|
|
1678
1413
|
fs$1.mkdirSync(astOutputDirectory, { recursive: true });
|
|
1414
|
+
const luteArgs = [
|
|
1415
|
+
"run",
|
|
1416
|
+
scriptPath,
|
|
1417
|
+
"--",
|
|
1418
|
+
path$1.resolve(luauRoot),
|
|
1419
|
+
astOutputDirectory
|
|
1420
|
+
];
|
|
1421
|
+
if (skipFiles !== void 0 && skipFiles.size > 0) {
|
|
1422
|
+
const skipListPath = toPosix(path$1.join(astOutputDirectory, "skip-list.json"));
|
|
1423
|
+
fs$1.writeFileSync(skipListPath, JSON.stringify([...skipFiles]));
|
|
1424
|
+
luteArgs.push(skipListPath);
|
|
1425
|
+
}
|
|
1679
1426
|
let fileListJson;
|
|
1680
1427
|
try {
|
|
1681
|
-
fileListJson = cp.execFileSync("lute",
|
|
1682
|
-
"run",
|
|
1683
|
-
scriptPath,
|
|
1684
|
-
"--",
|
|
1685
|
-
path$1.resolve(luauRoot),
|
|
1686
|
-
astOutputDirectory
|
|
1687
|
-
], {
|
|
1428
|
+
fileListJson = cp.execFileSync("lute", luteArgs, {
|
|
1688
1429
|
encoding: "utf-8",
|
|
1689
1430
|
maxBuffer: 1024 * 1024
|
|
1690
1431
|
});
|
|
@@ -1732,7 +1473,7 @@ function instrumentRoot(options) {
|
|
|
1732
1473
|
functionCount: collectorResult.functions.length,
|
|
1733
1474
|
instrumentedLuauPath,
|
|
1734
1475
|
originalLuauPath,
|
|
1735
|
-
sourceHash: hashBuffer(sourceBuffer),
|
|
1476
|
+
sourceHash: hashBuffer$1(sourceBuffer),
|
|
1736
1477
|
sourceMapPath,
|
|
1737
1478
|
statementCount: collectorResult.statements.length
|
|
1738
1479
|
};
|
|
@@ -1841,7 +1582,7 @@ const previousManifestSchema = type({
|
|
|
1841
1582
|
}).as();
|
|
1842
1583
|
function collectLuauRootsFromRojo(project, config) {
|
|
1843
1584
|
const paths = [];
|
|
1844
|
-
collectPaths(project.tree, paths);
|
|
1585
|
+
collectPaths$1(project.tree, paths);
|
|
1845
1586
|
const ignorePatterns = config.coveragePathIgnorePatterns;
|
|
1846
1587
|
const isIgnored = picomatch(ignorePatterns, { contains: true });
|
|
1847
1588
|
return paths.filter((directoryPath) => {
|
|
@@ -1851,6 +1592,16 @@ function collectLuauRootsFromRojo(project, config) {
|
|
|
1851
1592
|
return containsLuauFiles(directoryPath);
|
|
1852
1593
|
});
|
|
1853
1594
|
}
|
|
1595
|
+
/**
|
|
1596
|
+
* Fast directory walk to discover instrumentable .luau/.lua files.
|
|
1597
|
+
* Must match parse-ast.luau's discoverFiles logic (same skip rules).
|
|
1598
|
+
*/
|
|
1599
|
+
function discoverInstrumentableFiles(luauRoot) {
|
|
1600
|
+
const posixRoot = luauRoot.replaceAll("\\", "/");
|
|
1601
|
+
const results = [];
|
|
1602
|
+
walkLuauDirectory(posixRoot, posixRoot, isInstrumentableFile, results);
|
|
1603
|
+
return new Set(results);
|
|
1604
|
+
}
|
|
1854
1605
|
function prepareCoverage(config, beforeBuild) {
|
|
1855
1606
|
const rojoProjectPath = findRojoProject(config);
|
|
1856
1607
|
const luauRoots = resolveLuauRootsWithRojo(config, rojoProjectPath);
|
|
@@ -1924,26 +1675,30 @@ function resolveLuauRootsWithRojo(config, rojoProjectPath) {
|
|
|
1924
1675
|
if (outDirectory !== void 0) return [outDirectory];
|
|
1925
1676
|
throw new Error("Could not determine luauRoots. Set luauRoots in config or ensure tsconfig has outDir.");
|
|
1926
1677
|
}
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
const
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1678
|
+
/**
|
|
1679
|
+
* Shared directory walker. Skips node_modules, .jest-roblox-coverage, and
|
|
1680
|
+
* dot-prefixed directories — matching parse-ast.luau:113-147.
|
|
1681
|
+
* `predicate` receives the entry name and returns true to collect the file.
|
|
1682
|
+
*/
|
|
1683
|
+
function walkLuauDirectory(directory, relativeTo, predicate, results) {
|
|
1684
|
+
const entries = fs$1.readdirSync(directory, { withFileTypes: true });
|
|
1685
|
+
for (const entry of entries) {
|
|
1686
|
+
const fullPath = path$1.join(directory, entry.name).replaceAll("\\", "/");
|
|
1687
|
+
if (entry.isDirectory()) {
|
|
1688
|
+
if (entry.name === "node_modules" || entry.name === COVERAGE_DIR) continue;
|
|
1689
|
+
if (entry.name.startsWith(".")) continue;
|
|
1690
|
+
walkLuauDirectory(fullPath, relativeTo, predicate, results);
|
|
1691
|
+
} else if (predicate(entry.name)) {
|
|
1692
|
+
const relative = fullPath.slice(relativeTo.length + 1);
|
|
1693
|
+
results.push(relative);
|
|
1694
|
+
}
|
|
1939
1695
|
}
|
|
1940
|
-
return skipFiles;
|
|
1941
1696
|
}
|
|
1942
|
-
function
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1697
|
+
function isInstrumentableFile(name) {
|
|
1698
|
+
return (name.endsWith(".luau") || name.endsWith(".lua")) && !isNonInstrumentedFile(name);
|
|
1699
|
+
}
|
|
1700
|
+
function validateRelativeRoots(luauRoots) {
|
|
1701
|
+
for (const root of luauRoots) if (path$1.isAbsolute(root)) throw new Error("luauRoots must be relative paths, got absolute path. Set a relative outDir in tsconfig or relative luauRoots in config.");
|
|
1947
1702
|
}
|
|
1948
1703
|
function carryForwardRecords(luauRoot, previousManifest, allFiles, skipFiles) {
|
|
1949
1704
|
const posixRoot = luauRoot.replaceAll("\\", "/");
|
|
@@ -1953,18 +1708,7 @@ function carryForwardRecords(luauRoot, previousManifest, allFiles, skipFiles) {
|
|
|
1953
1708
|
}
|
|
1954
1709
|
}
|
|
1955
1710
|
function discoverNonInstrumentedFiles(directory, relativeTo, results) {
|
|
1956
|
-
|
|
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
|
-
}
|
|
1711
|
+
walkLuauDirectory(directory, relativeTo, isNonInstrumentedFile, results);
|
|
1968
1712
|
}
|
|
1969
1713
|
function pruneStaleNonInstrumented(posixRoot, previousNonInstrumented, currentFiles) {
|
|
1970
1714
|
if (previousNonInstrumented === void 0) return false;
|
|
@@ -1988,7 +1732,7 @@ function syncNonInstrumentedFiles(luauRoot, shadowDirectory, previousNonInstrume
|
|
|
1988
1732
|
for (const relativePath of discovered) {
|
|
1989
1733
|
const sourcePath = `${posixRoot}/${relativePath}`;
|
|
1990
1734
|
const shadowPath = `${shadowDirectory}/${relativePath}`;
|
|
1991
|
-
const currentHash = hashBuffer(fs$1.readFileSync(path$1.resolve(sourcePath)));
|
|
1735
|
+
const currentHash = hashBuffer$1(fs$1.readFileSync(path$1.resolve(sourcePath)));
|
|
1992
1736
|
const previousRecord = previousNonInstrumented?.[sourcePath];
|
|
1993
1737
|
if (previousRecord?.sourceHash === currentHash) {
|
|
1994
1738
|
files[sourcePath] = previousRecord;
|
|
@@ -2010,6 +1754,59 @@ function syncNonInstrumentedFiles(luauRoot, shadowDirectory, previousNonInstrume
|
|
|
2010
1754
|
files
|
|
2011
1755
|
};
|
|
2012
1756
|
}
|
|
1757
|
+
function computeSkipFiles(luauRoot, previousManifest) {
|
|
1758
|
+
const skipFiles = /* @__PURE__ */ new Set();
|
|
1759
|
+
const posixRoot = luauRoot.replaceAll("\\", "/");
|
|
1760
|
+
for (const [fileKey, record] of Object.entries(previousManifest.files)) {
|
|
1761
|
+
if (!fileKey.startsWith(`${posixRoot}/`)) continue;
|
|
1762
|
+
const relativePath = fileKey.slice(posixRoot.length + 1);
|
|
1763
|
+
const sourcePath = path$1.resolve(record.originalLuauPath);
|
|
1764
|
+
if (!fs$1.existsSync(sourcePath)) continue;
|
|
1765
|
+
if (hashBuffer$1(fs$1.readFileSync(sourcePath)) === record.sourceHash) skipFiles.add(relativePath);
|
|
1766
|
+
}
|
|
1767
|
+
return skipFiles;
|
|
1768
|
+
}
|
|
1769
|
+
function countPreviousFilesForRoot(luauRoot, previousManifest) {
|
|
1770
|
+
const posixRoot = luauRoot.replaceAll("\\", "/");
|
|
1771
|
+
let count = 0;
|
|
1772
|
+
for (const fileKey of Object.keys(previousManifest.files)) if (fileKey.startsWith(`${posixRoot}/`)) count++;
|
|
1773
|
+
return count;
|
|
1774
|
+
}
|
|
1775
|
+
/**
|
|
1776
|
+
* Check if all files in this root are unchanged (full cache hit).
|
|
1777
|
+
*
|
|
1778
|
+
* `changed` means previous files were deleted or modified — it does NOT cover
|
|
1779
|
+
* new files appearing on disk. When `allCached` is false but `changed` is also
|
|
1780
|
+
* false, new files exist and the caller detects them when `instrumentRoot`
|
|
1781
|
+
* returns non-empty results.
|
|
1782
|
+
*/
|
|
1783
|
+
function computeIncrementalState(luauRoot, previousManifest) {
|
|
1784
|
+
const skipFiles = computeSkipFiles(luauRoot, previousManifest);
|
|
1785
|
+
const previousCount = countPreviousFilesForRoot(luauRoot, previousManifest);
|
|
1786
|
+
const changed = skipFiles.size !== previousCount;
|
|
1787
|
+
if (changed) return {
|
|
1788
|
+
allCached: false,
|
|
1789
|
+
changed,
|
|
1790
|
+
skipFiles
|
|
1791
|
+
};
|
|
1792
|
+
return {
|
|
1793
|
+
allCached: discoverInstrumentableFiles(luauRoot).size === previousCount,
|
|
1794
|
+
changed,
|
|
1795
|
+
skipFiles
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1798
|
+
function buildFullCacheResult(options) {
|
|
1799
|
+
const { luauRoot, previousManifest, rootEntry, shadowDirectory, skipFiles } = options;
|
|
1800
|
+
const allFiles = {};
|
|
1801
|
+
carryForwardRecords(luauRoot, previousManifest, allFiles, skipFiles);
|
|
1802
|
+
const syncResult = syncNonInstrumentedFiles(luauRoot, shadowDirectory, previousManifest.nonInstrumentedFiles);
|
|
1803
|
+
return {
|
|
1804
|
+
changed: syncResult.changed,
|
|
1805
|
+
files: allFiles,
|
|
1806
|
+
nonInstrumentedFiles: syncResult.files,
|
|
1807
|
+
rootEntry
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
2013
1810
|
function instrumentRootWithCache(luauRoot, useIncremental, previousManifest) {
|
|
2014
1811
|
const shadowDirectory = path$1.join(COVERAGE_DIR, luauRoot).replaceAll("\\", "/");
|
|
2015
1812
|
let changed = false;
|
|
@@ -2017,11 +1814,23 @@ function instrumentRootWithCache(luauRoot, useIncremental, previousManifest) {
|
|
|
2017
1814
|
fs$1.mkdirSync(shadowDirectory, { recursive: true });
|
|
2018
1815
|
fs$1.cpSync(luauRoot, shadowDirectory, { recursive: true });
|
|
2019
1816
|
}
|
|
1817
|
+
const rootEntry = {
|
|
1818
|
+
luauRoot,
|
|
1819
|
+
relocatedShadowDirectory: path$1.relative(COVERAGE_DIR, shadowDirectory).replaceAll("\\", "/"),
|
|
1820
|
+
shadowDir: shadowDirectory
|
|
1821
|
+
};
|
|
2020
1822
|
let skipFiles;
|
|
2021
1823
|
if (useIncremental && previousManifest !== void 0) {
|
|
2022
|
-
skipFiles =
|
|
2023
|
-
|
|
2024
|
-
|
|
1824
|
+
const { allCached, changed: hasChanges, skipFiles: computed } = computeIncrementalState(luauRoot, previousManifest);
|
|
1825
|
+
skipFiles = computed;
|
|
1826
|
+
changed = hasChanges;
|
|
1827
|
+
if (allCached) return buildFullCacheResult({
|
|
1828
|
+
luauRoot,
|
|
1829
|
+
previousManifest,
|
|
1830
|
+
rootEntry,
|
|
1831
|
+
shadowDirectory,
|
|
1832
|
+
skipFiles
|
|
1833
|
+
});
|
|
2025
1834
|
}
|
|
2026
1835
|
const files = instrumentRoot({
|
|
2027
1836
|
luauRoot,
|
|
@@ -2033,16 +1842,11 @@ function instrumentRootWithCache(luauRoot, useIncremental, previousManifest) {
|
|
|
2033
1842
|
if (useIncremental && previousManifest !== void 0 && skipFiles !== void 0) carryForwardRecords(luauRoot, previousManifest, allFiles, skipFiles);
|
|
2034
1843
|
const syncResult = syncNonInstrumentedFiles(luauRoot, shadowDirectory, previousManifest?.nonInstrumentedFiles);
|
|
2035
1844
|
if (syncResult.changed) changed = true;
|
|
2036
|
-
const relocatedShadowDirectory = path$1.relative(COVERAGE_DIR, shadowDirectory).replaceAll("\\", "/");
|
|
2037
1845
|
return {
|
|
2038
1846
|
changed,
|
|
2039
1847
|
files: allFiles,
|
|
2040
1848
|
nonInstrumentedFiles: syncResult.files,
|
|
2041
|
-
rootEntry
|
|
2042
|
-
luauRoot,
|
|
2043
|
-
relocatedShadowDirectory,
|
|
2044
|
-
shadowDir: shadowDirectory
|
|
2045
|
-
}
|
|
1849
|
+
rootEntry
|
|
2046
1850
|
};
|
|
2047
1851
|
}
|
|
2048
1852
|
function writeManifest(options) {
|
|
@@ -2312,6 +2116,8 @@ Options:
|
|
|
2312
2116
|
--formatters <name...> Output formatters (default, agent, json, github-actions)
|
|
2313
2117
|
--no-cache Force re-upload place file (skip cache)
|
|
2314
2118
|
--pollInterval <ms> Open Cloud poll interval in ms (default: 500)
|
|
2119
|
+
--parallel [n] Open-Cloud-only: number of concurrent sessions
|
|
2120
|
+
(or "auto" = min(jobs, 3); default: 1 session)
|
|
2315
2121
|
--project <name...> Filter which named projects to run
|
|
2316
2122
|
--setupFiles <path...> Setup scripts (package specifiers or relative paths)
|
|
2317
2123
|
--setupFilesAfterEnv <path...> Post-env setup scripts (package specifiers or relative paths)
|
|
@@ -2338,7 +2144,7 @@ Examples:
|
|
|
2338
2144
|
function parseArgs(args) {
|
|
2339
2145
|
const { positionals, values } = parseArgs$1({
|
|
2340
2146
|
allowPositionals: true,
|
|
2341
|
-
args,
|
|
2147
|
+
args: normalizeParallelFlag(args),
|
|
2342
2148
|
options: {
|
|
2343
2149
|
"backend": { type: "string" },
|
|
2344
2150
|
"cache": { type: "boolean" },
|
|
@@ -2367,6 +2173,7 @@ function parseArgs(args) {
|
|
|
2367
2173
|
"no-color": { type: "boolean" },
|
|
2368
2174
|
"no-show-luau": { type: "boolean" },
|
|
2369
2175
|
"outputFile": { type: "string" },
|
|
2176
|
+
"parallel": { type: "string" },
|
|
2370
2177
|
"passWithNoTests": { type: "boolean" },
|
|
2371
2178
|
"pollInterval": { type: "string" },
|
|
2372
2179
|
"port": { type: "string" },
|
|
@@ -2424,6 +2231,7 @@ function parseArgs(args) {
|
|
|
2424
2231
|
gameOutput: values.gameOutput,
|
|
2425
2232
|
help: values.help,
|
|
2426
2233
|
outputFile: values.outputFile,
|
|
2234
|
+
parallel: parseParallelValue(values.parallel),
|
|
2427
2235
|
passWithNoTests: values.passWithNoTests,
|
|
2428
2236
|
pollInterval,
|
|
2429
2237
|
port,
|
|
@@ -2466,11 +2274,8 @@ function mergeProjectResults(results) {
|
|
|
2466
2274
|
let startTime = Number.POSITIVE_INFINITY;
|
|
2467
2275
|
let success = true;
|
|
2468
2276
|
const testResults = [];
|
|
2469
|
-
let executionMs = 0;
|
|
2470
2277
|
let testsMs = 0;
|
|
2471
|
-
let
|
|
2472
|
-
let uploadMs = 0;
|
|
2473
|
-
let coverageMs = 0;
|
|
2278
|
+
let setupMs = 0;
|
|
2474
2279
|
let mergedCoverage;
|
|
2475
2280
|
for (const result of results) {
|
|
2476
2281
|
numberFailedTests += result.result.numFailedTests;
|
|
@@ -2481,13 +2286,14 @@ function mergeProjectResults(results) {
|
|
|
2481
2286
|
startTime = Math.min(startTime, result.result.startTime);
|
|
2482
2287
|
success &&= result.result.success;
|
|
2483
2288
|
testResults.push(...result.result.testResults);
|
|
2484
|
-
executionMs += result.timing.executionMs;
|
|
2485
2289
|
testsMs += result.timing.testsMs;
|
|
2486
|
-
|
|
2487
|
-
uploadMs += result.timing.uploadMs ?? 0;
|
|
2488
|
-
coverageMs += result.timing.coverageMs ?? 0;
|
|
2290
|
+
setupMs += result.timing.setupMs ?? 0;
|
|
2489
2291
|
if (result.coverageData !== void 0) mergedCoverage = mergeRawCoverage(mergedCoverage, result.coverageData);
|
|
2490
2292
|
}
|
|
2293
|
+
const [sharedTiming] = results;
|
|
2294
|
+
const mergedStartTime = Math.min(...results.map((result) => result.timing.startTime));
|
|
2295
|
+
const totalMs = Math.max(...results.map((result) => result.timing.totalMs));
|
|
2296
|
+
const mergedSourceMapper = combineSourceMappers(results.flatMap((result) => result.sourceMapper !== void 0 ? [result.sourceMapper] : []));
|
|
2491
2297
|
return {
|
|
2492
2298
|
coverageData: mergedCoverage,
|
|
2493
2299
|
exitCode: success ? 0 : 1,
|
|
@@ -2502,14 +2308,16 @@ function mergeProjectResults(results) {
|
|
|
2502
2308
|
success,
|
|
2503
2309
|
testResults
|
|
2504
2310
|
},
|
|
2311
|
+
sourceMapper: mergedSourceMapper,
|
|
2505
2312
|
timing: {
|
|
2506
|
-
coverageMs: coverageMs
|
|
2507
|
-
executionMs,
|
|
2508
|
-
|
|
2313
|
+
coverageMs: sharedTiming.timing.coverageMs,
|
|
2314
|
+
executionMs: sharedTiming.timing.executionMs,
|
|
2315
|
+
setupMs: setupMs > 0 ? setupMs : void 0,
|
|
2316
|
+
startTime: mergedStartTime,
|
|
2509
2317
|
testsMs,
|
|
2510
2318
|
totalMs,
|
|
2511
|
-
uploadCached:
|
|
2512
|
-
uploadMs
|
|
2319
|
+
uploadCached: sharedTiming.timing.uploadCached,
|
|
2320
|
+
uploadMs: sharedTiming.timing.uploadMs
|
|
2513
2321
|
}
|
|
2514
2322
|
};
|
|
2515
2323
|
}
|
|
@@ -2525,6 +2333,36 @@ async function main() {
|
|
|
2525
2333
|
const exitCode = await run(process.argv.slice(2));
|
|
2526
2334
|
process.exit(exitCode);
|
|
2527
2335
|
}
|
|
2336
|
+
/**
|
|
2337
|
+
* `--parallel` with no value means `"auto"`. Node's `parseArgs` can't express
|
|
2338
|
+
* optional values, so rewrite bare `--parallel` (at the end of argv, or
|
|
2339
|
+
* followed by another `--flag`, or followed by a non-numeric, non-"auto" token)
|
|
2340
|
+
* into `--parallel auto` before handing it off.
|
|
2341
|
+
*/
|
|
2342
|
+
const PARALLEL_FLAG = "--parallel";
|
|
2343
|
+
function normalizeParallelFlag(args) {
|
|
2344
|
+
const out = [];
|
|
2345
|
+
for (let index = 0; index < args.length; index++) {
|
|
2346
|
+
const current = args[index];
|
|
2347
|
+
if (current !== PARALLEL_FLAG) {
|
|
2348
|
+
out.push(current);
|
|
2349
|
+
continue;
|
|
2350
|
+
}
|
|
2351
|
+
const next = args[index + 1];
|
|
2352
|
+
if (next !== void 0 && !next.startsWith("-") && (next === "auto" || /^-?\d+$/.test(next))) {
|
|
2353
|
+
out.push(PARALLEL_FLAG, next);
|
|
2354
|
+
index += 1;
|
|
2355
|
+
} else out.push(PARALLEL_FLAG, "auto");
|
|
2356
|
+
}
|
|
2357
|
+
return out;
|
|
2358
|
+
}
|
|
2359
|
+
function parseParallelValue(raw) {
|
|
2360
|
+
if (raw === void 0) return;
|
|
2361
|
+
if (raw === "auto") return "auto";
|
|
2362
|
+
const parsed = Number.parseInt(raw, 10);
|
|
2363
|
+
if (Number.isNaN(parsed) || parsed < 1) throw new Error(`Invalid --parallel value "${raw}". Must be a positive integer or "auto".`);
|
|
2364
|
+
return parsed;
|
|
2365
|
+
}
|
|
2528
2366
|
function formatGameOutputLines(raw) {
|
|
2529
2367
|
if (raw === void 0) return;
|
|
2530
2368
|
const entries = parseGameOutput(raw);
|
|
@@ -2563,6 +2401,21 @@ function writeGameOutputIfConfigured(config, gameOutput, options) {
|
|
|
2563
2401
|
if (notice) console.error(notice);
|
|
2564
2402
|
}
|
|
2565
2403
|
}
|
|
2404
|
+
/**
|
|
2405
|
+
* Multi-project variant: `--gameOutput` used to silently drop when a config
|
|
2406
|
+
* declared `projects`, because the output path here never called
|
|
2407
|
+
* `writeGameOutputIfConfigured`. Aggregate every project's parsed entries into
|
|
2408
|
+
* one file so the contract matches the single-project path.
|
|
2409
|
+
*/
|
|
2410
|
+
function writeAggregatedGameOutput(config, projectResults, options) {
|
|
2411
|
+
if (config.gameOutput === void 0) return;
|
|
2412
|
+
const entries = projectResults.flatMap((project) => parseGameOutput(project.result.gameOutput));
|
|
2413
|
+
writeGameOutput(config.gameOutput, entries);
|
|
2414
|
+
if (!config.silent && options.hintsShown !== true) {
|
|
2415
|
+
const notice = formatGameOutputNotice(config.gameOutput, entries.length);
|
|
2416
|
+
if (notice) console.error(notice);
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2566
2419
|
function printFinalStatus(passed) {
|
|
2567
2420
|
const badge = passed ? color.bgGreen(color.black(color.bold(" PASS "))) : color.bgRed(color.white(color.bold(" FAIL ")));
|
|
2568
2421
|
process.stdout.write(`${badge}\n`);
|
|
@@ -2746,6 +2599,7 @@ async function outputMultiProjectResults(config, projectResults, typecheckResult
|
|
|
2746
2599
|
}
|
|
2747
2600
|
const coveragePassed = processCoverage(config, merged.coverageData);
|
|
2748
2601
|
if (config.outputFile !== void 0) await writeJsonFile(mergedResult, config.outputFile);
|
|
2602
|
+
writeAggregatedGameOutput(config, projectResults, { hintsShown: !mergedResult.success });
|
|
2749
2603
|
runGitHubActionsFormatter(config, mergedResult, merged.sourceMapper);
|
|
2750
2604
|
const passed = mergedResult.success && coveragePassed;
|
|
2751
2605
|
if (!config.silent && config.collectCoverage) printFinalStatus(passed);
|
|
@@ -2814,6 +2668,15 @@ function applySetupResolver(config, resolve) {
|
|
|
2814
2668
|
if (config.setupFiles !== void 0) config.setupFiles = config.setupFiles.map(resolve);
|
|
2815
2669
|
if (config.setupFilesAfterEnv !== void 0) config.setupFilesAfterEnv = config.setupFilesAfterEnv.map(resolve);
|
|
2816
2670
|
}
|
|
2671
|
+
/**
|
|
2672
|
+
* Drop `parallel` on any non-open-cloud backend. Studio has no concept of
|
|
2673
|
+
* multi-session, so passing `parallel` there is a silent noop — this lets
|
|
2674
|
+
* users keep `parallel: 3` in `jest.config.ts` and still drop to
|
|
2675
|
+
* `--backend studio` for debugging without editing config.
|
|
2676
|
+
*/
|
|
2677
|
+
function effectiveParallelForBackend(parallel, backend) {
|
|
2678
|
+
return backend.kind === "open-cloud" ? parallel : void 0;
|
|
2679
|
+
}
|
|
2817
2680
|
async function runMultiProject(cli, rootConfig, projectEntries) {
|
|
2818
2681
|
const allProjects = await resolveAllProjects(projectEntries, rootConfig, loadRojoTree(rootConfig), rootConfig.rootDir);
|
|
2819
2682
|
const rojoConfigPath = path$1.resolve(rootConfig.rootDir, rootConfig.rojoProject ?? DEFAULT_ROJO_PROJECT);
|
|
@@ -2827,7 +2690,8 @@ async function runMultiProject(cli, rootConfig, projectEntries) {
|
|
|
2827
2690
|
if (!rootConfig.collectCoverage) buildWithRojo(path$1.resolve(rootConfig.rootDir, rootConfig.rojoProject ?? DEFAULT_ROJO_PROJECT), path$1.resolve(rootConfig.rootDir, rootConfig.placeFile));
|
|
2828
2691
|
const { effectiveConfig, preCoverageMs } = prepareMultiProjectCoverage(rootConfig, projects);
|
|
2829
2692
|
const backend = await resolveBackend(effectiveConfig);
|
|
2830
|
-
const
|
|
2693
|
+
const parallel = effectiveParallelForBackend(effectiveConfig.parallel, backend);
|
|
2694
|
+
const pendingJobs = [];
|
|
2831
2695
|
const allTypeTestFiles = [];
|
|
2832
2696
|
for (const project of projects) {
|
|
2833
2697
|
const discoveryConfig = {
|
|
@@ -2842,19 +2706,46 @@ async function runMultiProject(cli, rootConfig, projectEntries) {
|
|
|
2842
2706
|
testMatch: project.testMatch
|
|
2843
2707
|
};
|
|
2844
2708
|
allTypeTestFiles.push(...typeTestFiles);
|
|
2845
|
-
if (runtimeFiles.length === 0
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2709
|
+
if (runtimeFiles.length === 0) continue;
|
|
2710
|
+
pendingJobs.push({
|
|
2711
|
+
config: projConfig,
|
|
2712
|
+
displayColor: project.displayColor,
|
|
2713
|
+
displayName: project.displayName,
|
|
2714
|
+
runtimeFiles
|
|
2715
|
+
});
|
|
2716
|
+
}
|
|
2717
|
+
const jobs = pendingJobs.map((pending) => {
|
|
2718
|
+
return buildProjectJob({
|
|
2719
|
+
config: pending.config,
|
|
2720
|
+
displayColor: pending.displayColor,
|
|
2721
|
+
displayName: pending.displayName,
|
|
2722
|
+
testFiles: pending.runtimeFiles
|
|
2723
|
+
});
|
|
2724
|
+
});
|
|
2725
|
+
const projectResults = [];
|
|
2726
|
+
if (jobs.length > 0) {
|
|
2727
|
+
const startTime = Date.now();
|
|
2728
|
+
let backendResult;
|
|
2729
|
+
try {
|
|
2730
|
+
backendResult = await executeBackend(backend, jobs, parallel);
|
|
2731
|
+
} finally {
|
|
2732
|
+
await backend.close?.();
|
|
2733
|
+
}
|
|
2734
|
+
const sharedTiming = backendResult.timing;
|
|
2735
|
+
for (const [index, entry] of backendResult.results.entries()) {
|
|
2736
|
+
const pending = pendingJobs[index];
|
|
2737
|
+
const jobConfig = jobs[index].config;
|
|
2738
|
+
const executeResult = processProjectResult(entry, {
|
|
2739
|
+
backendTiming: sharedTiming,
|
|
2740
|
+
config: jobConfig,
|
|
2850
2741
|
deferFormatting: true,
|
|
2851
|
-
|
|
2742
|
+
startTime,
|
|
2852
2743
|
version: VERSION
|
|
2853
2744
|
});
|
|
2854
2745
|
projectResults.push({
|
|
2855
|
-
displayColor:
|
|
2856
|
-
displayName:
|
|
2857
|
-
result
|
|
2746
|
+
displayColor: pending.displayColor,
|
|
2747
|
+
displayName: pending.displayName,
|
|
2748
|
+
result: executeResult
|
|
2858
2749
|
});
|
|
2859
2750
|
}
|
|
2860
2751
|
}
|
|
@@ -2877,13 +2768,18 @@ async function runMultiProject(cli, rootConfig, projectEntries) {
|
|
|
2877
2768
|
}
|
|
2878
2769
|
async function executeRuntimeTests(config, testFiles, totalFiles) {
|
|
2879
2770
|
if (!config.silent && !usesAgentFormatter(config) && !hasFormatter(config, "json") && testFiles.length !== totalFiles) process.stderr.write(`Running ${String(testFiles.length)} of ${String(totalFiles)} test files\n`);
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2771
|
+
const backend = await resolveBackend(config);
|
|
2772
|
+
try {
|
|
2773
|
+
return await execute({
|
|
2774
|
+
backend,
|
|
2775
|
+
config,
|
|
2776
|
+
deferFormatting: true,
|
|
2777
|
+
testFiles,
|
|
2778
|
+
version: VERSION
|
|
2779
|
+
});
|
|
2780
|
+
} finally {
|
|
2781
|
+
await backend.close?.();
|
|
2782
|
+
}
|
|
2887
2783
|
}
|
|
2888
2784
|
function resolveSetupFilePaths(config) {
|
|
2889
2785
|
if (config.setupFiles === void 0 && config.setupFilesAfterEnv === void 0) return;
|
|
@@ -2893,9 +2789,9 @@ function resolveSetupFilePaths(config) {
|
|
|
2893
2789
|
rojoConfigPath
|
|
2894
2790
|
}));
|
|
2895
2791
|
}
|
|
2896
|
-
async function runSingleProject(
|
|
2792
|
+
async function runSingleProject(cli, config) {
|
|
2897
2793
|
resolveSetupFilePaths(config);
|
|
2898
|
-
const discovery = discoverTestFiles(config,
|
|
2794
|
+
const discovery = discoverTestFiles(config, cli.files);
|
|
2899
2795
|
if (discovery.files.length === 0) {
|
|
2900
2796
|
if (config.passWithNoTests) return 0;
|
|
2901
2797
|
console.error("No test files found");
|
|
@@ -2941,7 +2837,7 @@ async function runInner(args) {
|
|
|
2941
2837
|
const config = mergeCliWithConfig(cli, await loadConfig$1(cli.config));
|
|
2942
2838
|
const rawProjects = config.projects;
|
|
2943
2839
|
if (rawProjects !== void 0 && rawProjects.length > 0) return runMultiProject(cli, config, rawProjects);
|
|
2944
|
-
return runSingleProject(
|
|
2840
|
+
return runSingleProject(cli, config);
|
|
2945
2841
|
}
|
|
2946
2842
|
const LUAU_ERROR_HINTS = [
|
|
2947
2843
|
[/Failed to find Jest instance in ReplicatedStorage/, "Set \"jestPath\" in your config to specify the Jest module location, e.g. \"ReplicatedStorage/rbxts_include/node_modules/@rbxts/jest/src\""],
|
|
@@ -3010,6 +2906,7 @@ function mergeCliWithConfig(cli, config) {
|
|
|
3010
2906
|
formatters: resolveFormatters(cli, config),
|
|
3011
2907
|
gameOutput: cli.gameOutput ?? config.gameOutput,
|
|
3012
2908
|
outputFile: cli.outputFile ?? config.outputFile,
|
|
2909
|
+
parallel: cli.parallel ?? config.parallel,
|
|
3013
2910
|
passWithNoTests: cli.passWithNoTests ?? config.passWithNoTests,
|
|
3014
2911
|
pollInterval: cli.pollInterval ?? config.pollInterval,
|
|
3015
2912
|
port: cli.port ?? config.port,
|