@isentinel/jest-roblox 0.2.7 → 0.3.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/README.md +234 -46
- package/dist/cli.d.mts +2 -4
- package/dist/cli.mjs +113 -2812
- package/dist/index.d.mts +595 -40
- package/dist/index.mjs +2 -2
- package/dist/run-Cl5gYSQr.mjs +9945 -0
- package/dist/{executor-COuwZJJX.d.mts → schema-BpjBo-Aw.d.mts} +139 -301
- package/dist/sea-entry.cjs +40329 -40257
- package/package.json +29 -25
- package/plugin/JestRobloxRunner.rbxm +0 -0
- package/plugin/plugin.project.json +1 -1
- package/plugin/src/init.server.luau +39 -0
- package/plugin/src/test-in-run-mode.server.luau +117 -1
- package/dist/game-output-CCPIQMWm.mjs +0 -3643
- package/dist/sea/jest-roblox +0 -0
- package/plugin/out/shared/entry.luau +0 -9
- package/plugin/out/shared/instance-resolver.luau +0 -88
- package/plugin/out/shared/mock/CoreScriptSyncService.luau +0 -19
- package/plugin/out/shared/mock/FileSystemService.luau +0 -30
- package/plugin/out/shared/promise.luau +0 -2006
- package/plugin/out/shared/runner.luau +0 -301
- package/plugin/out/shared/setup-timing.luau +0 -89
- package/plugin/out/shared/snapshot-patch.luau +0 -94
package/dist/cli.mjs
CHANGED
|
@@ -1,2207 +1,33 @@
|
|
|
1
|
-
import { A as
|
|
2
|
-
import {
|
|
3
|
-
import { type } from "arktype";
|
|
4
|
-
import assert from "node:assert";
|
|
5
|
-
import * as fs$1 from "node:fs";
|
|
6
|
-
import fs from "node:fs";
|
|
7
|
-
import * as path$1 from "node:path";
|
|
8
|
-
import path from "node:path";
|
|
1
|
+
import { A as mergeCliWithConfig, D as LuauScriptError, E as formatBanner, H as ConfigError, R as VALID_BACKENDS, U as version, V as isValidBackend, f as outputMultiResult, j as loadConfig, p as outputSingleResult, t as runJestRoblox, y as parseGameOutput } from "./run-Cl5gYSQr.mjs";
|
|
2
|
+
import { OpenCloudError } from "@bedrock-rbx/ocale";
|
|
9
3
|
import process from "node:process";
|
|
10
4
|
import { parseArgs as parseArgs$1 } from "node:util";
|
|
11
|
-
import { isAgent } from "std-env";
|
|
12
5
|
import color from "tinyrainbow";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
//#region package.json
|
|
26
|
-
var version = "0.2.7";
|
|
27
|
-
//#endregion
|
|
28
|
-
//#region src/backends/auto.ts
|
|
29
|
-
var StudioWithFallback = class {
|
|
30
|
-
studio;
|
|
31
|
-
kind = "studio";
|
|
32
|
-
constructor(studio) {
|
|
33
|
-
this.studio = studio;
|
|
34
|
-
}
|
|
35
|
-
async close() {
|
|
36
|
-
await this.studio.close?.();
|
|
37
|
-
}
|
|
38
|
-
async runTests(options) {
|
|
39
|
-
try {
|
|
40
|
-
return await this.studio.runTests(options);
|
|
41
|
-
} catch (err) {
|
|
42
|
-
if (isStudioBusyError(err)) {
|
|
43
|
-
process.stderr.write("Studio busy, falling back to Open Cloud\n");
|
|
44
|
-
return createOpenCloudBackend().runTests(options);
|
|
45
|
-
}
|
|
46
|
-
throw err;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
function isStudioBusyError(error) {
|
|
51
|
-
if (error instanceof LuauScriptError) return /previous call to start play session/i.test(error.message);
|
|
52
|
-
return typeof error === "object" && error !== null && "code" in error && error.code === "EADDRINUSE";
|
|
53
|
-
}
|
|
54
|
-
async function probeStudioPlugin(port, timeoutMs, createServer = (wsPort) => {
|
|
55
|
-
return new WebSocketServer({ port: wsPort });
|
|
56
|
-
}) {
|
|
57
|
-
return new Promise((resolve) => {
|
|
58
|
-
const wss = createServer(port);
|
|
59
|
-
const timer = setTimeout(() => {
|
|
60
|
-
wss.close();
|
|
61
|
-
resolve({ detected: false });
|
|
62
|
-
}, timeoutMs);
|
|
63
|
-
wss.on("connection", (ws) => {
|
|
64
|
-
clearTimeout(timer);
|
|
65
|
-
resolve({
|
|
66
|
-
detected: true,
|
|
67
|
-
server: wss,
|
|
68
|
-
socket: ws
|
|
69
|
-
});
|
|
70
|
-
});
|
|
71
|
-
wss.on("error", () => {
|
|
72
|
-
clearTimeout(timer);
|
|
73
|
-
wss.close();
|
|
74
|
-
resolve({ detected: false });
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
async function resolveBackend(config, probe = probeStudioPlugin) {
|
|
79
|
-
if (config.backend === "studio") return createStudioBackend({
|
|
80
|
-
port: config.port,
|
|
81
|
-
timeout: config.timeout
|
|
82
|
-
});
|
|
83
|
-
if (config.backend === "open-cloud") return createOpenCloudBackend();
|
|
84
|
-
const probeResult = await probe(config.port, 500);
|
|
85
|
-
if (probeResult.detected) {
|
|
86
|
-
process.stderr.write("Backend: studio (plugin detected)\n");
|
|
87
|
-
const studio = createStudioBackend({
|
|
88
|
-
port: config.port,
|
|
89
|
-
preConnected: {
|
|
90
|
-
server: probeResult.server,
|
|
91
|
-
socket: probeResult.socket
|
|
92
|
-
},
|
|
93
|
-
timeout: config.timeout
|
|
94
|
-
});
|
|
95
|
-
if (hasOpenCloudCredentials()) return new StudioWithFallback(studio);
|
|
96
|
-
return studio;
|
|
97
|
-
}
|
|
98
|
-
if (hasOpenCloudCredentials()) {
|
|
99
|
-
process.stderr.write("Backend: open-cloud (no plugin, using Open Cloud)\n");
|
|
100
|
-
return createOpenCloudBackend();
|
|
101
|
-
}
|
|
102
|
-
throw new Error("No backend available: Studio plugin not detected and Open Cloud env vars (ROBLOX_OPEN_CLOUD_API_KEY, ROBLOX_UNIVERSE_ID, ROBLOX_PLACE_ID) are missing");
|
|
103
|
-
}
|
|
104
|
-
function hasOpenCloudCredentials() {
|
|
105
|
-
return process.env["ROBLOX_OPEN_CLOUD_API_KEY"] !== void 0 && process.env["ROBLOX_UNIVERSE_ID"] !== void 0 && process.env["ROBLOX_PLACE_ID"] !== void 0;
|
|
106
|
-
}
|
|
107
|
-
//#endregion
|
|
108
|
-
//#region src/config/errors.ts
|
|
109
|
-
var ConfigError = class extends Error {
|
|
110
|
-
hint;
|
|
111
|
-
constructor(message, hint) {
|
|
112
|
-
super(message);
|
|
113
|
-
this.hint = hint;
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
//#endregion
|
|
117
|
-
//#region src/utils/extensions.ts
|
|
118
|
-
function stripTsExtension(pattern) {
|
|
119
|
-
return pattern.replace(/\.(tsx?|luau?)$/, "");
|
|
120
|
-
}
|
|
121
|
-
//#endregion
|
|
122
|
-
//#region src/luau/eval-literals.ts
|
|
123
|
-
/**
|
|
124
|
-
* Evaluate the first return expression in a Lute-stripped AST root block,
|
|
125
|
-
* supporting only literal values (string, boolean, number, nil, table, cast).
|
|
126
|
-
*
|
|
127
|
-
* Accepts `unknown` and narrows safely — no type casts on JSON.parse needed.
|
|
128
|
-
*/
|
|
129
|
-
function evalLuauReturnLiterals(root) {
|
|
130
|
-
if (!isObject(root) || !Array.isArray(root["statements"])) throw new Error("Config file has no return statement");
|
|
131
|
-
const returnStat = root["statements"].find((stat) => isObject(stat) && stat["tag"] === "return");
|
|
132
|
-
if (!isObject(returnStat) || !Array.isArray(returnStat["expressions"])) throw new Error("Config file has no return statement");
|
|
133
|
-
const first = returnStat["expressions"][0];
|
|
134
|
-
if (!isObject(first) || !("node" in first)) throw new Error("Return statement has no expressions");
|
|
135
|
-
return evalExpr(first["node"]);
|
|
136
|
-
}
|
|
137
|
-
function isObject(value) {
|
|
138
|
-
return typeof value === "object" && value !== null;
|
|
139
|
-
}
|
|
140
|
-
function evalExpr(node) {
|
|
141
|
-
if (!isObject(node)) return;
|
|
142
|
-
let current = node;
|
|
143
|
-
while (current["tag"] === "cast" && isObject(current["operand"])) current = current["operand"];
|
|
144
|
-
const { tag } = current;
|
|
145
|
-
if (tag === "boolean" || tag === "number") return current["value"];
|
|
146
|
-
if (tag === "string") return current["text"];
|
|
147
|
-
if (tag === "table" && Array.isArray(current["entries"])) return evalTable(current["entries"]);
|
|
148
|
-
}
|
|
149
|
-
function evalTable(entries) {
|
|
150
|
-
if (entries.length === 0) return {};
|
|
151
|
-
const first = entries[0];
|
|
152
|
-
if (isObject(first) && first["kind"] === "list") return entries.map((entry) => isObject(entry) ? evalExpr(entry["value"]) : void 0);
|
|
153
|
-
const result = {};
|
|
154
|
-
for (const entry of entries) {
|
|
155
|
-
if (!isObject(entry) || entry["kind"] !== "record") continue;
|
|
156
|
-
const { key, value } = entry;
|
|
157
|
-
if (isObject(key) && typeof key["text"] === "string") result[key["text"]] = evalExpr(value);
|
|
158
|
-
}
|
|
159
|
-
return result;
|
|
160
|
-
}
|
|
161
|
-
//#endregion
|
|
162
|
-
//#region src/luau/parse-ast.luau
|
|
163
|
-
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";
|
|
164
|
-
//#endregion
|
|
165
|
-
//#region src/config/luau-config-loader.ts
|
|
166
|
-
let cachedTemporaryDirectory$1;
|
|
167
|
-
/**
|
|
168
|
-
* Parse a .luau config file via Lute and evaluate its return expression.
|
|
169
|
-
*/
|
|
170
|
-
function loadLuauConfig(filePath) {
|
|
171
|
-
const temporaryDirectory = getTemporaryDirectory$1();
|
|
172
|
-
const scriptPath = path$1.join(temporaryDirectory, "parse-ast.luau");
|
|
173
|
-
fs$1.writeFileSync(scriptPath, parse_ast_default);
|
|
174
|
-
let stdout;
|
|
175
|
-
try {
|
|
176
|
-
stdout = cp.execFileSync("lute", [
|
|
177
|
-
"run",
|
|
178
|
-
scriptPath,
|
|
179
|
-
"--",
|
|
180
|
-
path$1.resolve(filePath)
|
|
181
|
-
], {
|
|
182
|
-
encoding: "utf-8",
|
|
183
|
-
maxBuffer: 1024 * 1024
|
|
184
|
-
});
|
|
185
|
-
} catch (err) {
|
|
186
|
-
if (err instanceof Error && "code" in err && err.code === "ENOENT") throw new Error("lute is required to load .luau config files but was not found on PATH");
|
|
187
|
-
throw new Error(`Failed to evaluate Luau config ${filePath}`, { cause: err });
|
|
188
|
-
}
|
|
189
|
-
let ast;
|
|
190
|
-
try {
|
|
191
|
-
ast = JSON.parse(stdout);
|
|
192
|
-
} catch (err) {
|
|
193
|
-
throw new Error(`Failed to parse AST JSON from Luau config ${filePath}`, { cause: err });
|
|
194
|
-
}
|
|
195
|
-
const result = evalLuauReturnLiterals(ast);
|
|
196
|
-
if (typeof result !== "object" || result === null) throw new Error(`Luau config ${filePath} must return a table`);
|
|
197
|
-
return result;
|
|
198
|
-
}
|
|
199
|
-
/**
|
|
200
|
-
* Check if `<cwd>/<directoryOrFile>/jest.config.luau` exists. Returns the
|
|
201
|
-
* resolved path if found, undefined otherwise.
|
|
202
|
-
*/
|
|
203
|
-
function findLuauConfigFile(directoryOrFile, cwd) {
|
|
204
|
-
const resolved = path$1.resolve(cwd, directoryOrFile, "jest.config.luau");
|
|
205
|
-
if (fs$1.existsSync(resolved)) return resolved;
|
|
206
|
-
}
|
|
207
|
-
function getTemporaryDirectory$1() {
|
|
208
|
-
if (cachedTemporaryDirectory$1 !== void 0 && fs$1.existsSync(cachedTemporaryDirectory$1)) return cachedTemporaryDirectory$1;
|
|
209
|
-
cachedTemporaryDirectory$1 = fs$1.mkdtempSync(path$1.join(os.tmpdir(), "jest-roblox-luau-config-"));
|
|
210
|
-
return cachedTemporaryDirectory$1;
|
|
211
|
-
}
|
|
212
|
-
//#endregion
|
|
213
|
-
//#region src/config/projects.ts
|
|
214
|
-
function extractStaticRoot(pattern) {
|
|
215
|
-
const globChars = new Set([
|
|
216
|
-
"*",
|
|
217
|
-
"?",
|
|
218
|
-
"[",
|
|
219
|
-
"{"
|
|
220
|
-
]);
|
|
221
|
-
let firstGlobIndex = -1;
|
|
222
|
-
for (const [index, char] of [...pattern].entries()) if (globChars.has(char)) {
|
|
223
|
-
firstGlobIndex = index;
|
|
224
|
-
break;
|
|
225
|
-
}
|
|
226
|
-
if (firstGlobIndex === -1) {
|
|
227
|
-
const directory = path$1.posix.dirname(pattern);
|
|
228
|
-
return {
|
|
229
|
-
glob: path$1.posix.basename(pattern),
|
|
230
|
-
root: directory
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
const lastSlash = pattern.slice(0, firstGlobIndex).lastIndexOf("/");
|
|
234
|
-
if (lastSlash === -1) throw new Error("Include pattern must have a static directory prefix");
|
|
235
|
-
return {
|
|
236
|
-
glob: pattern.slice(lastSlash + 1),
|
|
237
|
-
root: pattern.slice(0, lastSlash)
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
function mapFsRootToDataModel(outDirectory, rojoTree) {
|
|
241
|
-
const normalized = outDirectory.replace(/\/$/, "");
|
|
242
|
-
const result = findInTree(rojoTree, normalized, "");
|
|
243
|
-
if (result === void 0) {
|
|
244
|
-
const available = [];
|
|
245
|
-
collectPaths(rojoTree, available);
|
|
246
|
-
let message = `No Rojo tree mapping found for path: ${normalized}`;
|
|
247
|
-
if (available.length > 0) message += `\n\nAvailable $path entries: ${available.join(", ")}`;
|
|
248
|
-
const hint = normalized.startsWith("src/") ? "Path starts with \"src/\" — if using roblox-ts, set \"outDir\" in your project config to the compiled output directory (e.g. \"out/client\")" : void 0;
|
|
249
|
-
throw new ConfigError(message, hint);
|
|
250
|
-
}
|
|
251
|
-
return result;
|
|
252
|
-
}
|
|
253
|
-
function extractProjectRoots(include) {
|
|
254
|
-
const rootMap = /* @__PURE__ */ new Map();
|
|
255
|
-
for (const pattern of include) {
|
|
256
|
-
const { glob, root } = extractStaticRoot(pattern);
|
|
257
|
-
const stripped = stripTsExtension(glob);
|
|
258
|
-
const qualified = stripped.includes("/") ? stripped : `**/${stripped}`;
|
|
259
|
-
let patterns = rootMap.get(root);
|
|
260
|
-
if (patterns === void 0) {
|
|
261
|
-
patterns = [];
|
|
262
|
-
rootMap.set(root, patterns);
|
|
263
|
-
}
|
|
264
|
-
patterns.push(qualified);
|
|
265
|
-
}
|
|
266
|
-
return [...rootMap.entries()].map(([root, testMatch]) => ({
|
|
267
|
-
root,
|
|
268
|
-
testMatch
|
|
269
|
-
}));
|
|
270
|
-
}
|
|
271
|
-
function applyProjectRoot(include, projectRoot) {
|
|
272
|
-
if (projectRoot === void 0) return include;
|
|
273
|
-
return include.map((pattern) => path$1.posix.join(projectRoot, pattern));
|
|
274
|
-
}
|
|
275
|
-
function createFsClassifier(rootDirectory) {
|
|
276
|
-
return function classify(fsPath) {
|
|
277
|
-
const absolute = path$1.isAbsolute(fsPath) ? fsPath : path$1.resolve(rootDirectory, fsPath);
|
|
278
|
-
const stat = fs.statSync(absolute, { throwIfNoEntry: false });
|
|
279
|
-
if (stat === void 0) return "missing";
|
|
280
|
-
return stat.isDirectory() ? "directory" : "file";
|
|
281
|
-
};
|
|
282
|
-
}
|
|
283
|
-
function validateProjects(projects) {
|
|
284
|
-
const names = /* @__PURE__ */ new Set();
|
|
285
|
-
for (const project of projects) {
|
|
286
|
-
const name = displayNameOf(project);
|
|
287
|
-
if (name === "") throw new Error("Project must have a non-empty displayName");
|
|
288
|
-
if (names.has(name)) throw new Error(`Duplicate project displayName: ${name}`);
|
|
289
|
-
names.add(name);
|
|
290
|
-
if (project.include.length === 0) throw new Error(`Project "${name}" must have at least one include pattern`);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
const PROJECT_ONLY_KEYS = new Set([
|
|
294
|
-
"displayName",
|
|
295
|
-
"exclude",
|
|
296
|
-
"include",
|
|
297
|
-
"outDir",
|
|
298
|
-
"root"
|
|
299
|
-
]);
|
|
300
|
-
function resolveProjectConfig(project, rootConfig, rojoTree, classify) {
|
|
301
|
-
const rootPrefixedInclude = applyProjectRoot(project.include, project.root);
|
|
302
|
-
const roots = extractProjectRoots(rootPrefixedInclude);
|
|
303
|
-
const testMatch = roots.flatMap((entry) => entry.testMatch);
|
|
304
|
-
const rojoMounts = resolveMounts(project, roots, rojoTree, classify);
|
|
305
|
-
const projects = rojoMounts.map((mount) => mount.dataModelPath);
|
|
306
|
-
const singleMount = rojoMounts.length === 1 ? rojoMounts[0] : void 0;
|
|
307
|
-
const config = mergeProjectConfig(rootConfig, project);
|
|
308
|
-
const displayName = displayNameOf(project);
|
|
309
|
-
return {
|
|
310
|
-
config,
|
|
311
|
-
displayColor: typeof project.displayName === "string" ? void 0 : project.displayName.color,
|
|
312
|
-
displayName,
|
|
313
|
-
include: rootPrefixedInclude,
|
|
314
|
-
outDir: singleMount?.fsPath,
|
|
315
|
-
projects,
|
|
316
|
-
rojoMounts,
|
|
317
|
-
testMatch
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
async function loadProjectConfigFile(filePath, cwd) {
|
|
321
|
-
const luauConfigPath = findLuauConfigFile(filePath, cwd);
|
|
322
|
-
if (luauConfigPath !== void 0) return buildProjectConfigFromLuau(luauConfigPath, filePath);
|
|
323
|
-
let result;
|
|
324
|
-
try {
|
|
325
|
-
result = await loadConfig({
|
|
326
|
-
name: "jest-project",
|
|
327
|
-
configFile: filePath,
|
|
328
|
-
configFileRequired: true,
|
|
329
|
-
cwd,
|
|
330
|
-
dotenv: false,
|
|
331
|
-
globalRc: false,
|
|
332
|
-
omit$Keys: true,
|
|
333
|
-
packageJson: false,
|
|
334
|
-
rcFile: false
|
|
6
|
+
//#region src/utils/error-chain.ts
|
|
7
|
+
const MAX_DEPTH = 5;
|
|
8
|
+
function walkErrorChain(err) {
|
|
9
|
+
const entries = [];
|
|
10
|
+
let current = err;
|
|
11
|
+
while (current instanceof Error && entries.length < MAX_DEPTH) {
|
|
12
|
+
entries.push({
|
|
13
|
+
name: current.constructor.name,
|
|
14
|
+
code: readStringProperty(current, "code"),
|
|
15
|
+
errno: readStringProperty(current, "errno"),
|
|
16
|
+
message: current.message,
|
|
17
|
+
syscall: readStringProperty(current, "syscall")
|
|
335
18
|
});
|
|
336
|
-
|
|
337
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
338
|
-
throw new Error(`Failed to load project config file ${filePath}: ${message}`, { cause: err });
|
|
19
|
+
current = current.cause;
|
|
339
20
|
}
|
|
340
|
-
|
|
341
|
-
if ((typeof config.displayName === "string" ? config.displayName : config.displayName.name) === "") throw new Error(`Project config file "${filePath}" must have a displayName`);
|
|
342
|
-
deriveIncludeFromTestMatch(config, path$1.posix.dirname(filePath), resolveTsconfigDirectories(cwd));
|
|
343
|
-
return config;
|
|
21
|
+
return entries;
|
|
344
22
|
}
|
|
345
|
-
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
const loaded = await loadProjectConfigFile(entry, cwd);
|
|
349
|
-
projects.push(loaded);
|
|
350
|
-
} else projects.push(entry.test);
|
|
351
|
-
validateProjects(projects);
|
|
352
|
-
const classify = createFsClassifier(cwd);
|
|
353
|
-
return projects.map((project) => resolveProjectConfig(project, rootConfig, rojoTree, classify));
|
|
354
|
-
}
|
|
355
|
-
function displayNameOf(project) {
|
|
356
|
-
return typeof project.displayName === "string" ? project.displayName : project.displayName.name;
|
|
357
|
-
}
|
|
358
|
-
function mergeProjectConfig(rootConfig, project) {
|
|
359
|
-
const merged = { ...rootConfig };
|
|
360
|
-
for (const [key, value] of Object.entries(project)) if (!PROJECT_ONLY_KEYS.has(key) && value !== void 0) merged[key] = value;
|
|
361
|
-
return merged;
|
|
362
|
-
}
|
|
363
|
-
function dedupeMounts(mounts) {
|
|
364
|
-
const seen = /* @__PURE__ */ new Set();
|
|
365
|
-
const result = [];
|
|
366
|
-
for (const mount of mounts) if (!seen.has(mount.dataModelPath)) {
|
|
367
|
-
seen.add(mount.dataModelPath);
|
|
368
|
-
result.push(mount);
|
|
369
|
-
}
|
|
370
|
-
return result;
|
|
371
|
-
}
|
|
372
|
-
function joinProjectRoot(relativePath, projectRoot) {
|
|
373
|
-
return projectRoot !== void 0 ? path$1.posix.join(projectRoot, relativePath) : relativePath;
|
|
374
|
-
}
|
|
375
|
-
function pruneAncestorMounts(mounts) {
|
|
376
|
-
const dataModelPaths = mounts.map((mount) => mount.dataModelPath);
|
|
377
|
-
const surviving = new Set(pruneAncestors(dataModelPaths));
|
|
378
|
-
return mounts.filter((mount) => surviving.has(mount.dataModelPath));
|
|
379
|
-
}
|
|
380
|
-
function unmappableRootError(project, root, rojoTree) {
|
|
381
|
-
const name = displayNameOf(project);
|
|
382
|
-
const available = [];
|
|
383
|
-
collectPaths(rojoTree, available);
|
|
384
|
-
let message = `Project "${name}": include root "${root}" did not match any Rojo $path entry or subdirectory.`;
|
|
385
|
-
if (available.length > 0) message += `\n\nAvailable $path entries: ${available.join(", ")}`;
|
|
386
|
-
const hint = root.startsWith("src/") ? "Path starts with \"src/\" — if using roblox-ts, set \"outDir\" in your project config to the compiled output directory (e.g. \"out/client\")" : void 0;
|
|
387
|
-
return new ConfigError(message, hint);
|
|
388
|
-
}
|
|
389
|
-
function filterMountsForRoot(allMounts, root) {
|
|
390
|
-
return allMounts.filter((mount) => mount.fsPath === root || mount.fsPath.startsWith(`${root}/`));
|
|
391
|
-
}
|
|
392
|
-
function resolveMounts(project, roots, rojoTree, classify) {
|
|
393
|
-
if (project.outDir !== void 0) {
|
|
394
|
-
const resolvedOutDirectory = joinProjectRoot(project.outDir, project.root);
|
|
395
|
-
return [{
|
|
396
|
-
dataModelPath: mapFsRootToDataModel(resolvedOutDirectory, rojoTree),
|
|
397
|
-
fsPath: resolvedOutDirectory
|
|
398
|
-
}];
|
|
399
|
-
}
|
|
400
|
-
let collectedMounts;
|
|
401
|
-
const allMounts = [];
|
|
402
|
-
for (const { root } of roots) {
|
|
403
|
-
const exact = findInTree(rojoTree, root, "");
|
|
404
|
-
if (exact !== void 0) {
|
|
405
|
-
allMounts.push({
|
|
406
|
-
dataModelPath: exact,
|
|
407
|
-
fsPath: root
|
|
408
|
-
});
|
|
409
|
-
continue;
|
|
410
|
-
}
|
|
411
|
-
collectedMounts ??= collectMounts(rojoTree, "", classify);
|
|
412
|
-
const expanded = filterMountsForRoot(collectedMounts, root);
|
|
413
|
-
if (expanded.length === 0) throw unmappableRootError(project, root, rojoTree);
|
|
414
|
-
allMounts.push(...expanded);
|
|
415
|
-
}
|
|
416
|
-
return pruneAncestorMounts(dedupeMounts(allMounts));
|
|
417
|
-
}
|
|
418
|
-
function copyLuauOptionalFields(raw, config) {
|
|
419
|
-
const record = config;
|
|
420
|
-
for (const key of LUAU_BOOLEAN_KEYS) if (typeof raw[key] === "boolean") record[key] = raw[key];
|
|
421
|
-
for (const key of LUAU_NUMBER_KEYS) if (typeof raw[key] === "number") record[key] = raw[key];
|
|
422
|
-
for (const key of LUAU_STRING_KEYS) if (typeof raw[key] === "string") record[key] = raw[key];
|
|
423
|
-
for (const key of LUAU_STRING_ARRAY_KEYS) if (Array.isArray(raw[key])) record[key] = raw[key];
|
|
424
|
-
}
|
|
425
|
-
function buildProjectConfigFromLuau(luauConfigPath, directoryPath) {
|
|
426
|
-
const raw = loadLuauConfig(luauConfigPath);
|
|
427
|
-
const { displayName } = raw;
|
|
428
|
-
if (typeof displayName !== "string" || displayName === "") throw new Error(`Luau config file "${luauConfigPath}" must have a displayName string`);
|
|
429
|
-
const testMatch = Array.isArray(raw["testMatch"]) ? raw["testMatch"] : void 0;
|
|
430
|
-
const config = {
|
|
431
|
-
displayName,
|
|
432
|
-
include: testMatch !== void 0 ? testMatch.map((pattern) => path$1.posix.join(directoryPath, `${pattern}.luau`)) : [path$1.posix.join(directoryPath, "**/*.spec.luau")]
|
|
433
|
-
};
|
|
434
|
-
if (testMatch !== void 0) config.testMatch = testMatch;
|
|
435
|
-
copyLuauOptionalFields(raw, config);
|
|
436
|
-
return config;
|
|
437
|
-
}
|
|
438
|
-
/**
|
|
439
|
-
* When a project config provides `testMatch` but not `include`, derive
|
|
440
|
-
* `include` by appending `.ts` and `.tsx` extensions. This lets users
|
|
441
|
-
* write project configs with the standard Jest `testMatch` field without
|
|
442
|
-
* needing the CLI-specific `include`.
|
|
443
|
-
*/
|
|
444
|
-
function deriveIncludeFromTestMatch(config, configDirectory, tsconfig) {
|
|
445
|
-
const raw = config;
|
|
446
|
-
if (raw["include"] !== void 0) return;
|
|
447
|
-
if (!Array.isArray(raw["testMatch"])) return;
|
|
448
|
-
config.include = raw["testMatch"].flatMap((pattern) => {
|
|
449
|
-
return (/\.(tsx?|luau?)$/.test(pattern) ? [pattern] : [`${pattern}.ts`, `${pattern}.tsx`]).map((extension) => path$1.posix.join(configDirectory, extension));
|
|
450
|
-
});
|
|
451
|
-
const { outDir, rootDir } = tsconfig;
|
|
452
|
-
if (raw["outDir"] === void 0 && rootDir !== void 0 && outDir !== void 0) {
|
|
453
|
-
const rootPrefix = `${rootDir}/`;
|
|
454
|
-
if (configDirectory.startsWith(rootPrefix)) config.outDir = `${outDir}/${configDirectory.slice(rootPrefix.length)}`;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
const LUAU_BOOLEAN_KEYS = [
|
|
458
|
-
"automock",
|
|
459
|
-
"clearMocks",
|
|
460
|
-
"injectGlobals",
|
|
461
|
-
"mockDataModel",
|
|
462
|
-
"resetMocks",
|
|
463
|
-
"resetModules",
|
|
464
|
-
"restoreMocks"
|
|
465
|
-
];
|
|
466
|
-
const LUAU_NUMBER_KEYS = ["slowTestThreshold", "testTimeout"];
|
|
467
|
-
const LUAU_STRING_KEYS = ["testEnvironment"];
|
|
468
|
-
const LUAU_STRING_ARRAY_KEYS = ["setupFiles", "setupFilesAfterEnv"];
|
|
469
|
-
//#endregion
|
|
470
|
-
//#region src/config/setup-resolver.ts
|
|
471
|
-
const PROBE_EXTENSIONS = [
|
|
472
|
-
".ts",
|
|
473
|
-
".tsx",
|
|
474
|
-
".lua",
|
|
475
|
-
".luau"
|
|
476
|
-
];
|
|
477
|
-
function createSetupResolver(options) {
|
|
478
|
-
const { configDirectory, resolveModule, rojoConfigPath } = options;
|
|
479
|
-
const resolve = resolveModule ?? createRequire(path$1.join(configDirectory, "noop.js")).resolve;
|
|
480
|
-
const rojoResolver = RojoResolver.fromPath(rojoConfigPath);
|
|
481
|
-
return (input) => {
|
|
482
|
-
let absolutePath;
|
|
483
|
-
if (isRelativePath(input)) absolutePath = path$1.resolve(configDirectory, input);
|
|
484
|
-
else {
|
|
485
|
-
resolvePackageSpecifier(resolve, input);
|
|
486
|
-
absolutePath = path$1.resolve(configDirectory, "node_modules", input);
|
|
487
|
-
}
|
|
488
|
-
const rbxPath = rojoResolver.getRbxPathFromFilePath(absolutePath);
|
|
489
|
-
if (rbxPath === void 0) throw new Error(`No matching path found in rojo project tree for "${input}" (resolved to: ${absolutePath})`);
|
|
490
|
-
return rbxPath.join("/");
|
|
491
|
-
};
|
|
492
|
-
}
|
|
493
|
-
function isRelativePath(input) {
|
|
494
|
-
return input.startsWith("./") || input.startsWith("../");
|
|
495
|
-
}
|
|
496
|
-
function resolvePackageSpecifier(resolve, input) {
|
|
497
|
-
try {
|
|
498
|
-
resolve(input);
|
|
499
|
-
return;
|
|
500
|
-
} catch {}
|
|
501
|
-
for (const extension of PROBE_EXTENSIONS) try {
|
|
502
|
-
resolve(`${input}${extension}`);
|
|
503
|
-
return;
|
|
504
|
-
} catch {}
|
|
505
|
-
throw new Error(`Could not resolve module "${input}". Ensure the package is installed.`);
|
|
506
|
-
}
|
|
507
|
-
//#endregion
|
|
508
|
-
//#region src/config/stubs.ts
|
|
509
|
-
const HEADER = "-- Auto-generated by jest-roblox (do not edit)\n";
|
|
510
|
-
const STUB_FILENAME = "jest.config.lua";
|
|
511
|
-
const USER_LUAU_FILENAME = "jest.config.luau";
|
|
512
|
-
const SKIP_FIELDS = new Set(["exclude", "include"]);
|
|
513
|
-
function serializeToLuau(config) {
|
|
514
|
-
let output = `${HEADER}return {\n`;
|
|
515
|
-
for (const [key, value] of Object.entries(config)) {
|
|
516
|
-
if (SKIP_FIELDS.has(key)) continue;
|
|
517
|
-
if (value === void 0) continue;
|
|
518
|
-
let serialized;
|
|
519
|
-
if (key === "testMatch" && Array.isArray(value)) serialized = serializeLuauValue(value.map((pattern) => stripTsExtension(pattern)), " ");
|
|
520
|
-
else serialized = serializeLuauValue(value, " ");
|
|
521
|
-
output += `\t${key} = ${serialized},\n`;
|
|
522
|
-
}
|
|
523
|
-
output += "}\n";
|
|
524
|
-
return output;
|
|
525
|
-
}
|
|
526
|
-
function generateProjectConfigs(projects) {
|
|
527
|
-
for (const project of projects) {
|
|
528
|
-
const directory = path.dirname(project.outputPath);
|
|
529
|
-
if (hasUserAuthoredConfig(directory)) continue;
|
|
530
|
-
const content = serializeToLuau(project.config);
|
|
531
|
-
fs.mkdirSync(directory, { recursive: true });
|
|
532
|
-
fs.writeFileSync(project.outputPath, content, "utf8");
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
/**
|
|
536
|
-
* Refuse to let any downstream write land outside `rootDirectory`. Mount
|
|
537
|
-
* fsPaths originate from the rojo tree and `resolveNestedProjects` can emit
|
|
538
|
-
* values with `..` segments (nested projects relocate paths); combined with
|
|
539
|
-
* `path.resolve` on Windows, a permissive fsPath could send stub writes to
|
|
540
|
-
* `D:\outside\...`. Guard at every write-site.
|
|
541
|
-
*/
|
|
542
|
-
function assertMountContained(project, fsPath, rootDirectory) {
|
|
543
|
-
if (path.isAbsolute(fsPath)) throw new Error(`Project "${project.displayName}" mount fsPath must be relative, got: ${fsPath}`);
|
|
544
|
-
const rootResolved = path.resolve(rootDirectory);
|
|
545
|
-
const mountResolved = path.resolve(rootDirectory, fsPath);
|
|
546
|
-
if (mountResolved !== rootResolved && !mountResolved.startsWith(rootResolved + path.sep)) throw new Error(`Project "${project.displayName}" mount fsPath escapes root directory: ${fsPath}`);
|
|
547
|
-
}
|
|
548
|
-
/**
|
|
549
|
-
* For a multi-mount project, the stubs-per-mount rule requires that a
|
|
550
|
-
* user-authored config at any tracked FS path must appear at every tracked
|
|
551
|
-
* FS path (or none). Throws `ConfigError` when the project has partial
|
|
552
|
-
* coverage — some mounts have a user config and others don't.
|
|
553
|
-
*/
|
|
554
|
-
function assertStubCollisionRule(project, rootDirectory) {
|
|
555
|
-
const withUser = [];
|
|
556
|
-
const withoutUser = [];
|
|
557
|
-
for (const mount of project.rojoMounts) {
|
|
558
|
-
assertMountContained(project, mount.fsPath, rootDirectory);
|
|
559
|
-
if (hasUserAuthoredConfig(path.resolve(rootDirectory, mount.fsPath))) withUser.push(mount.fsPath);
|
|
560
|
-
else withoutUser.push(mount.fsPath);
|
|
561
|
-
}
|
|
562
|
-
if (withUser.length === 0 || withoutUser.length === 0) return;
|
|
563
|
-
const withUserList = withUser.join(", ");
|
|
564
|
-
const withoutUserList = withoutUser.join(", ");
|
|
565
|
-
throw new ConfigError(`Project "${project.displayName}": user-authored jest config present at some mounts but not others.\n\nWith user config: ${withUserList}\nWithout user config: ${withoutUserList}\n\nFor multi-mount projects, either every tracked mount must have a user config, or none.`);
|
|
566
|
-
}
|
|
567
|
-
function syncStubsToShadowDirectory(projects, rootDirectory, shadowDirectory) {
|
|
568
|
-
let changed = false;
|
|
569
|
-
const expectedPaths = /* @__PURE__ */ new Set();
|
|
570
|
-
for (const project of projects) for (const mount of project.rojoMounts) {
|
|
571
|
-
const result = syncMountStub(project, mount.fsPath, rootDirectory, shadowDirectory);
|
|
572
|
-
if (result.targetPath !== void 0) expectedPaths.add(result.targetPath);
|
|
573
|
-
changed ||= result.changed;
|
|
574
|
-
}
|
|
575
|
-
for (const existing of findShadowStubs(shadowDirectory)) if (!expectedPaths.has(existing)) {
|
|
576
|
-
fs.unlinkSync(existing);
|
|
577
|
-
removeEmptyParents(path.dirname(existing), shadowDirectory);
|
|
578
|
-
changed = true;
|
|
579
|
-
}
|
|
580
|
-
return changed;
|
|
581
|
-
}
|
|
582
|
-
function escapeString(value) {
|
|
583
|
-
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
584
|
-
}
|
|
585
|
-
function serializeLuauValue(value, indent) {
|
|
586
|
-
if (typeof value === "string") return `"${escapeString(value)}"`;
|
|
587
|
-
if (typeof value === "boolean") return value ? "true" : "false";
|
|
588
|
-
if (typeof value === "number") return String(value);
|
|
589
|
-
if (Array.isArray(value)) return `{ ${value.map((item) => serializeLuauValue(item, indent)).join(", ")} }`;
|
|
590
|
-
if (typeof value === "object" && value !== null) {
|
|
591
|
-
const nextIndent = `${indent}\t`;
|
|
592
|
-
return `{ ${Object.entries(value).filter(([, value_]) => value_ !== void 0).map(([key, value_]) => `${key} = ${serializeLuauValue(value_, nextIndent)}`).join(", ")} }`;
|
|
593
|
-
}
|
|
23
|
+
function readStringProperty(err, key) {
|
|
24
|
+
const value = Reflect.get(err, key);
|
|
25
|
+
if (value === void 0 || value === null) return;
|
|
594
26
|
return String(value);
|
|
595
27
|
}
|
|
596
|
-
function hasUserAuthoredConfig(directoryPath) {
|
|
597
|
-
const luauPath = path.join(directoryPath, USER_LUAU_FILENAME);
|
|
598
|
-
if (fs.existsSync(luauPath)) return true;
|
|
599
|
-
const luaPath = path.join(directoryPath, STUB_FILENAME);
|
|
600
|
-
if (!fs.existsSync(luaPath)) return false;
|
|
601
|
-
return !fs.readFileSync(luaPath, "utf8").startsWith(HEADER);
|
|
602
|
-
}
|
|
603
|
-
function syncMountStub(project, fsPath, rootDirectory, shadowDirectory) {
|
|
604
|
-
assertMountContained(project, fsPath, rootDirectory);
|
|
605
|
-
const sourcePath = path.resolve(rootDirectory, fsPath, STUB_FILENAME);
|
|
606
|
-
if (!fs.existsSync(sourcePath)) return {
|
|
607
|
-
changed: false,
|
|
608
|
-
targetPath: void 0
|
|
609
|
-
};
|
|
610
|
-
const targetPath = path.resolve(shadowDirectory, fsPath, STUB_FILENAME);
|
|
611
|
-
const sourceContent = fs.readFileSync(sourcePath);
|
|
612
|
-
if (fs.existsSync(targetPath)) {
|
|
613
|
-
const targetContent = fs.readFileSync(targetPath);
|
|
614
|
-
if (Buffer.compare(sourceContent, targetContent) === 0) return {
|
|
615
|
-
changed: false,
|
|
616
|
-
targetPath
|
|
617
|
-
};
|
|
618
|
-
}
|
|
619
|
-
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
620
|
-
fs.copyFileSync(sourcePath, targetPath);
|
|
621
|
-
return {
|
|
622
|
-
changed: true,
|
|
623
|
-
targetPath
|
|
624
|
-
};
|
|
625
|
-
}
|
|
626
|
-
function findShadowStubs(directory) {
|
|
627
|
-
const results = [];
|
|
628
|
-
if (!fs.existsSync(directory)) return results;
|
|
629
|
-
const entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
630
|
-
for (const entry of entries) {
|
|
631
|
-
const fullPath = path.resolve(directory, entry.name);
|
|
632
|
-
if (entry.isDirectory()) results.push(...findShadowStubs(fullPath));
|
|
633
|
-
else if (entry.name === STUB_FILENAME) results.push(fullPath);
|
|
634
|
-
}
|
|
635
|
-
return results;
|
|
636
|
-
}
|
|
637
|
-
function removeEmptyParents(directory, stopAt) {
|
|
638
|
-
const resolved = path.resolve(directory);
|
|
639
|
-
const resolvedStop = path.resolve(stopAt);
|
|
640
|
-
if (resolved === resolvedStop || !resolved.startsWith(resolvedStop)) return;
|
|
641
|
-
try {
|
|
642
|
-
if (fs.readdirSync(resolved).length === 0) {
|
|
643
|
-
fs.rmdirSync(resolved);
|
|
644
|
-
removeEmptyParents(path.dirname(resolved), stopAt);
|
|
645
|
-
}
|
|
646
|
-
} catch {}
|
|
647
|
-
}
|
|
648
|
-
//#endregion
|
|
649
|
-
//#region src/coverage/derive-coverage-from.ts
|
|
650
|
-
/**
|
|
651
|
-
* Derives `collectCoverageFrom` glob patterns from project `include` patterns.
|
|
652
|
-
*
|
|
653
|
-
* Extracts the static root directory from each include pattern and generates
|
|
654
|
-
* coverage globs that match source files within those roots, excluding test
|
|
655
|
-
* files. The source extension (`.ts`, `.tsx`, `.luau`, `.lua`) is inferred from
|
|
656
|
-
* each include pattern. Returns `undefined` when no roots can be extracted
|
|
657
|
-
* (preserving default all-files behavior).
|
|
658
|
-
*/
|
|
659
|
-
function deriveCoverageFromIncludes(projects) {
|
|
660
|
-
const rootsByExtension = /* @__PURE__ */ new Map();
|
|
661
|
-
for (const project of projects) for (const pattern of project.include) {
|
|
662
|
-
const extension = inferSourceExtension(pattern);
|
|
663
|
-
try {
|
|
664
|
-
const { root } = extractStaticRoot(pattern);
|
|
665
|
-
const roots = rootsByExtension.get(extension) ?? /* @__PURE__ */ new Set();
|
|
666
|
-
roots.add(root);
|
|
667
|
-
rootsByExtension.set(extension, roots);
|
|
668
|
-
} catch {}
|
|
669
|
-
}
|
|
670
|
-
if (rootsByExtension.size === 0) return;
|
|
671
|
-
const patterns = [];
|
|
672
|
-
for (const [extension, roots] of rootsByExtension) for (const root of roots) patterns.push(`${root}/**/*${extension}`);
|
|
673
|
-
for (const extension of rootsByExtension.keys()) patterns.push(`!**/*.spec${extension}`, `!**/*.test${extension}`);
|
|
674
|
-
return patterns;
|
|
675
|
-
}
|
|
676
|
-
/**
|
|
677
|
-
* Infers the source file extension from an include pattern by stripping the
|
|
678
|
-
* `.spec` or `.test` suffix. Throws when the pattern has no recognizable test
|
|
679
|
-
* extension so that misconfigured globs fail loudly.
|
|
680
|
-
*/
|
|
681
|
-
function inferSourceExtension(pattern) {
|
|
682
|
-
const match = pattern.match(/\.(?:spec|test)(\.\w+)$/);
|
|
683
|
-
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).`);
|
|
684
|
-
const [, extension] = match;
|
|
685
|
-
return extension;
|
|
686
|
-
}
|
|
687
|
-
//#endregion
|
|
688
|
-
//#region src/coverage/mapper.ts
|
|
689
|
-
const positionSchema = type({
|
|
690
|
-
column: "number",
|
|
691
|
-
line: "number"
|
|
692
|
-
});
|
|
693
|
-
const spanSchema = type({
|
|
694
|
-
end: positionSchema,
|
|
695
|
-
start: positionSchema
|
|
696
|
-
});
|
|
697
|
-
const functionEntrySchema = type({
|
|
698
|
-
name: "string",
|
|
699
|
-
location: type({
|
|
700
|
-
end: positionSchema,
|
|
701
|
-
start: positionSchema
|
|
702
|
-
})
|
|
703
|
-
});
|
|
704
|
-
const branchEntrySchema = type({
|
|
705
|
-
locations: type("Record<string, unknown>[]"),
|
|
706
|
-
type: "string"
|
|
707
|
-
});
|
|
708
|
-
const NESTED_RECORD = "Record<string, Record<string, unknown>>";
|
|
709
|
-
const coverageMapSchema = type({
|
|
710
|
-
"branchMap?": NESTED_RECORD,
|
|
711
|
-
"functionMap?": NESTED_RECORD,
|
|
712
|
-
"statementMap": type(NESTED_RECORD)
|
|
713
|
-
});
|
|
714
|
-
function mapCoverageToTypeScript(coverageData, manifest) {
|
|
715
|
-
const pendingStatements = /* @__PURE__ */ new Map();
|
|
716
|
-
const pendingFunctions = /* @__PURE__ */ new Map();
|
|
717
|
-
const pendingBranches = /* @__PURE__ */ new Map();
|
|
718
|
-
for (const [fileKey, fileCoverage] of Object.entries(coverageData)) {
|
|
719
|
-
const record = manifest.files[fileKey];
|
|
720
|
-
if (record === void 0) continue;
|
|
721
|
-
const resources = loadFileResources(record);
|
|
722
|
-
if (resources === void 0) continue;
|
|
723
|
-
if (resources.traceMap === void 0) {
|
|
724
|
-
passthroughFileStatements(resources, fileCoverage, pendingStatements);
|
|
725
|
-
passthroughFileFunctions(resources, fileCoverage, pendingFunctions);
|
|
726
|
-
passthroughFileBranches(resources, fileCoverage, pendingBranches);
|
|
727
|
-
} else {
|
|
728
|
-
const mapped = {
|
|
729
|
-
coverageMap: resources.coverageMap,
|
|
730
|
-
sourceMapDirectory: resources.sourceMapDirectory,
|
|
731
|
-
traceMap: resources.traceMap
|
|
732
|
-
};
|
|
733
|
-
mapFileFunctions(mapped, fileCoverage, pendingFunctions, mapFileStatements(mapped, fileCoverage, pendingStatements));
|
|
734
|
-
mapFileBranches(mapped, fileCoverage, pendingBranches);
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
return buildResult(pendingStatements, pendingFunctions, pendingBranches);
|
|
738
|
-
}
|
|
739
|
-
function loadFileResources(record) {
|
|
740
|
-
let coverageMapRaw;
|
|
741
|
-
try {
|
|
742
|
-
coverageMapRaw = fs$1.readFileSync(record.coverageMapPath, "utf-8");
|
|
743
|
-
} catch {
|
|
744
|
-
return;
|
|
745
|
-
}
|
|
746
|
-
const parsed = coverageMapSchema(JSON.parse(coverageMapRaw));
|
|
747
|
-
if (parsed instanceof type.errors) return;
|
|
748
|
-
let traceMap;
|
|
749
|
-
try {
|
|
750
|
-
traceMap = new TraceMap(fs$1.readFileSync(record.sourceMapPath, "utf-8"));
|
|
751
|
-
} catch {}
|
|
752
|
-
const sourceMapDirectory = path$1.posix.dirname(record.sourceMapPath);
|
|
753
|
-
return {
|
|
754
|
-
coverageMap: parsed,
|
|
755
|
-
sourceKey: record.key,
|
|
756
|
-
sourceMapDirectory,
|
|
757
|
-
traceMap
|
|
758
|
-
};
|
|
759
|
-
}
|
|
760
|
-
function toIstanbulColumn(luauColumn) {
|
|
761
|
-
return Math.max(0, luauColumn - 1);
|
|
762
|
-
}
|
|
763
|
-
function passthroughFileStatements(resources, fileCoverage, pending) {
|
|
764
|
-
let fileStatements = pending.get(resources.sourceKey);
|
|
765
|
-
if (fileStatements === void 0) {
|
|
766
|
-
fileStatements = /* @__PURE__ */ new Map();
|
|
767
|
-
pending.set(resources.sourceKey, fileStatements);
|
|
768
|
-
}
|
|
769
|
-
for (const [statementId, rawSpan] of Object.entries(resources.coverageMap.statementMap)) {
|
|
770
|
-
const span = spanSchema(rawSpan);
|
|
771
|
-
if (span instanceof type.errors) continue;
|
|
772
|
-
const hitCount = fileCoverage.s[statementId] ?? 0;
|
|
773
|
-
fileStatements.set(statementId, {
|
|
774
|
-
end: {
|
|
775
|
-
column: toIstanbulColumn(span.end.column),
|
|
776
|
-
line: span.end.line
|
|
777
|
-
},
|
|
778
|
-
hitCount,
|
|
779
|
-
start: {
|
|
780
|
-
column: toIstanbulColumn(span.start.column),
|
|
781
|
-
line: span.start.line
|
|
782
|
-
}
|
|
783
|
-
});
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
function passthroughFileFunctions(resources, fileCoverage, pendingFunctions) {
|
|
787
|
-
if (resources.coverageMap.functionMap === void 0) return;
|
|
788
|
-
let fileFunctions = pendingFunctions.get(resources.sourceKey);
|
|
789
|
-
if (fileFunctions === void 0) {
|
|
790
|
-
fileFunctions = [];
|
|
791
|
-
pendingFunctions.set(resources.sourceKey, fileFunctions);
|
|
792
|
-
}
|
|
793
|
-
for (const [functionId, rawEntry] of Object.entries(resources.coverageMap.functionMap)) {
|
|
794
|
-
const entry = functionEntrySchema(rawEntry);
|
|
795
|
-
if (entry instanceof type.errors) continue;
|
|
796
|
-
fileFunctions.push({
|
|
797
|
-
name: entry.name,
|
|
798
|
-
hitCount: fileCoverage.f?.[functionId] ?? 0,
|
|
799
|
-
loc: {
|
|
800
|
-
end: {
|
|
801
|
-
column: toIstanbulColumn(entry.location.end.column),
|
|
802
|
-
line: entry.location.end.line
|
|
803
|
-
},
|
|
804
|
-
start: {
|
|
805
|
-
column: toIstanbulColumn(entry.location.start.column),
|
|
806
|
-
line: entry.location.start.line
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
});
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
function passthroughFileBranches(resources, fileCoverage, pendingBranches) {
|
|
813
|
-
if (resources.coverageMap.branchMap === void 0) return;
|
|
814
|
-
let fileBranches = pendingBranches.get(resources.sourceKey);
|
|
815
|
-
if (fileBranches === void 0) {
|
|
816
|
-
fileBranches = [];
|
|
817
|
-
pendingBranches.set(resources.sourceKey, fileBranches);
|
|
818
|
-
}
|
|
819
|
-
for (const [branchId, rawEntry] of Object.entries(resources.coverageMap.branchMap)) {
|
|
820
|
-
const entry = branchEntrySchema(rawEntry);
|
|
821
|
-
if (entry instanceof type.errors) continue;
|
|
822
|
-
const armHitCounts = fileCoverage.b?.[branchId] ?? [];
|
|
823
|
-
const locations = [];
|
|
824
|
-
for (const rawLocation of entry.locations) {
|
|
825
|
-
const location = spanSchema(rawLocation);
|
|
826
|
-
if (location instanceof type.errors) continue;
|
|
827
|
-
locations.push({
|
|
828
|
-
end: {
|
|
829
|
-
column: toIstanbulColumn(location.end.column),
|
|
830
|
-
line: location.end.line
|
|
831
|
-
},
|
|
832
|
-
start: {
|
|
833
|
-
column: toIstanbulColumn(location.start.column),
|
|
834
|
-
line: location.start.line
|
|
835
|
-
}
|
|
836
|
-
});
|
|
837
|
-
}
|
|
838
|
-
if (locations.length === 0) continue;
|
|
839
|
-
const firstLocation = locations[0];
|
|
840
|
-
const lastLocation = locations[locations.length - 1];
|
|
841
|
-
assert(firstLocation !== void 0 && lastLocation !== void 0, "Branch locations must not be empty after filtering");
|
|
842
|
-
fileBranches.push({
|
|
843
|
-
armHitCounts: entry.locations.map((_, index) => armHitCounts[index] ?? 0),
|
|
844
|
-
loc: {
|
|
845
|
-
end: lastLocation.end,
|
|
846
|
-
start: firstLocation.start
|
|
847
|
-
},
|
|
848
|
-
locations,
|
|
849
|
-
type: entry.type
|
|
850
|
-
});
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
/**
|
|
854
|
-
* Resolves a source path from a source map against the source map's directory.
|
|
855
|
-
* Source maps produce paths relative to the .map file (e.g.,
|
|
856
|
-
* `../../../packages/src/file.ts` from `out/packages/src/file.lua.map`).
|
|
857
|
-
* Joining with the map directory normalizes these to cwd-relative paths.
|
|
858
|
-
* Paths that are already cwd-relative (no `..` prefix) pass through unchanged.
|
|
859
|
-
*/
|
|
860
|
-
function resolveSourcePath(source, sourceMapDirectory) {
|
|
861
|
-
const normalized = source.replaceAll("\\", "/");
|
|
862
|
-
if (!normalized.startsWith("..")) return normalized;
|
|
863
|
-
return path$1.posix.normalize(path$1.posix.join(sourceMapDirectory, normalized));
|
|
864
|
-
}
|
|
865
|
-
function mapStatement(traceMap, span, sourceMapDirectory) {
|
|
866
|
-
const mappedStart = originalPositionFor(traceMap, {
|
|
867
|
-
column: Math.max(0, span.start.column - 1),
|
|
868
|
-
line: span.start.line
|
|
869
|
-
});
|
|
870
|
-
const mappedEnd = originalPositionFor(traceMap, {
|
|
871
|
-
column: Math.max(0, span.end.column - 1),
|
|
872
|
-
line: span.end.line
|
|
873
|
-
});
|
|
874
|
-
if (mappedStart.source === null || mappedEnd.source === null || mappedStart.source !== mappedEnd.source) return;
|
|
875
|
-
const resolvedSource = resolveSourcePath(mappedStart.source, sourceMapDirectory);
|
|
876
|
-
return {
|
|
877
|
-
end: {
|
|
878
|
-
column: mappedEnd.column,
|
|
879
|
-
line: mappedEnd.line,
|
|
880
|
-
source: resolvedSource
|
|
881
|
-
},
|
|
882
|
-
start: {
|
|
883
|
-
column: mappedStart.column,
|
|
884
|
-
line: mappedStart.line,
|
|
885
|
-
source: resolvedSource
|
|
886
|
-
}
|
|
887
|
-
};
|
|
888
|
-
}
|
|
889
|
-
function maxPosition(a, b) {
|
|
890
|
-
if (a.line > b.line) return a;
|
|
891
|
-
if (b.line > a.line) return b;
|
|
892
|
-
return a.column >= b.column ? a : b;
|
|
893
|
-
}
|
|
894
|
-
function addOrCoalesce(pending, start, end, hitCount) {
|
|
895
|
-
const tsPath = start.source;
|
|
896
|
-
let fileStatements = pending.get(tsPath);
|
|
897
|
-
if (fileStatements === void 0) {
|
|
898
|
-
fileStatements = /* @__PURE__ */ new Map();
|
|
899
|
-
pending.set(tsPath, fileStatements);
|
|
900
|
-
}
|
|
901
|
-
const coalescenceKey = `${String(start.line)}:${String(start.column)}`;
|
|
902
|
-
const existing = fileStatements.get(coalescenceKey);
|
|
903
|
-
if (existing !== void 0) {
|
|
904
|
-
existing.hitCount += hitCount;
|
|
905
|
-
existing.end = maxPosition(existing.end, {
|
|
906
|
-
column: end.column,
|
|
907
|
-
line: end.line
|
|
908
|
-
});
|
|
909
|
-
} else fileStatements.set(coalescenceKey, {
|
|
910
|
-
end: {
|
|
911
|
-
column: end.column,
|
|
912
|
-
line: end.line
|
|
913
|
-
},
|
|
914
|
-
hitCount,
|
|
915
|
-
start: {
|
|
916
|
-
column: start.column,
|
|
917
|
-
line: start.line
|
|
918
|
-
}
|
|
919
|
-
});
|
|
920
|
-
}
|
|
921
|
-
function mapFileStatements(resources, fileCoverage, pending) {
|
|
922
|
-
const resolvedTsPaths = /* @__PURE__ */ new Set();
|
|
923
|
-
for (const [statementId, rawSpan] of Object.entries(resources.coverageMap.statementMap)) {
|
|
924
|
-
const span = spanSchema(rawSpan);
|
|
925
|
-
if (span instanceof type.errors) continue;
|
|
926
|
-
const hitCount = fileCoverage.s[statementId] ?? 0;
|
|
927
|
-
const mapped = mapStatement(resources.traceMap, span, resources.sourceMapDirectory);
|
|
928
|
-
if (mapped === void 0) continue;
|
|
929
|
-
resolvedTsPaths.add(mapped.start.source);
|
|
930
|
-
addOrCoalesce(pending, mapped.start, mapped.end, hitCount);
|
|
931
|
-
}
|
|
932
|
-
return resolvedTsPaths;
|
|
933
|
-
}
|
|
934
|
-
function mapFileFunctions(resources, fileCoverage, pendingFunctions, resolvedTsPaths) {
|
|
935
|
-
if (resources.coverageMap.functionMap === void 0) return;
|
|
936
|
-
for (const [functionId, rawEntry] of Object.entries(resources.coverageMap.functionMap)) {
|
|
937
|
-
const entry = functionEntrySchema(rawEntry);
|
|
938
|
-
if (entry instanceof type.errors) continue;
|
|
939
|
-
const hitCount = fileCoverage.f?.[functionId] ?? 0;
|
|
940
|
-
const mapped = mapStatement(resources.traceMap, entry.location, resources.sourceMapDirectory);
|
|
941
|
-
if (mapped !== void 0) {
|
|
942
|
-
const tsPath = mapped.start.source;
|
|
943
|
-
let fileFunctions = pendingFunctions.get(tsPath);
|
|
944
|
-
if (fileFunctions === void 0) {
|
|
945
|
-
fileFunctions = [];
|
|
946
|
-
pendingFunctions.set(tsPath, fileFunctions);
|
|
947
|
-
}
|
|
948
|
-
fileFunctions.push({
|
|
949
|
-
name: entry.name,
|
|
950
|
-
hitCount,
|
|
951
|
-
loc: {
|
|
952
|
-
end: {
|
|
953
|
-
column: mapped.end.column,
|
|
954
|
-
line: mapped.end.line
|
|
955
|
-
},
|
|
956
|
-
start: {
|
|
957
|
-
column: mapped.start.column,
|
|
958
|
-
line: mapped.start.line
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
});
|
|
962
|
-
continue;
|
|
963
|
-
}
|
|
964
|
-
const fallbackPath = resolvedTsPaths.values().next().value;
|
|
965
|
-
if (fallbackPath === void 0) continue;
|
|
966
|
-
let fileFunctions = pendingFunctions.get(fallbackPath);
|
|
967
|
-
if (fileFunctions === void 0) {
|
|
968
|
-
fileFunctions = [];
|
|
969
|
-
pendingFunctions.set(fallbackPath, fileFunctions);
|
|
970
|
-
}
|
|
971
|
-
fileFunctions.push({
|
|
972
|
-
name: entry.name,
|
|
973
|
-
hitCount,
|
|
974
|
-
loc: {
|
|
975
|
-
end: {
|
|
976
|
-
column: 0,
|
|
977
|
-
line: 1
|
|
978
|
-
},
|
|
979
|
-
start: {
|
|
980
|
-
column: 0,
|
|
981
|
-
line: 1
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
});
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
function mapBranchArmLocations(traceMap, rawLocations, sourceMapDirectory) {
|
|
988
|
-
const mappedLocations = [];
|
|
989
|
-
let tsPath;
|
|
990
|
-
for (const rawLocation of rawLocations) {
|
|
991
|
-
const location = spanSchema(rawLocation);
|
|
992
|
-
if (location instanceof type.errors) return;
|
|
993
|
-
const mapped = mapStatement(traceMap, location, sourceMapDirectory);
|
|
994
|
-
if (mapped === void 0) return;
|
|
995
|
-
if (tsPath === void 0) tsPath = mapped.start.source;
|
|
996
|
-
else if (tsPath !== mapped.start.source) return;
|
|
997
|
-
mappedLocations.push({
|
|
998
|
-
end: {
|
|
999
|
-
column: mapped.end.column,
|
|
1000
|
-
line: mapped.end.line
|
|
1001
|
-
},
|
|
1002
|
-
start: {
|
|
1003
|
-
column: mapped.start.column,
|
|
1004
|
-
line: mapped.start.line
|
|
1005
|
-
}
|
|
1006
|
-
});
|
|
1007
|
-
}
|
|
1008
|
-
if (tsPath === void 0 || mappedLocations.length === 0) return;
|
|
1009
|
-
return {
|
|
1010
|
-
locations: mappedLocations,
|
|
1011
|
-
tsPath
|
|
1012
|
-
};
|
|
1013
|
-
}
|
|
1014
|
-
function mapFileBranches(resources, fileCoverage, pendingBranches) {
|
|
1015
|
-
if (resources.coverageMap.branchMap === void 0) return;
|
|
1016
|
-
for (const [branchId, rawEntry] of Object.entries(resources.coverageMap.branchMap)) {
|
|
1017
|
-
const entry = branchEntrySchema(rawEntry);
|
|
1018
|
-
if (entry instanceof type.errors) continue;
|
|
1019
|
-
const armHitCounts = fileCoverage.b?.[branchId] ?? [];
|
|
1020
|
-
const result = mapBranchArmLocations(resources.traceMap, entry.locations, resources.sourceMapDirectory);
|
|
1021
|
-
if (result === void 0) continue;
|
|
1022
|
-
let fileBranches = pendingBranches.get(result.tsPath);
|
|
1023
|
-
if (fileBranches === void 0) {
|
|
1024
|
-
fileBranches = [];
|
|
1025
|
-
pendingBranches.set(result.tsPath, fileBranches);
|
|
1026
|
-
}
|
|
1027
|
-
const firstLocation = result.locations[0];
|
|
1028
|
-
const lastLocation = result.locations[result.locations.length - 1];
|
|
1029
|
-
assert(firstLocation !== void 0 && lastLocation !== void 0, "Branch locations must not be empty after successful mapping");
|
|
1030
|
-
fileBranches.push({
|
|
1031
|
-
armHitCounts: entry.locations.map((_, index) => armHitCounts[index] ?? 0),
|
|
1032
|
-
loc: {
|
|
1033
|
-
end: lastLocation.end,
|
|
1034
|
-
start: firstLocation.start
|
|
1035
|
-
},
|
|
1036
|
-
locations: result.locations,
|
|
1037
|
-
type: entry.type
|
|
1038
|
-
});
|
|
1039
|
-
}
|
|
1040
|
-
}
|
|
1041
|
-
function populateStatements(file, statementMap) {
|
|
1042
|
-
if (statementMap === void 0) return;
|
|
1043
|
-
let index = 0;
|
|
1044
|
-
for (const statement of statementMap.values()) {
|
|
1045
|
-
const id = String(index);
|
|
1046
|
-
file.statementMap[id] = {
|
|
1047
|
-
end: statement.end,
|
|
1048
|
-
start: statement.start
|
|
1049
|
-
};
|
|
1050
|
-
file.s[id] = statement.hitCount;
|
|
1051
|
-
index++;
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
function populateFunctions(file, fileFunctions) {
|
|
1055
|
-
if (fileFunctions === void 0) return;
|
|
1056
|
-
let functionIndex = 0;
|
|
1057
|
-
for (const func of fileFunctions) {
|
|
1058
|
-
const id = String(functionIndex);
|
|
1059
|
-
file.fnMap[id] = {
|
|
1060
|
-
name: func.name,
|
|
1061
|
-
loc: func.loc
|
|
1062
|
-
};
|
|
1063
|
-
file.f[id] = func.hitCount;
|
|
1064
|
-
functionIndex++;
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
function populateBranches(file, fileBranches) {
|
|
1068
|
-
if (fileBranches === void 0) return;
|
|
1069
|
-
let branchIndex = 0;
|
|
1070
|
-
for (const branch of fileBranches) {
|
|
1071
|
-
const id = String(branchIndex);
|
|
1072
|
-
file.branchMap[id] = {
|
|
1073
|
-
loc: branch.loc,
|
|
1074
|
-
locations: branch.locations,
|
|
1075
|
-
type: branch.type
|
|
1076
|
-
};
|
|
1077
|
-
file.b[id] = branch.armHitCounts;
|
|
1078
|
-
branchIndex++;
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
function buildResult(pending, pendingFunctions, pendingBranches) {
|
|
1082
|
-
const files = {};
|
|
1083
|
-
const allPaths = new Set([
|
|
1084
|
-
...pending.keys(),
|
|
1085
|
-
...pendingFunctions.keys(),
|
|
1086
|
-
...pendingBranches.keys()
|
|
1087
|
-
]);
|
|
1088
|
-
for (const tsPath of allPaths) {
|
|
1089
|
-
const file = {
|
|
1090
|
-
b: {},
|
|
1091
|
-
branchMap: {},
|
|
1092
|
-
f: {},
|
|
1093
|
-
fnMap: {},
|
|
1094
|
-
path: tsPath,
|
|
1095
|
-
s: {},
|
|
1096
|
-
statementMap: {}
|
|
1097
|
-
};
|
|
1098
|
-
populateStatements(file, pending.get(tsPath));
|
|
1099
|
-
populateFunctions(file, pendingFunctions.get(tsPath));
|
|
1100
|
-
populateBranches(file, pendingBranches.get(tsPath));
|
|
1101
|
-
files[tsPath] = file;
|
|
1102
|
-
}
|
|
1103
|
-
return { files };
|
|
1104
|
-
}
|
|
1105
|
-
//#endregion
|
|
1106
|
-
//#region src/coverage/merge-raw-coverage.ts
|
|
1107
|
-
/**
|
|
1108
|
-
* Additively merge two raw coverage datasets. Overlapping files have their
|
|
1109
|
-
* hit counts summed (matching istanbul-lib-coverage's semantics).
|
|
1110
|
-
*/
|
|
1111
|
-
function mergeRawCoverage(target, source) {
|
|
1112
|
-
if (target === void 0) return source;
|
|
1113
|
-
if (source === void 0) return target;
|
|
1114
|
-
const result = { ...target };
|
|
1115
|
-
for (const [filePath, fileCoverage] of Object.entries(source)) {
|
|
1116
|
-
const existing = result[filePath];
|
|
1117
|
-
result[filePath] = existing === void 0 ? { ...fileCoverage } : mergeFileCoverage(existing, fileCoverage);
|
|
1118
|
-
}
|
|
1119
|
-
return result;
|
|
1120
|
-
}
|
|
1121
|
-
function sumScalars(a, b) {
|
|
1122
|
-
const result = { ...a };
|
|
1123
|
-
for (const [key, value] of Object.entries(b)) result[key] = (result[key] ?? 0) + value;
|
|
1124
|
-
return result;
|
|
1125
|
-
}
|
|
1126
|
-
function sumBranches(a, b) {
|
|
1127
|
-
const result = { ...a };
|
|
1128
|
-
for (const [key, bArms] of Object.entries(b)) {
|
|
1129
|
-
const aArms = result[key];
|
|
1130
|
-
if (aArms === void 0) {
|
|
1131
|
-
result[key] = [...bArms];
|
|
1132
|
-
continue;
|
|
1133
|
-
}
|
|
1134
|
-
const length = Math.max(aArms.length, bArms.length);
|
|
1135
|
-
result[key] = Array.from({ length }, (_, index) => (aArms[index] ?? 0) + (bArms[index] ?? 0));
|
|
1136
|
-
}
|
|
1137
|
-
return result;
|
|
1138
|
-
}
|
|
1139
|
-
function mergeFileCoverage(a, b) {
|
|
1140
|
-
const merged = { s: sumScalars(a.s, b.s) };
|
|
1141
|
-
if (a.f !== void 0 || b.f !== void 0) merged.f = sumScalars(a.f ?? {}, b.f ?? {});
|
|
1142
|
-
if (a.b !== void 0 || b.b !== void 0) merged.b = sumBranches(a.b ?? {}, b.b ?? {});
|
|
1143
|
-
return merged;
|
|
1144
|
-
}
|
|
1145
|
-
//#endregion
|
|
1146
|
-
//#region src/coverage/coverage-collector.ts
|
|
1147
|
-
const INSTRUMENTABLE_STATEMENT_TAGS = new Set([
|
|
1148
|
-
"assign",
|
|
1149
|
-
"break",
|
|
1150
|
-
"compoundassign",
|
|
1151
|
-
"conditional",
|
|
1152
|
-
"continue",
|
|
1153
|
-
"do",
|
|
1154
|
-
"expression",
|
|
1155
|
-
"for",
|
|
1156
|
-
"forin",
|
|
1157
|
-
"function",
|
|
1158
|
-
"local",
|
|
1159
|
-
"localfunction",
|
|
1160
|
-
"repeat",
|
|
1161
|
-
"return",
|
|
1162
|
-
"while"
|
|
1163
|
-
]);
|
|
1164
|
-
const END_KEYWORD_LENGTH = 3;
|
|
1165
|
-
function collectCoverage(root) {
|
|
1166
|
-
let statementIndex = 1;
|
|
1167
|
-
let functionIndex = 1;
|
|
1168
|
-
let branchIndex = 1;
|
|
1169
|
-
const statements = [];
|
|
1170
|
-
const functions = [];
|
|
1171
|
-
const branches = [];
|
|
1172
|
-
const implicitElseProbes = [];
|
|
1173
|
-
const exprIfProbes = [];
|
|
1174
|
-
const namedFunctions = /* @__PURE__ */ new Set();
|
|
1175
|
-
visitBlock(root, {
|
|
1176
|
-
visitExprFunction(node) {
|
|
1177
|
-
if (namedFunctions.has(node)) return true;
|
|
1178
|
-
const first = getBodyFirstStatement(node.body);
|
|
1179
|
-
functions.push({
|
|
1180
|
-
name: "(anonymous)",
|
|
1181
|
-
bodyFirstColumn: first.column,
|
|
1182
|
-
bodyFirstLine: first.line,
|
|
1183
|
-
index: functionIndex,
|
|
1184
|
-
location: { ...node.location }
|
|
1185
|
-
});
|
|
1186
|
-
functionIndex++;
|
|
1187
|
-
return true;
|
|
1188
|
-
},
|
|
1189
|
-
visitExprIfElse(node) {
|
|
1190
|
-
const branch = {
|
|
1191
|
-
arms: [],
|
|
1192
|
-
branchType: "expr-if",
|
|
1193
|
-
index: branchIndex
|
|
1194
|
-
};
|
|
1195
|
-
let armIndex = 1;
|
|
1196
|
-
branch.arms.push({
|
|
1197
|
-
bodyFirstColumn: 0,
|
|
1198
|
-
bodyFirstLine: 0,
|
|
1199
|
-
location: { ...node.thenExpr.location }
|
|
1200
|
-
});
|
|
1201
|
-
exprIfProbes.push({
|
|
1202
|
-
armIndex,
|
|
1203
|
-
branchIndex,
|
|
1204
|
-
exprLocation: { ...node.thenExpr.location }
|
|
1205
|
-
});
|
|
1206
|
-
armIndex++;
|
|
1207
|
-
for (const elseif of node.elseifs) {
|
|
1208
|
-
branch.arms.push({
|
|
1209
|
-
bodyFirstColumn: 0,
|
|
1210
|
-
bodyFirstLine: 0,
|
|
1211
|
-
location: { ...elseif.thenExpr.location }
|
|
1212
|
-
});
|
|
1213
|
-
exprIfProbes.push({
|
|
1214
|
-
armIndex,
|
|
1215
|
-
branchIndex,
|
|
1216
|
-
exprLocation: { ...elseif.thenExpr.location }
|
|
1217
|
-
});
|
|
1218
|
-
armIndex++;
|
|
1219
|
-
}
|
|
1220
|
-
branch.arms.push({
|
|
1221
|
-
bodyFirstColumn: 0,
|
|
1222
|
-
bodyFirstLine: 0,
|
|
1223
|
-
location: { ...node.elseExpr.location }
|
|
1224
|
-
});
|
|
1225
|
-
exprIfProbes.push({
|
|
1226
|
-
armIndex,
|
|
1227
|
-
branchIndex,
|
|
1228
|
-
exprLocation: { ...node.elseExpr.location }
|
|
1229
|
-
});
|
|
1230
|
-
branches.push(branch);
|
|
1231
|
-
branchIndex++;
|
|
1232
|
-
return true;
|
|
1233
|
-
},
|
|
1234
|
-
visitStatBlock(block) {
|
|
1235
|
-
for (const stmt of block.statements) if (INSTRUMENTABLE_STATEMENT_TAGS.has(stmt.tag)) {
|
|
1236
|
-
statements.push({
|
|
1237
|
-
index: statementIndex,
|
|
1238
|
-
location: { ...stmt.location }
|
|
1239
|
-
});
|
|
1240
|
-
statementIndex++;
|
|
1241
|
-
}
|
|
1242
|
-
return true;
|
|
1243
|
-
},
|
|
1244
|
-
visitStatFunction(node) {
|
|
1245
|
-
const name = extractFunctionName(node);
|
|
1246
|
-
const first = getBodyFirstStatement(node.func.body);
|
|
1247
|
-
namedFunctions.add(node.func);
|
|
1248
|
-
functions.push({
|
|
1249
|
-
name,
|
|
1250
|
-
bodyFirstColumn: first.column,
|
|
1251
|
-
bodyFirstLine: first.line,
|
|
1252
|
-
index: functionIndex,
|
|
1253
|
-
location: { ...node.location }
|
|
1254
|
-
});
|
|
1255
|
-
functionIndex++;
|
|
1256
|
-
return true;
|
|
1257
|
-
},
|
|
1258
|
-
visitStatIf(node) {
|
|
1259
|
-
const branch = {
|
|
1260
|
-
arms: [],
|
|
1261
|
-
branchType: "if",
|
|
1262
|
-
index: branchIndex
|
|
1263
|
-
};
|
|
1264
|
-
const thenFirst = getBodyFirstStatement(node.thenBlock);
|
|
1265
|
-
branch.arms.push({
|
|
1266
|
-
bodyFirstColumn: thenFirst.column,
|
|
1267
|
-
bodyFirstLine: thenFirst.line,
|
|
1268
|
-
location: { ...node.thenBlock.location }
|
|
1269
|
-
});
|
|
1270
|
-
for (const elseif of node.elseifs) {
|
|
1271
|
-
const elseifFirst = getBodyFirstStatement(elseif.thenBlock);
|
|
1272
|
-
branch.arms.push({
|
|
1273
|
-
bodyFirstColumn: elseifFirst.column,
|
|
1274
|
-
bodyFirstLine: elseifFirst.line,
|
|
1275
|
-
location: { ...elseif.thenBlock.location }
|
|
1276
|
-
});
|
|
1277
|
-
}
|
|
1278
|
-
const { elseBlock } = node;
|
|
1279
|
-
const hasExplicitElse = elseBlock !== void 0 && elseBlock.statements.length > 0;
|
|
1280
|
-
if (hasExplicitElse) {
|
|
1281
|
-
const elseFirst = getBodyFirstStatement(elseBlock);
|
|
1282
|
-
branch.arms.push({
|
|
1283
|
-
bodyFirstColumn: elseFirst.column,
|
|
1284
|
-
bodyFirstLine: elseFirst.line,
|
|
1285
|
-
location: { ...elseBlock.location }
|
|
1286
|
-
});
|
|
1287
|
-
}
|
|
1288
|
-
if (!hasExplicitElse) {
|
|
1289
|
-
branch.arms.push({
|
|
1290
|
-
bodyFirstColumn: 0,
|
|
1291
|
-
bodyFirstLine: 0,
|
|
1292
|
-
location: {
|
|
1293
|
-
beginColumn: node.location.beginColumn,
|
|
1294
|
-
beginLine: node.location.beginLine,
|
|
1295
|
-
endColumn: node.location.beginColumn,
|
|
1296
|
-
endLine: node.location.beginLine
|
|
1297
|
-
}
|
|
1298
|
-
});
|
|
1299
|
-
implicitElseProbes.push({
|
|
1300
|
-
armIndex: branch.arms.length,
|
|
1301
|
-
branchIndex,
|
|
1302
|
-
endColumn: node.location.endColumn - END_KEYWORD_LENGTH,
|
|
1303
|
-
endLine: node.location.endLine
|
|
1304
|
-
});
|
|
1305
|
-
}
|
|
1306
|
-
branches.push(branch);
|
|
1307
|
-
branchIndex++;
|
|
1308
|
-
return true;
|
|
1309
|
-
},
|
|
1310
|
-
visitStatLocalFunction(node) {
|
|
1311
|
-
const name = node.name.name.text;
|
|
1312
|
-
const first = getBodyFirstStatement(node.func.body);
|
|
1313
|
-
namedFunctions.add(node.func);
|
|
1314
|
-
functions.push({
|
|
1315
|
-
name,
|
|
1316
|
-
bodyFirstColumn: first.column,
|
|
1317
|
-
bodyFirstLine: first.line,
|
|
1318
|
-
index: functionIndex,
|
|
1319
|
-
location: { ...node.location }
|
|
1320
|
-
});
|
|
1321
|
-
functionIndex++;
|
|
1322
|
-
return true;
|
|
1323
|
-
}
|
|
1324
|
-
});
|
|
1325
|
-
return {
|
|
1326
|
-
branches,
|
|
1327
|
-
exprIfProbes,
|
|
1328
|
-
functions,
|
|
1329
|
-
implicitElseProbes,
|
|
1330
|
-
statements
|
|
1331
|
-
};
|
|
1332
|
-
}
|
|
1333
|
-
function getBodyFirstStatement(block) {
|
|
1334
|
-
const first = block.statements[0];
|
|
1335
|
-
if (first !== void 0) return {
|
|
1336
|
-
column: first.location.beginColumn,
|
|
1337
|
-
line: first.location.beginLine
|
|
1338
|
-
};
|
|
1339
|
-
return {
|
|
1340
|
-
column: block.location.beginColumn,
|
|
1341
|
-
line: block.location.beginLine
|
|
1342
|
-
};
|
|
1343
|
-
}
|
|
1344
|
-
function extractExprName(expr) {
|
|
1345
|
-
if (expr.tag === "global") return expr.name.text;
|
|
1346
|
-
if (expr.tag === "indexname") return `${extractExprName(expr.expression)}${expr.accessor.text}${expr.index.text}`;
|
|
1347
|
-
return "(anonymous)";
|
|
1348
|
-
}
|
|
1349
|
-
function extractFunctionName(node) {
|
|
1350
|
-
return extractExprName(node.name);
|
|
1351
|
-
}
|
|
1352
|
-
//#endregion
|
|
1353
|
-
//#region src/coverage/coverage-map-builder.ts
|
|
1354
|
-
function buildCoverageMap$1(result) {
|
|
1355
|
-
const statementMap = {};
|
|
1356
|
-
for (const statement of result.statements) statementMap[String(statement.index)] = {
|
|
1357
|
-
end: {
|
|
1358
|
-
column: statement.location.endColumn,
|
|
1359
|
-
line: statement.location.endLine
|
|
1360
|
-
},
|
|
1361
|
-
start: {
|
|
1362
|
-
column: statement.location.beginColumn,
|
|
1363
|
-
line: statement.location.beginLine
|
|
1364
|
-
}
|
|
1365
|
-
};
|
|
1366
|
-
const functionMap = {};
|
|
1367
|
-
for (const func of result.functions) functionMap[String(func.index)] = {
|
|
1368
|
-
name: func.name,
|
|
1369
|
-
location: {
|
|
1370
|
-
end: {
|
|
1371
|
-
column: func.location.endColumn,
|
|
1372
|
-
line: func.location.endLine
|
|
1373
|
-
},
|
|
1374
|
-
start: {
|
|
1375
|
-
column: func.location.beginColumn,
|
|
1376
|
-
line: func.location.beginLine
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
};
|
|
1380
|
-
const branchMap = {};
|
|
1381
|
-
for (const branch of result.branches) branchMap[String(branch.index)] = {
|
|
1382
|
-
locations: branch.arms.map((arm) => {
|
|
1383
|
-
return {
|
|
1384
|
-
end: {
|
|
1385
|
-
column: arm.location.endColumn,
|
|
1386
|
-
line: arm.location.endLine
|
|
1387
|
-
},
|
|
1388
|
-
start: {
|
|
1389
|
-
column: arm.location.beginColumn,
|
|
1390
|
-
line: arm.location.beginLine
|
|
1391
|
-
}
|
|
1392
|
-
};
|
|
1393
|
-
}),
|
|
1394
|
-
type: branch.branchType
|
|
1395
|
-
};
|
|
1396
|
-
return {
|
|
1397
|
-
branchMap,
|
|
1398
|
-
functionMap,
|
|
1399
|
-
statementMap
|
|
1400
|
-
};
|
|
1401
|
-
}
|
|
1402
|
-
//#endregion
|
|
1403
|
-
//#region src/coverage/probe-inserter.ts
|
|
1404
|
-
function insertProbes(source, result, fileKey) {
|
|
1405
|
-
const lines = splitLines(source);
|
|
1406
|
-
applyProbes(lines, collectProbes(result));
|
|
1407
|
-
return buildPreamble(extractModeDirective(lines), fileKey, result) + lines.join("\n");
|
|
1408
|
-
}
|
|
1409
|
-
function collectProbes(result) {
|
|
1410
|
-
const probes = [];
|
|
1411
|
-
for (const stmt of result.statements) probes.push({
|
|
1412
|
-
column: stmt.location.beginColumn,
|
|
1413
|
-
line: stmt.location.beginLine,
|
|
1414
|
-
text: `__cov_s[${stmt.index}] += 1; `
|
|
1415
|
-
});
|
|
1416
|
-
for (const func of result.functions) if (func.bodyFirstLine > 0) probes.push({
|
|
1417
|
-
column: func.bodyFirstColumn,
|
|
1418
|
-
line: func.bodyFirstLine,
|
|
1419
|
-
text: `__cov_f[${func.index}] += 1; `
|
|
1420
|
-
});
|
|
1421
|
-
for (const branch of result.branches) for (let armIndex = 0; armIndex < branch.arms.length; armIndex++) {
|
|
1422
|
-
const arm = branch.arms[armIndex];
|
|
1423
|
-
if (arm !== void 0 && arm.bodyFirstLine > 0) probes.push({
|
|
1424
|
-
column: arm.bodyFirstColumn,
|
|
1425
|
-
line: arm.bodyFirstLine,
|
|
1426
|
-
text: `__cov_b[${branch.index}][${armIndex + 1}] += 1; `
|
|
1427
|
-
});
|
|
1428
|
-
}
|
|
1429
|
-
for (const probe of result.implicitElseProbes) probes.push({
|
|
1430
|
-
column: probe.endColumn,
|
|
1431
|
-
line: probe.endLine,
|
|
1432
|
-
text: `else __cov_b[${probe.branchIndex}][${probe.armIndex}] += 1 `
|
|
1433
|
-
});
|
|
1434
|
-
for (const probe of result.exprIfProbes) probes.push({
|
|
1435
|
-
column: probe.exprLocation.beginColumn,
|
|
1436
|
-
line: probe.exprLocation.beginLine,
|
|
1437
|
-
text: `__cov_br(${probe.branchIndex}, ${probe.armIndex}, `
|
|
1438
|
-
}, {
|
|
1439
|
-
column: probe.exprLocation.endColumn,
|
|
1440
|
-
line: probe.exprLocation.endLine,
|
|
1441
|
-
text: ")"
|
|
1442
|
-
});
|
|
1443
|
-
probes.sort((a, b) => {
|
|
1444
|
-
if (a.line === b.line) return b.column - a.column;
|
|
1445
|
-
return b.line - a.line;
|
|
1446
|
-
});
|
|
1447
|
-
return probes;
|
|
1448
|
-
}
|
|
1449
|
-
/** Mutates `mutableLines` in place, inserting probe text at each probe's position. */
|
|
1450
|
-
function applyProbes(mutableLines, probes) {
|
|
1451
|
-
for (const { column, line: probeLine, text } of probes) {
|
|
1452
|
-
const lineIndex = probeLine - 1;
|
|
1453
|
-
const line = mutableLines[lineIndex];
|
|
1454
|
-
assert(line !== void 0, `Invalid probe line number: ${probeLine}`);
|
|
1455
|
-
const before = line.slice(0, column - 1);
|
|
1456
|
-
const after = line.slice(column - 1);
|
|
1457
|
-
mutableLines[lineIndex] = before + (before.length > 0 && !/\s$/.test(before) && /^[a-zA-Z_]/.test(text) ? " " : "") + text + after;
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
function extractModeDirective(lines) {
|
|
1461
|
-
if (lines.length > 0 && lines[0] !== void 0 && /^--![a-z]+/.test(lines[0])) {
|
|
1462
|
-
const directive = `${lines[0]}\n`;
|
|
1463
|
-
lines.splice(0, 1);
|
|
1464
|
-
return directive;
|
|
1465
|
-
}
|
|
1466
|
-
return "";
|
|
1467
|
-
}
|
|
1468
|
-
function splitLines(source) {
|
|
1469
|
-
const lines = [];
|
|
1470
|
-
let position = 0;
|
|
1471
|
-
while (position < source.length) {
|
|
1472
|
-
const nlPosition = source.indexOf("\n", position);
|
|
1473
|
-
if (nlPosition !== -1) {
|
|
1474
|
-
let lineEnd = nlPosition;
|
|
1475
|
-
if (lineEnd > position && source[lineEnd - 1] === "\r") lineEnd--;
|
|
1476
|
-
lines.push(source.slice(position, lineEnd));
|
|
1477
|
-
position = nlPosition + 1;
|
|
1478
|
-
} else {
|
|
1479
|
-
lines.push(source.slice(position));
|
|
1480
|
-
position = source.length;
|
|
1481
|
-
}
|
|
1482
|
-
}
|
|
1483
|
-
if (lines.length === 0) lines.push("");
|
|
1484
|
-
return lines;
|
|
1485
|
-
}
|
|
1486
|
-
function buildPreamble(modeDirective, fileKey, result) {
|
|
1487
|
-
const escapedKey = fileKey.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"").replaceAll("\n", "\\n").replaceAll("\r", "\\r").replaceAll("\0", "");
|
|
1488
|
-
let preamble = modeDirective;
|
|
1489
|
-
preamble += "if _G.__jest_roblox_cov == nil then _G.__jest_roblox_cov = {} end\n";
|
|
1490
|
-
preamble += `local __cov_file_key = "${escapedKey}"\n`;
|
|
1491
|
-
preamble += "if _G.__jest_roblox_cov[__cov_file_key] == nil then _G.__jest_roblox_cov[__cov_file_key] = {} end\n";
|
|
1492
|
-
preamble += "if _G.__jest_roblox_cov[__cov_file_key].s == nil then _G.__jest_roblox_cov[__cov_file_key].s = {} end\n";
|
|
1493
|
-
preamble += "local __cov_s = _G.__jest_roblox_cov[__cov_file_key].s\n";
|
|
1494
|
-
if (result.statements.length > 0) preamble += `for __i = 1, ${result.statements.length} do if __cov_s[__i] == nil then __cov_s[__i] = 0 end end\n`;
|
|
1495
|
-
if (result.functions.length > 0) {
|
|
1496
|
-
preamble += "if _G.__jest_roblox_cov[__cov_file_key].f == nil then _G.__jest_roblox_cov[__cov_file_key].f = {} end\n";
|
|
1497
|
-
preamble += "local __cov_f = _G.__jest_roblox_cov[__cov_file_key].f\n";
|
|
1498
|
-
preamble += `for __i = 1, ${result.functions.length} do if __cov_f[__i] == nil then __cov_f[__i] = 0 end end\n`;
|
|
1499
|
-
}
|
|
1500
|
-
if (result.branches.length > 0) {
|
|
1501
|
-
preamble += "if _G.__jest_roblox_cov[__cov_file_key].b == nil then _G.__jest_roblox_cov[__cov_file_key].b = {} end\n";
|
|
1502
|
-
preamble += "local __cov_b = _G.__jest_roblox_cov[__cov_file_key].b\n";
|
|
1503
|
-
for (const branch of result.branches) {
|
|
1504
|
-
const zeros = branch.arms.map(() => "0").join(", ");
|
|
1505
|
-
preamble += `if __cov_b[${branch.index}] == nil then __cov_b[${branch.index}] = {${zeros}} end\n`;
|
|
1506
|
-
}
|
|
1507
|
-
if (result.exprIfProbes.length > 0) preamble += "local function __cov_br(__bi, __ai, ...) __cov_b[__bi][__ai] += 1; return ... end\n";
|
|
1508
|
-
}
|
|
1509
|
-
return preamble;
|
|
1510
|
-
}
|
|
1511
|
-
//#endregion
|
|
1512
|
-
//#region src/coverage/instrumenter.ts
|
|
1513
|
-
let cachedTemporaryDirectory;
|
|
1514
|
-
/**
|
|
1515
|
-
* Instrument a single luauRoot directory. Returns the files map without
|
|
1516
|
-
* writing a manifest — used by `prepareCoverage()` to merge multiple roots.
|
|
1517
|
-
*/
|
|
1518
|
-
function instrumentRoot(options) {
|
|
1519
|
-
const { astOutputDirectory: astOutputDirectoryOption, luauRoot, parseScript, shadowDir, skipFiles } = options;
|
|
1520
|
-
const lazyTemporaryDirectory = parseScript === void 0 || astOutputDirectoryOption === void 0 ? getTemporaryDirectory() : "";
|
|
1521
|
-
const scriptPath = parseScript ?? path$1.join(lazyTemporaryDirectory, "parse-ast.luau");
|
|
1522
|
-
const astOutputDirectory = astOutputDirectoryOption ?? path$1.join(lazyTemporaryDirectory, "asts");
|
|
1523
|
-
if (parseScript === void 0) fs$1.writeFileSync(scriptPath, parse_ast_default);
|
|
1524
|
-
fs$1.mkdirSync(astOutputDirectory, { recursive: true });
|
|
1525
|
-
const luteArgs = [
|
|
1526
|
-
"run",
|
|
1527
|
-
scriptPath,
|
|
1528
|
-
"--",
|
|
1529
|
-
path$1.resolve(luauRoot),
|
|
1530
|
-
astOutputDirectory
|
|
1531
|
-
];
|
|
1532
|
-
if (skipFiles !== void 0 && skipFiles.size > 0) {
|
|
1533
|
-
const skipListPath = toPosix(path$1.join(astOutputDirectory, "skip-list.json"));
|
|
1534
|
-
fs$1.writeFileSync(skipListPath, JSON.stringify([...skipFiles]));
|
|
1535
|
-
luteArgs.push(skipListPath);
|
|
1536
|
-
}
|
|
1537
|
-
let fileListJson;
|
|
1538
|
-
try {
|
|
1539
|
-
fileListJson = cp.execFileSync("lute", luteArgs, {
|
|
1540
|
-
encoding: "utf-8",
|
|
1541
|
-
maxBuffer: 1024 * 1024
|
|
1542
|
-
});
|
|
1543
|
-
} catch (err) {
|
|
1544
|
-
if (err instanceof Error && "code" in err && err.code === "ENOENT") throw new Error("lute is required for instrumentation but was not found on PATH");
|
|
1545
|
-
throw new Error("Failed to parse Luau files", { cause: err });
|
|
1546
|
-
}
|
|
1547
|
-
let fileList;
|
|
1548
|
-
try {
|
|
1549
|
-
fileList = JSON.parse(fileListJson);
|
|
1550
|
-
} catch (err) {
|
|
1551
|
-
throw new Error("Failed to parse file list from lute", { cause: err });
|
|
1552
|
-
}
|
|
1553
|
-
if (!Array.isArray(fileList)) throw new Error("Expected file list array from lute");
|
|
1554
|
-
const files = {};
|
|
1555
|
-
const posixLuauRoot = toPosix(luauRoot);
|
|
1556
|
-
for (const relativePath of fileList) {
|
|
1557
|
-
if (shouldSkipFile(relativePath, skipFiles)) continue;
|
|
1558
|
-
const astJsonPath = path$1.join(astOutputDirectory, `${relativePath}.json`);
|
|
1559
|
-
let astJson;
|
|
1560
|
-
try {
|
|
1561
|
-
astJson = fs$1.readFileSync(astJsonPath, "utf-8");
|
|
1562
|
-
} catch (err) {
|
|
1563
|
-
throw new Error(`Failed to read AST for ${relativePath}`, { cause: err });
|
|
1564
|
-
}
|
|
1565
|
-
const ast = JSON.parse(astJson);
|
|
1566
|
-
const fileKey = toPosix(path$1.join(posixLuauRoot, relativePath));
|
|
1567
|
-
const originalLuauPath = fileKey;
|
|
1568
|
-
const instrumentedLuauPath = toPosix(path$1.join(shadowDir, relativePath));
|
|
1569
|
-
const coverageMapOutputPath = path$1.join(shadowDir, relativePath.replace(/\.luau$/, ".cov-map.json"));
|
|
1570
|
-
const sourceMapPath = `${originalLuauPath}.map`;
|
|
1571
|
-
const outputDirectory = path$1.dirname(path$1.join(shadowDir, relativePath));
|
|
1572
|
-
fs$1.mkdirSync(outputDirectory, { recursive: true });
|
|
1573
|
-
const sourceBuffer = fs$1.readFileSync(path$1.resolve(originalLuauPath));
|
|
1574
|
-
const source = sourceBuffer.toString("utf-8");
|
|
1575
|
-
const collectorResult = collectCoverage(ast);
|
|
1576
|
-
const instrumentedSource = insertProbes(source, collectorResult, fileKey);
|
|
1577
|
-
const coverageMap = buildCoverageMap$1(collectorResult);
|
|
1578
|
-
fs$1.writeFileSync(path$1.join(shadowDir, relativePath), instrumentedSource);
|
|
1579
|
-
fs$1.writeFileSync(coverageMapOutputPath, JSON.stringify(coverageMap, void 0, " "));
|
|
1580
|
-
files[fileKey] = {
|
|
1581
|
-
key: fileKey,
|
|
1582
|
-
branchCount: collectorResult.branches.length,
|
|
1583
|
-
coverageMapPath: toPosix(coverageMapOutputPath),
|
|
1584
|
-
functionCount: collectorResult.functions.length,
|
|
1585
|
-
instrumentedLuauPath,
|
|
1586
|
-
originalLuauPath,
|
|
1587
|
-
sourceHash: hashBuffer(sourceBuffer),
|
|
1588
|
-
sourceMapPath,
|
|
1589
|
-
statementCount: collectorResult.statements.length
|
|
1590
|
-
};
|
|
1591
|
-
}
|
|
1592
|
-
return files;
|
|
1593
|
-
}
|
|
1594
|
-
function shouldSkipFile(relativePath, skipFiles) {
|
|
1595
|
-
if (relativePath.endsWith(".snap.luau") || relativePath.endsWith(".snap.lua")) return true;
|
|
1596
|
-
return skipFiles?.has(relativePath) === true;
|
|
1597
|
-
}
|
|
1598
|
-
function getTemporaryDirectory() {
|
|
1599
|
-
if (cachedTemporaryDirectory !== void 0 && fs$1.existsSync(cachedTemporaryDirectory)) return cachedTemporaryDirectory;
|
|
1600
|
-
cachedTemporaryDirectory = fs$1.mkdtempSync(path$1.join(os.tmpdir(), "jest-roblox-instrument-"));
|
|
1601
|
-
return cachedTemporaryDirectory;
|
|
1602
|
-
}
|
|
1603
|
-
function toPosix(value) {
|
|
1604
|
-
return value.replaceAll("\\", "/");
|
|
1605
|
-
}
|
|
1606
|
-
//#endregion
|
|
1607
|
-
//#region src/coverage/rojo-builder.ts
|
|
1608
|
-
function buildWithRojo(projectPath, outputPath) {
|
|
1609
|
-
try {
|
|
1610
|
-
cp.execFileSync("rojo", [
|
|
1611
|
-
"build",
|
|
1612
|
-
projectPath,
|
|
1613
|
-
"-o",
|
|
1614
|
-
outputPath
|
|
1615
|
-
], { stdio: "pipe" });
|
|
1616
|
-
} catch (err) {
|
|
1617
|
-
if (err instanceof Error && "code" in err && err.code === "ENOENT") throw new Error("rojo is required for --coverage but was not found on PATH");
|
|
1618
|
-
const stderr = err instanceof Error && "stderr" in err && Buffer.isBuffer(err.stderr) ? err.stderr.toString().trim() : void 0;
|
|
1619
|
-
const message = stderr !== void 0 && stderr.length > 0 ? `rojo build failed: ${stderr}` : "rojo build failed";
|
|
1620
|
-
throw new Error(message, { cause: err });
|
|
1621
|
-
}
|
|
1622
|
-
}
|
|
1623
|
-
//#endregion
|
|
1624
|
-
//#region src/coverage/rojo-rewriter.ts
|
|
1625
|
-
function rewriteRojoProject(project, options) {
|
|
1626
|
-
const roots = isMultiRoot(options) ? options.roots.map(buildRootContext) : [buildRootContext(options)];
|
|
1627
|
-
const context = {
|
|
1628
|
-
relocation: options.projectRelocation?.replaceAll("\\", "/"),
|
|
1629
|
-
roots
|
|
1630
|
-
};
|
|
1631
|
-
return {
|
|
1632
|
-
...project,
|
|
1633
|
-
tree: walkTree(project.tree, context)
|
|
1634
|
-
};
|
|
1635
|
-
}
|
|
1636
|
-
function isMultiRoot(options) {
|
|
1637
|
-
return "roots" in options;
|
|
1638
|
-
}
|
|
1639
|
-
function buildRootContext(entry) {
|
|
1640
|
-
return {
|
|
1641
|
-
luauRoot: entry.luauRoot.replaceAll("\\", "/").replace(/\/$/, ""),
|
|
1642
|
-
relocatedShadowDirectory: entry.relocatedShadowDirectory?.replaceAll("\\", "/"),
|
|
1643
|
-
shadowDirectory: entry.shadowDir
|
|
1644
|
-
};
|
|
1645
|
-
}
|
|
1646
|
-
function rewritePath(value, context) {
|
|
1647
|
-
const normalized = value.replaceAll("\\", "/");
|
|
1648
|
-
for (const root of context.roots) if (normalized === root.luauRoot || normalized.startsWith(`${root.luauRoot}/`)) {
|
|
1649
|
-
const suffix = normalized.slice(root.luauRoot.length);
|
|
1650
|
-
if (context.relocation === void 0 || root.relocatedShadowDirectory === void 0) return root.shadowDirectory + suffix;
|
|
1651
|
-
return root.relocatedShadowDirectory + suffix;
|
|
1652
|
-
}
|
|
1653
|
-
if (context.relocation !== void 0) return `${context.relocation}/${normalized}`;
|
|
1654
|
-
return value;
|
|
1655
|
-
}
|
|
1656
|
-
function walkTree(node, context) {
|
|
1657
|
-
const result = {};
|
|
1658
|
-
for (const [key, value] of Object.entries(node)) if (key === "$path" && typeof value === "string") result[key] = rewritePath(value, context);
|
|
1659
|
-
else if (typeof value === "object" && !Array.isArray(value) && !key.startsWith("$")) result[key] = walkTree(value, context);
|
|
1660
|
-
else result[key] = value;
|
|
1661
|
-
return result;
|
|
1662
|
-
}
|
|
1663
|
-
//#endregion
|
|
1664
|
-
//#region src/coverage/prepare.ts
|
|
1665
|
-
const COVERAGE_DIR = ".jest-roblox-coverage";
|
|
1666
|
-
/**
|
|
1667
|
-
* Suffixes for files that are not instrumented for coverage but still need
|
|
1668
|
-
* syncing to the shadow directory. Matches parse-ast.luau:131-139.
|
|
1669
|
-
*/
|
|
1670
|
-
const NON_INSTRUMENTED_SUFFIXES = [
|
|
1671
|
-
".spec.luau",
|
|
1672
|
-
".test.luau",
|
|
1673
|
-
".spec.lua",
|
|
1674
|
-
".test.lua",
|
|
1675
|
-
".snap.luau",
|
|
1676
|
-
".snap.lua"
|
|
1677
|
-
];
|
|
1678
|
-
function isNonInstrumentedFile(filename) {
|
|
1679
|
-
return NON_INSTRUMENTED_SUFFIXES.some((suffix) => filename.endsWith(suffix));
|
|
1680
|
-
}
|
|
1681
|
-
const previousManifestSchema = type({
|
|
1682
|
-
"files": type({ "[string]": { sourceHash: "string" } }),
|
|
1683
|
-
"instrumenterVersion": "number",
|
|
1684
|
-
"luauRoots": "string[]",
|
|
1685
|
-
"nonInstrumentedFiles?": type({ "[string]": {
|
|
1686
|
-
shadowPath: "string",
|
|
1687
|
-
sourceHash: "string",
|
|
1688
|
-
sourcePath: "string"
|
|
1689
|
-
} }),
|
|
1690
|
-
"placeFilePath?": "string",
|
|
1691
|
-
"shadowDir": "string",
|
|
1692
|
-
"version": "number"
|
|
1693
|
-
}).as();
|
|
1694
|
-
function collectLuauRootsFromRojo(project, config) {
|
|
1695
|
-
const paths = [];
|
|
1696
|
-
collectPaths(project.tree, paths);
|
|
1697
|
-
const ignorePatterns = config.coveragePathIgnorePatterns;
|
|
1698
|
-
const isIgnored = picomatch(ignorePatterns, { contains: true });
|
|
1699
|
-
return paths.filter((directoryPath) => {
|
|
1700
|
-
if (!fs$1.existsSync(directoryPath)) return false;
|
|
1701
|
-
if (!fs$1.statSync(directoryPath).isDirectory()) return false;
|
|
1702
|
-
if (isIgnored(directoryPath)) return false;
|
|
1703
|
-
return containsLuauFiles(directoryPath);
|
|
1704
|
-
});
|
|
1705
|
-
}
|
|
1706
|
-
/**
|
|
1707
|
-
* Fast directory walk to discover instrumentable .luau/.lua files.
|
|
1708
|
-
* Must match parse-ast.luau's discoverFiles logic (same skip rules).
|
|
1709
|
-
*/
|
|
1710
|
-
function discoverInstrumentableFiles(luauRoot) {
|
|
1711
|
-
const posixRoot = luauRoot.replaceAll("\\", "/");
|
|
1712
|
-
const results = [];
|
|
1713
|
-
walkLuauDirectory(posixRoot, posixRoot, isInstrumentableFile, results);
|
|
1714
|
-
return new Set(results);
|
|
1715
|
-
}
|
|
1716
|
-
function prepareCoverage(config, beforeBuild) {
|
|
1717
|
-
const rojoProjectPath = findRojoProject(config);
|
|
1718
|
-
const luauRoots = resolveLuauRootsWithRojo(config, rojoProjectPath);
|
|
1719
|
-
validateRelativeRoots(luauRoots);
|
|
1720
|
-
const manifestPath = path$1.join(COVERAGE_DIR, "manifest.json");
|
|
1721
|
-
const previousManifest = loadPreviousManifest(manifestPath);
|
|
1722
|
-
const useIncremental = canUseIncremental(previousManifest, config);
|
|
1723
|
-
if (!useIncremental && fs$1.existsSync(COVERAGE_DIR)) fs$1.rmSync(COVERAGE_DIR, { recursive: true });
|
|
1724
|
-
const allFiles = {};
|
|
1725
|
-
const allNonInstrumented = {};
|
|
1726
|
-
const roots = [];
|
|
1727
|
-
let hasChanges = !useIncremental;
|
|
1728
|
-
for (const luauRoot of luauRoots) {
|
|
1729
|
-
const rootResult = instrumentRootWithCache(luauRoot, useIncremental, previousManifest);
|
|
1730
|
-
if (rootResult.changed) hasChanges = true;
|
|
1731
|
-
Object.assign(allFiles, rootResult.files);
|
|
1732
|
-
Object.assign(allNonInstrumented, rootResult.nonInstrumentedFiles);
|
|
1733
|
-
roots.push(rootResult.rootEntry);
|
|
1734
|
-
}
|
|
1735
|
-
if (useIncremental && previousManifest !== void 0) {
|
|
1736
|
-
const deleted = detectDeletedFiles(previousManifest, allFiles);
|
|
1737
|
-
cleanupDeletedFiles(deleted);
|
|
1738
|
-
if (deleted.length > 0) hasChanges = true;
|
|
1739
|
-
}
|
|
1740
|
-
if (beforeBuild !== void 0) {
|
|
1741
|
-
if (beforeBuild(COVERAGE_DIR)) hasChanges = true;
|
|
1742
|
-
}
|
|
1743
|
-
const placeFile = path$1.join(COVERAGE_DIR, "game.rbxl");
|
|
1744
|
-
const manifest = writeManifest({
|
|
1745
|
-
allFiles,
|
|
1746
|
-
luauRoots,
|
|
1747
|
-
manifestPath,
|
|
1748
|
-
nonInstrumentedFiles: allNonInstrumented,
|
|
1749
|
-
placeFile
|
|
1750
|
-
});
|
|
1751
|
-
if (!hasChanges && previousManifest?.placeFilePath !== void 0) return {
|
|
1752
|
-
manifest,
|
|
1753
|
-
placeFile: previousManifest.placeFilePath
|
|
1754
|
-
};
|
|
1755
|
-
buildRojoProject(rojoProjectPath, roots, placeFile);
|
|
1756
|
-
return {
|
|
1757
|
-
manifest,
|
|
1758
|
-
placeFile
|
|
1759
|
-
};
|
|
1760
|
-
}
|
|
1761
|
-
function containsLuauFiles(directoryPath) {
|
|
1762
|
-
return fs$1.readdirSync(directoryPath, { withFileTypes: true }).some((entry) => {
|
|
1763
|
-
if (entry.isFile() && entry.name.endsWith(".luau")) return true;
|
|
1764
|
-
if (entry.isDirectory()) return containsLuauFiles(path$1.join(directoryPath, entry.name));
|
|
1765
|
-
return false;
|
|
1766
|
-
});
|
|
1767
|
-
}
|
|
1768
|
-
function findRojoProject(config) {
|
|
1769
|
-
if (config.rojoProject !== void 0) return config.rojoProject;
|
|
1770
|
-
const defaultPath = path$1.join(config.rootDir, "default.project.json");
|
|
1771
|
-
if (fs$1.existsSync(defaultPath)) return defaultPath;
|
|
1772
|
-
const projectFile = fs$1.readdirSync(config.rootDir, "utf-8").find((file) => file.endsWith(".project.json"));
|
|
1773
|
-
if (projectFile !== void 0) return path$1.join(config.rootDir, projectFile);
|
|
1774
|
-
throw new Error("No Rojo project found. Set rojoProject in config or add a .project.json file.");
|
|
1775
|
-
}
|
|
1776
|
-
function resolveLuauRootsWithRojo(config, rojoProjectPath) {
|
|
1777
|
-
if (config.luauRoots !== void 0 && config.luauRoots.length > 0) return config.luauRoots;
|
|
1778
|
-
try {
|
|
1779
|
-
const resolvedPath = rojoProjectPath ?? findRojoProject(config);
|
|
1780
|
-
const roots = collectLuauRootsFromRojo(JSON.parse(fs$1.readFileSync(resolvedPath, "utf-8")), config);
|
|
1781
|
-
if (roots.length > 0) return roots;
|
|
1782
|
-
} catch (err) {
|
|
1783
|
-
if (err instanceof SyntaxError) throw new Error(`Malformed Rojo project JSON: ${err.message}`, { cause: err });
|
|
1784
|
-
}
|
|
1785
|
-
const outDirectory = (getTsconfig(config.rootDir) ?? void 0)?.config.compilerOptions?.outDir;
|
|
1786
|
-
if (outDirectory !== void 0) return [outDirectory];
|
|
1787
|
-
throw new Error("Could not determine luauRoots. Set luauRoots in config or ensure tsconfig has outDir.");
|
|
1788
|
-
}
|
|
1789
|
-
/**
|
|
1790
|
-
* Shared directory walker. Skips node_modules, .jest-roblox-coverage, and
|
|
1791
|
-
* dot-prefixed directories — matching parse-ast.luau:113-147.
|
|
1792
|
-
* `predicate` receives the entry name and returns true to collect the file.
|
|
1793
|
-
*/
|
|
1794
|
-
function walkLuauDirectory(directory, relativeTo, predicate, results) {
|
|
1795
|
-
const entries = fs$1.readdirSync(directory, { withFileTypes: true });
|
|
1796
|
-
for (const entry of entries) {
|
|
1797
|
-
const fullPath = path$1.join(directory, entry.name).replaceAll("\\", "/");
|
|
1798
|
-
if (entry.isDirectory()) {
|
|
1799
|
-
if (entry.name === "node_modules" || entry.name === COVERAGE_DIR) continue;
|
|
1800
|
-
if (entry.name.startsWith(".")) continue;
|
|
1801
|
-
walkLuauDirectory(fullPath, relativeTo, predicate, results);
|
|
1802
|
-
} else if (predicate(entry.name)) {
|
|
1803
|
-
const relative = fullPath.slice(relativeTo.length + 1);
|
|
1804
|
-
results.push(relative);
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
1807
|
-
}
|
|
1808
|
-
function isInstrumentableFile(name) {
|
|
1809
|
-
return (name.endsWith(".luau") || name.endsWith(".lua")) && !isNonInstrumentedFile(name);
|
|
1810
|
-
}
|
|
1811
|
-
function validateRelativeRoots(luauRoots) {
|
|
1812
|
-
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.");
|
|
1813
|
-
}
|
|
1814
|
-
function carryForwardRecords(luauRoot, previousManifest, allFiles, skipFiles) {
|
|
1815
|
-
const posixRoot = luauRoot.replaceAll("\\", "/");
|
|
1816
|
-
for (const relativePath of skipFiles) {
|
|
1817
|
-
const fileKey = `${posixRoot}/${relativePath}`;
|
|
1818
|
-
Object.assign(allFiles, { [fileKey]: previousManifest.files[fileKey] });
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
function discoverNonInstrumentedFiles(directory, relativeTo, results) {
|
|
1822
|
-
walkLuauDirectory(directory, relativeTo, isNonInstrumentedFile, results);
|
|
1823
|
-
}
|
|
1824
|
-
function pruneStaleNonInstrumented(posixRoot, previousNonInstrumented, currentFiles) {
|
|
1825
|
-
if (previousNonInstrumented === void 0) return false;
|
|
1826
|
-
let changed = false;
|
|
1827
|
-
for (const [fileKey, record] of Object.entries(previousNonInstrumented)) {
|
|
1828
|
-
if (!fileKey.startsWith(`${posixRoot}/`)) continue;
|
|
1829
|
-
if (fileKey in currentFiles) continue;
|
|
1830
|
-
try {
|
|
1831
|
-
if (fs$1.existsSync(record.shadowPath)) fs$1.unlinkSync(record.shadowPath);
|
|
1832
|
-
} catch {}
|
|
1833
|
-
changed = true;
|
|
1834
|
-
}
|
|
1835
|
-
return changed;
|
|
1836
|
-
}
|
|
1837
|
-
function syncNonInstrumentedFiles(luauRoot, shadowDirectory, previousNonInstrumented) {
|
|
1838
|
-
const posixRoot = luauRoot.replaceAll("\\", "/");
|
|
1839
|
-
const discovered = [];
|
|
1840
|
-
discoverNonInstrumentedFiles(posixRoot, posixRoot, discovered);
|
|
1841
|
-
const files = {};
|
|
1842
|
-
let changed = false;
|
|
1843
|
-
for (const relativePath of discovered) {
|
|
1844
|
-
const sourcePath = `${posixRoot}/${relativePath}`;
|
|
1845
|
-
const shadowPath = `${shadowDirectory}/${relativePath}`;
|
|
1846
|
-
const currentHash = hashBuffer(fs$1.readFileSync(path$1.resolve(sourcePath)));
|
|
1847
|
-
const previousRecord = previousNonInstrumented?.[sourcePath];
|
|
1848
|
-
if (previousRecord?.sourceHash === currentHash) {
|
|
1849
|
-
files[sourcePath] = previousRecord;
|
|
1850
|
-
continue;
|
|
1851
|
-
}
|
|
1852
|
-
const outputDirectory = path$1.dirname(shadowPath);
|
|
1853
|
-
fs$1.mkdirSync(outputDirectory, { recursive: true });
|
|
1854
|
-
fs$1.copyFileSync(path$1.resolve(sourcePath), shadowPath);
|
|
1855
|
-
files[sourcePath] = {
|
|
1856
|
-
shadowPath,
|
|
1857
|
-
sourceHash: currentHash,
|
|
1858
|
-
sourcePath
|
|
1859
|
-
};
|
|
1860
|
-
changed = true;
|
|
1861
|
-
}
|
|
1862
|
-
changed = pruneStaleNonInstrumented(posixRoot, previousNonInstrumented, files) || changed;
|
|
1863
|
-
return {
|
|
1864
|
-
changed,
|
|
1865
|
-
files
|
|
1866
|
-
};
|
|
1867
|
-
}
|
|
1868
|
-
function computeSkipFiles(luauRoot, previousManifest) {
|
|
1869
|
-
const skipFiles = /* @__PURE__ */ new Set();
|
|
1870
|
-
const posixRoot = luauRoot.replaceAll("\\", "/");
|
|
1871
|
-
for (const [fileKey, record] of Object.entries(previousManifest.files)) {
|
|
1872
|
-
if (!fileKey.startsWith(`${posixRoot}/`)) continue;
|
|
1873
|
-
const relativePath = fileKey.slice(posixRoot.length + 1);
|
|
1874
|
-
const sourcePath = path$1.resolve(record.originalLuauPath);
|
|
1875
|
-
if (!fs$1.existsSync(sourcePath)) continue;
|
|
1876
|
-
if (hashBuffer(fs$1.readFileSync(sourcePath)) === record.sourceHash) skipFiles.add(relativePath);
|
|
1877
|
-
}
|
|
1878
|
-
return skipFiles;
|
|
1879
|
-
}
|
|
1880
|
-
function countPreviousFilesForRoot(luauRoot, previousManifest) {
|
|
1881
|
-
const posixRoot = luauRoot.replaceAll("\\", "/");
|
|
1882
|
-
let count = 0;
|
|
1883
|
-
for (const fileKey of Object.keys(previousManifest.files)) if (fileKey.startsWith(`${posixRoot}/`)) count++;
|
|
1884
|
-
return count;
|
|
1885
|
-
}
|
|
1886
|
-
/**
|
|
1887
|
-
* Check if all files in this root are unchanged (full cache hit).
|
|
1888
|
-
*
|
|
1889
|
-
* `changed` means previous files were deleted or modified — it does NOT cover
|
|
1890
|
-
* new files appearing on disk. When `allCached` is false but `changed` is also
|
|
1891
|
-
* false, new files exist and the caller detects them when `instrumentRoot`
|
|
1892
|
-
* returns non-empty results.
|
|
1893
|
-
*/
|
|
1894
|
-
function computeIncrementalState(luauRoot, previousManifest) {
|
|
1895
|
-
const skipFiles = computeSkipFiles(luauRoot, previousManifest);
|
|
1896
|
-
const previousCount = countPreviousFilesForRoot(luauRoot, previousManifest);
|
|
1897
|
-
const changed = skipFiles.size !== previousCount;
|
|
1898
|
-
if (changed) return {
|
|
1899
|
-
allCached: false,
|
|
1900
|
-
changed,
|
|
1901
|
-
skipFiles
|
|
1902
|
-
};
|
|
1903
|
-
return {
|
|
1904
|
-
allCached: discoverInstrumentableFiles(luauRoot).size === previousCount,
|
|
1905
|
-
changed,
|
|
1906
|
-
skipFiles
|
|
1907
|
-
};
|
|
1908
|
-
}
|
|
1909
|
-
function buildFullCacheResult(options) {
|
|
1910
|
-
const { luauRoot, previousManifest, rootEntry, shadowDirectory, skipFiles } = options;
|
|
1911
|
-
const allFiles = {};
|
|
1912
|
-
carryForwardRecords(luauRoot, previousManifest, allFiles, skipFiles);
|
|
1913
|
-
const syncResult = syncNonInstrumentedFiles(luauRoot, shadowDirectory, previousManifest.nonInstrumentedFiles);
|
|
1914
|
-
return {
|
|
1915
|
-
changed: syncResult.changed,
|
|
1916
|
-
files: allFiles,
|
|
1917
|
-
nonInstrumentedFiles: syncResult.files,
|
|
1918
|
-
rootEntry
|
|
1919
|
-
};
|
|
1920
|
-
}
|
|
1921
|
-
function instrumentRootWithCache(luauRoot, useIncremental, previousManifest) {
|
|
1922
|
-
const shadowDirectory = path$1.join(COVERAGE_DIR, luauRoot).replaceAll("\\", "/");
|
|
1923
|
-
let changed = false;
|
|
1924
|
-
if (!useIncremental) {
|
|
1925
|
-
fs$1.mkdirSync(shadowDirectory, { recursive: true });
|
|
1926
|
-
fs$1.cpSync(luauRoot, shadowDirectory, { recursive: true });
|
|
1927
|
-
}
|
|
1928
|
-
const rootEntry = {
|
|
1929
|
-
luauRoot,
|
|
1930
|
-
relocatedShadowDirectory: path$1.relative(COVERAGE_DIR, shadowDirectory).replaceAll("\\", "/"),
|
|
1931
|
-
shadowDir: shadowDirectory
|
|
1932
|
-
};
|
|
1933
|
-
let skipFiles;
|
|
1934
|
-
if (useIncremental && previousManifest !== void 0) {
|
|
1935
|
-
const { allCached, changed: hasChanges, skipFiles: computed } = computeIncrementalState(luauRoot, previousManifest);
|
|
1936
|
-
skipFiles = computed;
|
|
1937
|
-
changed = hasChanges;
|
|
1938
|
-
if (allCached) return buildFullCacheResult({
|
|
1939
|
-
luauRoot,
|
|
1940
|
-
previousManifest,
|
|
1941
|
-
rootEntry,
|
|
1942
|
-
shadowDirectory,
|
|
1943
|
-
skipFiles
|
|
1944
|
-
});
|
|
1945
|
-
}
|
|
1946
|
-
const files = instrumentRoot({
|
|
1947
|
-
luauRoot,
|
|
1948
|
-
shadowDir: shadowDirectory,
|
|
1949
|
-
skipFiles
|
|
1950
|
-
});
|
|
1951
|
-
if (Object.keys(files).length > 0) changed = true;
|
|
1952
|
-
const allFiles = { ...files };
|
|
1953
|
-
if (useIncremental && previousManifest !== void 0 && skipFiles !== void 0) carryForwardRecords(luauRoot, previousManifest, allFiles, skipFiles);
|
|
1954
|
-
const syncResult = syncNonInstrumentedFiles(luauRoot, shadowDirectory, previousManifest?.nonInstrumentedFiles);
|
|
1955
|
-
if (syncResult.changed) changed = true;
|
|
1956
|
-
return {
|
|
1957
|
-
changed,
|
|
1958
|
-
files: allFiles,
|
|
1959
|
-
nonInstrumentedFiles: syncResult.files,
|
|
1960
|
-
rootEntry
|
|
1961
|
-
};
|
|
1962
|
-
}
|
|
1963
|
-
function writeManifest(options) {
|
|
1964
|
-
const { allFiles, luauRoots, manifestPath, nonInstrumentedFiles, placeFile } = options;
|
|
1965
|
-
const manifest = {
|
|
1966
|
-
files: allFiles,
|
|
1967
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1968
|
-
instrumenterVersion: 2,
|
|
1969
|
-
luauRoots,
|
|
1970
|
-
nonInstrumentedFiles,
|
|
1971
|
-
placeFilePath: placeFile,
|
|
1972
|
-
shadowDir: COVERAGE_DIR,
|
|
1973
|
-
version: 1
|
|
1974
|
-
};
|
|
1975
|
-
fs$1.mkdirSync(path$1.dirname(manifestPath), { recursive: true });
|
|
1976
|
-
fs$1.writeFileSync(manifestPath, JSON.stringify(manifest, void 0, " "));
|
|
1977
|
-
return manifest;
|
|
1978
|
-
}
|
|
1979
|
-
function buildRojoProject(rojoProjectPath, roots, placeFile) {
|
|
1980
|
-
const rojoProjectRaw = rojoProjectSchema(JSON.parse(fs$1.readFileSync(rojoProjectPath, "utf-8")));
|
|
1981
|
-
if (rojoProjectRaw instanceof type.errors) throw new Error(`Malformed Rojo project JSON: ${rojoProjectRaw.toString()}`);
|
|
1982
|
-
const projectRelocation = path$1.relative(COVERAGE_DIR, path$1.dirname(rojoProjectPath)).replaceAll("\\", "/");
|
|
1983
|
-
const rewritten = rewriteRojoProject({
|
|
1984
|
-
...rojoProjectRaw,
|
|
1985
|
-
tree: resolveNestedProjects(rojoProjectRaw.tree, path$1.dirname(rojoProjectPath))
|
|
1986
|
-
}, {
|
|
1987
|
-
projectRelocation,
|
|
1988
|
-
roots
|
|
1989
|
-
});
|
|
1990
|
-
const rewrittenProjectPath = path$1.join(COVERAGE_DIR, path$1.basename(rojoProjectPath));
|
|
1991
|
-
fs$1.writeFileSync(rewrittenProjectPath, JSON.stringify(rewritten, void 0, " "));
|
|
1992
|
-
buildWithRojo(rewrittenProjectPath, placeFile);
|
|
1993
|
-
}
|
|
1994
|
-
function loadPreviousManifest(manifestPath) {
|
|
1995
|
-
if (!fs$1.existsSync(manifestPath)) return;
|
|
1996
|
-
try {
|
|
1997
|
-
const result = previousManifestSchema(JSON.parse(fs$1.readFileSync(manifestPath, "utf-8")));
|
|
1998
|
-
if (result instanceof type.errors) return;
|
|
1999
|
-
return result;
|
|
2000
|
-
} catch {
|
|
2001
|
-
return;
|
|
2002
|
-
}
|
|
2003
|
-
}
|
|
2004
|
-
function canUseIncremental(previousManifest, config) {
|
|
2005
|
-
if (!config.cache) return false;
|
|
2006
|
-
if (previousManifest === void 0) return false;
|
|
2007
|
-
if (previousManifest.instrumenterVersion !== 2) return false;
|
|
2008
|
-
if (previousManifest.nonInstrumentedFiles === void 0) return false;
|
|
2009
|
-
return true;
|
|
2010
|
-
}
|
|
2011
|
-
function detectDeletedFiles(previousManifest, currentFiles) {
|
|
2012
|
-
const deleted = [];
|
|
2013
|
-
for (const [fileKey, record] of Object.entries(previousManifest.files)) if (!(fileKey in currentFiles)) deleted.push(record);
|
|
2014
|
-
return deleted;
|
|
2015
|
-
}
|
|
2016
|
-
function cleanupDeletedFiles(records) {
|
|
2017
|
-
for (const record of records) try {
|
|
2018
|
-
if (fs$1.existsSync(record.instrumentedLuauPath)) fs$1.unlinkSync(record.instrumentedLuauPath);
|
|
2019
|
-
if (fs$1.existsSync(record.coverageMapPath)) fs$1.unlinkSync(record.coverageMapPath);
|
|
2020
|
-
} catch {}
|
|
2021
|
-
}
|
|
2022
|
-
//#endregion
|
|
2023
|
-
//#region src/coverage/reporter.ts
|
|
2024
|
-
const VALID_REPORTERS = new Set([
|
|
2025
|
-
"clover",
|
|
2026
|
-
"cobertura",
|
|
2027
|
-
"html",
|
|
2028
|
-
"html-spa",
|
|
2029
|
-
"json",
|
|
2030
|
-
"json-summary",
|
|
2031
|
-
"lcov",
|
|
2032
|
-
"lcovonly",
|
|
2033
|
-
"none",
|
|
2034
|
-
"teamcity",
|
|
2035
|
-
"text",
|
|
2036
|
-
"text-lcov",
|
|
2037
|
-
"text-summary"
|
|
2038
|
-
]);
|
|
2039
|
-
function printCoverageHeader() {
|
|
2040
|
-
const header = ` ${color.blue("%")} ${color.dim("Coverage report from")} ${color.yellow("istanbul")}`;
|
|
2041
|
-
process.stdout.write(`\n${header}\n`);
|
|
2042
|
-
}
|
|
2043
|
-
const TEXT_REPORTERS = new Set(["text", "text-summary"]);
|
|
2044
|
-
function generateReports(options) {
|
|
2045
|
-
const coverageMap = buildCoverageMap(filterMappedFiles(options.mapped, options.collectCoverageFrom));
|
|
2046
|
-
const context = istanbulReport.createContext({
|
|
2047
|
-
coverageMap,
|
|
2048
|
-
defaultSummarizer: options.agentMode === true ? "flat" : "pkg",
|
|
2049
|
-
dir: options.coverageDirectory
|
|
2050
|
-
});
|
|
2051
|
-
const terminalColumns = getTerminalColumns();
|
|
2052
|
-
const allFilesFull = options.agentMode === true && isAllFilesFull(coverageMap);
|
|
2053
|
-
for (const reporterName of options.reporters) {
|
|
2054
|
-
if (!isValidReporter(reporterName)) throw new Error(`Unknown coverage reporter: ${reporterName}`);
|
|
2055
|
-
if (allFilesFull && TEXT_REPORTERS.has(reporterName)) {
|
|
2056
|
-
const fileCount = coverageMap.files().length;
|
|
2057
|
-
const label = fileCount === 1 ? "file" : "files";
|
|
2058
|
-
process.stdout.write(`Coverage: 100% (${fileCount} ${label})\n`);
|
|
2059
|
-
continue;
|
|
2060
|
-
}
|
|
2061
|
-
let reporterOptions = {};
|
|
2062
|
-
if (reporterName === "text") reporterOptions = {
|
|
2063
|
-
maxCols: terminalColumns,
|
|
2064
|
-
skipFull: options.agentMode === true
|
|
2065
|
-
};
|
|
2066
|
-
else if (TEXT_REPORTERS.has(reporterName)) reporterOptions = { skipFull: options.agentMode === true };
|
|
2067
|
-
istanbulReports.create(reporterName, reporterOptions).execute(context);
|
|
2068
|
-
}
|
|
2069
|
-
}
|
|
2070
|
-
function checkThresholds(mapped, thresholds, collectCoverageFrom) {
|
|
2071
|
-
const summary = buildCoverageMap(filterMappedFiles(mapped, collectCoverageFrom)).getCoverageSummary();
|
|
2072
|
-
const failures = [];
|
|
2073
|
-
const checks = [
|
|
2074
|
-
{
|
|
2075
|
-
metric: "statements",
|
|
2076
|
-
threshold: thresholds.statements
|
|
2077
|
-
},
|
|
2078
|
-
{
|
|
2079
|
-
metric: "functions",
|
|
2080
|
-
threshold: thresholds.functions
|
|
2081
|
-
},
|
|
2082
|
-
{
|
|
2083
|
-
metric: "branches",
|
|
2084
|
-
threshold: thresholds.branches
|
|
2085
|
-
},
|
|
2086
|
-
{
|
|
2087
|
-
metric: "lines",
|
|
2088
|
-
threshold: thresholds.lines
|
|
2089
|
-
}
|
|
2090
|
-
];
|
|
2091
|
-
const summaryData = type({ "[string]": { pct: "number | string" } })(summary.toJSON());
|
|
2092
|
-
assert(!(summaryData instanceof type.errors), "Istanbul summary produced invalid data");
|
|
2093
|
-
for (const { metric, threshold } of checks) {
|
|
2094
|
-
if (threshold === void 0) continue;
|
|
2095
|
-
const pct = summaryData[metric]?.pct;
|
|
2096
|
-
if (typeof pct !== "number") continue;
|
|
2097
|
-
if (pct < threshold) failures.push({
|
|
2098
|
-
actual: pct,
|
|
2099
|
-
metric,
|
|
2100
|
-
threshold
|
|
2101
|
-
});
|
|
2102
|
-
}
|
|
2103
|
-
return {
|
|
2104
|
-
failures,
|
|
2105
|
-
passed: failures.length === 0
|
|
2106
|
-
};
|
|
2107
|
-
}
|
|
2108
|
-
function getTerminalColumns() {
|
|
2109
|
-
if (process.stdout.columns !== void 0) return process.stdout.columns;
|
|
2110
|
-
const columnsEnvironment = process.env["COLUMNS"];
|
|
2111
|
-
if (columnsEnvironment === void 0) return;
|
|
2112
|
-
const parsed = Number(columnsEnvironment);
|
|
2113
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
2114
|
-
}
|
|
2115
|
-
function isAllFilesFull(coverageMap) {
|
|
2116
|
-
const files = coverageMap.files();
|
|
2117
|
-
if (files.length === 0) return false;
|
|
2118
|
-
return files.every((file) => {
|
|
2119
|
-
const summary = coverageMap.fileCoverageFor(file).toSummary();
|
|
2120
|
-
return summary.statements.pct === 100 && summary.branches.pct === 100 && summary.functions.pct === 100 && summary.lines.pct === 100;
|
|
2121
|
-
});
|
|
2122
|
-
}
|
|
2123
|
-
function buildCoverageMap(mapped) {
|
|
2124
|
-
const coverageMap = istanbulCoverage.createCoverageMap({});
|
|
2125
|
-
for (const [filePath, fileCoverage] of Object.entries(mapped.files)) {
|
|
2126
|
-
const fileCoverageData = {
|
|
2127
|
-
b: fileCoverage.b,
|
|
2128
|
-
branchMap: Object.fromEntries(Object.entries(fileCoverage.branchMap).map(([id, entry]) => {
|
|
2129
|
-
return [id, {
|
|
2130
|
-
line: entry.loc.start.line,
|
|
2131
|
-
loc: entry.loc,
|
|
2132
|
-
locations: entry.locations,
|
|
2133
|
-
type: entry.type
|
|
2134
|
-
}];
|
|
2135
|
-
})),
|
|
2136
|
-
f: fileCoverage.f,
|
|
2137
|
-
fnMap: Object.fromEntries(Object.entries(fileCoverage.fnMap).map(([id, entry]) => {
|
|
2138
|
-
return [id, {
|
|
2139
|
-
name: entry.name,
|
|
2140
|
-
decl: entry.loc,
|
|
2141
|
-
line: entry.loc.start.line,
|
|
2142
|
-
loc: entry.loc
|
|
2143
|
-
}];
|
|
2144
|
-
})),
|
|
2145
|
-
path: path$1.resolve(filePath),
|
|
2146
|
-
s: fileCoverage.s,
|
|
2147
|
-
statementMap: fileCoverage.statementMap
|
|
2148
|
-
};
|
|
2149
|
-
coverageMap.addFileCoverage(fileCoverageData);
|
|
2150
|
-
}
|
|
2151
|
-
return coverageMap;
|
|
2152
|
-
}
|
|
2153
|
-
function createGlobMatcher(patterns) {
|
|
2154
|
-
const withPath = patterns.filter((pattern) => pattern.includes("/"));
|
|
2155
|
-
const withoutPath = patterns.filter((pattern) => !pattern.includes("/"));
|
|
2156
|
-
const matchers = [];
|
|
2157
|
-
if (withPath.length > 0) matchers.push(picomatch(withPath));
|
|
2158
|
-
if (withoutPath.length > 0) matchers.push(picomatch(withoutPath, { matchBase: true }));
|
|
2159
|
-
return (filePath) => matchers.some((matcher) => matcher(filePath));
|
|
2160
|
-
}
|
|
2161
|
-
function filterMappedFiles(mapped, collectCoverageFrom) {
|
|
2162
|
-
if (collectCoverageFrom === void 0 || collectCoverageFrom.length === 0) return mapped;
|
|
2163
|
-
const includePatterns = collectCoverageFrom.filter((pattern) => !pattern.startsWith("!"));
|
|
2164
|
-
const excludePatterns = collectCoverageFrom.filter((pattern) => pattern.startsWith("!")).map((pattern) => pattern.slice(1));
|
|
2165
|
-
const isIncluded = includePatterns.length > 0 ? createGlobMatcher(includePatterns) : () => true;
|
|
2166
|
-
const isExcluded = excludePatterns.length > 0 ? createGlobMatcher(excludePatterns) : () => false;
|
|
2167
|
-
const cwd = process.cwd();
|
|
2168
|
-
return { files: Object.fromEntries(Object.entries(mapped.files).filter(([filePath]) => {
|
|
2169
|
-
const relativePath = path$1.isAbsolute(filePath) ? path$1.relative(cwd, filePath).replaceAll("\\", "/") : filePath;
|
|
2170
|
-
return isIncluded(relativePath) && !isExcluded(relativePath);
|
|
2171
|
-
})) };
|
|
2172
|
-
}
|
|
2173
|
-
function isValidReporter(name) {
|
|
2174
|
-
return VALID_REPORTERS.has(name);
|
|
2175
|
-
}
|
|
2176
|
-
//#endregion
|
|
2177
|
-
//#region src/utils/glob.ts
|
|
2178
|
-
function globSync(pattern, options = {}) {
|
|
2179
|
-
const cwd = options.cwd ?? process.cwd();
|
|
2180
|
-
return walkDirectory(cwd, cwd).filter((file) => matchesGlobPattern(file, pattern));
|
|
2181
|
-
}
|
|
2182
|
-
function matchesGlobPattern(filePath, pattern) {
|
|
2183
|
-
const regexPattern = pattern.replace(/\./g, "\\.").replace(/\*\*\//g, "{{DOUBLESTAR_SLASH}}").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\{\{DOUBLESTAR_SLASH\}\}/g, "(.+/)?");
|
|
2184
|
-
return new RegExp(`^${regexPattern}$`).test(filePath);
|
|
2185
|
-
}
|
|
2186
|
-
function walkDirectory(directoryPath, baseDirectory) {
|
|
2187
|
-
const results = [];
|
|
2188
|
-
try {
|
|
2189
|
-
const entries = fs$1.readdirSync(directoryPath, { withFileTypes: true });
|
|
2190
|
-
for (const entry of entries) {
|
|
2191
|
-
const fullPath = path$1.join(directoryPath, entry.name);
|
|
2192
|
-
const relativePath = path$1.relative(baseDirectory, fullPath).replace(/\\/g, "/");
|
|
2193
|
-
if (entry.isDirectory()) {
|
|
2194
|
-
if (!entry.name.startsWith(".") && entry.name !== "node_modules") results.push(...walkDirectory(fullPath, baseDirectory));
|
|
2195
|
-
} else results.push(relativePath);
|
|
2196
|
-
}
|
|
2197
|
-
} catch {}
|
|
2198
|
-
return results;
|
|
2199
|
-
}
|
|
2200
28
|
//#endregion
|
|
2201
29
|
//#region src/cli.ts
|
|
2202
30
|
const VERSION = version;
|
|
2203
|
-
const DEFAULT_ROJO_PROJECT = "default.project.json";
|
|
2204
|
-
const TYPE_TEST_PATTERN = /\.(test-d|spec-d)\.ts$/;
|
|
2205
31
|
const HELP_TEXT = `
|
|
2206
32
|
Usage: jest-roblox [options] [files...]
|
|
2207
33
|
|
|
@@ -2225,8 +51,12 @@ Options:
|
|
|
2225
51
|
--coverageDirectory <path> Directory for coverage output (default: coverage)
|
|
2226
52
|
--coverageReporters <r...> Coverage reporters (default: text, lcov)
|
|
2227
53
|
--formatters <name...> Output formatters (default, agent, json, github-actions)
|
|
2228
|
-
--
|
|
2229
|
-
--
|
|
54
|
+
--workspace Run tests across all workspace packages
|
|
55
|
+
--packages <names> Comma-separated package names (workspace mode)
|
|
56
|
+
--workspace-root <path> Directory to load the workspace config from
|
|
57
|
+
(use when running outside any package)
|
|
58
|
+
--affected-since <ref> Run only packages affected since git ref via turbo/nx
|
|
59
|
+
--no-coverage-cache Force a clean coverage re-instrumentation (skip incremental cache)
|
|
2230
60
|
--parallel [n] Open-Cloud-only: number of concurrent sessions
|
|
2231
61
|
(or "auto" = min(jobs, 3); default: 1 session)
|
|
2232
62
|
--project <name...> Filter which named projects to run
|
|
@@ -2236,13 +66,22 @@ Options:
|
|
|
2236
66
|
--typecheck Enable type testing (*.test-d.ts, *.spec-d.ts)
|
|
2237
67
|
--typecheckOnly Run only type tests, skip runtime tests
|
|
2238
68
|
--typecheckTsconfig <path> tsconfig for type testing
|
|
69
|
+
--apiKey <key> Roblox Open Cloud API key
|
|
70
|
+
--universeId <id> Target universe ID
|
|
71
|
+
--placeId <id> Target place ID
|
|
2239
72
|
--help Show this help message
|
|
2240
73
|
--version Show version number
|
|
2241
74
|
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
75
|
+
Open Cloud credentials (open-cloud backend only):
|
|
76
|
+
Sources, in precedence order:
|
|
77
|
+
1. CLI flags (--apiKey, --universeId, --placeId)
|
|
78
|
+
2. JEST_ROBLOX_* env vars (JEST_ROBLOX_OPEN_CLOUD_API_KEY,
|
|
79
|
+
JEST_ROBLOX_UNIVERSE_ID, JEST_ROBLOX_PLACE_ID)
|
|
80
|
+
3. ROBLOX_* env vars (ROBLOX_OPEN_CLOUD_API_KEY, ROBLOX_UNIVERSE_ID,
|
|
81
|
+
ROBLOX_PLACE_ID)
|
|
82
|
+
4. jest.config.ts (universeId, placeId — apiKey is CLI/env only)
|
|
83
|
+
|
|
84
|
+
--apiKey is visible in process listings; prefer env vars in CI.
|
|
2246
85
|
|
|
2247
86
|
Examples:
|
|
2248
87
|
jest-roblox Run all tests (open-cloud)
|
|
@@ -2257,8 +96,9 @@ function parseArgs(args) {
|
|
|
2257
96
|
allowPositionals: true,
|
|
2258
97
|
args: normalizeParallelFlag(args),
|
|
2259
98
|
options: {
|
|
99
|
+
"affected-since": { type: "string" },
|
|
100
|
+
"apiKey": { type: "string" },
|
|
2260
101
|
"backend": { type: "string" },
|
|
2261
|
-
"cache": { type: "boolean" },
|
|
2262
102
|
"collectCoverageFrom": {
|
|
2263
103
|
multiple: true,
|
|
2264
104
|
type: "string"
|
|
@@ -2266,6 +106,7 @@ function parseArgs(args) {
|
|
|
2266
106
|
"color": { type: "boolean" },
|
|
2267
107
|
"config": { type: "string" },
|
|
2268
108
|
"coverage": { type: "boolean" },
|
|
109
|
+
"coverage-cache": { type: "boolean" },
|
|
2269
110
|
"coverageDirectory": { type: "string" },
|
|
2270
111
|
"coverageReporters": {
|
|
2271
112
|
multiple: true,
|
|
@@ -2280,13 +121,14 @@ function parseArgs(args) {
|
|
|
2280
121
|
default: false,
|
|
2281
122
|
type: "boolean"
|
|
2282
123
|
},
|
|
2283
|
-
"no-cache": { type: "boolean" },
|
|
2284
124
|
"no-color": { type: "boolean" },
|
|
125
|
+
"no-coverage-cache": { type: "boolean" },
|
|
2285
126
|
"no-show-luau": { type: "boolean" },
|
|
2286
127
|
"outputFile": { type: "string" },
|
|
128
|
+
"packages": { type: "string" },
|
|
2287
129
|
"parallel": { type: "string" },
|
|
2288
130
|
"passWithNoTests": { type: "boolean" },
|
|
2289
|
-
"
|
|
131
|
+
"placeId": { type: "string" },
|
|
2290
132
|
"port": { type: "string" },
|
|
2291
133
|
"project": {
|
|
2292
134
|
multiple: true,
|
|
@@ -2313,6 +155,7 @@ function parseArgs(args) {
|
|
|
2313
155
|
"typecheck": { type: "boolean" },
|
|
2314
156
|
"typecheckOnly": { type: "boolean" },
|
|
2315
157
|
"typecheckTsconfig": { type: "string" },
|
|
158
|
+
"universeId": { type: "string" },
|
|
2316
159
|
"updateSnapshot": {
|
|
2317
160
|
short: "u",
|
|
2318
161
|
type: "boolean"
|
|
@@ -2321,20 +164,23 @@ function parseArgs(args) {
|
|
|
2321
164
|
"version": {
|
|
2322
165
|
default: false,
|
|
2323
166
|
type: "boolean"
|
|
2324
|
-
}
|
|
167
|
+
},
|
|
168
|
+
"workspace": { type: "boolean" },
|
|
169
|
+
"workspace-root": { type: "string" }
|
|
2325
170
|
},
|
|
2326
171
|
strict: true
|
|
2327
172
|
});
|
|
2328
|
-
const pollInterval = values.pollInterval !== void 0 ? Number.parseInt(values.pollInterval, 10) : void 0;
|
|
2329
173
|
const port = values.port !== void 0 ? Number.parseInt(values.port, 10) : void 0;
|
|
2330
174
|
const timeout = values.timeout !== void 0 ? Number.parseInt(values.timeout, 10) : void 0;
|
|
2331
175
|
return {
|
|
176
|
+
affectedSince: values["affected-since"],
|
|
177
|
+
apiKey: values.apiKey,
|
|
2332
178
|
backend: validateBackend(values.backend),
|
|
2333
|
-
cache: values["no-cache"] === true ? false : values.cache,
|
|
2334
179
|
collectCoverage: values.coverage,
|
|
2335
180
|
collectCoverageFrom: values.collectCoverageFrom,
|
|
2336
181
|
color: values["no-color"] === true ? false : values.color,
|
|
2337
182
|
config: values.config,
|
|
183
|
+
coverageCache: values["no-coverage-cache"] === true ? false : values["coverage-cache"],
|
|
2338
184
|
coverageDirectory: values.coverageDirectory,
|
|
2339
185
|
coverageReporters: values.coverageReporters,
|
|
2340
186
|
files: positionals.length > 0 ? positionals : void 0,
|
|
@@ -2342,9 +188,10 @@ function parseArgs(args) {
|
|
|
2342
188
|
gameOutput: values.gameOutput,
|
|
2343
189
|
help: values.help,
|
|
2344
190
|
outputFile: values.outputFile,
|
|
191
|
+
packages: values.packages,
|
|
2345
192
|
parallel: parseParallelValue(values.parallel),
|
|
2346
193
|
passWithNoTests: values.passWithNoTests,
|
|
2347
|
-
|
|
194
|
+
placeId: values.placeId,
|
|
2348
195
|
port,
|
|
2349
196
|
project: values.project,
|
|
2350
197
|
rojoProject: values.rojoProject,
|
|
@@ -2359,77 +206,12 @@ function parseArgs(args) {
|
|
|
2359
206
|
typecheck: values.typecheckOnly === true ? true : values.typecheck,
|
|
2360
207
|
typecheckOnly: values.typecheckOnly,
|
|
2361
208
|
typecheckTsconfig: values.typecheckTsconfig,
|
|
209
|
+
universeId: values.universeId,
|
|
2362
210
|
updateSnapshot: values.updateSnapshot,
|
|
2363
211
|
verbose: values.verbose,
|
|
2364
|
-
version: values.version
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
function filterByName(projects, names) {
|
|
2368
|
-
const available = new Set(projects.map((project) => project.displayName));
|
|
2369
|
-
const unknown = names.filter((name) => !available.has(name));
|
|
2370
|
-
if (unknown.length > 0) throw new Error(`Unknown project name(s): ${unknown.join(", ")}. Available: ${[...available].join(", ")}`);
|
|
2371
|
-
const nameSet = new Set(names);
|
|
2372
|
-
return projects.filter((project) => nameSet.has(project.displayName));
|
|
2373
|
-
}
|
|
2374
|
-
function mergeProjectResults(results) {
|
|
2375
|
-
assert(results.length > 0, "mergeProjectResults requires at least one result");
|
|
2376
|
-
if (results.length === 1) {
|
|
2377
|
-
const [first] = results;
|
|
2378
|
-
return first;
|
|
2379
|
-
}
|
|
2380
|
-
let numberFailedTests = 0;
|
|
2381
|
-
let numberPassedTests = 0;
|
|
2382
|
-
let numberPendingTests = 0;
|
|
2383
|
-
let numberTodoTests = 0;
|
|
2384
|
-
let numberTotalTests = 0;
|
|
2385
|
-
let startTime = Number.POSITIVE_INFINITY;
|
|
2386
|
-
let success = true;
|
|
2387
|
-
const testResults = [];
|
|
2388
|
-
let testsMs = 0;
|
|
2389
|
-
let setupMs = 0;
|
|
2390
|
-
let mergedCoverage;
|
|
2391
|
-
for (const result of results) {
|
|
2392
|
-
numberFailedTests += result.result.numFailedTests;
|
|
2393
|
-
numberPassedTests += result.result.numPassedTests;
|
|
2394
|
-
numberPendingTests += result.result.numPendingTests;
|
|
2395
|
-
numberTodoTests += result.result.numTodoTests ?? 0;
|
|
2396
|
-
numberTotalTests += result.result.numTotalTests;
|
|
2397
|
-
startTime = Math.min(startTime, result.result.startTime);
|
|
2398
|
-
success &&= result.result.success;
|
|
2399
|
-
testResults.push(...result.result.testResults);
|
|
2400
|
-
testsMs += result.timing.testsMs;
|
|
2401
|
-
setupMs += result.timing.setupMs ?? 0;
|
|
2402
|
-
if (result.coverageData !== void 0) mergedCoverage = mergeRawCoverage(mergedCoverage, result.coverageData);
|
|
2403
|
-
}
|
|
2404
|
-
const [sharedTiming] = results;
|
|
2405
|
-
const mergedStartTime = Math.min(...results.map((result) => result.timing.startTime));
|
|
2406
|
-
const totalMs = Math.max(...results.map((result) => result.timing.totalMs));
|
|
2407
|
-
const mergedSourceMapper = combineSourceMappers(results.flatMap((result) => result.sourceMapper !== void 0 ? [result.sourceMapper] : []));
|
|
2408
|
-
return {
|
|
2409
|
-
coverageData: mergedCoverage,
|
|
2410
|
-
exitCode: success ? 0 : 1,
|
|
2411
|
-
output: "",
|
|
2412
|
-
result: {
|
|
2413
|
-
numFailedTests: numberFailedTests,
|
|
2414
|
-
numPassedTests: numberPassedTests,
|
|
2415
|
-
numPendingTests: numberPendingTests,
|
|
2416
|
-
numTodoTests: numberTodoTests,
|
|
2417
|
-
numTotalTests: numberTotalTests,
|
|
2418
|
-
startTime,
|
|
2419
|
-
success,
|
|
2420
|
-
testResults
|
|
2421
|
-
},
|
|
2422
|
-
sourceMapper: mergedSourceMapper,
|
|
2423
|
-
timing: {
|
|
2424
|
-
coverageMs: sharedTiming.timing.coverageMs,
|
|
2425
|
-
executionMs: sharedTiming.timing.executionMs,
|
|
2426
|
-
setupMs: setupMs > 0 ? setupMs : void 0,
|
|
2427
|
-
startTime: mergedStartTime,
|
|
2428
|
-
testsMs,
|
|
2429
|
-
totalMs,
|
|
2430
|
-
uploadCached: sharedTiming.timing.uploadCached,
|
|
2431
|
-
uploadMs: sharedTiming.timing.uploadMs
|
|
2432
|
-
}
|
|
212
|
+
version: values.version,
|
|
213
|
+
workspace: values.workspace,
|
|
214
|
+
workspaceRoot: values["workspace-root"]
|
|
2433
215
|
};
|
|
2434
216
|
}
|
|
2435
217
|
async function run(args) {
|
|
@@ -2443,12 +225,6 @@ async function run(args) {
|
|
|
2443
225
|
async function main() {
|
|
2444
226
|
process.exitCode = await run(process.argv.slice(2));
|
|
2445
227
|
}
|
|
2446
|
-
/**
|
|
2447
|
-
* `--parallel` with no value means `"auto"`. Node's `parseArgs` can't express
|
|
2448
|
-
* optional values, so rewrite bare `--parallel` (at the end of argv, or
|
|
2449
|
-
* followed by another `--flag`, or followed by a non-numeric, non-"auto" token)
|
|
2450
|
-
* into `--parallel auto` before handing it off.
|
|
2451
|
-
*/
|
|
2452
228
|
const PARALLEL_FLAG = "--parallel";
|
|
2453
229
|
function normalizeParallelFlag(args) {
|
|
2454
230
|
const out = [];
|
|
@@ -2479,6 +255,46 @@ function formatGameOutputLines(raw) {
|
|
|
2479
255
|
if (entries.length === 0) return;
|
|
2480
256
|
return entries.map((entry) => entry.message.replace(/^/gm, " ")).join("\n");
|
|
2481
257
|
}
|
|
258
|
+
const EXIT_CODE_MESSAGE = /^Exited with code: \d+$/;
|
|
259
|
+
function formatLuauErrorBanner(err) {
|
|
260
|
+
const bannerLines = formatGameOutputLines(err.bannerOutput);
|
|
261
|
+
if (EXIT_CODE_MESSAGE.test(err.message) && bannerLines !== void 0) return formatBanner({
|
|
262
|
+
body: [bannerLines, `\n ${color.dim(err.message)}`],
|
|
263
|
+
level: "error",
|
|
264
|
+
title: "Test Run Failed"
|
|
265
|
+
});
|
|
266
|
+
const body = [color.red(err.message)];
|
|
267
|
+
const hint = getLuauErrorHint(err.message);
|
|
268
|
+
if (hint !== void 0) body.push(`\n ${color.dim("Hint:")} ${hint}`);
|
|
269
|
+
if (bannerLines !== void 0) body.push(`\n ${color.dim("Game output:")}\n${bannerLines}`);
|
|
270
|
+
return formatBanner({
|
|
271
|
+
body,
|
|
272
|
+
level: "error",
|
|
273
|
+
title: "Luau Error"
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
function formatChainExtras(entry) {
|
|
277
|
+
const pieces = [];
|
|
278
|
+
if (entry.code !== void 0) pieces.push(`code=${entry.code}`);
|
|
279
|
+
if (entry.errno !== void 0) pieces.push(`errno=${entry.errno}`);
|
|
280
|
+
if (entry.syscall !== void 0) pieces.push(`syscall=${entry.syscall}`);
|
|
281
|
+
return pieces.length > 0 ? color.dim(` (${pieces.join(" ")})`) : "";
|
|
282
|
+
}
|
|
283
|
+
function formatBackendErrorBanner(err) {
|
|
284
|
+
const body = [color.red(err.message)];
|
|
285
|
+
const chain = walkErrorChain(err.cause);
|
|
286
|
+
body.push(`\n ${color.dim("Caused by:")}`);
|
|
287
|
+
for (const [index, entry] of chain.entries()) {
|
|
288
|
+
const extras = formatChainExtras(entry);
|
|
289
|
+
const label = color.dim(`[${index.toString()}]`);
|
|
290
|
+
body.push(` ${label} ${entry.name}: ${entry.message}${extras}`);
|
|
291
|
+
}
|
|
292
|
+
return formatBanner({
|
|
293
|
+
body,
|
|
294
|
+
level: "error",
|
|
295
|
+
title: "Backend Error"
|
|
296
|
+
});
|
|
297
|
+
}
|
|
2482
298
|
function printError(err) {
|
|
2483
299
|
if (err instanceof ConfigError) {
|
|
2484
300
|
const body = [color.red(err.message)];
|
|
@@ -2488,452 +304,22 @@ function printError(err) {
|
|
|
2488
304
|
level: "error",
|
|
2489
305
|
title: "Config Error"
|
|
2490
306
|
}));
|
|
2491
|
-
} else if (err instanceof LuauScriptError)
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
if (hint !== void 0) body.push(`\n ${color.dim("Hint:")} ${hint}`);
|
|
2495
|
-
const gameLines = formatGameOutputLines(err.gameOutput);
|
|
2496
|
-
if (gameLines !== void 0) body.push(`\n ${color.dim("Game output:")}\n${gameLines}`);
|
|
2497
|
-
process.stderr.write(formatBanner({
|
|
2498
|
-
body,
|
|
2499
|
-
level: "error",
|
|
2500
|
-
title: "Luau Error"
|
|
2501
|
-
}));
|
|
2502
|
-
} else if (err instanceof Error) console.error(`Error: ${err.message}`);
|
|
307
|
+
} else if (err instanceof LuauScriptError) process.stderr.write(formatLuauErrorBanner(err));
|
|
308
|
+
else if (err instanceof Error && err.cause instanceof OpenCloudError) process.stderr.write(formatBackendErrorBanner(err));
|
|
309
|
+
else if (err instanceof Error) console.error(`Error: ${err.message}`);
|
|
2503
310
|
else console.error("An unknown error occurred");
|
|
2504
311
|
}
|
|
2505
|
-
function
|
|
2506
|
-
if (
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
if (!config.silent && options.hintsShown !== true) {
|
|
2510
|
-
const notice = formatGameOutputNotice(config.gameOutput, entries.length);
|
|
2511
|
-
if (notice) console.error(notice);
|
|
2512
|
-
}
|
|
2513
|
-
}
|
|
2514
|
-
/**
|
|
2515
|
-
* Multi-project variant: `--gameOutput` used to silently drop when a config
|
|
2516
|
-
* declared `projects`, because the output path here never called
|
|
2517
|
-
* `writeGameOutputIfConfigured`. Aggregate every project's parsed entries into
|
|
2518
|
-
* one file so the contract matches the single-project path.
|
|
2519
|
-
*/
|
|
2520
|
-
function writeAggregatedGameOutput(config, projectResults, options) {
|
|
2521
|
-
if (config.gameOutput === void 0) return;
|
|
2522
|
-
const entries = projectResults.flatMap((project) => parseGameOutput(project.result.gameOutput));
|
|
2523
|
-
writeGameOutput(config.gameOutput, entries);
|
|
2524
|
-
if (!config.silent && options.hintsShown !== true) {
|
|
2525
|
-
const notice = formatGameOutputNotice(config.gameOutput, entries.length);
|
|
2526
|
-
if (notice) console.error(notice);
|
|
2527
|
-
}
|
|
2528
|
-
}
|
|
2529
|
-
function printFinalStatus(passed) {
|
|
2530
|
-
const badge = passed ? color.bgGreen(color.black(color.bold(" PASS "))) : color.bgRed(color.white(color.bold(" FAIL ")));
|
|
2531
|
-
process.stdout.write(`${badge}\n`);
|
|
2532
|
-
}
|
|
2533
|
-
function hasFormatter(config, name) {
|
|
2534
|
-
return config.formatters?.some((entry) => Array.isArray(entry) ? entry[0] === name : entry === name) === true;
|
|
2535
|
-
}
|
|
2536
|
-
function usesAgentFormatter(config) {
|
|
2537
|
-
return hasFormatter(config, "agent") && !config.verbose;
|
|
2538
|
-
}
|
|
2539
|
-
function processCoverage(config, coverageData) {
|
|
2540
|
-
if (!config.collectCoverage) return true;
|
|
2541
|
-
if (coverageData === void 0) {
|
|
2542
|
-
if (!config.silent) process.stderr.write("Warning: coverage data was empty — the Rojo project may point at uninstrumented source\n");
|
|
2543
|
-
return true;
|
|
2544
|
-
}
|
|
2545
|
-
const manifest = loadCoverageManifest(config.rootDir);
|
|
2546
|
-
if (manifest === void 0) {
|
|
2547
|
-
if (!config.silent) process.stderr.write("Warning: Coverage manifest not found, skipping TS mapping\n");
|
|
2548
|
-
return true;
|
|
2549
|
-
}
|
|
2550
|
-
const mapped = mapCoverageToTypeScript(coverageData, manifest);
|
|
2551
|
-
const coverageDirectory = path$1.resolve(config.rootDir, config.coverageDirectory);
|
|
2552
|
-
if (!config.silent) printCoverageHeader();
|
|
2553
|
-
generateReports({
|
|
2554
|
-
agentMode: usesAgentFormatter(config),
|
|
2555
|
-
collectCoverageFrom: config.collectCoverageFrom,
|
|
2556
|
-
coverageDirectory,
|
|
2557
|
-
mapped,
|
|
2558
|
-
reporters: config.coverageReporters
|
|
2559
|
-
});
|
|
2560
|
-
if (config.coverageThreshold !== void 0) {
|
|
2561
|
-
const result = checkThresholds(mapped, config.coverageThreshold, config.collectCoverageFrom);
|
|
2562
|
-
if (!result.passed) {
|
|
2563
|
-
for (const failure of result.failures) process.stderr.write(`Coverage threshold not met for ${failure.metric}: ${String(failure.actual.toFixed(2))}% < ${String(failure.threshold)}%\n`);
|
|
2564
|
-
return false;
|
|
2565
|
-
}
|
|
2566
|
-
}
|
|
2567
|
-
return true;
|
|
2568
|
-
}
|
|
2569
|
-
function runGitHubActionsFormatter(config, result, sourceMapper) {
|
|
2570
|
-
assert(config.formatters !== void 0, "formatters is set by resolveFormatters");
|
|
2571
|
-
const userOptions = findFormatterOptions(config.formatters, "github-actions");
|
|
2572
|
-
if (userOptions === void 0) return;
|
|
2573
|
-
const typedOptions = userOptions;
|
|
2574
|
-
const options = resolveGitHubActionsOptions(typedOptions, sourceMapper);
|
|
2575
|
-
if (typedOptions.displayAnnotations !== false) {
|
|
2576
|
-
const annotations = formatAnnotations(result, options);
|
|
2577
|
-
if (annotations !== "") process.stderr.write(`${annotations}\n`);
|
|
2578
|
-
}
|
|
2579
|
-
const { jobSummary } = typedOptions;
|
|
2580
|
-
if (jobSummary?.enabled !== false) {
|
|
2581
|
-
const outputPath = jobSummary?.outputPath ?? process.env["GITHUB_STEP_SUMMARY"];
|
|
2582
|
-
if (outputPath !== void 0) {
|
|
2583
|
-
const summary = formatJobSummary(result, options);
|
|
2584
|
-
fs$1.appendFileSync(outputPath, summary);
|
|
2585
|
-
}
|
|
2586
|
-
}
|
|
2587
|
-
}
|
|
2588
|
-
function getAgentMaxFailures(config) {
|
|
2589
|
-
assert(config.formatters !== void 0, "formatters is set by resolveFormatters");
|
|
2590
|
-
const options = findFormatterOptions(config.formatters, "agent");
|
|
2591
|
-
if (options !== void 0 && typeof options["maxFailures"] === "number") return options["maxFailures"];
|
|
2592
|
-
return 10;
|
|
2593
|
-
}
|
|
2594
|
-
function usesDefaultFormatter(config) {
|
|
2595
|
-
return !hasFormatter(config, "json") && !usesAgentFormatter(config);
|
|
2596
|
-
}
|
|
2597
|
-
function printOutput(output) {
|
|
2598
|
-
if (output !== "") console.log(output);
|
|
2599
|
-
}
|
|
2600
|
-
function formatRuntimeOutput(config, runtimeResult, timing) {
|
|
2601
|
-
return formatExecuteOutput({
|
|
2602
|
-
config,
|
|
2603
|
-
result: runtimeResult.result,
|
|
2604
|
-
sourceMapper: runtimeResult.sourceMapper,
|
|
2605
|
-
timing,
|
|
2606
|
-
version: VERSION
|
|
2607
|
-
});
|
|
2608
|
-
}
|
|
2609
|
-
function printFormattedOutput(options) {
|
|
2610
|
-
const { config, mergedResult, runtimeResult, timing, typecheckResult } = options;
|
|
2611
|
-
if (typecheckResult !== void 0 && runtimeResult !== void 0 && timing !== void 0) {
|
|
2612
|
-
if (usesDefaultFormatter(config)) printOutput(formatResult(mergedResult, timing, {
|
|
2613
|
-
collectCoverage: config.collectCoverage,
|
|
2614
|
-
color: config.color,
|
|
2615
|
-
rootDir: config.rootDir,
|
|
2616
|
-
showLuau: config.showLuau,
|
|
2617
|
-
sourceMapper: runtimeResult.sourceMapper,
|
|
2618
|
-
typeErrors: typecheckResult.numFailedTests,
|
|
2619
|
-
verbose: config.verbose,
|
|
2620
|
-
version: VERSION
|
|
2621
|
-
}));
|
|
2622
|
-
else {
|
|
2623
|
-
printOutput(formatRuntimeOutput(config, runtimeResult, timing));
|
|
2624
|
-
process.stderr.write(formatTypecheckSummary(typecheckResult));
|
|
2625
|
-
}
|
|
2626
|
-
return;
|
|
2627
|
-
}
|
|
2628
|
-
if (typecheckResult !== void 0) {
|
|
2629
|
-
process.stdout.write(formatTypecheckSummary(typecheckResult));
|
|
2630
|
-
return;
|
|
2631
|
-
}
|
|
2632
|
-
assert(runtimeResult !== void 0 && timing !== void 0, "runtime result required");
|
|
2633
|
-
printOutput(formatRuntimeOutput(config, runtimeResult, timing));
|
|
2634
|
-
}
|
|
2635
|
-
function addCoverageTiming(timing, coverageMs) {
|
|
2636
|
-
return {
|
|
2637
|
-
...timing,
|
|
2638
|
-
coverageMs,
|
|
2639
|
-
totalMs: timing.totalMs + coverageMs
|
|
2640
|
-
};
|
|
2641
|
-
}
|
|
2642
|
-
async function outputResults(config, typecheckResult, runtimeResult, preCoverageMs) {
|
|
2643
|
-
const mergedResult = mergeResults(typecheckResult, runtimeResult?.result);
|
|
2644
|
-
if (!config.silent) printFormattedOutput({
|
|
2645
|
-
config,
|
|
2646
|
-
mergedResult,
|
|
2647
|
-
runtimeResult,
|
|
2648
|
-
timing: runtimeResult !== void 0 ? addCoverageTiming(runtimeResult.timing, preCoverageMs) : void 0,
|
|
2649
|
-
typecheckResult
|
|
2650
|
-
});
|
|
2651
|
-
const coveragePassed = processCoverage(config, runtimeResult?.coverageData);
|
|
2652
|
-
if (config.outputFile !== void 0) await writeJsonFile(mergedResult, config.outputFile);
|
|
2653
|
-
if (runtimeResult !== void 0) writeGameOutputIfConfigured(config, runtimeResult.gameOutput, { hintsShown: !mergedResult.success });
|
|
2654
|
-
runGitHubActionsFormatter(config, mergedResult, runtimeResult?.sourceMapper);
|
|
2655
|
-
const passed = mergedResult.success && coveragePassed;
|
|
2656
|
-
if (!config.silent && config.collectCoverage) printFinalStatus(passed);
|
|
2657
|
-
return passed ? 0 : 1;
|
|
2658
|
-
}
|
|
2659
|
-
function toProjectEntries(projectResults) {
|
|
2660
|
-
return projectResults.map((pr) => {
|
|
2661
|
-
return {
|
|
2662
|
-
displayColor: pr.displayColor,
|
|
2663
|
-
displayName: pr.displayName,
|
|
2664
|
-
result: pr.result.result
|
|
2665
|
-
};
|
|
2666
|
-
});
|
|
2667
|
-
}
|
|
2668
|
-
function printMultiProjectOutput(options) {
|
|
2669
|
-
const { config, merged, preCoverageMs, projectResults, typecheckResult } = options;
|
|
2670
|
-
const timing = addCoverageTiming(merged.timing, preCoverageMs);
|
|
2671
|
-
if (usesAgentFormatter(config)) {
|
|
2672
|
-
printOutput(formatAgentMultiProject(toProjectEntries(projectResults), {
|
|
2673
|
-
gameOutput: config.gameOutput,
|
|
2674
|
-
maxFailures: getAgentMaxFailures(config),
|
|
2675
|
-
outputFile: config.outputFile,
|
|
2676
|
-
rootDir: config.rootDir,
|
|
2677
|
-
sourceMapper: merged.sourceMapper,
|
|
2678
|
-
typeErrorCount: typecheckResult?.numFailedTests
|
|
2679
|
-
}));
|
|
2680
|
-
return;
|
|
2681
|
-
}
|
|
2682
|
-
if (hasFormatter(config, "json")) {
|
|
2683
|
-
printOutput(formatRuntimeOutput(config, merged, timing));
|
|
2684
|
-
return;
|
|
2685
|
-
}
|
|
2686
|
-
printOutput(formatMultiProjectResult(toProjectEntries(projectResults), timing, {
|
|
2687
|
-
collectCoverage: config.collectCoverage,
|
|
2688
|
-
color: config.color,
|
|
2689
|
-
rootDir: config.rootDir,
|
|
2690
|
-
showLuau: config.showLuau,
|
|
2691
|
-
sourceMapper: merged.sourceMapper,
|
|
2692
|
-
typeErrors: typecheckResult?.numFailedTests,
|
|
2693
|
-
verbose: config.verbose,
|
|
2694
|
-
version: VERSION
|
|
2695
|
-
}));
|
|
2696
|
-
}
|
|
2697
|
-
async function outputMultiProjectResults(config, projectResults, typecheckResult, preCoverageMs) {
|
|
2698
|
-
const merged = mergeProjectResults(projectResults.map((pr) => pr.result));
|
|
2699
|
-
const mergedResult = mergeResults(typecheckResult, merged.result);
|
|
2700
|
-
if (!config.silent) {
|
|
2701
|
-
printMultiProjectOutput({
|
|
2702
|
-
config,
|
|
2703
|
-
merged,
|
|
2704
|
-
preCoverageMs,
|
|
2705
|
-
projectResults,
|
|
2706
|
-
typecheckResult
|
|
2707
|
-
});
|
|
2708
|
-
if (typecheckResult !== void 0 && !usesDefaultFormatter(config)) process.stderr.write(formatTypecheckSummary(typecheckResult));
|
|
2709
|
-
}
|
|
2710
|
-
const coveragePassed = processCoverage(config, merged.coverageData);
|
|
2711
|
-
if (config.outputFile !== void 0) await writeJsonFile(mergedResult, config.outputFile);
|
|
2712
|
-
writeAggregatedGameOutput(config, projectResults, { hintsShown: !mergedResult.success });
|
|
2713
|
-
runGitHubActionsFormatter(config, mergedResult, merged.sourceMapper);
|
|
2714
|
-
const passed = mergedResult.success && coveragePassed;
|
|
2715
|
-
if (!config.silent && config.collectCoverage) printFinalStatus(passed);
|
|
2716
|
-
return passed ? 0 : 1;
|
|
2717
|
-
}
|
|
2718
|
-
function loadRojoTree(config) {
|
|
2719
|
-
const rojoPath = path$1.resolve(config.rootDir, config.rojoProject ?? DEFAULT_ROJO_PROJECT);
|
|
2720
|
-
const content = fs$1.readFileSync(rojoPath, "utf8");
|
|
2721
|
-
const validated = rojoProjectSchema(JSON.parse(content));
|
|
2722
|
-
if (validated instanceof type.errors) throw new Error(`Invalid Rojo project: ${validated.summary}`);
|
|
2723
|
-
return resolveNestedProjects(validated.tree, path$1.dirname(rojoPath));
|
|
2724
|
-
}
|
|
2725
|
-
const STUB_SKIP_KEYS = new Set([
|
|
2726
|
-
"outDir",
|
|
2727
|
-
"projects",
|
|
2728
|
-
"root"
|
|
2729
|
-
]);
|
|
2730
|
-
function buildStubConfig(config) {
|
|
2731
|
-
const result = {};
|
|
2732
|
-
for (const [key, value] of Object.entries(config)) if (!ROOT_ONLY_KEYS.has(key) && !STUB_SKIP_KEYS.has(key) && value !== void 0) result[key] = value;
|
|
2733
|
-
return result;
|
|
2734
|
-
}
|
|
2735
|
-
function generateProjectStubs(projects, rootDirectory) {
|
|
2736
|
-
const entries = [];
|
|
2737
|
-
for (const project of projects) {
|
|
2738
|
-
assertStubCollisionRule(project, rootDirectory);
|
|
2739
|
-
const stubConfig = {
|
|
2740
|
-
...buildStubConfig(project.config),
|
|
2741
|
-
displayName: project.displayName,
|
|
2742
|
-
include: [],
|
|
2743
|
-
testMatch: project.testMatch
|
|
2744
|
-
};
|
|
2745
|
-
for (const mount of project.rojoMounts) {
|
|
2746
|
-
const outputPath = path$1.resolve(rootDirectory, mount.fsPath, "jest.config.lua");
|
|
2747
|
-
entries.push({
|
|
2748
|
-
config: stubConfig,
|
|
2749
|
-
outputPath
|
|
2750
|
-
});
|
|
2751
|
-
}
|
|
2752
|
-
}
|
|
2753
|
-
generateProjectConfigs(entries);
|
|
2754
|
-
}
|
|
2755
|
-
function prepareMultiProjectCoverage(rootConfig, projects) {
|
|
2756
|
-
if (!rootConfig.collectCoverage) return {
|
|
2757
|
-
effectiveConfig: rootConfig,
|
|
2758
|
-
preCoverageMs: 0
|
|
2759
|
-
};
|
|
2760
|
-
const start = Date.now();
|
|
2761
|
-
const { placeFile } = prepareCoverage(rootConfig, (shadowDirectory) => {
|
|
2762
|
-
return syncStubsToShadowDirectory(projects, rootConfig.rootDir, shadowDirectory);
|
|
2763
|
-
});
|
|
2764
|
-
return {
|
|
2765
|
-
effectiveConfig: {
|
|
2766
|
-
...rootConfig,
|
|
2767
|
-
placeFile
|
|
2768
|
-
},
|
|
2769
|
-
preCoverageMs: Date.now() - start
|
|
2770
|
-
};
|
|
2771
|
-
}
|
|
2772
|
-
function classifyTestFiles(files, config) {
|
|
2773
|
-
const typeTestFiles = config.typecheck ? files.filter((file) => TYPE_TEST_PATTERN.test(file)) : [];
|
|
2774
|
-
return {
|
|
2775
|
-
runtimeFiles: config.typecheckOnly ? [] : files.filter((file) => !TYPE_TEST_PATTERN.test(file)),
|
|
2776
|
-
typeTestFiles
|
|
2777
|
-
};
|
|
2778
|
-
}
|
|
2779
|
-
function applySetupResolver(config, resolve) {
|
|
2780
|
-
if (config.setupFiles !== void 0) config.setupFiles = config.setupFiles.map(resolve);
|
|
2781
|
-
if (config.setupFilesAfterEnv !== void 0) config.setupFilesAfterEnv = config.setupFilesAfterEnv.map(resolve);
|
|
2782
|
-
}
|
|
2783
|
-
/**
|
|
2784
|
-
* Drop `parallel` on any non-open-cloud backend. Studio has no concept of
|
|
2785
|
-
* multi-session, so passing `parallel` there is a silent noop — this lets
|
|
2786
|
-
* users keep `parallel: 3` in `jest.config.ts` and still drop to
|
|
2787
|
-
* `--backend studio` for debugging without editing config.
|
|
2788
|
-
*/
|
|
2789
|
-
function effectiveParallelForBackend(parallel, backend) {
|
|
2790
|
-
return backend.kind === "open-cloud" ? parallel : void 0;
|
|
2791
|
-
}
|
|
2792
|
-
async function runMultiProject(cli, rootConfig, projectEntries) {
|
|
2793
|
-
const allProjects = await resolveAllProjects(projectEntries, rootConfig, loadRojoTree(rootConfig), rootConfig.rootDir);
|
|
2794
|
-
const rojoConfigPath = path$1.resolve(rootConfig.rootDir, rootConfig.rojoProject ?? DEFAULT_ROJO_PROJECT);
|
|
2795
|
-
const resolveSetup = createSetupResolver({
|
|
2796
|
-
configDirectory: rootConfig.rootDir,
|
|
2797
|
-
rojoConfigPath
|
|
2798
|
-
});
|
|
2799
|
-
for (const project of allProjects) applySetupResolver(project.config, resolveSetup);
|
|
2800
|
-
const projects = cli.project !== void 0 ? filterByName(allProjects, cli.project) : allProjects;
|
|
2801
|
-
generateProjectStubs(projects, rootConfig.rootDir);
|
|
2802
|
-
if (!rootConfig.collectCoverage) buildWithRojo(path$1.resolve(rootConfig.rootDir, rootConfig.rojoProject ?? DEFAULT_ROJO_PROJECT), path$1.resolve(rootConfig.rootDir, rootConfig.placeFile));
|
|
2803
|
-
const { effectiveConfig, preCoverageMs } = prepareMultiProjectCoverage(rootConfig, projects);
|
|
2804
|
-
const backend = await resolveBackend(effectiveConfig);
|
|
2805
|
-
const parallel = effectiveParallelForBackend(effectiveConfig.parallel, backend);
|
|
2806
|
-
const pendingJobs = [];
|
|
2807
|
-
const allTypeTestFiles = [];
|
|
2808
|
-
for (const project of projects) {
|
|
2809
|
-
const discoveryConfig = {
|
|
2810
|
-
...project.config,
|
|
2811
|
-
placeFile: effectiveConfig.placeFile,
|
|
2812
|
-
projects: project.projects,
|
|
2813
|
-
testMatch: project.include
|
|
2814
|
-
};
|
|
2815
|
-
const { runtimeFiles, typeTestFiles } = classifyTestFiles(discoverTestFiles(discoveryConfig, cli.files).files, rootConfig);
|
|
2816
|
-
const projConfig = {
|
|
2817
|
-
...discoveryConfig,
|
|
2818
|
-
testMatch: project.testMatch
|
|
2819
|
-
};
|
|
2820
|
-
allTypeTestFiles.push(...typeTestFiles);
|
|
2821
|
-
if (runtimeFiles.length === 0) continue;
|
|
2822
|
-
pendingJobs.push({
|
|
2823
|
-
config: projConfig,
|
|
2824
|
-
displayColor: project.displayColor,
|
|
2825
|
-
displayName: project.displayName,
|
|
2826
|
-
runtimeFiles
|
|
2827
|
-
});
|
|
312
|
+
async function dispatchResult(config, result) {
|
|
313
|
+
if (result.validationExitCode !== void 0) {
|
|
314
|
+
if ("validationMessage" in result && result.validationMessage !== void 0) process.stderr.write(result.validationMessage);
|
|
315
|
+
return result.validationExitCode;
|
|
2828
316
|
}
|
|
2829
|
-
|
|
2830
|
-
return
|
|
2831
|
-
|
|
2832
|
-
displayColor: pending.displayColor,
|
|
2833
|
-
displayName: pending.displayName,
|
|
2834
|
-
testFiles: pending.runtimeFiles
|
|
2835
|
-
});
|
|
2836
|
-
});
|
|
2837
|
-
const projectResults = [];
|
|
2838
|
-
if (jobs.length > 0) {
|
|
2839
|
-
const startTime = Date.now();
|
|
2840
|
-
let backendResult;
|
|
2841
|
-
try {
|
|
2842
|
-
backendResult = await executeBackend(backend, jobs, parallel);
|
|
2843
|
-
} finally {
|
|
2844
|
-
await backend.close?.();
|
|
2845
|
-
}
|
|
2846
|
-
const sharedTiming = backendResult.timing;
|
|
2847
|
-
for (const [index, entry] of backendResult.results.entries()) {
|
|
2848
|
-
const pending = pendingJobs[index];
|
|
2849
|
-
const jobConfig = jobs[index].config;
|
|
2850
|
-
const executeResult = processProjectResult(entry, {
|
|
2851
|
-
backendTiming: sharedTiming,
|
|
2852
|
-
config: jobConfig,
|
|
2853
|
-
deferFormatting: true,
|
|
2854
|
-
startTime,
|
|
2855
|
-
version: VERSION
|
|
2856
|
-
});
|
|
2857
|
-
projectResults.push({
|
|
2858
|
-
displayColor: pending.displayColor,
|
|
2859
|
-
displayName: pending.displayName,
|
|
2860
|
-
result: executeResult
|
|
2861
|
-
});
|
|
2862
|
-
}
|
|
2863
|
-
}
|
|
2864
|
-
const uniqueTypeTestFiles = [...new Set(allTypeTestFiles)];
|
|
2865
|
-
const typecheckResult = uniqueTypeTestFiles.length > 0 ? runTypecheck({
|
|
2866
|
-
files: uniqueTypeTestFiles,
|
|
2867
|
-
rootDir: rootConfig.rootDir,
|
|
2868
|
-
tsconfig: rootConfig.typecheckTsconfig
|
|
2869
|
-
}) : void 0;
|
|
2870
|
-
if (projectResults.length === 0 && typecheckResult === void 0) {
|
|
2871
|
-
if (rootConfig.passWithNoTests) return 0;
|
|
2872
|
-
console.error("No test files found in any project");
|
|
2873
|
-
return 2;
|
|
2874
|
-
}
|
|
2875
|
-
if (projectResults.length === 0) return outputResults(rootConfig, typecheckResult, void 0, preCoverageMs);
|
|
2876
|
-
return outputMultiProjectResults({
|
|
2877
|
-
...rootConfig,
|
|
2878
|
-
collectCoverageFrom: rootConfig.collectCoverageFrom ?? deriveCoverageFromIncludes(projects)
|
|
2879
|
-
}, projectResults, typecheckResult, preCoverageMs);
|
|
2880
|
-
}
|
|
2881
|
-
async function executeRuntimeTests(config, testFiles, totalFiles) {
|
|
2882
|
-
if (!config.silent && !usesAgentFormatter(config) && !hasFormatter(config, "json") && testFiles.length !== totalFiles) process.stderr.write(`Running ${String(testFiles.length)} of ${String(totalFiles)} test files\n`);
|
|
2883
|
-
const backend = await resolveBackend(config);
|
|
2884
|
-
try {
|
|
2885
|
-
return await execute({
|
|
2886
|
-
backend,
|
|
2887
|
-
config,
|
|
2888
|
-
deferFormatting: true,
|
|
2889
|
-
testFiles,
|
|
2890
|
-
version: VERSION
|
|
2891
|
-
});
|
|
2892
|
-
} finally {
|
|
2893
|
-
await backend.close?.();
|
|
2894
|
-
}
|
|
2895
|
-
}
|
|
2896
|
-
function resolveSetupFilePaths(config) {
|
|
2897
|
-
if (config.setupFiles === void 0 && config.setupFilesAfterEnv === void 0) return;
|
|
2898
|
-
const rojoConfigPath = path$1.resolve(config.rootDir, config.rojoProject ?? DEFAULT_ROJO_PROJECT);
|
|
2899
|
-
applySetupResolver(config, createSetupResolver({
|
|
2900
|
-
configDirectory: config.rootDir,
|
|
2901
|
-
rojoConfigPath
|
|
2902
|
-
}));
|
|
2903
|
-
}
|
|
2904
|
-
async function runSingleProject(cli, config) {
|
|
2905
|
-
resolveSetupFilePaths(config);
|
|
2906
|
-
const discovery = discoverTestFiles(config, cli.files);
|
|
2907
|
-
if (discovery.files.length === 0) {
|
|
2908
|
-
if (config.passWithNoTests) return 0;
|
|
2909
|
-
console.error("No test files found");
|
|
2910
|
-
return 2;
|
|
2911
|
-
}
|
|
2912
|
-
const typeTestFiles = config.typecheck ? discovery.files.filter((file) => TYPE_TEST_PATTERN.test(file)) : [];
|
|
2913
|
-
const runtimeTestFiles = config.typecheckOnly ? [] : discovery.files.filter((file) => !TYPE_TEST_PATTERN.test(file));
|
|
2914
|
-
if (typeTestFiles.length === 0 && runtimeTestFiles.length === 0) {
|
|
2915
|
-
if (config.passWithNoTests) return 0;
|
|
2916
|
-
console.error("No test files found for the selected mode");
|
|
2917
|
-
return 2;
|
|
2918
|
-
}
|
|
2919
|
-
let preCoverageMs = 0;
|
|
2920
|
-
let effectiveConfig = config;
|
|
2921
|
-
if (config.collectCoverage && !config.typecheckOnly && runtimeTestFiles.length > 0) {
|
|
2922
|
-
const preCoverageStart = Date.now();
|
|
2923
|
-
const { placeFile } = prepareCoverage(config);
|
|
2924
|
-
preCoverageMs = Date.now() - preCoverageStart;
|
|
2925
|
-
effectiveConfig = {
|
|
2926
|
-
...config,
|
|
2927
|
-
placeFile
|
|
2928
|
-
};
|
|
317
|
+
if (result.mode === "single") {
|
|
318
|
+
if (result.runtimeResult === void 0 && result.typecheckResult === void 0) return 0;
|
|
319
|
+
return outputSingleResult(config, result);
|
|
2929
320
|
}
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
rootDir: effectiveConfig.rootDir,
|
|
2933
|
-
tsconfig: effectiveConfig.typecheckTsconfig
|
|
2934
|
-
}) : void 0;
|
|
2935
|
-
const runtimeResult = runtimeTestFiles.length > 0 ? await executeRuntimeTests(effectiveConfig, runtimeTestFiles, discovery.totalFiles) : void 0;
|
|
2936
|
-
return outputResults(effectiveConfig, typecheckResult, runtimeResult, preCoverageMs);
|
|
321
|
+
if (result.projectResults.length === 0 && result.typecheckResult === void 0) return 0;
|
|
322
|
+
return outputMultiResult(config, result);
|
|
2937
323
|
}
|
|
2938
324
|
async function runInner(args) {
|
|
2939
325
|
const cli = parseArgs(args);
|
|
@@ -2946,47 +332,17 @@ async function runInner(args) {
|
|
|
2946
332
|
return 0;
|
|
2947
333
|
}
|
|
2948
334
|
if (process.env["JEST_ROBLOX_SEA"] === "true" && cli.typecheck === true) throw new ConfigError("--typecheck is not available in the standalone binary. Install via npm instead.");
|
|
2949
|
-
const config = mergeCliWithConfig(cli, await loadConfig
|
|
2950
|
-
|
|
2951
|
-
if (rawProjects !== void 0 && rawProjects.length > 0) return runMultiProject(cli, config, rawProjects);
|
|
2952
|
-
return runSingleProject(cli, config);
|
|
335
|
+
const config = mergeCliWithConfig(cli, await loadConfig(cli.config, cli.workspaceRoot));
|
|
336
|
+
return dispatchResult(config, await runJestRoblox(cli, config));
|
|
2953
337
|
}
|
|
2954
338
|
const LUAU_ERROR_HINTS = [
|
|
2955
339
|
[/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\""],
|
|
2956
340
|
[/Failed to find Jest instance at path/, "The configured jestPath does not resolve to a valid instance. Verify the path matches your Rojo project tree."],
|
|
2957
341
|
[/Failed to find service/, "The first segment of jestPath must be a valid Roblox service name (e.g. ReplicatedStorage, ServerScriptService)."],
|
|
2958
|
-
[/No projects configured/, "Set \"projects\" in jest.config.ts (e.g. [\"ReplicatedStorage/client\", \"ServerScriptService/server\"])
|
|
342
|
+
[/No projects configured/, "Set \"projects\" in jest.config.ts (e.g. [\"ReplicatedStorage/client\", \"ServerScriptService/server\"])."],
|
|
2959
343
|
[/Infinite yield detected/, "A :WaitForChild() call is waiting for an instance that doesn't exist. Check your DataModel paths and Rojo project configuration."],
|
|
2960
344
|
[/loadstring\(\) is not available/, "loadstring() must be enabled for Jest to run. Add \"LoadStringEnabled\": true to ServerScriptService.$properties in your project.json."]
|
|
2961
345
|
];
|
|
2962
|
-
function discoverTestFiles(config, cliFiles) {
|
|
2963
|
-
if (cliFiles && cliFiles.length > 0) {
|
|
2964
|
-
const files = cliFiles.map((file) => path$1.resolve(config.rootDir, file));
|
|
2965
|
-
return {
|
|
2966
|
-
files,
|
|
2967
|
-
totalFiles: files.length
|
|
2968
|
-
};
|
|
2969
|
-
}
|
|
2970
|
-
const allFiles = [];
|
|
2971
|
-
for (const pattern of config.testMatch) {
|
|
2972
|
-
const matches = globSync(pattern, { cwd: config.rootDir });
|
|
2973
|
-
allFiles.push(...matches);
|
|
2974
|
-
}
|
|
2975
|
-
const ignoredPatterns = config.testPathIgnorePatterns.map((pat) => new RegExp(pat));
|
|
2976
|
-
const baseFiles = allFiles.filter((file) => {
|
|
2977
|
-
return !ignoredPatterns.some((pattern) => pattern.test(file));
|
|
2978
|
-
});
|
|
2979
|
-
const totalFiles = new Set(baseFiles).size;
|
|
2980
|
-
let filtered = baseFiles;
|
|
2981
|
-
if (config.testPathPattern !== void 0) {
|
|
2982
|
-
const pathPattern = new RegExp(config.testPathPattern);
|
|
2983
|
-
filtered = filtered.filter((file) => pathPattern.test(file));
|
|
2984
|
-
}
|
|
2985
|
-
return {
|
|
2986
|
-
files: [...new Set(filtered)],
|
|
2987
|
-
totalFiles
|
|
2988
|
-
};
|
|
2989
|
-
}
|
|
2990
346
|
function validateBackend(value) {
|
|
2991
347
|
if (value === void 0) return;
|
|
2992
348
|
if (!isValidBackend(value)) {
|
|
@@ -2998,60 +354,5 @@ function validateBackend(value) {
|
|
|
2998
354
|
function getLuauErrorHint(message) {
|
|
2999
355
|
for (const [pattern, hint] of LUAU_ERROR_HINTS) if (pattern.test(message)) return hint;
|
|
3000
356
|
}
|
|
3001
|
-
function resolveFormatters(cli, config) {
|
|
3002
|
-
const explicit = cli.formatters ?? config.formatters;
|
|
3003
|
-
if (explicit !== void 0) return explicit;
|
|
3004
|
-
const defaults = isAgent ? ["agent"] : ["default"];
|
|
3005
|
-
if (process.env["GITHUB_ACTIONS"] === "true") defaults.push("github-actions");
|
|
3006
|
-
return defaults;
|
|
3007
|
-
}
|
|
3008
|
-
function mergeCliWithConfig(cli, config) {
|
|
3009
|
-
return {
|
|
3010
|
-
...config,
|
|
3011
|
-
backend: cli.backend ?? config.backend,
|
|
3012
|
-
cache: cli.cache ?? config.cache,
|
|
3013
|
-
collectCoverage: cli.collectCoverage ?? config.collectCoverage,
|
|
3014
|
-
collectCoverageFrom: cli.collectCoverageFrom ?? config.collectCoverageFrom,
|
|
3015
|
-
color: cli.color ?? config.color,
|
|
3016
|
-
coverageDirectory: cli.coverageDirectory ?? config.coverageDirectory,
|
|
3017
|
-
coverageReporters: cli.coverageReporters ?? config.coverageReporters,
|
|
3018
|
-
formatters: resolveFormatters(cli, config),
|
|
3019
|
-
gameOutput: cli.gameOutput ?? config.gameOutput,
|
|
3020
|
-
outputFile: cli.outputFile ?? config.outputFile,
|
|
3021
|
-
parallel: cli.parallel ?? config.parallel,
|
|
3022
|
-
passWithNoTests: cli.passWithNoTests ?? config.passWithNoTests,
|
|
3023
|
-
pollInterval: cli.pollInterval ?? config.pollInterval,
|
|
3024
|
-
port: cli.port ?? config.port,
|
|
3025
|
-
rojoProject: cli.rojoProject ?? config.rojoProject,
|
|
3026
|
-
setupFiles: cli.setupFiles ?? config.setupFiles,
|
|
3027
|
-
setupFilesAfterEnv: cli.setupFilesAfterEnv ?? config.setupFilesAfterEnv,
|
|
3028
|
-
showLuau: cli.showLuau ?? config.showLuau,
|
|
3029
|
-
silent: cli.silent ?? config.silent,
|
|
3030
|
-
sourceMap: cli.sourceMap ?? config.sourceMap,
|
|
3031
|
-
testNamePattern: cli.testNamePattern ?? config.testNamePattern,
|
|
3032
|
-
testPathPattern: cli.testPathPattern ?? config.testPathPattern,
|
|
3033
|
-
timeout: cli.timeout ?? config.timeout,
|
|
3034
|
-
typecheck: cli.typecheck ?? config.typecheck,
|
|
3035
|
-
typecheckOnly: cli.typecheckOnly ?? config.typecheckOnly,
|
|
3036
|
-
typecheckTsconfig: cli.typecheckTsconfig ?? config.typecheckTsconfig,
|
|
3037
|
-
updateSnapshot: cli.updateSnapshot ?? config.updateSnapshot,
|
|
3038
|
-
verbose: cli.verbose ?? config.verbose
|
|
3039
|
-
};
|
|
3040
|
-
}
|
|
3041
|
-
function mergeResults(typecheck, runtime) {
|
|
3042
|
-
if (typecheck !== void 0 && runtime !== void 0) return {
|
|
3043
|
-
numFailedTests: typecheck.numFailedTests + runtime.numFailedTests,
|
|
3044
|
-
numPassedTests: typecheck.numPassedTests + runtime.numPassedTests,
|
|
3045
|
-
numPendingTests: typecheck.numPendingTests + runtime.numPendingTests,
|
|
3046
|
-
numTodoTests: (typecheck.numTodoTests ?? 0) + (runtime.numTodoTests ?? 0),
|
|
3047
|
-
numTotalTests: typecheck.numTotalTests + runtime.numTotalTests,
|
|
3048
|
-
startTime: Math.min(typecheck.startTime, runtime.startTime),
|
|
3049
|
-
success: typecheck.success && runtime.success,
|
|
3050
|
-
testResults: [...typecheck.testResults, ...runtime.testResults]
|
|
3051
|
-
};
|
|
3052
|
-
const result = typecheck ?? runtime;
|
|
3053
|
-
assert(result !== void 0, "mergeResults requires at least one result");
|
|
3054
|
-
return result;
|
|
3055
|
-
}
|
|
3056
357
|
//#endregion
|
|
3057
|
-
export {
|
|
358
|
+
export { main, parseArgs, run };
|