@isentinel/jest-roblox 0.0.8 → 0.1.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/LICENSE.md +21 -0
- package/README.md +131 -130
- package/bin/jest-roblox.js +14 -2
- package/dist/cli.d.mts +4 -2
- package/dist/cli.mjs +920 -183
- package/dist/{schema-DcDQmTyn.d.mts → executor-DqZE3wME.d.mts} +236 -31
- package/dist/{game-output-M8du29nj.mjs → game-output-C0_-YIAY.mjs} +1095 -468
- package/dist/index.d.mts +6 -145
- package/dist/index.mjs +2 -3
- package/dist/sea/jest-roblox +0 -0
- package/dist/sea-entry.cjs +61580 -0
- package/package.json +17 -42
- package/plugin/JestRobloxRunner.rbxm +0 -0
- package/plugin/out/shared/instance-resolver.luau +3 -8
- package/plugin/out/shared/runner.luau +62 -13
- package/plugin/out/shared/snapshot-patch.luau +2 -7
package/dist/cli.mjs
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { D as createOpenCloudBackend, I as isValidBackend, L as LuauScriptError, M as ROOT_ONLY_KEYS, N as VALID_BACKENDS, O as hashBuffer, S as loadConfig$1, T as createStudioBackend, _ as formatResult, a as formatAnnotations, b as formatBanner, c as execute, d as findFormatterOptions, g as formatMultiProjectResult, i as runTypecheck, l as formatExecuteOutput, m as formatCompactMultiProject, n as parseGameOutput, o as formatJobSummary, p as writeJsonFile, r as writeGameOutput, s as resolveGitHubActionsOptions, t as formatGameOutputNotice, u as loadCoverageManifest, x as rojoProjectSchema, y as formatTypecheckSummary } from "./game-output-C0_-YIAY.mjs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { type } from "arktype";
|
|
2
4
|
import assert from "node:assert";
|
|
3
|
-
import * as fs from "node:fs";
|
|
5
|
+
import * as fs$1 from "node:fs";
|
|
6
|
+
import fs from "node:fs";
|
|
4
7
|
import * as path$1 from "node:path";
|
|
8
|
+
import path from "node:path";
|
|
5
9
|
import process from "node:process";
|
|
6
10
|
import { parseArgs as parseArgs$1 } from "node:util";
|
|
11
|
+
import { isAgent } from "std-env";
|
|
7
12
|
import color from "tinyrainbow";
|
|
8
13
|
import { WebSocketServer } from "ws";
|
|
9
|
-
import {
|
|
14
|
+
import { loadConfig } from "c12";
|
|
10
15
|
import * as os from "node:os";
|
|
11
16
|
import { Buffer } from "node:buffer";
|
|
17
|
+
import { RojoResolver } from "@roblox-ts/rojo-resolver";
|
|
12
18
|
import { TraceMap, originalPositionFor } from "@jridgewell/trace-mapping";
|
|
13
19
|
import { getTsconfig } from "get-tsconfig";
|
|
14
20
|
import picomatch from "picomatch";
|
|
@@ -16,10 +22,8 @@ import * as cp from "node:child_process";
|
|
|
16
22
|
import istanbulCoverage from "istanbul-lib-coverage";
|
|
17
23
|
import istanbulReport from "istanbul-lib-report";
|
|
18
24
|
import istanbulReports from "istanbul-reports";
|
|
19
|
-
|
|
20
25
|
//#region package.json
|
|
21
|
-
var version = "0.
|
|
22
|
-
|
|
26
|
+
var version = "0.1.1";
|
|
23
27
|
//#endregion
|
|
24
28
|
//#region src/backends/auto.ts
|
|
25
29
|
var StudioWithFallback = class {
|
|
@@ -96,7 +100,311 @@ async function resolveBackend(config, probe = probeStudioPlugin) {
|
|
|
96
100
|
function hasOpenCloudCredentials() {
|
|
97
101
|
return process.env["ROBLOX_OPEN_CLOUD_API_KEY"] !== void 0 && process.env["ROBLOX_UNIVERSE_ID"] !== void 0 && process.env["ROBLOX_PLACE_ID"] !== void 0;
|
|
98
102
|
}
|
|
99
|
-
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/config/errors.ts
|
|
105
|
+
var ConfigError = class extends Error {
|
|
106
|
+
hint;
|
|
107
|
+
constructor(message, hint) {
|
|
108
|
+
super(message);
|
|
109
|
+
this.hint = hint;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
//#endregion
|
|
113
|
+
//#region src/utils/extensions.ts
|
|
114
|
+
function stripTsExtension(pattern) {
|
|
115
|
+
return pattern.replace(/\.(tsx?|luau?)$/, "");
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/utils/rojo-tree.ts
|
|
119
|
+
function collectPaths(node, result) {
|
|
120
|
+
for (const [key, value] of Object.entries(node)) if (key === "$path" && typeof value === "string") result.push(value.replaceAll("\\", "/"));
|
|
121
|
+
else if (typeof value === "object" && !Array.isArray(value) && !key.startsWith("$")) collectPaths(value, result);
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
//#region src/config/projects.ts
|
|
125
|
+
function extractStaticRoot(pattern) {
|
|
126
|
+
const globChars = new Set([
|
|
127
|
+
"*",
|
|
128
|
+
"?",
|
|
129
|
+
"[",
|
|
130
|
+
"{"
|
|
131
|
+
]);
|
|
132
|
+
let firstGlobIndex = -1;
|
|
133
|
+
for (const [index, char] of [...pattern].entries()) if (globChars.has(char)) {
|
|
134
|
+
firstGlobIndex = index;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
if (firstGlobIndex === -1) {
|
|
138
|
+
const directory = path$1.posix.dirname(pattern);
|
|
139
|
+
return {
|
|
140
|
+
glob: path$1.posix.basename(pattern),
|
|
141
|
+
root: directory
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const lastSlash = pattern.slice(0, firstGlobIndex).lastIndexOf("/");
|
|
145
|
+
if (lastSlash === -1) throw new Error("Include pattern must have a static directory prefix");
|
|
146
|
+
return {
|
|
147
|
+
glob: pattern.slice(lastSlash + 1),
|
|
148
|
+
root: pattern.slice(0, lastSlash)
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function extractProjectRoots(include) {
|
|
152
|
+
const rootMap = /* @__PURE__ */ new Map();
|
|
153
|
+
for (const pattern of include) {
|
|
154
|
+
const { glob, root } = extractStaticRoot(pattern);
|
|
155
|
+
const stripped = stripTsExtension(glob);
|
|
156
|
+
const qualified = stripped.includes("/") ? stripped : `**/${stripped}`;
|
|
157
|
+
let patterns = rootMap.get(root);
|
|
158
|
+
if (patterns === void 0) {
|
|
159
|
+
patterns = [];
|
|
160
|
+
rootMap.set(root, patterns);
|
|
161
|
+
}
|
|
162
|
+
patterns.push(qualified);
|
|
163
|
+
}
|
|
164
|
+
return [...rootMap.entries()].map(([root, testMatch]) => ({
|
|
165
|
+
root,
|
|
166
|
+
testMatch
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
function mapFsRootToDataModel(outDirectory, rojoTree) {
|
|
170
|
+
const normalized = outDirectory.replace(/\/$/, "");
|
|
171
|
+
const result = findInTree(rojoTree, normalized, "");
|
|
172
|
+
if (result === void 0) {
|
|
173
|
+
const available = [];
|
|
174
|
+
collectPaths(rojoTree, available);
|
|
175
|
+
let message = `No Rojo tree mapping found for path: ${normalized}`;
|
|
176
|
+
if (available.length > 0) message += `\n\nAvailable $path entries: ${available.join(", ")}`;
|
|
177
|
+
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;
|
|
178
|
+
throw new ConfigError(message, hint);
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
function validateProjects(projects) {
|
|
183
|
+
const names = /* @__PURE__ */ new Set();
|
|
184
|
+
for (const project of projects) {
|
|
185
|
+
const name = typeof project.displayName === "string" ? project.displayName : project.displayName.name;
|
|
186
|
+
if (name === "") throw new Error("Project must have a non-empty displayName");
|
|
187
|
+
if (names.has(name)) throw new Error(`Duplicate project displayName: ${name}`);
|
|
188
|
+
names.add(name);
|
|
189
|
+
if (project.include.length === 0) throw new Error(`Project "${name}" must have at least one include pattern`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const PROJECT_ONLY_KEYS = new Set([
|
|
193
|
+
"displayName",
|
|
194
|
+
"exclude",
|
|
195
|
+
"include",
|
|
196
|
+
"outDir",
|
|
197
|
+
"root"
|
|
198
|
+
]);
|
|
199
|
+
function resolveProjectConfig(project, rootConfig, rojoTree) {
|
|
200
|
+
const roots = extractProjectRoots(project.include);
|
|
201
|
+
const testMatch = roots.flatMap((entry) => entry.testMatch);
|
|
202
|
+
const projectRoot = project.root;
|
|
203
|
+
if (roots.length > 1 && project.outDir === void 0) {
|
|
204
|
+
const name = typeof project.displayName === "string" ? project.displayName : project.displayName.name;
|
|
205
|
+
throw new Error(`Project "${name}" has multiple include roots but no outDir. Set outDir or split into separate projects.`);
|
|
206
|
+
}
|
|
207
|
+
const resolvedOutDirectory = resolveOutDirectory(project.outDir, projectRoot, roots[0]?.root);
|
|
208
|
+
const dataModelPath = resolvedOutDirectory !== void 0 ? mapFsRootToDataModel(resolvedOutDirectory, rojoTree) : void 0;
|
|
209
|
+
const resolvedInclude = projectRoot === void 0 ? project.include : project.include.map((pattern) => path$1.posix.join(projectRoot, pattern));
|
|
210
|
+
const config = mergeProjectConfig(rootConfig, project);
|
|
211
|
+
const displayName = typeof project.displayName === "string" ? project.displayName : project.displayName.name;
|
|
212
|
+
return {
|
|
213
|
+
config,
|
|
214
|
+
displayColor: typeof project.displayName === "string" ? void 0 : project.displayName.color,
|
|
215
|
+
displayName,
|
|
216
|
+
include: resolvedInclude,
|
|
217
|
+
outDir: resolvedOutDirectory,
|
|
218
|
+
projects: dataModelPath !== void 0 ? [dataModelPath] : [],
|
|
219
|
+
testMatch
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
async function loadProjectConfigFile(filePath, cwd) {
|
|
223
|
+
let result;
|
|
224
|
+
try {
|
|
225
|
+
result = await loadConfig({
|
|
226
|
+
name: "jest-project",
|
|
227
|
+
configFile: filePath,
|
|
228
|
+
configFileRequired: true,
|
|
229
|
+
cwd,
|
|
230
|
+
dotenv: false,
|
|
231
|
+
globalRc: false,
|
|
232
|
+
omit$Keys: true,
|
|
233
|
+
packageJson: false,
|
|
234
|
+
rcFile: false
|
|
235
|
+
});
|
|
236
|
+
} catch (err) {
|
|
237
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
238
|
+
throw new Error(`Failed to load project config file ${filePath}: ${message}`, { cause: err });
|
|
239
|
+
}
|
|
240
|
+
const { config } = result;
|
|
241
|
+
if ((typeof config.displayName === "string" ? config.displayName : config.displayName.name) === "") throw new Error(`Project config file "${filePath}" must have a displayName`);
|
|
242
|
+
return config;
|
|
243
|
+
}
|
|
244
|
+
async function resolveAllProjects(entries, rootConfig, rojoTree, cwd) {
|
|
245
|
+
const projects = [];
|
|
246
|
+
for (const entry of entries) if (typeof entry === "string") {
|
|
247
|
+
const loaded = await loadProjectConfigFile(entry, cwd);
|
|
248
|
+
projects.push(loaded);
|
|
249
|
+
} else projects.push(entry.test);
|
|
250
|
+
validateProjects(projects);
|
|
251
|
+
return projects.map((project) => resolveProjectConfig(project, rootConfig, rojoTree));
|
|
252
|
+
}
|
|
253
|
+
/** When outDir is omitted (pure Luau), falls back to include pattern's static root. */
|
|
254
|
+
function resolveOutDirectory(projectOutDirectory, projectRoot, fallbackRoot) {
|
|
255
|
+
const base = projectOutDirectory ?? fallbackRoot;
|
|
256
|
+
if (base === void 0) return;
|
|
257
|
+
return projectRoot !== void 0 ? path$1.posix.join(projectRoot, base) : base;
|
|
258
|
+
}
|
|
259
|
+
function mergeProjectConfig(rootConfig, project) {
|
|
260
|
+
const merged = { ...rootConfig };
|
|
261
|
+
for (const [key, value] of Object.entries(project)) if (!PROJECT_ONLY_KEYS.has(key) && value !== void 0) merged[key] = value;
|
|
262
|
+
return merged;
|
|
263
|
+
}
|
|
264
|
+
function matchNodePath(childNode, targetPath, childDataModelPath) {
|
|
265
|
+
const nodePath = childNode.$path;
|
|
266
|
+
if (typeof nodePath !== "string") return;
|
|
267
|
+
const normalizedNodePath = nodePath.replace(/\/$/, "");
|
|
268
|
+
if (normalizedNodePath === targetPath) return childDataModelPath;
|
|
269
|
+
if (targetPath.startsWith(`${normalizedNodePath}/`)) return `${childDataModelPath}/${targetPath.slice(normalizedNodePath.length + 1)}`;
|
|
270
|
+
}
|
|
271
|
+
function findInTree(node, targetPath, currentDataModelPath) {
|
|
272
|
+
for (const [key, value] of Object.entries(node)) {
|
|
273
|
+
if (key.startsWith("$") || typeof value !== "object") continue;
|
|
274
|
+
const childNode = value;
|
|
275
|
+
const childDataModelPath = currentDataModelPath === "" ? key : `${currentDataModelPath}/${key}`;
|
|
276
|
+
const pathMatch = matchNodePath(childNode, targetPath, childDataModelPath);
|
|
277
|
+
if (pathMatch !== void 0) return pathMatch;
|
|
278
|
+
const found = findInTree(childNode, targetPath, childDataModelPath);
|
|
279
|
+
if (found !== void 0) return found;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
//#endregion
|
|
283
|
+
//#region src/config/setup-resolver.ts
|
|
284
|
+
const PROBE_EXTENSIONS = [
|
|
285
|
+
".ts",
|
|
286
|
+
".tsx",
|
|
287
|
+
".lua",
|
|
288
|
+
".luau"
|
|
289
|
+
];
|
|
290
|
+
function createSetupResolver(options) {
|
|
291
|
+
const { configDirectory, resolveModule, rojoConfigPath } = options;
|
|
292
|
+
const resolve = resolveModule ?? createRequire(path$1.join(configDirectory, "noop.js")).resolve;
|
|
293
|
+
const rojoResolver = RojoResolver.fromPath(rojoConfigPath);
|
|
294
|
+
return (input) => {
|
|
295
|
+
let absolutePath;
|
|
296
|
+
if (isRelativePath(input)) absolutePath = path$1.resolve(configDirectory, input);
|
|
297
|
+
else {
|
|
298
|
+
resolvePackageSpecifier(resolve, input);
|
|
299
|
+
absolutePath = path$1.resolve(configDirectory, "node_modules", input);
|
|
300
|
+
}
|
|
301
|
+
const rbxPath = rojoResolver.getRbxPathFromFilePath(absolutePath);
|
|
302
|
+
if (rbxPath === void 0) throw new Error(`No matching path found in rojo project tree for "${input}" (resolved to: ${absolutePath})`);
|
|
303
|
+
return rbxPath.join("/");
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
function isRelativePath(input) {
|
|
307
|
+
return input.startsWith("./") || input.startsWith("../");
|
|
308
|
+
}
|
|
309
|
+
function resolvePackageSpecifier(resolve, input) {
|
|
310
|
+
try {
|
|
311
|
+
resolve(input);
|
|
312
|
+
return;
|
|
313
|
+
} catch {}
|
|
314
|
+
for (const extension of PROBE_EXTENSIONS) try {
|
|
315
|
+
resolve(`${input}${extension}`);
|
|
316
|
+
return;
|
|
317
|
+
} catch {}
|
|
318
|
+
throw new Error(`Could not resolve module "${input}". Ensure the package is installed.`);
|
|
319
|
+
}
|
|
320
|
+
//#endregion
|
|
321
|
+
//#region src/config/stubs.ts
|
|
322
|
+
const HEADER = "-- Auto-generated by jest-roblox (do not edit)\n";
|
|
323
|
+
const SKIP_FIELDS = new Set(["exclude", "include"]);
|
|
324
|
+
function serializeToLuau(config) {
|
|
325
|
+
let output = `${HEADER}return {\n`;
|
|
326
|
+
for (const [key, value] of Object.entries(config)) {
|
|
327
|
+
if (SKIP_FIELDS.has(key)) continue;
|
|
328
|
+
if (value === void 0) continue;
|
|
329
|
+
let serialized;
|
|
330
|
+
if (key === "testMatch" && Array.isArray(value)) serialized = serializeLuauValue(value.map((pattern) => stripTsExtension(pattern)), " ");
|
|
331
|
+
else serialized = serializeLuauValue(value, " ");
|
|
332
|
+
output += `\t${key} = ${serialized},\n`;
|
|
333
|
+
}
|
|
334
|
+
output += "}\n";
|
|
335
|
+
return output;
|
|
336
|
+
}
|
|
337
|
+
function generateProjectConfigs(projects) {
|
|
338
|
+
for (const project of projects) {
|
|
339
|
+
const content = serializeToLuau(project.config);
|
|
340
|
+
const directory = path.dirname(project.outputPath);
|
|
341
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
342
|
+
fs.writeFileSync(project.outputPath, content, "utf8");
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function syncStubsToShadowDirectory(projects, rootDirectory, shadowDirectory) {
|
|
346
|
+
let changed = false;
|
|
347
|
+
const expectedPaths = /* @__PURE__ */ new Set();
|
|
348
|
+
for (const project of projects) {
|
|
349
|
+
if (project.outDir === void 0) continue;
|
|
350
|
+
if (path.isAbsolute(project.outDir)) throw new Error(`Project "${project.displayName}" outDir must be a relative path, got: ${project.outDir}`);
|
|
351
|
+
const stubName = "jest.config.lua";
|
|
352
|
+
const sourcePath = path.resolve(rootDirectory, project.outDir, stubName);
|
|
353
|
+
if (!fs.existsSync(sourcePath)) continue;
|
|
354
|
+
const targetPath = path.resolve(shadowDirectory, project.outDir, stubName);
|
|
355
|
+
expectedPaths.add(targetPath);
|
|
356
|
+
const sourceContent = fs.readFileSync(sourcePath);
|
|
357
|
+
if (fs.existsSync(targetPath)) {
|
|
358
|
+
const targetContent = fs.readFileSync(targetPath);
|
|
359
|
+
if (Buffer.compare(sourceContent, targetContent) === 0) continue;
|
|
360
|
+
}
|
|
361
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
362
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
363
|
+
changed = true;
|
|
364
|
+
}
|
|
365
|
+
for (const existing of findShadowStubs(shadowDirectory)) if (!expectedPaths.has(existing)) {
|
|
366
|
+
fs.unlinkSync(existing);
|
|
367
|
+
removeEmptyParents(path.dirname(existing), shadowDirectory);
|
|
368
|
+
changed = true;
|
|
369
|
+
}
|
|
370
|
+
return changed;
|
|
371
|
+
}
|
|
372
|
+
function escapeString(value) {
|
|
373
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
374
|
+
}
|
|
375
|
+
function serializeLuauValue(value, indent) {
|
|
376
|
+
if (typeof value === "string") return `"${escapeString(value)}"`;
|
|
377
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
378
|
+
if (typeof value === "number") return String(value);
|
|
379
|
+
if (Array.isArray(value)) return `{ ${value.map((item) => serializeLuauValue(item, indent)).join(", ")} }`;
|
|
380
|
+
if (typeof value === "object" && value !== null) {
|
|
381
|
+
const nextIndent = `${indent}\t`;
|
|
382
|
+
return `{ ${Object.entries(value).filter(([, value_]) => value_ !== void 0).map(([key, value_]) => `${key} = ${serializeLuauValue(value_, nextIndent)}`).join(", ")} }`;
|
|
383
|
+
}
|
|
384
|
+
return String(value);
|
|
385
|
+
}
|
|
386
|
+
function removeEmptyParents(directory, stopAt) {
|
|
387
|
+
const resolved = path.resolve(directory);
|
|
388
|
+
const resolvedStop = path.resolve(stopAt);
|
|
389
|
+
if (resolved === resolvedStop || !resolved.startsWith(resolvedStop)) return;
|
|
390
|
+
try {
|
|
391
|
+
if (fs.readdirSync(resolved).length === 0) {
|
|
392
|
+
fs.rmdirSync(resolved);
|
|
393
|
+
removeEmptyParents(path.dirname(resolved), stopAt);
|
|
394
|
+
}
|
|
395
|
+
} catch {}
|
|
396
|
+
}
|
|
397
|
+
function findShadowStubs(directory) {
|
|
398
|
+
const results = [];
|
|
399
|
+
if (!fs.existsSync(directory)) return results;
|
|
400
|
+
const entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
401
|
+
for (const entry of entries) {
|
|
402
|
+
const fullPath = path.resolve(directory, entry.name);
|
|
403
|
+
if (entry.isDirectory()) results.push(...findShadowStubs(fullPath));
|
|
404
|
+
else if (entry.name === "jest.config.lua") results.push(fullPath);
|
|
405
|
+
}
|
|
406
|
+
return results;
|
|
407
|
+
}
|
|
100
408
|
//#endregion
|
|
101
409
|
//#region src/coverage/mapper.ts
|
|
102
410
|
const positionSchema = type({
|
|
@@ -142,8 +450,8 @@ function loadFileResources(record) {
|
|
|
142
450
|
let coverageMapRaw;
|
|
143
451
|
let sourceMapRaw;
|
|
144
452
|
try {
|
|
145
|
-
coverageMapRaw = fs.readFileSync(record.coverageMapPath, "utf-8");
|
|
146
|
-
sourceMapRaw = fs.readFileSync(record.sourceMapPath, "utf-8");
|
|
453
|
+
coverageMapRaw = fs$1.readFileSync(record.coverageMapPath, "utf-8");
|
|
454
|
+
sourceMapRaw = fs$1.readFileSync(record.sourceMapPath, "utf-8");
|
|
147
455
|
} catch {
|
|
148
456
|
return;
|
|
149
457
|
}
|
|
@@ -393,7 +701,6 @@ function buildResult(pending, pendingFunctions, pendingBranches) {
|
|
|
393
701
|
}
|
|
394
702
|
return { files };
|
|
395
703
|
}
|
|
396
|
-
|
|
397
704
|
//#endregion
|
|
398
705
|
//#region src/types/luau-ast.ts
|
|
399
706
|
const INSTRUMENTABLE_STATEMENT_TAGS = new Set([
|
|
@@ -413,7 +720,6 @@ const INSTRUMENTABLE_STATEMENT_TAGS = new Set([
|
|
|
413
720
|
"return",
|
|
414
721
|
"while"
|
|
415
722
|
]);
|
|
416
|
-
|
|
417
723
|
//#endregion
|
|
418
724
|
//#region src/coverage/luau-visitor.ts
|
|
419
725
|
function visitExpression(expression, visitor) {
|
|
@@ -684,7 +990,6 @@ function visitExprInstantiate(node, visitor) {
|
|
|
684
990
|
if (visitor.visitExprInstantiate?.(node) === false) return;
|
|
685
991
|
visitExpression(node.expr, visitor);
|
|
686
992
|
}
|
|
687
|
-
|
|
688
993
|
//#endregion
|
|
689
994
|
//#region src/coverage/coverage-collector.ts
|
|
690
995
|
const END_KEYWORD_LENGTH = 3;
|
|
@@ -875,7 +1180,6 @@ function extractExprName(expr) {
|
|
|
875
1180
|
function extractFunctionName(node) {
|
|
876
1181
|
return extractExprName(node.name);
|
|
877
1182
|
}
|
|
878
|
-
|
|
879
1183
|
//#endregion
|
|
880
1184
|
//#region src/coverage/coverage-map-builder.ts
|
|
881
1185
|
function buildCoverageMap$1(result) {
|
|
@@ -926,11 +1230,9 @@ function buildCoverageMap$1(result) {
|
|
|
926
1230
|
statementMap
|
|
927
1231
|
};
|
|
928
1232
|
}
|
|
929
|
-
|
|
930
1233
|
//#endregion
|
|
931
1234
|
//#region src/coverage/parse-ast.luau
|
|
932
|
-
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]\nlocal outputDir = userArgs[2]\nif not luauRoot or not outputDir then\n error(\"Usage: lute run parse-ast.luau -- <luau-root> <output-dir>\")\nend\n\nluauRoot = string.gsub(luauRoot, \"\\\\\", \"/\")\noutputDir = string.gsub(outputDir, \"\\\\\", \"/\")\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 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 [\"repeat\"] = { \"body\", \"condition\" },\n [\"return\"] = { \"expressions\" },\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-- 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 then\n continue\n end\n\n -- Compute relative path\n local relative = string.sub(fullPath, #relativeTo + 2)\n table.insert(results, relative)\n end\n end\nend\n\nlocal function dirname(filepath: string): string\n local pos = string.find(filepath, \"/[^/]*$\")\n if pos then\n return string.sub(filepath, 1, pos - 1)\n end\n\n return \"\"\nend\n\nlocal files: { string } = {}\ndiscoverFiles(luauRoot, luauRoot, files)\n\n-- Parse, strip, and write per-file AST JSON\nfor _, relativePath in files do\n local fullPath = luauRoot .. \"/\" .. relativePath\n local source = fs.readfiletostring(fullPath)\n local parseResult = syntax.parse(source)\n local stripped = strip(parseResult.root)\n\n local outPath = outputDir .. \"/\" .. relativePath .. \".json\"\n local dir = dirname(outPath)\n if dir ~= \"\" then\n fs.createdirectory(dir, { makeparents = true })\n end\n\n fs.writestringtofile(outPath, json.serialize(stripped))\nend\n\n-- Print file list to stdout (tiny — just paths)\nprint(json.serialize(files :: json.array))\n";
|
|
933
|
-
|
|
1235
|
+
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]\nlocal outputDir = userArgs[2]\nif not luauRoot or not outputDir then\n error(\"Usage: lute run parse-ast.luau -- <luau-root> <output-dir>\")\nend\n\nluauRoot = string.gsub(luauRoot, \"\\\\\", \"/\")\noutputDir = string.gsub(outputDir, \"\\\\\", \"/\")\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 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 [\"repeat\"] = { \"body\", \"condition\" },\n [\"return\"] = { \"expressions\" },\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-- Discover .luau files recursively, skipping node_modules, dot dirs, spec/test files\nlocal function discoverFiles(directory: string, relativeTo: string, results: { string })\n local entries = fs.listdirectory(directory)\n for _, entry in entries do\n local fullPath = directory .. \"/\" .. entry.name\n if entry.type == \"dir\" then\n if entry.name == \"node_modules\" or entry.name == \".jest-roblox-coverage\" then\n continue\n end\n\n if string.sub(entry.name, 1, 1) == \".\" then\n continue\n end\n\n discoverFiles(fullPath, relativeTo, results)\n elseif\n entry.type == \"file\"\n and (string.sub(entry.name, -5) == \".luau\" or string.sub(entry.name, -4) == \".lua\")\n then\n if\n string.sub(entry.name, -10) == \".spec.luau\"\n or string.sub(entry.name, -10) == \".test.luau\"\n or string.sub(entry.name, -9) == \".spec.lua\"\n or string.sub(entry.name, -9) == \".test.lua\"\n or string.sub(entry.name, -10) == \".snap.luau\"\n or string.sub(entry.name, -9) == \".snap.lua\"\n then\n continue\n end\n\n -- Compute relative path\n local relative = string.sub(fullPath, #relativeTo + 2)\n table.insert(results, relative)\n end\n end\nend\n\nlocal function dirname(filepath: string): string\n local pos = string.find(filepath, \"/[^/]*$\")\n if pos then\n return string.sub(filepath, 1, pos - 1)\n end\n\n return \"\"\nend\n\nlocal files: { string } = {}\ndiscoverFiles(luauRoot, luauRoot, files)\n\n-- Parse, strip, and write per-file AST JSON\nfor _, relativePath in files do\n local fullPath = luauRoot .. \"/\" .. relativePath\n local source = fs.readfiletostring(fullPath)\n local parseResult = syntax.parse(source)\n local stripped = strip(parseResult.root)\n\n local outPath = outputDir .. \"/\" .. relativePath .. \".json\"\n local dir = dirname(outPath)\n if dir ~= \"\" then\n fs.createdirectory(dir, { makeparents = true })\n end\n\n fs.writestringtofile(outPath, json.serialize(stripped))\nend\n\n-- Print file list to stdout (tiny — just paths)\nprint(json.serialize(files :: json.array))\n";
|
|
934
1236
|
//#endregion
|
|
935
1237
|
//#region src/coverage/probe-inserter.ts
|
|
936
1238
|
function insertProbes(source, result, fileKey) {
|
|
@@ -1040,7 +1342,6 @@ function buildPreamble(modeDirective, fileKey, result) {
|
|
|
1040
1342
|
}
|
|
1041
1343
|
return preamble;
|
|
1042
1344
|
}
|
|
1043
|
-
|
|
1044
1345
|
//#endregion
|
|
1045
1346
|
//#region src/coverage/instrumenter.ts
|
|
1046
1347
|
let cachedTemporaryDirectory;
|
|
@@ -1049,12 +1350,12 @@ let cachedTemporaryDirectory;
|
|
|
1049
1350
|
* writing a manifest — used by `prepareCoverage()` to merge multiple roots.
|
|
1050
1351
|
*/
|
|
1051
1352
|
function instrumentRoot(options) {
|
|
1052
|
-
const { luauRoot, parseScript, shadowDir } = options;
|
|
1053
|
-
const lazyTemporaryDirectory = parseScript === void 0 ||
|
|
1353
|
+
const { astOutputDirectory: astOutputDirectoryOption, luauRoot, parseScript, shadowDir, skipFiles } = options;
|
|
1354
|
+
const lazyTemporaryDirectory = parseScript === void 0 || astOutputDirectoryOption === void 0 ? getTemporaryDirectory() : "";
|
|
1054
1355
|
const scriptPath = parseScript ?? path$1.join(lazyTemporaryDirectory, "parse-ast.luau");
|
|
1055
|
-
const astOutputDirectory =
|
|
1056
|
-
if (parseScript === void 0) fs.writeFileSync(scriptPath, parse_ast_default);
|
|
1057
|
-
fs.mkdirSync(astOutputDirectory, { recursive: true });
|
|
1356
|
+
const astOutputDirectory = astOutputDirectoryOption ?? path$1.join(lazyTemporaryDirectory, "asts");
|
|
1357
|
+
if (parseScript === void 0) fs$1.writeFileSync(scriptPath, parse_ast_default);
|
|
1358
|
+
fs$1.mkdirSync(astOutputDirectory, { recursive: true });
|
|
1058
1359
|
let fileListJson;
|
|
1059
1360
|
try {
|
|
1060
1361
|
fileListJson = cp.execFileSync("lute", [
|
|
@@ -1081,10 +1382,11 @@ function instrumentRoot(options) {
|
|
|
1081
1382
|
const files = {};
|
|
1082
1383
|
const posixLuauRoot = toPosix(luauRoot);
|
|
1083
1384
|
for (const relativePath of fileList) {
|
|
1385
|
+
if (shouldSkipFile(relativePath, skipFiles)) continue;
|
|
1084
1386
|
const astJsonPath = path$1.join(astOutputDirectory, `${relativePath}.json`);
|
|
1085
1387
|
let astJson;
|
|
1086
1388
|
try {
|
|
1087
|
-
astJson = fs.readFileSync(astJsonPath, "utf-8");
|
|
1389
|
+
astJson = fs$1.readFileSync(astJsonPath, "utf-8");
|
|
1088
1390
|
} catch (err) {
|
|
1089
1391
|
throw new Error(`Failed to read AST for ${relativePath}`, { cause: err });
|
|
1090
1392
|
}
|
|
@@ -1095,13 +1397,14 @@ function instrumentRoot(options) {
|
|
|
1095
1397
|
const coverageMapOutputPath = path$1.join(shadowDir, relativePath.replace(/\.luau$/, ".cov-map.json"));
|
|
1096
1398
|
const sourceMapPath = `${originalLuauPath}.map`;
|
|
1097
1399
|
const outputDirectory = path$1.dirname(path$1.join(shadowDir, relativePath));
|
|
1098
|
-
fs.mkdirSync(outputDirectory, { recursive: true });
|
|
1099
|
-
const
|
|
1400
|
+
fs$1.mkdirSync(outputDirectory, { recursive: true });
|
|
1401
|
+
const sourceBuffer = fs$1.readFileSync(path$1.resolve(originalLuauPath));
|
|
1402
|
+
const source = sourceBuffer.toString("utf-8");
|
|
1100
1403
|
const collectorResult = collectCoverage(ast);
|
|
1101
1404
|
const instrumentedSource = insertProbes(source, collectorResult, fileKey);
|
|
1102
1405
|
const coverageMap = buildCoverageMap$1(collectorResult);
|
|
1103
|
-
fs.writeFileSync(path$1.join(shadowDir, relativePath), instrumentedSource);
|
|
1104
|
-
fs.writeFileSync(coverageMapOutputPath, JSON.stringify(coverageMap, void 0, " "));
|
|
1406
|
+
fs$1.writeFileSync(path$1.join(shadowDir, relativePath), instrumentedSource);
|
|
1407
|
+
fs$1.writeFileSync(coverageMapOutputPath, JSON.stringify(coverageMap, void 0, " "));
|
|
1105
1408
|
files[fileKey] = {
|
|
1106
1409
|
key: fileKey,
|
|
1107
1410
|
branchCount: collectorResult.branches.length,
|
|
@@ -1109,21 +1412,25 @@ function instrumentRoot(options) {
|
|
|
1109
1412
|
functionCount: collectorResult.functions.length,
|
|
1110
1413
|
instrumentedLuauPath,
|
|
1111
1414
|
originalLuauPath,
|
|
1415
|
+
sourceHash: hashBuffer(sourceBuffer),
|
|
1112
1416
|
sourceMapPath,
|
|
1113
1417
|
statementCount: collectorResult.statements.length
|
|
1114
1418
|
};
|
|
1115
1419
|
}
|
|
1116
1420
|
return files;
|
|
1117
1421
|
}
|
|
1422
|
+
function shouldSkipFile(relativePath, skipFiles) {
|
|
1423
|
+
if (relativePath.endsWith(".snap.luau") || relativePath.endsWith(".snap.lua")) return true;
|
|
1424
|
+
return skipFiles?.has(relativePath) === true;
|
|
1425
|
+
}
|
|
1118
1426
|
function getTemporaryDirectory() {
|
|
1119
|
-
if (cachedTemporaryDirectory !== void 0 && fs.existsSync(cachedTemporaryDirectory)) return cachedTemporaryDirectory;
|
|
1120
|
-
cachedTemporaryDirectory = fs.mkdtempSync(path$1.join(os.tmpdir(), "jest-roblox-instrument-"));
|
|
1427
|
+
if (cachedTemporaryDirectory !== void 0 && fs$1.existsSync(cachedTemporaryDirectory)) return cachedTemporaryDirectory;
|
|
1428
|
+
cachedTemporaryDirectory = fs$1.mkdtempSync(path$1.join(os.tmpdir(), "jest-roblox-instrument-"));
|
|
1121
1429
|
return cachedTemporaryDirectory;
|
|
1122
1430
|
}
|
|
1123
1431
|
function toPosix(value) {
|
|
1124
1432
|
return value.replaceAll("\\", "/");
|
|
1125
1433
|
}
|
|
1126
|
-
|
|
1127
1434
|
//#endregion
|
|
1128
1435
|
//#region src/coverage/rojo-builder.ts
|
|
1129
1436
|
function buildWithRojo(projectPath, outputPath) {
|
|
@@ -1141,7 +1448,6 @@ function buildWithRojo(projectPath, outputPath) {
|
|
|
1141
1448
|
throw new Error(message, { cause: err });
|
|
1142
1449
|
}
|
|
1143
1450
|
}
|
|
1144
|
-
|
|
1145
1451
|
//#endregion
|
|
1146
1452
|
//#region src/coverage/rojo-rewriter.ts
|
|
1147
1453
|
function rewriteRojoProject(project, options) {
|
|
@@ -1182,76 +1488,68 @@ function walkTree(node, context) {
|
|
|
1182
1488
|
else result[key] = value;
|
|
1183
1489
|
return result;
|
|
1184
1490
|
}
|
|
1185
|
-
|
|
1186
1491
|
//#endregion
|
|
1187
1492
|
//#region src/coverage/prepare.ts
|
|
1188
1493
|
const COVERAGE_DIR = ".jest-roblox-coverage";
|
|
1494
|
+
const previousManifestSchema = type({
|
|
1495
|
+
"files": type({ "[string]": { sourceHash: "string" } }),
|
|
1496
|
+
"instrumenterVersion": "number",
|
|
1497
|
+
"luauRoots": "string[]",
|
|
1498
|
+
"placeFilePath?": "string",
|
|
1499
|
+
"shadowDir": "string",
|
|
1500
|
+
"version": "number"
|
|
1501
|
+
}).as();
|
|
1189
1502
|
function collectLuauRootsFromRojo(project, config) {
|
|
1190
1503
|
const paths = [];
|
|
1191
1504
|
collectPaths(project.tree, paths);
|
|
1192
1505
|
const ignorePatterns = config.coveragePathIgnorePatterns;
|
|
1193
1506
|
const isIgnored = picomatch(ignorePatterns, { contains: true });
|
|
1194
1507
|
return paths.filter((directoryPath) => {
|
|
1195
|
-
if (!fs.existsSync(directoryPath)) return false;
|
|
1196
|
-
if (!fs.statSync(directoryPath).isDirectory()) return false;
|
|
1508
|
+
if (!fs$1.existsSync(directoryPath)) return false;
|
|
1509
|
+
if (!fs$1.statSync(directoryPath).isDirectory()) return false;
|
|
1197
1510
|
if (isIgnored(directoryPath)) return false;
|
|
1198
1511
|
return containsLuauFiles(directoryPath);
|
|
1199
1512
|
});
|
|
1200
1513
|
}
|
|
1201
|
-
function prepareCoverage(config) {
|
|
1514
|
+
function prepareCoverage(config, beforeBuild) {
|
|
1202
1515
|
const rojoProjectPath = findRojoProject(config);
|
|
1203
1516
|
const luauRoots = resolveLuauRootsWithRojo(config, rojoProjectPath);
|
|
1204
|
-
|
|
1517
|
+
validateRelativeRoots(luauRoots);
|
|
1205
1518
|
const manifestPath = path$1.join(COVERAGE_DIR, "manifest.json");
|
|
1206
|
-
|
|
1519
|
+
const previousManifest = loadPreviousManifest(manifestPath);
|
|
1520
|
+
const useIncremental = canUseIncremental(previousManifest, config);
|
|
1521
|
+
if (!useIncremental && fs$1.existsSync(COVERAGE_DIR)) fs$1.rmSync(COVERAGE_DIR, { recursive: true });
|
|
1207
1522
|
const allFiles = {};
|
|
1208
1523
|
const roots = [];
|
|
1524
|
+
let hasChanges = !useIncremental;
|
|
1209
1525
|
for (const luauRoot of luauRoots) {
|
|
1210
|
-
const
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
shadowDir: shadowDirectory
|
|
1223
|
-
});
|
|
1526
|
+
const rootResult = instrumentRootWithCache(luauRoot, useIncremental, previousManifest);
|
|
1527
|
+
if (rootResult.changed) hasChanges = true;
|
|
1528
|
+
Object.assign(allFiles, rootResult.files);
|
|
1529
|
+
roots.push(rootResult.rootEntry);
|
|
1530
|
+
}
|
|
1531
|
+
if (useIncremental && previousManifest !== void 0) {
|
|
1532
|
+
const deleted = detectDeletedFiles(previousManifest, allFiles);
|
|
1533
|
+
cleanupDeletedFiles(deleted);
|
|
1534
|
+
if (deleted.length > 0) hasChanges = true;
|
|
1535
|
+
}
|
|
1536
|
+
if (beforeBuild !== void 0) {
|
|
1537
|
+
if (beforeBuild(COVERAGE_DIR)) hasChanges = true;
|
|
1224
1538
|
}
|
|
1225
|
-
const manifest = {
|
|
1226
|
-
files: allFiles,
|
|
1227
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1228
|
-
luauRoots,
|
|
1229
|
-
shadowDir: COVERAGE_DIR,
|
|
1230
|
-
version: 1
|
|
1231
|
-
};
|
|
1232
|
-
fs.mkdirSync(path$1.dirname(manifestPath), { recursive: true });
|
|
1233
|
-
fs.writeFileSync(manifestPath, JSON.stringify(manifest, void 0, " "));
|
|
1234
|
-
const rojoProjectRaw = rojoProjectSchema(JSON.parse(fs.readFileSync(rojoProjectPath, "utf-8")));
|
|
1235
|
-
if (rojoProjectRaw instanceof type.errors) throw new Error(`Malformed Rojo project JSON: ${rojoProjectRaw.toString()}`);
|
|
1236
|
-
const rewritten = rewriteRojoProject(rojoProjectRaw, {
|
|
1237
|
-
projectRelocation: path$1.relative(COVERAGE_DIR, path$1.dirname(rojoProjectPath)).replaceAll("\\", "/"),
|
|
1238
|
-
roots
|
|
1239
|
-
});
|
|
1240
|
-
const rewrittenProjectPath = path$1.join(COVERAGE_DIR, path$1.basename(rojoProjectPath));
|
|
1241
|
-
fs.writeFileSync(rewrittenProjectPath, JSON.stringify(rewritten, void 0, " "));
|
|
1242
1539
|
const placeFile = path$1.join(COVERAGE_DIR, "game.rbxl");
|
|
1243
|
-
|
|
1540
|
+
const manifest = writeManifest(manifestPath, allFiles, luauRoots, placeFile);
|
|
1541
|
+
if (!hasChanges && previousManifest?.placeFilePath !== void 0) return {
|
|
1542
|
+
manifest,
|
|
1543
|
+
placeFile: previousManifest.placeFilePath
|
|
1544
|
+
};
|
|
1545
|
+
buildRojoProject(rojoProjectPath, roots, placeFile);
|
|
1244
1546
|
return {
|
|
1245
1547
|
manifest,
|
|
1246
1548
|
placeFile
|
|
1247
1549
|
};
|
|
1248
1550
|
}
|
|
1249
|
-
function collectPaths(node, result) {
|
|
1250
|
-
for (const [key, value] of Object.entries(node)) if (key === "$path" && typeof value === "string") result.push(value.replaceAll("\\", "/"));
|
|
1251
|
-
else if (typeof value === "object" && !Array.isArray(value) && !key.startsWith("$")) collectPaths(value, result);
|
|
1252
|
-
}
|
|
1253
1551
|
function containsLuauFiles(directoryPath) {
|
|
1254
|
-
return fs.readdirSync(directoryPath, { withFileTypes: true }).some((entry) => {
|
|
1552
|
+
return fs$1.readdirSync(directoryPath, { withFileTypes: true }).some((entry) => {
|
|
1255
1553
|
if (entry.isFile() && entry.name.endsWith(".luau")) return true;
|
|
1256
1554
|
if (entry.isDirectory()) return containsLuauFiles(path$1.join(directoryPath, entry.name));
|
|
1257
1555
|
return false;
|
|
@@ -1260,8 +1558,8 @@ function containsLuauFiles(directoryPath) {
|
|
|
1260
1558
|
function findRojoProject(config) {
|
|
1261
1559
|
if (config.rojoProject !== void 0) return config.rojoProject;
|
|
1262
1560
|
const defaultPath = path$1.join(config.rootDir, "default.project.json");
|
|
1263
|
-
if (fs.existsSync(defaultPath)) return defaultPath;
|
|
1264
|
-
const projectFile = fs.readdirSync(config.rootDir, "utf-8").find((file) => file.endsWith(".project.json"));
|
|
1561
|
+
if (fs$1.existsSync(defaultPath)) return defaultPath;
|
|
1562
|
+
const projectFile = fs$1.readdirSync(config.rootDir, "utf-8").find((file) => file.endsWith(".project.json"));
|
|
1265
1563
|
if (projectFile !== void 0) return path$1.join(config.rootDir, projectFile);
|
|
1266
1564
|
throw new Error("No Rojo project found. Set rojoProject in config or add a .project.json file.");
|
|
1267
1565
|
}
|
|
@@ -1269,7 +1567,7 @@ function resolveLuauRootsWithRojo(config, rojoProjectPath) {
|
|
|
1269
1567
|
if (config.luauRoots !== void 0 && config.luauRoots.length > 0) return config.luauRoots;
|
|
1270
1568
|
try {
|
|
1271
1569
|
const resolvedPath = rojoProjectPath ?? findRojoProject(config);
|
|
1272
|
-
const roots = collectLuauRootsFromRojo(JSON.parse(fs.readFileSync(resolvedPath, "utf-8")), config);
|
|
1570
|
+
const roots = collectLuauRootsFromRojo(JSON.parse(fs$1.readFileSync(resolvedPath, "utf-8")), config);
|
|
1273
1571
|
if (roots.length > 0) return roots;
|
|
1274
1572
|
} catch (err) {
|
|
1275
1573
|
if (err instanceof SyntaxError) throw new Error(`Malformed Rojo project JSON: ${err.message}`, { cause: err });
|
|
@@ -1278,7 +1576,118 @@ function resolveLuauRootsWithRojo(config, rojoProjectPath) {
|
|
|
1278
1576
|
if (outDirectory !== void 0) return [outDirectory];
|
|
1279
1577
|
throw new Error("Could not determine luauRoots. Set luauRoots in config or ensure tsconfig has outDir.");
|
|
1280
1578
|
}
|
|
1281
|
-
|
|
1579
|
+
function validateRelativeRoots(luauRoots) {
|
|
1580
|
+
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.");
|
|
1581
|
+
}
|
|
1582
|
+
function computeSkipFiles(luauRoot, previousManifest) {
|
|
1583
|
+
const skipFiles = /* @__PURE__ */ new Set();
|
|
1584
|
+
const posixRoot = luauRoot.replaceAll("\\", "/");
|
|
1585
|
+
for (const [fileKey, record] of Object.entries(previousManifest.files)) {
|
|
1586
|
+
if (!fileKey.startsWith(`${posixRoot}/`)) continue;
|
|
1587
|
+
const relativePath = fileKey.slice(posixRoot.length + 1);
|
|
1588
|
+
const sourcePath = path$1.resolve(record.originalLuauPath);
|
|
1589
|
+
if (!fs$1.existsSync(sourcePath)) continue;
|
|
1590
|
+
if (hashBuffer(fs$1.readFileSync(sourcePath)) === record.sourceHash) skipFiles.add(relativePath);
|
|
1591
|
+
}
|
|
1592
|
+
return skipFiles;
|
|
1593
|
+
}
|
|
1594
|
+
function countPreviousFilesForRoot(luauRoot, previousManifest) {
|
|
1595
|
+
const posixRoot = luauRoot.replaceAll("\\", "/");
|
|
1596
|
+
let count = 0;
|
|
1597
|
+
for (const fileKey of Object.keys(previousManifest.files)) if (fileKey.startsWith(`${posixRoot}/`)) count++;
|
|
1598
|
+
return count;
|
|
1599
|
+
}
|
|
1600
|
+
function carryForwardRecords(luauRoot, previousManifest, allFiles, skipFiles) {
|
|
1601
|
+
const posixRoot = luauRoot.replaceAll("\\", "/");
|
|
1602
|
+
for (const relativePath of skipFiles) {
|
|
1603
|
+
const fileKey = `${posixRoot}/${relativePath}`;
|
|
1604
|
+
Object.assign(allFiles, { [fileKey]: previousManifest.files[fileKey] });
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
function instrumentRootWithCache(luauRoot, useIncremental, previousManifest) {
|
|
1608
|
+
const shadowDirectory = path$1.join(COVERAGE_DIR, luauRoot).replaceAll("\\", "/");
|
|
1609
|
+
let changed = false;
|
|
1610
|
+
if (!useIncremental) {
|
|
1611
|
+
fs$1.mkdirSync(shadowDirectory, { recursive: true });
|
|
1612
|
+
fs$1.cpSync(luauRoot, shadowDirectory, { recursive: true });
|
|
1613
|
+
}
|
|
1614
|
+
let skipFiles;
|
|
1615
|
+
if (useIncremental && previousManifest !== void 0) {
|
|
1616
|
+
skipFiles = computeSkipFiles(luauRoot, previousManifest);
|
|
1617
|
+
const previousCount = countPreviousFilesForRoot(luauRoot, previousManifest);
|
|
1618
|
+
if (skipFiles.size !== previousCount) changed = true;
|
|
1619
|
+
}
|
|
1620
|
+
const files = instrumentRoot({
|
|
1621
|
+
luauRoot,
|
|
1622
|
+
shadowDir: shadowDirectory,
|
|
1623
|
+
skipFiles
|
|
1624
|
+
});
|
|
1625
|
+
if (Object.keys(files).length > 0) changed = true;
|
|
1626
|
+
const allFiles = { ...files };
|
|
1627
|
+
if (useIncremental && previousManifest !== void 0 && skipFiles !== void 0) carryForwardRecords(luauRoot, previousManifest, allFiles, skipFiles);
|
|
1628
|
+
const relocatedShadowDirectory = path$1.relative(COVERAGE_DIR, shadowDirectory).replaceAll("\\", "/");
|
|
1629
|
+
return {
|
|
1630
|
+
changed,
|
|
1631
|
+
files: allFiles,
|
|
1632
|
+
rootEntry: {
|
|
1633
|
+
luauRoot,
|
|
1634
|
+
relocatedShadowDirectory,
|
|
1635
|
+
shadowDir: shadowDirectory
|
|
1636
|
+
}
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
function writeManifest(manifestPath, allFiles, luauRoots, placeFile) {
|
|
1640
|
+
const manifest = {
|
|
1641
|
+
files: allFiles,
|
|
1642
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1643
|
+
instrumenterVersion: 2,
|
|
1644
|
+
luauRoots,
|
|
1645
|
+
placeFilePath: placeFile,
|
|
1646
|
+
shadowDir: COVERAGE_DIR,
|
|
1647
|
+
version: 1
|
|
1648
|
+
};
|
|
1649
|
+
fs$1.mkdirSync(path$1.dirname(manifestPath), { recursive: true });
|
|
1650
|
+
fs$1.writeFileSync(manifestPath, JSON.stringify(manifest, void 0, " "));
|
|
1651
|
+
return manifest;
|
|
1652
|
+
}
|
|
1653
|
+
function buildRojoProject(rojoProjectPath, roots, placeFile) {
|
|
1654
|
+
const rojoProjectRaw = rojoProjectSchema(JSON.parse(fs$1.readFileSync(rojoProjectPath, "utf-8")));
|
|
1655
|
+
if (rojoProjectRaw instanceof type.errors) throw new Error(`Malformed Rojo project JSON: ${rojoProjectRaw.toString()}`);
|
|
1656
|
+
const rewritten = rewriteRojoProject(rojoProjectRaw, {
|
|
1657
|
+
projectRelocation: path$1.relative(COVERAGE_DIR, path$1.dirname(rojoProjectPath)).replaceAll("\\", "/"),
|
|
1658
|
+
roots
|
|
1659
|
+
});
|
|
1660
|
+
const rewrittenProjectPath = path$1.join(COVERAGE_DIR, path$1.basename(rojoProjectPath));
|
|
1661
|
+
fs$1.writeFileSync(rewrittenProjectPath, JSON.stringify(rewritten, void 0, " "));
|
|
1662
|
+
buildWithRojo(rewrittenProjectPath, placeFile);
|
|
1663
|
+
}
|
|
1664
|
+
function loadPreviousManifest(manifestPath) {
|
|
1665
|
+
if (!fs$1.existsSync(manifestPath)) return;
|
|
1666
|
+
try {
|
|
1667
|
+
const result = previousManifestSchema(JSON.parse(fs$1.readFileSync(manifestPath, "utf-8")));
|
|
1668
|
+
if (result instanceof type.errors) return;
|
|
1669
|
+
return result;
|
|
1670
|
+
} catch {
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
function canUseIncremental(previousManifest, config) {
|
|
1675
|
+
if (!config.cache) return false;
|
|
1676
|
+
if (previousManifest === void 0) return false;
|
|
1677
|
+
if (previousManifest.instrumenterVersion !== 2) return false;
|
|
1678
|
+
return true;
|
|
1679
|
+
}
|
|
1680
|
+
function detectDeletedFiles(previousManifest, currentFiles) {
|
|
1681
|
+
const deleted = [];
|
|
1682
|
+
for (const [fileKey, record] of Object.entries(previousManifest.files)) if (!(fileKey in currentFiles)) deleted.push(record);
|
|
1683
|
+
return deleted;
|
|
1684
|
+
}
|
|
1685
|
+
function cleanupDeletedFiles(records) {
|
|
1686
|
+
for (const record of records) try {
|
|
1687
|
+
if (fs$1.existsSync(record.instrumentedLuauPath)) fs$1.unlinkSync(record.instrumentedLuauPath);
|
|
1688
|
+
if (fs$1.existsSync(record.coverageMapPath)) fs$1.unlinkSync(record.coverageMapPath);
|
|
1689
|
+
} catch {}
|
|
1690
|
+
}
|
|
1282
1691
|
//#endregion
|
|
1283
1692
|
//#region src/coverage/reporter.ts
|
|
1284
1693
|
const VALID_REPORTERS = new Set([
|
|
@@ -1382,7 +1791,6 @@ function buildCoverageMap(mapped) {
|
|
|
1382
1791
|
function isValidReporter(name) {
|
|
1383
1792
|
return VALID_REPORTERS.has(name);
|
|
1384
1793
|
}
|
|
1385
|
-
|
|
1386
1794
|
//#endregion
|
|
1387
1795
|
//#region src/utils/glob.ts
|
|
1388
1796
|
function globSync(pattern, options = {}) {
|
|
@@ -1396,7 +1804,7 @@ function matchesGlobPattern(filePath, pattern) {
|
|
|
1396
1804
|
function walkDirectory(directoryPath, baseDirectory) {
|
|
1397
1805
|
const results = [];
|
|
1398
1806
|
try {
|
|
1399
|
-
const entries = fs.readdirSync(directoryPath, { withFileTypes: true });
|
|
1807
|
+
const entries = fs$1.readdirSync(directoryPath, { withFileTypes: true });
|
|
1400
1808
|
for (const entry of entries) {
|
|
1401
1809
|
const fullPath = path$1.join(directoryPath, entry.name);
|
|
1402
1810
|
const relativePath = path$1.relative(baseDirectory, fullPath).replace(/\\/g, "/");
|
|
@@ -1407,45 +1815,43 @@ function walkDirectory(directoryPath, baseDirectory) {
|
|
|
1407
1815
|
} catch {}
|
|
1408
1816
|
return results;
|
|
1409
1817
|
}
|
|
1410
|
-
|
|
1411
1818
|
//#endregion
|
|
1412
1819
|
//#region src/cli.ts
|
|
1820
|
+
const VERSION = version;
|
|
1821
|
+
const DEFAULT_ROJO_PROJECT = "default.project.json";
|
|
1413
1822
|
const TYPE_TEST_PATTERN = /\.(test-d|spec-d)\.ts$/;
|
|
1414
1823
|
const HELP_TEXT = `
|
|
1415
1824
|
Usage: jest-roblox [options] [files...]
|
|
1416
1825
|
|
|
1417
1826
|
Options:
|
|
1418
|
-
--backend <type>
|
|
1419
|
-
--port <number>
|
|
1420
|
-
--config <path>
|
|
1421
|
-
--testPathPattern <regex>
|
|
1422
|
-
-t, --testNamePattern <regex>
|
|
1423
|
-
--
|
|
1424
|
-
--
|
|
1425
|
-
--
|
|
1426
|
-
--
|
|
1427
|
-
--
|
|
1428
|
-
--
|
|
1429
|
-
--
|
|
1430
|
-
--
|
|
1431
|
-
--
|
|
1432
|
-
--
|
|
1433
|
-
|
|
1434
|
-
--
|
|
1435
|
-
--
|
|
1436
|
-
--
|
|
1437
|
-
--
|
|
1438
|
-
--
|
|
1439
|
-
--
|
|
1440
|
-
--
|
|
1441
|
-
--
|
|
1442
|
-
--
|
|
1443
|
-
--
|
|
1444
|
-
--
|
|
1445
|
-
--
|
|
1446
|
-
--typecheckTsconfig <path> tsconfig for type testing
|
|
1447
|
-
--help Show this help message
|
|
1448
|
-
--version Show version number
|
|
1827
|
+
--backend <type> Backend: "auto", "open-cloud", or "studio" (default: auto)
|
|
1828
|
+
--port <number> WebSocket port for studio backend (default: 3001)
|
|
1829
|
+
--config <path> Path to config file
|
|
1830
|
+
--testPathPattern <regex> Filter test files by path pattern
|
|
1831
|
+
-t, --testNamePattern <regex> Filter tests by name pattern
|
|
1832
|
+
--outputFile <path> Write results to file
|
|
1833
|
+
--gameOutput <path> Write game output (print/warn/error) to file
|
|
1834
|
+
--sourceMap Map Luau stack traces to TypeScript source
|
|
1835
|
+
--rojoProject <path> Path to rojo project file (auto-detected if not set)
|
|
1836
|
+
--verbose Show individual test results
|
|
1837
|
+
--silent Suppress output
|
|
1838
|
+
--no-color Disable colored output
|
|
1839
|
+
-u, --updateSnapshot Update snapshot files
|
|
1840
|
+
--coverage Enable coverage collection
|
|
1841
|
+
--coverageDirectory <path> Directory for coverage output (default: coverage)
|
|
1842
|
+
--coverageReporters <r...> Coverage reporters (default: text, lcov)
|
|
1843
|
+
--formatters <name...> Output formatters (default, agent, json, github-actions)
|
|
1844
|
+
--no-cache Force re-upload place file (skip cache)
|
|
1845
|
+
--pollInterval <ms> Open Cloud poll interval in ms (default: 500)
|
|
1846
|
+
--project <name...> Filter which named projects to run
|
|
1847
|
+
--setupFiles <path...> Setup scripts (package specifiers or relative paths)
|
|
1848
|
+
--setupFilesAfterEnv <path...> Post-env setup scripts (package specifiers or relative paths)
|
|
1849
|
+
--no-show-luau Hide Luau code in failure output
|
|
1850
|
+
--typecheck Enable type testing (*.test-d.ts, *.spec-d.ts)
|
|
1851
|
+
--typecheckOnly Run only type tests, skip runtime tests
|
|
1852
|
+
--typecheckTsconfig <path> tsconfig for type testing
|
|
1853
|
+
--help Show this help message
|
|
1854
|
+
--version Show version number
|
|
1449
1855
|
|
|
1450
1856
|
Environment Variables (open-cloud backend only):
|
|
1451
1857
|
ROBLOX_OPEN_CLOUD_API_KEY API key for Roblox Open Cloud
|
|
@@ -1457,7 +1863,7 @@ Examples:
|
|
|
1457
1863
|
jest-roblox --backend studio Run tests via Studio plugin
|
|
1458
1864
|
jest-roblox src/player.spec.ts Run specific test file
|
|
1459
1865
|
jest-roblox -t "should spawn" Run tests matching pattern
|
|
1460
|
-
jest-roblox --json
|
|
1866
|
+
jest-roblox --formatters json Output JSON to file
|
|
1461
1867
|
jest-roblox --coverage Run tests with coverage instrumentation
|
|
1462
1868
|
`;
|
|
1463
1869
|
function parseArgs(args) {
|
|
@@ -1468,8 +1874,6 @@ function parseArgs(args) {
|
|
|
1468
1874
|
"backend": { type: "string" },
|
|
1469
1875
|
"cache": { type: "boolean" },
|
|
1470
1876
|
"color": { type: "boolean" },
|
|
1471
|
-
"compact": { type: "boolean" },
|
|
1472
|
-
"compactMaxFailures": { type: "string" },
|
|
1473
1877
|
"config": { type: "string" },
|
|
1474
1878
|
"coverage": { type: "boolean" },
|
|
1475
1879
|
"coverageDirectory": { type: "string" },
|
|
@@ -1486,14 +1890,13 @@ function parseArgs(args) {
|
|
|
1486
1890
|
default: false,
|
|
1487
1891
|
type: "boolean"
|
|
1488
1892
|
},
|
|
1489
|
-
"json": { type: "boolean" },
|
|
1490
1893
|
"no-cache": { type: "boolean" },
|
|
1491
1894
|
"no-color": { type: "boolean" },
|
|
1492
1895
|
"no-show-luau": { type: "boolean" },
|
|
1493
1896
|
"outputFile": { type: "string" },
|
|
1494
1897
|
"pollInterval": { type: "string" },
|
|
1495
1898
|
"port": { type: "string" },
|
|
1496
|
-
"
|
|
1899
|
+
"project": {
|
|
1497
1900
|
multiple: true,
|
|
1498
1901
|
type: "string"
|
|
1499
1902
|
},
|
|
@@ -1530,7 +1933,6 @@ function parseArgs(args) {
|
|
|
1530
1933
|
},
|
|
1531
1934
|
strict: true
|
|
1532
1935
|
});
|
|
1533
|
-
const compactMaxFailures = values.compactMaxFailures !== void 0 ? Number.parseInt(values.compactMaxFailures, 10) : void 0;
|
|
1534
1936
|
const pollInterval = values.pollInterval !== void 0 ? Number.parseInt(values.pollInterval, 10) : void 0;
|
|
1535
1937
|
const port = values.port !== void 0 ? Number.parseInt(values.port, 10) : void 0;
|
|
1536
1938
|
const timeout = values.timeout !== void 0 ? Number.parseInt(values.timeout, 10) : void 0;
|
|
@@ -1539,8 +1941,6 @@ function parseArgs(args) {
|
|
|
1539
1941
|
cache: values["no-cache"] === true ? false : values.cache,
|
|
1540
1942
|
collectCoverage: values.coverage,
|
|
1541
1943
|
color: values["no-color"] === true ? false : values.color,
|
|
1542
|
-
compact: values.compact,
|
|
1543
|
-
compactMaxFailures,
|
|
1544
1944
|
config: values.config,
|
|
1545
1945
|
coverageDirectory: values.coverageDirectory,
|
|
1546
1946
|
coverageReporters: values.coverageReporters,
|
|
@@ -1548,11 +1948,10 @@ function parseArgs(args) {
|
|
|
1548
1948
|
formatters: values.formatters,
|
|
1549
1949
|
gameOutput: values.gameOutput,
|
|
1550
1950
|
help: values.help,
|
|
1551
|
-
json: values.json,
|
|
1552
1951
|
outputFile: values.outputFile,
|
|
1553
1952
|
pollInterval,
|
|
1554
1953
|
port,
|
|
1555
|
-
|
|
1954
|
+
project: values.project,
|
|
1556
1955
|
rojoProject: values.rojoProject,
|
|
1557
1956
|
setupFiles: values.setupFiles,
|
|
1558
1957
|
setupFilesAfterEnv: values.setupFilesAfterEnv,
|
|
@@ -1570,6 +1969,77 @@ function parseArgs(args) {
|
|
|
1570
1969
|
version: values.version
|
|
1571
1970
|
};
|
|
1572
1971
|
}
|
|
1972
|
+
function filterByName(projects, names) {
|
|
1973
|
+
const available = new Set(projects.map((project) => project.displayName));
|
|
1974
|
+
const unknown = names.filter((name) => !available.has(name));
|
|
1975
|
+
if (unknown.length > 0) throw new Error(`Unknown project name(s): ${unknown.join(", ")}. Available: ${[...available].join(", ")}`);
|
|
1976
|
+
const nameSet = new Set(names);
|
|
1977
|
+
return projects.filter((project) => nameSet.has(project.displayName));
|
|
1978
|
+
}
|
|
1979
|
+
function mergeProjectResults(results) {
|
|
1980
|
+
assert(results.length > 0, "mergeProjectResults requires at least one result");
|
|
1981
|
+
if (results.length === 1) {
|
|
1982
|
+
const [first] = results;
|
|
1983
|
+
return first;
|
|
1984
|
+
}
|
|
1985
|
+
let numberFailedTests = 0;
|
|
1986
|
+
let numberPassedTests = 0;
|
|
1987
|
+
let numberPendingTests = 0;
|
|
1988
|
+
let numberTodoTests = 0;
|
|
1989
|
+
let numberTotalTests = 0;
|
|
1990
|
+
let startTime = Number.POSITIVE_INFINITY;
|
|
1991
|
+
let success = true;
|
|
1992
|
+
const testResults = [];
|
|
1993
|
+
let executionMs = 0;
|
|
1994
|
+
let testsMs = 0;
|
|
1995
|
+
let totalMs = 0;
|
|
1996
|
+
let uploadMs = 0;
|
|
1997
|
+
let coverageMs = 0;
|
|
1998
|
+
let mergedCoverage;
|
|
1999
|
+
for (const result of results) {
|
|
2000
|
+
numberFailedTests += result.result.numFailedTests;
|
|
2001
|
+
numberPassedTests += result.result.numPassedTests;
|
|
2002
|
+
numberPendingTests += result.result.numPendingTests;
|
|
2003
|
+
numberTodoTests += result.result.numTodoTests ?? 0;
|
|
2004
|
+
numberTotalTests += result.result.numTotalTests;
|
|
2005
|
+
startTime = Math.min(startTime, result.result.startTime);
|
|
2006
|
+
success &&= result.result.success;
|
|
2007
|
+
testResults.push(...result.result.testResults);
|
|
2008
|
+
executionMs += result.timing.executionMs;
|
|
2009
|
+
testsMs += result.timing.testsMs;
|
|
2010
|
+
totalMs += result.timing.totalMs;
|
|
2011
|
+
uploadMs += result.timing.uploadMs ?? 0;
|
|
2012
|
+
coverageMs += result.timing.coverageMs ?? 0;
|
|
2013
|
+
if (result.coverageData !== void 0) mergedCoverage = {
|
|
2014
|
+
...mergedCoverage,
|
|
2015
|
+
...result.coverageData
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
return {
|
|
2019
|
+
coverageData: mergedCoverage,
|
|
2020
|
+
exitCode: success ? 0 : 1,
|
|
2021
|
+
output: "",
|
|
2022
|
+
result: {
|
|
2023
|
+
numFailedTests: numberFailedTests,
|
|
2024
|
+
numPassedTests: numberPassedTests,
|
|
2025
|
+
numPendingTests: numberPendingTests,
|
|
2026
|
+
numTodoTests: numberTodoTests,
|
|
2027
|
+
numTotalTests: numberTotalTests,
|
|
2028
|
+
startTime,
|
|
2029
|
+
success,
|
|
2030
|
+
testResults
|
|
2031
|
+
},
|
|
2032
|
+
timing: {
|
|
2033
|
+
coverageMs: coverageMs > 0 ? coverageMs : void 0,
|
|
2034
|
+
executionMs,
|
|
2035
|
+
startTime: Math.min(...results.map((result) => result.timing.startTime)),
|
|
2036
|
+
testsMs,
|
|
2037
|
+
totalMs,
|
|
2038
|
+
uploadCached: results.every((result) => result.timing.uploadCached === true),
|
|
2039
|
+
uploadMs
|
|
2040
|
+
}
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
1573
2043
|
async function run(args) {
|
|
1574
2044
|
try {
|
|
1575
2045
|
return await runInner(args);
|
|
@@ -1582,24 +2052,35 @@ async function main() {
|
|
|
1582
2052
|
const exitCode = await run(process.argv.slice(2));
|
|
1583
2053
|
process.exit(exitCode);
|
|
1584
2054
|
}
|
|
2055
|
+
function formatGameOutputLines(raw) {
|
|
2056
|
+
if (raw === void 0) return;
|
|
2057
|
+
const entries = parseGameOutput(raw);
|
|
2058
|
+
if (entries.length === 0) return;
|
|
2059
|
+
return entries.map((entry) => entry.message.replace(/^/gm, " ")).join("\n");
|
|
2060
|
+
}
|
|
1585
2061
|
function printError(err) {
|
|
1586
|
-
if (err instanceof
|
|
2062
|
+
if (err instanceof ConfigError) {
|
|
2063
|
+
const body = [color.red(err.message)];
|
|
2064
|
+
if (err.hint !== void 0) body.push(`\n ${color.dim("Hint:")} ${err.hint}`);
|
|
2065
|
+
process.stderr.write(formatBanner({
|
|
2066
|
+
body,
|
|
2067
|
+
level: "error",
|
|
2068
|
+
title: "Config Error"
|
|
2069
|
+
}));
|
|
2070
|
+
} else if (err instanceof LuauScriptError) {
|
|
2071
|
+
const body = [color.red(err.message)];
|
|
1587
2072
|
const hint = getLuauErrorHint(err.message);
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
2073
|
+
if (hint !== void 0) body.push(`\n ${color.dim("Hint:")} ${hint}`);
|
|
2074
|
+
const gameLines = formatGameOutputLines(err.gameOutput);
|
|
2075
|
+
if (gameLines !== void 0) body.push(`\n ${color.dim("Game output:")}\n${gameLines}`);
|
|
2076
|
+
process.stderr.write(formatBanner({
|
|
2077
|
+
body,
|
|
2078
|
+
level: "error",
|
|
2079
|
+
title: "Luau Error"
|
|
2080
|
+
}));
|
|
1591
2081
|
} else if (err instanceof Error) console.error(`Error: ${err.message}`);
|
|
1592
2082
|
else console.error("An unknown error occurred");
|
|
1593
2083
|
}
|
|
1594
|
-
async function executeRuntimeTests(config, testFiles, totalFiles) {
|
|
1595
|
-
if (!config.silent && (!config.compact || config.verbose) && !config.json && testFiles.length !== totalFiles) process.stderr.write(`Running ${String(testFiles.length)} of ${String(totalFiles)} test files\n`);
|
|
1596
|
-
return execute({
|
|
1597
|
-
backend: await resolveBackend(config),
|
|
1598
|
-
config,
|
|
1599
|
-
testFiles,
|
|
1600
|
-
version
|
|
1601
|
-
});
|
|
1602
|
-
}
|
|
1603
2084
|
function writeGameOutputIfConfigured(config, gameOutput, options) {
|
|
1604
2085
|
if (config.gameOutput === void 0) return;
|
|
1605
2086
|
const entries = parseGameOutput(gameOutput);
|
|
@@ -1637,13 +2118,8 @@ function processCoverage(config, coverageData) {
|
|
|
1637
2118
|
}
|
|
1638
2119
|
return true;
|
|
1639
2120
|
}
|
|
1640
|
-
function findFormatterOptions(formatters, name) {
|
|
1641
|
-
for (const entry of formatters) {
|
|
1642
|
-
if (entry === name) return {};
|
|
1643
|
-
if (Array.isArray(entry) && entry[0] === name) return entry[1];
|
|
1644
|
-
}
|
|
1645
|
-
}
|
|
1646
2121
|
function runGitHubActionsFormatter(config, result, sourceMapper) {
|
|
2122
|
+
assert(config.formatters !== void 0, "formatters is set by resolveFormatters");
|
|
1647
2123
|
const userOptions = findFormatterOptions(config.formatters, "github-actions");
|
|
1648
2124
|
if (userOptions === void 0) return;
|
|
1649
2125
|
const typedOptions = userOptions;
|
|
@@ -1657,15 +2133,80 @@ function runGitHubActionsFormatter(config, result, sourceMapper) {
|
|
|
1657
2133
|
const outputPath = jobSummary?.outputPath ?? process.env["GITHUB_STEP_SUMMARY"];
|
|
1658
2134
|
if (outputPath !== void 0) {
|
|
1659
2135
|
const summary = formatJobSummary(result, options);
|
|
1660
|
-
fs.appendFileSync(outputPath, summary);
|
|
2136
|
+
fs$1.appendFileSync(outputPath, summary);
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
function hasFormatter(config, name) {
|
|
2141
|
+
return config.formatters?.some((entry) => Array.isArray(entry) ? entry[0] === name : entry === name) === true;
|
|
2142
|
+
}
|
|
2143
|
+
function getCompactMaxFailures(config) {
|
|
2144
|
+
assert(config.formatters !== void 0, "formatters is set by resolveFormatters");
|
|
2145
|
+
const options = findFormatterOptions(config.formatters, "agent");
|
|
2146
|
+
if (options !== void 0 && typeof options["maxFailures"] === "number") return options["maxFailures"];
|
|
2147
|
+
return 10;
|
|
2148
|
+
}
|
|
2149
|
+
function usesAgentFormatter(config) {
|
|
2150
|
+
return hasFormatter(config, "agent") && !config.verbose;
|
|
2151
|
+
}
|
|
2152
|
+
function usesDefaultFormatter(config) {
|
|
2153
|
+
return !hasFormatter(config, "json") && !usesAgentFormatter(config);
|
|
2154
|
+
}
|
|
2155
|
+
function printOutput(output) {
|
|
2156
|
+
if (output !== "") console.log(output);
|
|
2157
|
+
}
|
|
2158
|
+
function formatRuntimeOutput(config, runtimeResult, timing) {
|
|
2159
|
+
return formatExecuteOutput({
|
|
2160
|
+
config,
|
|
2161
|
+
result: runtimeResult.result,
|
|
2162
|
+
sourceMapper: runtimeResult.sourceMapper,
|
|
2163
|
+
timing,
|
|
2164
|
+
version: VERSION
|
|
2165
|
+
});
|
|
2166
|
+
}
|
|
2167
|
+
function printFormattedOutput(options) {
|
|
2168
|
+
const { config, mergedResult, runtimeResult, timing, typecheckResult } = options;
|
|
2169
|
+
if (typecheckResult !== void 0 && runtimeResult !== void 0 && timing !== void 0) {
|
|
2170
|
+
if (usesDefaultFormatter(config)) printOutput(formatResult(mergedResult, timing, {
|
|
2171
|
+
collectCoverage: config.collectCoverage,
|
|
2172
|
+
color: config.color,
|
|
2173
|
+
rootDir: config.rootDir,
|
|
2174
|
+
showLuau: config.showLuau,
|
|
2175
|
+
sourceMapper: runtimeResult.sourceMapper,
|
|
2176
|
+
typeErrors: typecheckResult.numFailedTests,
|
|
2177
|
+
verbose: config.verbose,
|
|
2178
|
+
version: VERSION
|
|
2179
|
+
}));
|
|
2180
|
+
else {
|
|
2181
|
+
printOutput(formatRuntimeOutput(config, runtimeResult, timing));
|
|
2182
|
+
process.stderr.write(formatTypecheckSummary(typecheckResult));
|
|
1661
2183
|
}
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
if (typecheckResult !== void 0) {
|
|
2187
|
+
process.stdout.write(formatTypecheckSummary(typecheckResult));
|
|
2188
|
+
return;
|
|
1662
2189
|
}
|
|
2190
|
+
assert(runtimeResult !== void 0 && timing !== void 0, "runtime result required");
|
|
2191
|
+
printOutput(formatRuntimeOutput(config, runtimeResult, timing));
|
|
1663
2192
|
}
|
|
1664
|
-
|
|
2193
|
+
function addCoverageTiming(timing, coverageMs) {
|
|
2194
|
+
return {
|
|
2195
|
+
...timing,
|
|
2196
|
+
coverageMs,
|
|
2197
|
+
totalMs: timing.totalMs + coverageMs
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
2200
|
+
async function outputResults(config, typecheckResult, runtimeResult, preCoverageMs) {
|
|
1665
2201
|
const mergedResult = mergeResults(typecheckResult, runtimeResult?.result);
|
|
1666
|
-
if (
|
|
2202
|
+
if (!config.silent) printFormattedOutput({
|
|
2203
|
+
config,
|
|
2204
|
+
mergedResult,
|
|
2205
|
+
runtimeResult,
|
|
2206
|
+
timing: runtimeResult !== void 0 ? addCoverageTiming(runtimeResult.timing, preCoverageMs) : void 0,
|
|
2207
|
+
typecheckResult
|
|
2208
|
+
});
|
|
1667
2209
|
const coveragePassed = processCoverage(config, runtimeResult?.coverageData);
|
|
1668
|
-
if (typecheckResult !== void 0 && !config.silent) printTypecheckSummary(typecheckResult);
|
|
1669
2210
|
if (config.outputFile !== void 0) await writeJsonFile(mergedResult, config.outputFile);
|
|
1670
2211
|
if (runtimeResult !== void 0) writeGameOutputIfConfigured(config, runtimeResult.gameOutput, { hintsShown: !mergedResult.success });
|
|
1671
2212
|
runGitHubActionsFormatter(config, mergedResult, runtimeResult?.sourceMapper);
|
|
@@ -1673,18 +2214,205 @@ async function outputResults(config, typecheckResult, runtimeResult) {
|
|
|
1673
2214
|
if (!config.silent && config.collectCoverage) printFinalStatus(passed);
|
|
1674
2215
|
return passed ? 0 : 1;
|
|
1675
2216
|
}
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
2217
|
+
function toProjectEntries(projectResults) {
|
|
2218
|
+
return projectResults.map((pr) => {
|
|
2219
|
+
return {
|
|
2220
|
+
displayColor: pr.displayColor,
|
|
2221
|
+
displayName: pr.displayName,
|
|
2222
|
+
result: pr.result.result
|
|
2223
|
+
};
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
2226
|
+
function printMultiProjectOutput(options) {
|
|
2227
|
+
const { config, merged, preCoverageMs, projectResults, typecheckResult } = options;
|
|
2228
|
+
const timing = addCoverageTiming(merged.timing, preCoverageMs);
|
|
2229
|
+
if (usesAgentFormatter(config)) {
|
|
2230
|
+
printOutput(formatCompactMultiProject(toProjectEntries(projectResults), {
|
|
2231
|
+
gameOutput: config.gameOutput,
|
|
2232
|
+
maxFailures: getCompactMaxFailures(config),
|
|
2233
|
+
outputFile: config.outputFile,
|
|
2234
|
+
rootDir: config.rootDir,
|
|
2235
|
+
sourceMapper: merged.sourceMapper,
|
|
2236
|
+
typeErrorCount: typecheckResult?.numFailedTests
|
|
2237
|
+
}));
|
|
2238
|
+
return;
|
|
1681
2239
|
}
|
|
1682
|
-
if (
|
|
1683
|
-
|
|
1684
|
-
return
|
|
2240
|
+
if (hasFormatter(config, "json")) {
|
|
2241
|
+
printOutput(formatRuntimeOutput(config, merged, timing));
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
printOutput(formatMultiProjectResult(toProjectEntries(projectResults), timing, {
|
|
2245
|
+
collectCoverage: config.collectCoverage,
|
|
2246
|
+
color: config.color,
|
|
2247
|
+
rootDir: config.rootDir,
|
|
2248
|
+
showLuau: config.showLuau,
|
|
2249
|
+
sourceMapper: merged.sourceMapper,
|
|
2250
|
+
typeErrors: typecheckResult?.numFailedTests,
|
|
2251
|
+
verbose: config.verbose,
|
|
2252
|
+
version: VERSION
|
|
2253
|
+
}));
|
|
2254
|
+
}
|
|
2255
|
+
async function outputMultiProjectResults(config, projectResults, typecheckResult, preCoverageMs) {
|
|
2256
|
+
const merged = mergeProjectResults(projectResults.map((pr) => pr.result));
|
|
2257
|
+
const mergedResult = mergeResults(typecheckResult, merged.result);
|
|
2258
|
+
if (!config.silent) {
|
|
2259
|
+
printMultiProjectOutput({
|
|
2260
|
+
config,
|
|
2261
|
+
merged,
|
|
2262
|
+
preCoverageMs,
|
|
2263
|
+
projectResults,
|
|
2264
|
+
typecheckResult
|
|
2265
|
+
});
|
|
2266
|
+
if (typecheckResult !== void 0 && !usesDefaultFormatter(config)) process.stderr.write(formatTypecheckSummary(typecheckResult));
|
|
2267
|
+
}
|
|
2268
|
+
const coveragePassed = processCoverage(config, merged.coverageData);
|
|
2269
|
+
if (config.outputFile !== void 0) await writeJsonFile(mergedResult, config.outputFile);
|
|
2270
|
+
runGitHubActionsFormatter(config, mergedResult, merged.sourceMapper);
|
|
2271
|
+
const passed = mergedResult.success && coveragePassed;
|
|
2272
|
+
if (!config.silent && config.collectCoverage) printFinalStatus(passed);
|
|
2273
|
+
return passed ? 0 : 1;
|
|
2274
|
+
}
|
|
2275
|
+
function loadRojoTree(config) {
|
|
2276
|
+
const rojoPath = path$1.resolve(config.rootDir, config.rojoProject ?? DEFAULT_ROJO_PROJECT);
|
|
2277
|
+
const content = fs$1.readFileSync(rojoPath, "utf8");
|
|
2278
|
+
const validated = rojoProjectSchema(JSON.parse(content));
|
|
2279
|
+
if (validated instanceof type.errors) throw new Error(`Invalid Rojo project: ${validated.summary}`);
|
|
2280
|
+
return validated.tree;
|
|
2281
|
+
}
|
|
2282
|
+
const STUB_SKIP_KEYS = new Set([
|
|
2283
|
+
"outDir",
|
|
2284
|
+
"projects",
|
|
2285
|
+
"root"
|
|
2286
|
+
]);
|
|
2287
|
+
function buildStubConfig(config) {
|
|
2288
|
+
const result = {};
|
|
2289
|
+
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;
|
|
2290
|
+
return result;
|
|
2291
|
+
}
|
|
2292
|
+
function generateProjectStubs(projects, rootDirectory) {
|
|
2293
|
+
const entries = [];
|
|
2294
|
+
for (const project of projects) {
|
|
2295
|
+
if (project.outDir === void 0) continue;
|
|
2296
|
+
const outputPath = path$1.resolve(rootDirectory, project.outDir, "jest.config.lua");
|
|
2297
|
+
const stubConfig = {
|
|
2298
|
+
...buildStubConfig(project.config),
|
|
2299
|
+
displayName: project.displayName,
|
|
2300
|
+
include: [],
|
|
2301
|
+
testMatch: project.testMatch
|
|
2302
|
+
};
|
|
2303
|
+
entries.push({
|
|
2304
|
+
config: stubConfig,
|
|
2305
|
+
outputPath
|
|
2306
|
+
});
|
|
1685
2307
|
}
|
|
1686
|
-
|
|
1687
|
-
|
|
2308
|
+
generateProjectConfigs(entries);
|
|
2309
|
+
}
|
|
2310
|
+
function prepareMultiProjectCoverage(rootConfig, projects) {
|
|
2311
|
+
if (!rootConfig.collectCoverage) return {
|
|
2312
|
+
effectiveConfig: rootConfig,
|
|
2313
|
+
preCoverageMs: 0
|
|
2314
|
+
};
|
|
2315
|
+
const start = Date.now();
|
|
2316
|
+
const { placeFile } = prepareCoverage(rootConfig, (shadowDirectory) => {
|
|
2317
|
+
return syncStubsToShadowDirectory(projects, rootConfig.rootDir, shadowDirectory);
|
|
2318
|
+
});
|
|
2319
|
+
return {
|
|
2320
|
+
effectiveConfig: {
|
|
2321
|
+
...rootConfig,
|
|
2322
|
+
placeFile
|
|
2323
|
+
},
|
|
2324
|
+
preCoverageMs: Date.now() - start
|
|
2325
|
+
};
|
|
2326
|
+
}
|
|
2327
|
+
function classifyTestFiles(files, config) {
|
|
2328
|
+
const typeTestFiles = config.typecheck ? files.filter((file) => TYPE_TEST_PATTERN.test(file)) : [];
|
|
2329
|
+
return {
|
|
2330
|
+
runtimeFiles: config.typecheckOnly ? [] : files.filter((file) => !TYPE_TEST_PATTERN.test(file)),
|
|
2331
|
+
typeTestFiles
|
|
2332
|
+
};
|
|
2333
|
+
}
|
|
2334
|
+
function applySetupResolver(config, resolve) {
|
|
2335
|
+
if (config.setupFiles !== void 0) config.setupFiles = config.setupFiles.map(resolve);
|
|
2336
|
+
if (config.setupFilesAfterEnv !== void 0) config.setupFilesAfterEnv = config.setupFilesAfterEnv.map(resolve);
|
|
2337
|
+
}
|
|
2338
|
+
async function runMultiProject(cli, rootConfig, projectEntries) {
|
|
2339
|
+
const allProjects = await resolveAllProjects(projectEntries, rootConfig, loadRojoTree(rootConfig), rootConfig.rootDir);
|
|
2340
|
+
const rojoConfigPath = path$1.resolve(rootConfig.rootDir, rootConfig.rojoProject ?? DEFAULT_ROJO_PROJECT);
|
|
2341
|
+
const resolveSetup = createSetupResolver({
|
|
2342
|
+
configDirectory: rootConfig.rootDir,
|
|
2343
|
+
rojoConfigPath
|
|
2344
|
+
});
|
|
2345
|
+
for (const project of allProjects) applySetupResolver(project.config, resolveSetup);
|
|
2346
|
+
const projects = cli.project !== void 0 ? filterByName(allProjects, cli.project) : allProjects;
|
|
2347
|
+
generateProjectStubs(projects, rootConfig.rootDir);
|
|
2348
|
+
if (!rootConfig.collectCoverage) buildWithRojo(path$1.resolve(rootConfig.rootDir, rootConfig.rojoProject ?? DEFAULT_ROJO_PROJECT), path$1.resolve(rootConfig.rootDir, rootConfig.placeFile));
|
|
2349
|
+
const { effectiveConfig, preCoverageMs } = prepareMultiProjectCoverage(rootConfig, projects);
|
|
2350
|
+
const backend = await resolveBackend(effectiveConfig);
|
|
2351
|
+
const projectResults = [];
|
|
2352
|
+
const allTypeTestFiles = [];
|
|
2353
|
+
for (const project of projects) {
|
|
2354
|
+
const discoveryConfig = {
|
|
2355
|
+
...project.config,
|
|
2356
|
+
placeFile: effectiveConfig.placeFile,
|
|
2357
|
+
projects: project.projects,
|
|
2358
|
+
testMatch: project.include
|
|
2359
|
+
};
|
|
2360
|
+
const { runtimeFiles, typeTestFiles } = classifyTestFiles(discoverTestFiles(discoveryConfig, cli.files).files, rootConfig);
|
|
2361
|
+
const projConfig = {
|
|
2362
|
+
...discoveryConfig,
|
|
2363
|
+
testMatch: project.testMatch
|
|
2364
|
+
};
|
|
2365
|
+
allTypeTestFiles.push(...typeTestFiles);
|
|
2366
|
+
if (runtimeFiles.length === 0 && typeTestFiles.length === 0) continue;
|
|
2367
|
+
if (runtimeFiles.length > 0) {
|
|
2368
|
+
const result = await execute({
|
|
2369
|
+
backend,
|
|
2370
|
+
config: projConfig,
|
|
2371
|
+
deferFormatting: true,
|
|
2372
|
+
testFiles: runtimeFiles,
|
|
2373
|
+
version: VERSION
|
|
2374
|
+
});
|
|
2375
|
+
projectResults.push({
|
|
2376
|
+
displayColor: project.displayColor,
|
|
2377
|
+
displayName: project.displayName,
|
|
2378
|
+
result
|
|
2379
|
+
});
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
const uniqueTypeTestFiles = [...new Set(allTypeTestFiles)];
|
|
2383
|
+
const typecheckResult = uniqueTypeTestFiles.length > 0 ? runTypecheck({
|
|
2384
|
+
files: uniqueTypeTestFiles,
|
|
2385
|
+
rootDir: rootConfig.rootDir,
|
|
2386
|
+
tsconfig: rootConfig.typecheckTsconfig
|
|
2387
|
+
}) : void 0;
|
|
2388
|
+
if (projectResults.length === 0 && typecheckResult === void 0) {
|
|
2389
|
+
console.error("No test files found in any project");
|
|
2390
|
+
return 2;
|
|
2391
|
+
}
|
|
2392
|
+
if (projectResults.length === 0) return outputResults(rootConfig, typecheckResult, void 0, preCoverageMs);
|
|
2393
|
+
return outputMultiProjectResults(rootConfig, projectResults, typecheckResult, preCoverageMs);
|
|
2394
|
+
}
|
|
2395
|
+
async function executeRuntimeTests(config, testFiles, totalFiles) {
|
|
2396
|
+
if (!config.silent && !usesAgentFormatter(config) && !hasFormatter(config, "json") && testFiles.length !== totalFiles) process.stderr.write(`Running ${String(testFiles.length)} of ${String(totalFiles)} test files\n`);
|
|
2397
|
+
return execute({
|
|
2398
|
+
backend: await resolveBackend(config),
|
|
2399
|
+
config,
|
|
2400
|
+
deferFormatting: true,
|
|
2401
|
+
testFiles,
|
|
2402
|
+
version: VERSION
|
|
2403
|
+
});
|
|
2404
|
+
}
|
|
2405
|
+
function resolveSetupFilePaths(config) {
|
|
2406
|
+
if (config.setupFiles === void 0 && config.setupFilesAfterEnv === void 0) return;
|
|
2407
|
+
const rojoConfigPath = path$1.resolve(config.rootDir, config.rojoProject ?? DEFAULT_ROJO_PROJECT);
|
|
2408
|
+
applySetupResolver(config, createSetupResolver({
|
|
2409
|
+
configDirectory: config.rootDir,
|
|
2410
|
+
rojoConfigPath
|
|
2411
|
+
}));
|
|
2412
|
+
}
|
|
2413
|
+
async function runSingleProject(config, cliFiles) {
|
|
2414
|
+
resolveSetupFilePaths(config);
|
|
2415
|
+
const discovery = discoverTestFiles(config, cliFiles);
|
|
1688
2416
|
if (discovery.files.length === 0) {
|
|
1689
2417
|
console.error("No test files found");
|
|
1690
2418
|
return 2;
|
|
@@ -1695,20 +2423,40 @@ async function runInner(args) {
|
|
|
1695
2423
|
console.error("No test files found for the selected mode");
|
|
1696
2424
|
return 2;
|
|
1697
2425
|
}
|
|
2426
|
+
let preCoverageMs = 0;
|
|
2427
|
+
let effectiveConfig = config;
|
|
1698
2428
|
if (config.collectCoverage && !config.typecheckOnly && runtimeTestFiles.length > 0) {
|
|
2429
|
+
const preCoverageStart = Date.now();
|
|
1699
2430
|
const { placeFile } = prepareCoverage(config);
|
|
1700
|
-
|
|
2431
|
+
preCoverageMs = Date.now() - preCoverageStart;
|
|
2432
|
+
effectiveConfig = {
|
|
1701
2433
|
...config,
|
|
1702
2434
|
placeFile
|
|
1703
2435
|
};
|
|
1704
2436
|
}
|
|
1705
2437
|
const typecheckResult = typeTestFiles.length > 0 ? runTypecheck({
|
|
1706
2438
|
files: typeTestFiles,
|
|
1707
|
-
rootDir:
|
|
1708
|
-
tsconfig:
|
|
2439
|
+
rootDir: effectiveConfig.rootDir,
|
|
2440
|
+
tsconfig: effectiveConfig.typecheckTsconfig
|
|
1709
2441
|
}) : void 0;
|
|
1710
|
-
const runtimeResult = runtimeTestFiles.length > 0 ? await executeRuntimeTests(
|
|
1711
|
-
return outputResults(
|
|
2442
|
+
const runtimeResult = runtimeTestFiles.length > 0 ? await executeRuntimeTests(effectiveConfig, runtimeTestFiles, discovery.totalFiles) : void 0;
|
|
2443
|
+
return outputResults(effectiveConfig, typecheckResult, runtimeResult, preCoverageMs);
|
|
2444
|
+
}
|
|
2445
|
+
async function runInner(args) {
|
|
2446
|
+
const cli = parseArgs(args);
|
|
2447
|
+
if (cli.help === true) {
|
|
2448
|
+
console.log(HELP_TEXT);
|
|
2449
|
+
return 0;
|
|
2450
|
+
}
|
|
2451
|
+
if (cli.version === true) {
|
|
2452
|
+
console.log(VERSION);
|
|
2453
|
+
return 0;
|
|
2454
|
+
}
|
|
2455
|
+
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.");
|
|
2456
|
+
const config = mergeCliWithConfig(cli, await loadConfig$1(cli.config));
|
|
2457
|
+
const rawProjects = config.projects;
|
|
2458
|
+
if (rawProjects !== void 0 && rawProjects.length > 0) return runMultiProject(cli, config, rawProjects);
|
|
2459
|
+
return runSingleProject(config, cli.files);
|
|
1712
2460
|
}
|
|
1713
2461
|
const LUAU_ERROR_HINTS = [
|
|
1714
2462
|
[/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\""],
|
|
@@ -1757,10 +2505,19 @@ function validateBackend(value) {
|
|
|
1757
2505
|
function getLuauErrorHint(message) {
|
|
1758
2506
|
for (const [pattern, hint] of LUAU_ERROR_HINTS) if (pattern.test(message)) return hint;
|
|
1759
2507
|
}
|
|
2508
|
+
function normalizeFormatterName(name) {
|
|
2509
|
+
return name === "compact" ? "agent" : name;
|
|
2510
|
+
}
|
|
2511
|
+
function normalizeFormatterEntry(entry) {
|
|
2512
|
+
if (Array.isArray(entry)) return [normalizeFormatterName(entry[0]), entry[1]];
|
|
2513
|
+
return normalizeFormatterName(entry);
|
|
2514
|
+
}
|
|
1760
2515
|
function resolveFormatters(cli, config) {
|
|
1761
2516
|
const explicit = cli.formatters ?? config.formatters;
|
|
1762
|
-
if (explicit !== void 0) return explicit;
|
|
1763
|
-
|
|
2517
|
+
if (explicit !== void 0) return explicit.map(normalizeFormatterEntry);
|
|
2518
|
+
const defaults = isAgent ? ["agent"] : ["default"];
|
|
2519
|
+
if (process.env["GITHUB_ACTIONS"] === "true") defaults.push("github-actions");
|
|
2520
|
+
return defaults;
|
|
1764
2521
|
}
|
|
1765
2522
|
function mergeCliWithConfig(cli, config) {
|
|
1766
2523
|
return {
|
|
@@ -1769,17 +2526,13 @@ function mergeCliWithConfig(cli, config) {
|
|
|
1769
2526
|
cache: cli.cache ?? config.cache,
|
|
1770
2527
|
collectCoverage: cli.collectCoverage ?? config.collectCoverage,
|
|
1771
2528
|
color: cli.color ?? config.color,
|
|
1772
|
-
compact: cli.compact ?? config.compact,
|
|
1773
|
-
compactMaxFailures: cli.compactMaxFailures ?? config.compactMaxFailures,
|
|
1774
2529
|
coverageDirectory: cli.coverageDirectory ?? config.coverageDirectory,
|
|
1775
2530
|
coverageReporters: cli.coverageReporters ?? config.coverageReporters,
|
|
1776
2531
|
formatters: resolveFormatters(cli, config),
|
|
1777
2532
|
gameOutput: cli.gameOutput ?? config.gameOutput,
|
|
1778
|
-
json: cli.json ?? config.json,
|
|
1779
2533
|
outputFile: cli.outputFile ?? config.outputFile,
|
|
1780
2534
|
pollInterval: cli.pollInterval ?? config.pollInterval,
|
|
1781
2535
|
port: cli.port ?? config.port,
|
|
1782
|
-
projects: cli.projects ?? config.projects,
|
|
1783
2536
|
rojoProject: cli.rojoProject ?? config.rojoProject,
|
|
1784
2537
|
setupFiles: cli.setupFiles ?? config.setupFiles,
|
|
1785
2538
|
setupFilesAfterEnv: cli.setupFilesAfterEnv ?? config.setupFilesAfterEnv,
|
|
@@ -1811,21 +2564,5 @@ function mergeResults(typecheck, runtime) {
|
|
|
1811
2564
|
assert(result !== void 0, "mergeResults requires at least one result");
|
|
1812
2565
|
return result;
|
|
1813
2566
|
}
|
|
1814
|
-
function printTypecheckFailures(result) {
|
|
1815
|
-
for (const file of result.testResults) for (const test of file.testResults) {
|
|
1816
|
-
if (test.status !== "failed") continue;
|
|
1817
|
-
process.stderr.write(` FAIL ${test.fullName}\n`);
|
|
1818
|
-
for (const message of test.failureMessages) process.stderr.write(` ${message}\n`);
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
function printTypecheckSummary(result) {
|
|
1822
|
-
const passed = result.numPassedTests;
|
|
1823
|
-
const failed = result.numFailedTests;
|
|
1824
|
-
const total = result.numTotalTests;
|
|
1825
|
-
if (failed > 0) printTypecheckFailures(result);
|
|
1826
|
-
const failedPart = failed > 0 ? `${String(failed)} failed, ` : "";
|
|
1827
|
-
process.stderr.write(`\nType Tests: ${failedPart}${String(passed)} passed, ${String(total)} total\n`);
|
|
1828
|
-
}
|
|
1829
|
-
|
|
1830
2567
|
//#endregion
|
|
1831
|
-
export { main, parseArgs, run };
|
|
2568
|
+
export { filterByName, main, mergeProjectResults, parseArgs, run };
|