@isentinel/jest-roblox 0.1.2 → 0.1.3
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/dist/cli.d.mts +1 -1
- package/dist/cli.mjs +265 -65
- package/dist/{executor-DqZE3wME.d.mts → executor-D6BzDfQ_.d.mts} +3 -0
- package/dist/{game-output-BMGxhjkE.mjs → game-output-BU-9pJ93.mjs} +61 -5
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/sea/jest-roblox +0 -0
- package/dist/sea-entry.cjs +304 -48
- package/package.json +2 -2
package/dist/cli.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as ResolvedProjectConfig, n as ExecuteResult, v as CliOptions } from "./executor-
|
|
1
|
+
import { _ as ResolvedProjectConfig, n as ExecuteResult, v as CliOptions } from "./executor-D6BzDfQ_.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/cli.d.ts
|
|
4
4
|
declare function parseArgs(args: Array<string>): CliOptions;
|
package/dist/cli.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as hashBuffer, C as resolveNestedProjects, D as createStudioBackend, F as VALID_BACKENDS, P as ROOT_ONLY_KEYS, R as isValidBackend, S as collectPaths, _ as formatResult, a as formatAnnotations, b as formatBanner, c as execute, d as findFormatterOptions, g as formatMultiProjectResult, i as runTypecheck, k as createOpenCloudBackend, l as formatExecuteOutput, m as formatCompactMultiProject, n as parseGameOutput, o as formatJobSummary, p as writeJsonFile, r as writeGameOutput, s as resolveGitHubActionsOptions, t as formatGameOutputNotice, u as loadCoverageManifest, w as loadConfig$1, x as rojoProjectSchema, y as formatTypecheckSummary, z as LuauScriptError } from "./game-output-BU-9pJ93.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { type } from "arktype";
|
|
4
4
|
import assert from "node:assert";
|
|
5
5
|
import * as fs$1 from "node:fs";
|
|
6
|
-
import fs
|
|
6
|
+
import fs from "node:fs";
|
|
7
7
|
import * as path$1 from "node:path";
|
|
8
|
-
import path
|
|
8
|
+
import path from "node:path";
|
|
9
9
|
import process from "node:process";
|
|
10
10
|
import { parseArgs as parseArgs$1 } from "node:util";
|
|
11
11
|
import { isAgent } from "std-env";
|
|
@@ -14,16 +14,16 @@ import { WebSocketServer } from "ws";
|
|
|
14
14
|
import { loadConfig } from "c12";
|
|
15
15
|
import * as os from "node:os";
|
|
16
16
|
import { Buffer } from "node:buffer";
|
|
17
|
+
import * as cp from "node:child_process";
|
|
17
18
|
import { RojoResolver } from "@roblox-ts/rojo-resolver";
|
|
18
19
|
import { TraceMap, originalPositionFor } from "@jridgewell/trace-mapping";
|
|
19
20
|
import { getTsconfig } from "get-tsconfig";
|
|
20
21
|
import picomatch from "picomatch";
|
|
21
|
-
import * as cp from "node:child_process";
|
|
22
22
|
import istanbulCoverage from "istanbul-lib-coverage";
|
|
23
23
|
import istanbulReport from "istanbul-lib-report";
|
|
24
24
|
import istanbulReports from "istanbul-reports";
|
|
25
25
|
//#region package.json
|
|
26
|
-
var version = "0.1.
|
|
26
|
+
var version = "0.1.3";
|
|
27
27
|
//#endregion
|
|
28
28
|
//#region src/backends/auto.ts
|
|
29
29
|
var StudioWithFallback = class {
|
|
@@ -115,39 +115,95 @@ function stripTsExtension(pattern) {
|
|
|
115
115
|
return pattern.replace(/\.(tsx?|luau?)$/, "");
|
|
116
116
|
}
|
|
117
117
|
//#endregion
|
|
118
|
-
//#region src/
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
118
|
+
//#region src/luau/eval-literals.ts
|
|
119
|
+
/**
|
|
120
|
+
* Evaluate the first return expression in a Lute-stripped AST root block,
|
|
121
|
+
* supporting only literal values (string, boolean, number, nil, table, cast).
|
|
122
|
+
*
|
|
123
|
+
* Accepts `unknown` and narrows safely — no type casts on JSON.parse needed.
|
|
124
|
+
*/
|
|
125
|
+
function evalLuauReturnLiterals(root) {
|
|
126
|
+
if (!isObject(root) || !Array.isArray(root["statements"])) throw new Error("Config file has no return statement");
|
|
127
|
+
const returnStat = root["statements"].find((stat) => isObject(stat) && stat["tag"] === "return");
|
|
128
|
+
if (!isObject(returnStat) || !Array.isArray(returnStat["expressions"])) throw new Error("Config file has no return statement");
|
|
129
|
+
const first = returnStat["expressions"][0];
|
|
130
|
+
if (!isObject(first) || !("node" in first)) throw new Error("Return statement has no expressions");
|
|
131
|
+
return evalExpr(first["node"]);
|
|
132
|
+
}
|
|
133
|
+
function isObject(value) {
|
|
134
|
+
return typeof value === "object" && value !== null;
|
|
135
|
+
}
|
|
136
|
+
function evalExpr(node) {
|
|
137
|
+
if (!isObject(node)) return;
|
|
138
|
+
let current = node;
|
|
139
|
+
while (current["tag"] === "cast" && isObject(current["operand"])) current = current["operand"];
|
|
140
|
+
const { tag } = current;
|
|
141
|
+
if (tag === "boolean" || tag === "number") return current["value"];
|
|
142
|
+
if (tag === "string") return current["text"];
|
|
143
|
+
if (tag === "table" && Array.isArray(current["entries"])) return evalTable(current["entries"]);
|
|
144
|
+
}
|
|
145
|
+
function evalTable(entries) {
|
|
146
|
+
if (entries.length === 0) return {};
|
|
147
|
+
const first = entries[0];
|
|
148
|
+
if (isObject(first) && first["kind"] === "list") return entries.map((entry) => isObject(entry) ? evalExpr(entry["value"]) : void 0);
|
|
149
|
+
const result = {};
|
|
150
|
+
for (const entry of entries) {
|
|
151
|
+
if (!isObject(entry) || entry["kind"] !== "record") continue;
|
|
152
|
+
const { key, value } = entry;
|
|
153
|
+
if (isObject(key) && typeof key["text"] === "string") result[key["text"]] = evalExpr(value);
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
125
156
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
for (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/luau/parse-ast.luau
|
|
159
|
+
var parse_ast_default = "local fs = require(\"@std/fs\")\nlocal json = require(\"@std/json\")\nlocal process = require(\"@std/process\")\nlocal syntax = require(\"@std/syntax\")\n\nlocal rawArgs = process.args\nlocal userArgs: { string } = {}\nlocal pastSeparator = false\n\nfor _, arg in rawArgs do\n if pastSeparator then\n table.insert(userArgs, arg)\n elseif arg == \"--\" then\n pastSeparator = true\n end\nend\n\nlocal luauRoot = userArgs[1]\nif not luauRoot then\n error(\"Usage: lute run parse-ast.luau -- <file.luau | luau-root> [output-dir]\")\nend\n\nluauRoot = string.gsub(luauRoot, \"\\\\\", \"/\")\n\n-- Fields to keep per AST tag (beyond tag/kind/location which are always kept).\n-- Tags shared by stat/expr variants are merged — nil fields are harmless.\nlocal KEEP: { [string]: { string } } = {\n assign = { \"values\", \"variables\" },\n binary = { \"lhsoperand\", \"rhsoperand\" },\n block = { \"statements\" },\n boolean = { \"value\" },\n call = { \"arguments\", \"func\" },\n cast = { \"operand\" },\n compoundassign = { \"value\", \"variable\" },\n conditional = { \"condition\", \"thenblock\", \"elseifs\", \"elseblock\", \"thenexpr\", \"elseexpr\" },\n [\"do\"] = { \"body\" },\n expression = { \"expression\" },\n [\"for\"] = { \"body\", \"from\", \"to\", \"step\" },\n forin = { \"body\", \"values\" },\n [\"function\"] = { \"body\", \"name\", \"func\" },\n global = { \"name\" },\n group = { \"expression\" },\n index = { \"expression\", \"index\" },\n indexname = { \"expression\", \"accessor\", \"index\" },\n instantiate = { \"expr\" },\n interpolatedstring = { \"expressions\" },\n [\"local\"] = { \"values\", \"variables\" },\n localfunction = { \"name\", \"func\" },\n number = { \"value\" },\n [\"repeat\"] = { \"body\", \"condition\" },\n [\"return\"] = { \"expressions\" },\n string = { \"text\" },\n table = { \"entries\" },\n unary = { \"operand\" },\n [\"while\"] = { \"body\", \"condition\" },\n}\n\nlocal function strip(value: any): any\n if type(value) ~= \"table\" then\n return value\n end\n\n -- LuauSpan — has beginline, no tag\n if value.beginline ~= nil then\n return value\n end\n\n -- Token — has text but no tag, reduce to {text}\n if value.text ~= nil and value.tag == nil then\n return { text = value.text }\n end\n\n -- AST node — has tag, keep only allowlisted fields\n if value.tag ~= nil then\n local result = { tag = value.tag, kind = value.kind, location = value.location }\n local fields = KEEP[value.tag :: string]\n if fields then\n for _, field in fields do\n if value[field] ~= nil then\n result[field] = strip(value[field])\n end\n end\n end\n\n return result\n end\n\n -- Other tables (arrays, Pairs, ElseIf structs) — recurse all fields\n local result: any = {}\n for k, v in value do\n result[k] = strip(v)\n end\n\n return result\nend\n\n-- Single-file mode: parse one file, print stripped AST to stdout\nif string.sub(luauRoot, -5) == \".luau\" or string.sub(luauRoot, -4) == \".lua\" then\n local source = fs.readfiletostring(luauRoot)\n local parseResult = syntax.parse(source)\n print(json.serialize(strip(parseResult.root)))\n return\nend\n\nlocal outputDir = userArgs[2]\nif not outputDir then\n error(\"Usage: lute run parse-ast.luau -- <luau-root> <output-dir>\")\nend\n\noutputDir = string.gsub(outputDir, \"\\\\\", \"/\")\n\n-- Discover .luau files recursively, skipping node_modules, dot dirs, spec/test files\nlocal function discoverFiles(directory: string, relativeTo: string, results: { string })\n local entries = fs.listdirectory(directory)\n for _, entry in entries do\n local fullPath = directory .. \"/\" .. entry.name\n if entry.type == \"dir\" then\n if entry.name == \"node_modules\" or entry.name == \".jest-roblox-coverage\" then\n continue\n end\n\n if string.sub(entry.name, 1, 1) == \".\" then\n continue\n end\n\n discoverFiles(fullPath, relativeTo, results)\n elseif\n entry.type == \"file\"\n and (string.sub(entry.name, -5) == \".luau\" or string.sub(entry.name, -4) == \".lua\")\n then\n if\n string.sub(entry.name, -10) == \".spec.luau\"\n or string.sub(entry.name, -10) == \".test.luau\"\n or string.sub(entry.name, -9) == \".spec.lua\"\n or string.sub(entry.name, -9) == \".test.lua\"\n or string.sub(entry.name, -10) == \".snap.luau\"\n or string.sub(entry.name, -9) == \".snap.lua\"\n then\n continue\n end\n\n -- Compute relative path\n local relative = string.sub(fullPath, #relativeTo + 2)\n table.insert(results, relative)\n end\n end\nend\n\nlocal function dirname(filepath: string): string\n local pos = string.find(filepath, \"/[^/]*$\")\n if pos then\n return string.sub(filepath, 1, pos - 1)\n end\n\n return \"\"\nend\n\nlocal files: { string } = {}\ndiscoverFiles(luauRoot, luauRoot, files)\n\n-- Parse, strip, and write per-file AST JSON\nfor _, relativePath in files do\n local fullPath = luauRoot .. \"/\" .. relativePath\n local source = fs.readfiletostring(fullPath)\n local parseResult = syntax.parse(source)\n local stripped = strip(parseResult.root)\n\n local outPath = outputDir .. \"/\" .. relativePath .. \".json\"\n local dir = dirname(outPath)\n if dir ~= \"\" then\n fs.createdirectory(dir, { makeparents = true })\n end\n\n fs.writestringtofile(outPath, json.serialize(stripped))\nend\n\n-- Print file list to stdout (tiny — just paths)\nprint(json.serialize(files :: json.array))\n";
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region src/config/luau-config-loader.ts
|
|
162
|
+
let cachedTemporaryDirectory$1;
|
|
163
|
+
/**
|
|
164
|
+
* Parse a .luau config file via Lute and evaluate its return expression.
|
|
165
|
+
*/
|
|
166
|
+
function loadLuauConfig(filePath) {
|
|
167
|
+
const temporaryDirectory = getTemporaryDirectory$1();
|
|
168
|
+
const scriptPath = path$1.join(temporaryDirectory, "parse-ast.luau");
|
|
169
|
+
fs$1.writeFileSync(scriptPath, parse_ast_default);
|
|
170
|
+
let stdout;
|
|
171
|
+
try {
|
|
172
|
+
stdout = cp.execFileSync("lute", [
|
|
173
|
+
"run",
|
|
174
|
+
scriptPath,
|
|
175
|
+
"--",
|
|
176
|
+
path$1.resolve(filePath)
|
|
177
|
+
], {
|
|
178
|
+
encoding: "utf-8",
|
|
179
|
+
maxBuffer: 1024 * 1024
|
|
180
|
+
});
|
|
181
|
+
} catch (err) {
|
|
182
|
+
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");
|
|
183
|
+
throw new Error(`Failed to evaluate Luau config ${filePath}`, { cause: err });
|
|
184
|
+
}
|
|
185
|
+
let ast;
|
|
186
|
+
try {
|
|
187
|
+
ast = JSON.parse(stdout);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
throw new Error(`Failed to parse AST JSON from Luau config ${filePath}`, { cause: err });
|
|
149
190
|
}
|
|
150
|
-
|
|
191
|
+
const result = evalLuauReturnLiterals(ast);
|
|
192
|
+
if (typeof result !== "object" || result === null) throw new Error(`Luau config ${filePath} must return a table`);
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Check if `<cwd>/<directoryOrFile>/jest.config.luau` exists. Returns the
|
|
197
|
+
* resolved path if found, undefined otherwise.
|
|
198
|
+
*/
|
|
199
|
+
function findLuauConfigFile(directoryOrFile, cwd) {
|
|
200
|
+
const resolved = path$1.resolve(cwd, directoryOrFile, "jest.config.luau");
|
|
201
|
+
if (fs$1.existsSync(resolved)) return resolved;
|
|
202
|
+
}
|
|
203
|
+
function getTemporaryDirectory$1() {
|
|
204
|
+
if (cachedTemporaryDirectory$1 !== void 0 && fs$1.existsSync(cachedTemporaryDirectory$1)) return cachedTemporaryDirectory$1;
|
|
205
|
+
cachedTemporaryDirectory$1 = fs$1.mkdtempSync(path$1.join(os.tmpdir(), "jest-roblox-luau-config-"));
|
|
206
|
+
return cachedTemporaryDirectory$1;
|
|
151
207
|
}
|
|
152
208
|
//#endregion
|
|
153
209
|
//#region src/config/projects.ts
|
|
@@ -249,6 +305,8 @@ function resolveProjectConfig(project, rootConfig, rojoTree) {
|
|
|
249
305
|
};
|
|
250
306
|
}
|
|
251
307
|
async function loadProjectConfigFile(filePath, cwd) {
|
|
308
|
+
const luauConfigPath = findLuauConfigFile(filePath, cwd);
|
|
309
|
+
if (luauConfigPath !== void 0) return buildProjectConfigFromLuau(luauConfigPath, filePath);
|
|
252
310
|
let result;
|
|
253
311
|
try {
|
|
254
312
|
result = await loadConfig({
|
|
@@ -290,6 +348,38 @@ function mergeProjectConfig(rootConfig, project) {
|
|
|
290
348
|
for (const [key, value] of Object.entries(project)) if (!PROJECT_ONLY_KEYS.has(key) && value !== void 0) merged[key] = value;
|
|
291
349
|
return merged;
|
|
292
350
|
}
|
|
351
|
+
const LUAU_BOOLEAN_KEYS = [
|
|
352
|
+
"automock",
|
|
353
|
+
"clearMocks",
|
|
354
|
+
"injectGlobals",
|
|
355
|
+
"mockDataModel",
|
|
356
|
+
"resetMocks",
|
|
357
|
+
"resetModules",
|
|
358
|
+
"restoreMocks"
|
|
359
|
+
];
|
|
360
|
+
const LUAU_NUMBER_KEYS = ["slowTestThreshold", "testTimeout"];
|
|
361
|
+
const LUAU_STRING_KEYS = ["testEnvironment"];
|
|
362
|
+
const LUAU_STRING_ARRAY_KEYS = ["setupFiles", "setupFilesAfterEnv"];
|
|
363
|
+
function copyLuauOptionalFields(raw, config) {
|
|
364
|
+
const record = config;
|
|
365
|
+
for (const key of LUAU_BOOLEAN_KEYS) if (typeof raw[key] === "boolean") record[key] = raw[key];
|
|
366
|
+
for (const key of LUAU_NUMBER_KEYS) if (typeof raw[key] === "number") record[key] = raw[key];
|
|
367
|
+
for (const key of LUAU_STRING_KEYS) if (typeof raw[key] === "string") record[key] = raw[key];
|
|
368
|
+
for (const key of LUAU_STRING_ARRAY_KEYS) if (Array.isArray(raw[key])) record[key] = raw[key];
|
|
369
|
+
}
|
|
370
|
+
function buildProjectConfigFromLuau(luauConfigPath, directoryPath) {
|
|
371
|
+
const raw = loadLuauConfig(luauConfigPath);
|
|
372
|
+
const { displayName } = raw;
|
|
373
|
+
if (typeof displayName !== "string" || displayName === "") throw new Error(`Luau config file "${luauConfigPath}" must have a displayName string`);
|
|
374
|
+
const testMatch = Array.isArray(raw["testMatch"]) ? raw["testMatch"] : void 0;
|
|
375
|
+
const config = {
|
|
376
|
+
displayName,
|
|
377
|
+
include: testMatch !== void 0 ? testMatch.map((pattern) => path$1.posix.join(directoryPath, `${pattern}.luau`)) : [path$1.posix.join(directoryPath, "**/*.spec.luau")]
|
|
378
|
+
};
|
|
379
|
+
if (testMatch !== void 0) config.testMatch = testMatch;
|
|
380
|
+
copyLuauOptionalFields(raw, config);
|
|
381
|
+
return config;
|
|
382
|
+
}
|
|
293
383
|
function matchNodePath(childNode, targetPath, childDataModelPath) {
|
|
294
384
|
const nodePath = childNode.$path;
|
|
295
385
|
if (typeof nodePath !== "string") return;
|
|
@@ -365,6 +455,8 @@ function serializeToLuau(config) {
|
|
|
365
455
|
}
|
|
366
456
|
function generateProjectConfigs(projects) {
|
|
367
457
|
for (const project of projects) {
|
|
458
|
+
const luauConfigPath = project.outputPath.replace(/\.lua$/, ".luau");
|
|
459
|
+
if (fs.existsSync(luauConfigPath)) continue;
|
|
368
460
|
const content = serializeToLuau(project.config);
|
|
369
461
|
const directory = path.dirname(project.outputPath);
|
|
370
462
|
fs.mkdirSync(directory, { recursive: true });
|
|
@@ -470,27 +562,133 @@ function mapCoverageToTypeScript(coverageData, manifest) {
|
|
|
470
562
|
if (record === void 0) continue;
|
|
471
563
|
const resources = loadFileResources(record);
|
|
472
564
|
if (resources === void 0) continue;
|
|
473
|
-
|
|
474
|
-
|
|
565
|
+
if (resources.traceMap === void 0) {
|
|
566
|
+
passthroughFileStatements(resources, fileCoverage, pendingStatements);
|
|
567
|
+
passthroughFileFunctions(resources, fileCoverage, pendingFunctions);
|
|
568
|
+
passthroughFileBranches(resources, fileCoverage, pendingBranches);
|
|
569
|
+
} else {
|
|
570
|
+
const mapped = {
|
|
571
|
+
coverageMap: resources.coverageMap,
|
|
572
|
+
traceMap: resources.traceMap
|
|
573
|
+
};
|
|
574
|
+
mapFileFunctions(mapped, fileCoverage, pendingFunctions, mapFileStatements(mapped, fileCoverage, pendingStatements));
|
|
575
|
+
mapFileBranches(mapped, fileCoverage, pendingBranches);
|
|
576
|
+
}
|
|
475
577
|
}
|
|
476
578
|
return buildResult(pendingStatements, pendingFunctions, pendingBranches);
|
|
477
579
|
}
|
|
478
580
|
function loadFileResources(record) {
|
|
479
581
|
let coverageMapRaw;
|
|
480
|
-
let sourceMapRaw;
|
|
481
582
|
try {
|
|
482
583
|
coverageMapRaw = fs$1.readFileSync(record.coverageMapPath, "utf-8");
|
|
483
|
-
sourceMapRaw = fs$1.readFileSync(record.sourceMapPath, "utf-8");
|
|
484
584
|
} catch {
|
|
485
585
|
return;
|
|
486
586
|
}
|
|
487
587
|
const parsed = coverageMapSchema(JSON.parse(coverageMapRaw));
|
|
488
588
|
if (parsed instanceof type.errors) return;
|
|
589
|
+
let traceMap;
|
|
590
|
+
try {
|
|
591
|
+
traceMap = new TraceMap(fs$1.readFileSync(record.sourceMapPath, "utf-8"));
|
|
592
|
+
} catch {}
|
|
489
593
|
return {
|
|
490
594
|
coverageMap: parsed,
|
|
491
|
-
|
|
595
|
+
sourceKey: record.key,
|
|
596
|
+
traceMap
|
|
492
597
|
};
|
|
493
598
|
}
|
|
599
|
+
function toIstanbulColumn(luauColumn) {
|
|
600
|
+
return Math.max(0, luauColumn - 1);
|
|
601
|
+
}
|
|
602
|
+
function passthroughFileStatements(resources, fileCoverage, pending) {
|
|
603
|
+
let fileStatements = pending.get(resources.sourceKey);
|
|
604
|
+
if (fileStatements === void 0) {
|
|
605
|
+
fileStatements = /* @__PURE__ */ new Map();
|
|
606
|
+
pending.set(resources.sourceKey, fileStatements);
|
|
607
|
+
}
|
|
608
|
+
for (const [statementId, rawSpan] of Object.entries(resources.coverageMap.statementMap)) {
|
|
609
|
+
const span = spanSchema(rawSpan);
|
|
610
|
+
if (span instanceof type.errors) continue;
|
|
611
|
+
const hitCount = fileCoverage.s[statementId] ?? 0;
|
|
612
|
+
fileStatements.set(statementId, {
|
|
613
|
+
end: {
|
|
614
|
+
column: toIstanbulColumn(span.end.column),
|
|
615
|
+
line: span.end.line
|
|
616
|
+
},
|
|
617
|
+
hitCount,
|
|
618
|
+
start: {
|
|
619
|
+
column: toIstanbulColumn(span.start.column),
|
|
620
|
+
line: span.start.line
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
function passthroughFileFunctions(resources, fileCoverage, pendingFunctions) {
|
|
626
|
+
if (resources.coverageMap.functionMap === void 0) return;
|
|
627
|
+
let fileFunctions = pendingFunctions.get(resources.sourceKey);
|
|
628
|
+
if (fileFunctions === void 0) {
|
|
629
|
+
fileFunctions = [];
|
|
630
|
+
pendingFunctions.set(resources.sourceKey, fileFunctions);
|
|
631
|
+
}
|
|
632
|
+
for (const [functionId, rawEntry] of Object.entries(resources.coverageMap.functionMap)) {
|
|
633
|
+
const entry = functionEntrySchema(rawEntry);
|
|
634
|
+
if (entry instanceof type.errors) continue;
|
|
635
|
+
fileFunctions.push({
|
|
636
|
+
name: entry.name,
|
|
637
|
+
hitCount: fileCoverage.f?.[functionId] ?? 0,
|
|
638
|
+
loc: {
|
|
639
|
+
end: {
|
|
640
|
+
column: toIstanbulColumn(entry.location.end.column),
|
|
641
|
+
line: entry.location.end.line
|
|
642
|
+
},
|
|
643
|
+
start: {
|
|
644
|
+
column: toIstanbulColumn(entry.location.start.column),
|
|
645
|
+
line: entry.location.start.line
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
function passthroughFileBranches(resources, fileCoverage, pendingBranches) {
|
|
652
|
+
if (resources.coverageMap.branchMap === void 0) return;
|
|
653
|
+
let fileBranches = pendingBranches.get(resources.sourceKey);
|
|
654
|
+
if (fileBranches === void 0) {
|
|
655
|
+
fileBranches = [];
|
|
656
|
+
pendingBranches.set(resources.sourceKey, fileBranches);
|
|
657
|
+
}
|
|
658
|
+
for (const [branchId, rawEntry] of Object.entries(resources.coverageMap.branchMap)) {
|
|
659
|
+
const entry = branchEntrySchema(rawEntry);
|
|
660
|
+
if (entry instanceof type.errors) continue;
|
|
661
|
+
const armHitCounts = fileCoverage.b?.[branchId] ?? [];
|
|
662
|
+
const locations = [];
|
|
663
|
+
for (const rawLocation of entry.locations) {
|
|
664
|
+
const location = spanSchema(rawLocation);
|
|
665
|
+
if (location instanceof type.errors) continue;
|
|
666
|
+
locations.push({
|
|
667
|
+
end: {
|
|
668
|
+
column: toIstanbulColumn(location.end.column),
|
|
669
|
+
line: location.end.line
|
|
670
|
+
},
|
|
671
|
+
start: {
|
|
672
|
+
column: toIstanbulColumn(location.start.column),
|
|
673
|
+
line: location.start.line
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
if (locations.length === 0) continue;
|
|
678
|
+
const firstLocation = locations[0];
|
|
679
|
+
const lastLocation = locations[locations.length - 1];
|
|
680
|
+
assert(firstLocation !== void 0 && lastLocation !== void 0, "Branch locations must not be empty after filtering");
|
|
681
|
+
fileBranches.push({
|
|
682
|
+
armHitCounts: entry.locations.map((_, index) => armHitCounts[index] ?? 0),
|
|
683
|
+
loc: {
|
|
684
|
+
end: lastLocation.end,
|
|
685
|
+
start: firstLocation.start
|
|
686
|
+
},
|
|
687
|
+
locations,
|
|
688
|
+
type: entry.type
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
}
|
|
494
692
|
function mapStatement(traceMap, span) {
|
|
495
693
|
const mappedStart = originalPositionFor(traceMap, {
|
|
496
694
|
column: Math.max(0, span.start.column - 1),
|
|
@@ -731,26 +929,7 @@ function buildResult(pending, pendingFunctions, pendingBranches) {
|
|
|
731
929
|
return { files };
|
|
732
930
|
}
|
|
733
931
|
//#endregion
|
|
734
|
-
//#region src/
|
|
735
|
-
const INSTRUMENTABLE_STATEMENT_TAGS = new Set([
|
|
736
|
-
"assign",
|
|
737
|
-
"break",
|
|
738
|
-
"compoundassign",
|
|
739
|
-
"conditional",
|
|
740
|
-
"continue",
|
|
741
|
-
"do",
|
|
742
|
-
"expression",
|
|
743
|
-
"for",
|
|
744
|
-
"forin",
|
|
745
|
-
"function",
|
|
746
|
-
"local",
|
|
747
|
-
"localfunction",
|
|
748
|
-
"repeat",
|
|
749
|
-
"return",
|
|
750
|
-
"while"
|
|
751
|
-
]);
|
|
752
|
-
//#endregion
|
|
753
|
-
//#region src/coverage/luau-visitor.ts
|
|
932
|
+
//#region src/luau/visitor.ts
|
|
754
933
|
function visitExpression(expression, visitor) {
|
|
755
934
|
if (visitor.visitExpr?.(expression) === false) return;
|
|
756
935
|
const { tag } = expression;
|
|
@@ -1021,6 +1200,23 @@ function visitExprInstantiate(node, visitor) {
|
|
|
1021
1200
|
}
|
|
1022
1201
|
//#endregion
|
|
1023
1202
|
//#region src/coverage/coverage-collector.ts
|
|
1203
|
+
const INSTRUMENTABLE_STATEMENT_TAGS = new Set([
|
|
1204
|
+
"assign",
|
|
1205
|
+
"break",
|
|
1206
|
+
"compoundassign",
|
|
1207
|
+
"conditional",
|
|
1208
|
+
"continue",
|
|
1209
|
+
"do",
|
|
1210
|
+
"expression",
|
|
1211
|
+
"for",
|
|
1212
|
+
"forin",
|
|
1213
|
+
"function",
|
|
1214
|
+
"local",
|
|
1215
|
+
"localfunction",
|
|
1216
|
+
"repeat",
|
|
1217
|
+
"return",
|
|
1218
|
+
"while"
|
|
1219
|
+
]);
|
|
1024
1220
|
const END_KEYWORD_LENGTH = 3;
|
|
1025
1221
|
function collectCoverage(root) {
|
|
1026
1222
|
let statementIndex = 1;
|
|
@@ -1260,9 +1456,6 @@ function buildCoverageMap$1(result) {
|
|
|
1260
1456
|
};
|
|
1261
1457
|
}
|
|
1262
1458
|
//#endregion
|
|
1263
|
-
//#region src/coverage/parse-ast.luau
|
|
1264
|
-
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";
|
|
1265
|
-
//#endregion
|
|
1266
1459
|
//#region src/coverage/probe-inserter.ts
|
|
1267
1460
|
function insertProbes(source, result, fileKey) {
|
|
1268
1461
|
const lines = splitLines(source);
|
|
@@ -1317,7 +1510,7 @@ function applyProbes(mutableLines, probes) {
|
|
|
1317
1510
|
assert(line !== void 0, `Invalid probe line number: ${probeLine}`);
|
|
1318
1511
|
const before = line.slice(0, column - 1);
|
|
1319
1512
|
const after = line.slice(column - 1);
|
|
1320
|
-
mutableLines[lineIndex] = before + text + after;
|
|
1513
|
+
mutableLines[lineIndex] = before + (before.length > 0 && !/\s$/.test(before) && /^[a-zA-Z_]/.test(text) ? " " : "") + text + after;
|
|
1321
1514
|
}
|
|
1322
1515
|
}
|
|
1323
1516
|
function extractModeDirective(lines) {
|
|
@@ -1866,6 +2059,7 @@ Options:
|
|
|
1866
2059
|
--gameOutput <path> Write game output (print/warn/error) to file
|
|
1867
2060
|
--sourceMap Map Luau stack traces to TypeScript source
|
|
1868
2061
|
--rojoProject <path> Path to rojo project file (auto-detected if not set)
|
|
2062
|
+
--passWithNoTests Exit with 0 when no test files are found
|
|
1869
2063
|
--verbose Show individual test results
|
|
1870
2064
|
--silent Suppress output
|
|
1871
2065
|
--no-color Disable colored output
|
|
@@ -1927,6 +2121,7 @@ function parseArgs(args) {
|
|
|
1927
2121
|
"no-color": { type: "boolean" },
|
|
1928
2122
|
"no-show-luau": { type: "boolean" },
|
|
1929
2123
|
"outputFile": { type: "string" },
|
|
2124
|
+
"passWithNoTests": { type: "boolean" },
|
|
1930
2125
|
"pollInterval": { type: "string" },
|
|
1931
2126
|
"port": { type: "string" },
|
|
1932
2127
|
"project": {
|
|
@@ -1982,6 +2177,7 @@ function parseArgs(args) {
|
|
|
1982
2177
|
gameOutput: values.gameOutput,
|
|
1983
2178
|
help: values.help,
|
|
1984
2179
|
outputFile: values.outputFile,
|
|
2180
|
+
passWithNoTests: values.passWithNoTests,
|
|
1985
2181
|
pollInterval,
|
|
1986
2182
|
port,
|
|
1987
2183
|
project: values.project,
|
|
@@ -2423,6 +2619,7 @@ async function runMultiProject(cli, rootConfig, projectEntries) {
|
|
|
2423
2619
|
tsconfig: rootConfig.typecheckTsconfig
|
|
2424
2620
|
}) : void 0;
|
|
2425
2621
|
if (projectResults.length === 0 && typecheckResult === void 0) {
|
|
2622
|
+
if (rootConfig.passWithNoTests) return 0;
|
|
2426
2623
|
console.error("No test files found in any project");
|
|
2427
2624
|
return 2;
|
|
2428
2625
|
}
|
|
@@ -2451,12 +2648,14 @@ async function runSingleProject(config, cliFiles) {
|
|
|
2451
2648
|
resolveSetupFilePaths(config);
|
|
2452
2649
|
const discovery = discoverTestFiles(config, cliFiles);
|
|
2453
2650
|
if (discovery.files.length === 0) {
|
|
2651
|
+
if (config.passWithNoTests) return 0;
|
|
2454
2652
|
console.error("No test files found");
|
|
2455
2653
|
return 2;
|
|
2456
2654
|
}
|
|
2457
2655
|
const typeTestFiles = config.typecheck ? discovery.files.filter((file) => TYPE_TEST_PATTERN.test(file)) : [];
|
|
2458
2656
|
const runtimeTestFiles = config.typecheckOnly ? [] : discovery.files.filter((file) => !TYPE_TEST_PATTERN.test(file));
|
|
2459
2657
|
if (typeTestFiles.length === 0 && runtimeTestFiles.length === 0) {
|
|
2658
|
+
if (config.passWithNoTests) return 0;
|
|
2460
2659
|
console.error("No test files found for the selected mode");
|
|
2461
2660
|
return 2;
|
|
2462
2661
|
}
|
|
@@ -2568,6 +2767,7 @@ function mergeCliWithConfig(cli, config) {
|
|
|
2568
2767
|
formatters: resolveFormatters(cli, config),
|
|
2569
2768
|
gameOutput: cli.gameOutput ?? config.gameOutput,
|
|
2570
2769
|
outputFile: cli.outputFile ?? config.outputFile,
|
|
2770
|
+
passWithNoTests: cli.passWithNoTests ?? config.passWithNoTests,
|
|
2571
2771
|
pollInterval: cli.pollInterval ?? config.pollInterval,
|
|
2572
2772
|
port: cli.port ?? config.port,
|
|
2573
2773
|
rojoProject: cli.rojoProject ?? config.rojoProject,
|
|
@@ -825,6 +825,7 @@ interface Config extends Except<Argv, "projects" | "rootDir" | "setupFiles" | "s
|
|
|
825
825
|
gameOutput?: string;
|
|
826
826
|
jestPath?: string;
|
|
827
827
|
luauRoots?: Array<string>;
|
|
828
|
+
passWithNoTests?: boolean;
|
|
828
829
|
placeFile?: string;
|
|
829
830
|
pollInterval?: number;
|
|
830
831
|
port?: number;
|
|
@@ -852,6 +853,7 @@ interface ResolvedConfig extends Except<Config, "projects"> {
|
|
|
852
853
|
coverageDirectory: string;
|
|
853
854
|
coveragePathIgnorePatterns: Array<string>;
|
|
854
855
|
coverageReporters: Array<CoverageReporter>;
|
|
856
|
+
passWithNoTests: boolean;
|
|
855
857
|
placeFile: string;
|
|
856
858
|
pollInterval: number;
|
|
857
859
|
port: number;
|
|
@@ -884,6 +886,7 @@ interface CliOptions {
|
|
|
884
886
|
gameOutput?: string;
|
|
885
887
|
help?: boolean;
|
|
886
888
|
outputFile?: string;
|
|
889
|
+
passWithNoTests?: boolean;
|
|
887
890
|
pollInterval?: number;
|
|
888
891
|
port?: number;
|
|
889
892
|
project?: Array<string>;
|
|
@@ -4,7 +4,7 @@ import assert from "node:assert";
|
|
|
4
4
|
import * as fs$1 from "node:fs";
|
|
5
5
|
import { existsSync, readFileSync } from "node:fs";
|
|
6
6
|
import * as path$1 from "node:path";
|
|
7
|
-
import path from "node:path";
|
|
7
|
+
import path, { dirname, join, relative } from "node:path";
|
|
8
8
|
import process from "node:process";
|
|
9
9
|
import color from "tinyrainbow";
|
|
10
10
|
import { WebSocketServer } from "ws";
|
|
@@ -14,9 +14,9 @@ import * as crypto from "node:crypto";
|
|
|
14
14
|
import { randomUUID } from "node:crypto";
|
|
15
15
|
import buffer from "node:buffer";
|
|
16
16
|
import { defuFn } from "defu";
|
|
17
|
+
import { execFileSync } from "node:child_process";
|
|
17
18
|
import { TraceMap, originalPositionFor, sourceContentFor } from "@jridgewell/trace-mapping";
|
|
18
19
|
import { getTsconfig } from "get-tsconfig";
|
|
19
|
-
import { execFileSync } from "node:child_process";
|
|
20
20
|
import hljs from "highlight.js/lib/core";
|
|
21
21
|
import typescript from "highlight.js/lib/languages/typescript";
|
|
22
22
|
import { parseJSONC } from "confbox";
|
|
@@ -243,6 +243,7 @@ const DEFAULT_CONFIG = {
|
|
|
243
243
|
"**/rbxts_include/**"
|
|
244
244
|
],
|
|
245
245
|
coverageReporters: ["text", "lcov"],
|
|
246
|
+
passWithNoTests: false,
|
|
246
247
|
placeFile: "./game.rbxl",
|
|
247
248
|
pollInterval: 500,
|
|
248
249
|
port: 3001,
|
|
@@ -361,6 +362,7 @@ const configSchema = type({
|
|
|
361
362
|
"maxWorkers?": type("number").or(type("string")),
|
|
362
363
|
"noStackTrace?": "boolean",
|
|
363
364
|
"outputFile?": "string",
|
|
365
|
+
"passWithNoTests?": "boolean",
|
|
364
366
|
"placeFile?": "string",
|
|
365
367
|
"pollInterval?": "number",
|
|
366
368
|
"port?": "number",
|
|
@@ -822,6 +824,52 @@ function resolveFunctionValues(config) {
|
|
|
822
824
|
return resolved;
|
|
823
825
|
}
|
|
824
826
|
//#endregion
|
|
827
|
+
//#region src/utils/rojo-tree.ts
|
|
828
|
+
function resolveNestedProjects(tree, rootDirectory) {
|
|
829
|
+
return resolveTree(tree, rootDirectory, rootDirectory, /* @__PURE__ */ new Set());
|
|
830
|
+
}
|
|
831
|
+
function collectPaths(node, result) {
|
|
832
|
+
for (const [key, value] of Object.entries(node)) if (key === "$path" && typeof value === "string") result.push(value.replaceAll("\\", "/"));
|
|
833
|
+
else if (typeof value === "object" && !Array.isArray(value) && !key.startsWith("$")) collectPaths(value, result);
|
|
834
|
+
}
|
|
835
|
+
function inlineNestedProject(projectPath, currentDirectory, originalRoot, visited) {
|
|
836
|
+
const chain = new Set(visited);
|
|
837
|
+
chain.add(projectPath);
|
|
838
|
+
let content;
|
|
839
|
+
try {
|
|
840
|
+
content = readFileSync(projectPath, "utf-8");
|
|
841
|
+
} catch (err) {
|
|
842
|
+
const relativePath = relative(currentDirectory, projectPath);
|
|
843
|
+
throw new Error(`Could not read nested Rojo project: ${relativePath}`, { cause: err });
|
|
844
|
+
}
|
|
845
|
+
return resolveTree(JSON.parse(content).tree, dirname(projectPath), originalRoot, chain);
|
|
846
|
+
}
|
|
847
|
+
function resolveRootRelativePath(currentDirectory, value, originalRoot) {
|
|
848
|
+
return relative(originalRoot, join(currentDirectory, value)).replaceAll("\\", "/");
|
|
849
|
+
}
|
|
850
|
+
function resolveTree(node, currentDirectory, originalRoot, visited) {
|
|
851
|
+
const resolved = {};
|
|
852
|
+
for (const [key, value] of Object.entries(node)) {
|
|
853
|
+
if (key === "$path" && typeof value === "string" && value.endsWith(".project.json")) {
|
|
854
|
+
const projectPath = join(currentDirectory, value);
|
|
855
|
+
if (visited.has(projectPath)) throw new Error(`Circular project reference: ${value}`);
|
|
856
|
+
const innerTree = inlineNestedProject(projectPath, currentDirectory, originalRoot, visited);
|
|
857
|
+
for (const [innerKey, innerValue] of Object.entries(innerTree)) resolved[innerKey] = innerValue;
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
if (key === "$path" && typeof value === "string") {
|
|
861
|
+
resolved[key] = resolveRootRelativePath(currentDirectory, value, originalRoot);
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
if (key.startsWith("$") || typeof value !== "object" || Array.isArray(value)) {
|
|
865
|
+
resolved[key] = value;
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
resolved[key] = resolveTree(value, currentDirectory, originalRoot, visited);
|
|
869
|
+
}
|
|
870
|
+
return resolved;
|
|
871
|
+
}
|
|
872
|
+
//#endregion
|
|
825
873
|
//#region src/types/rojo.ts
|
|
826
874
|
const rojoProjectSchema = type({
|
|
827
875
|
"name": "string",
|
|
@@ -2487,9 +2535,13 @@ function buildSourceMapper(config, tsconfigMappings) {
|
|
|
2487
2535
|
try {
|
|
2488
2536
|
const rojoResult = rojoProjectSchema(JSON.parse(fs$1.readFileSync(rojoProjectPath, "utf-8")));
|
|
2489
2537
|
if (rojoResult instanceof type.errors) return;
|
|
2538
|
+
const resolvedTree = resolveNestedProjects(rojoResult.tree, path$1.dirname(rojoProjectPath));
|
|
2490
2539
|
return createSourceMapper({
|
|
2491
2540
|
mappings: tsconfigMappings,
|
|
2492
|
-
rojoProject:
|
|
2541
|
+
rojoProject: {
|
|
2542
|
+
...rojoResult,
|
|
2543
|
+
tree: resolvedTree
|
|
2544
|
+
}
|
|
2493
2545
|
});
|
|
2494
2546
|
} catch {
|
|
2495
2547
|
return;
|
|
@@ -2567,9 +2619,13 @@ function writeSnapshots(snapshotWrites, config, tsconfigMappings) {
|
|
|
2567
2619
|
process.stderr.write("Warning: Cannot write snapshots - invalid rojo project\n");
|
|
2568
2620
|
return;
|
|
2569
2621
|
}
|
|
2622
|
+
const resolvedTree = resolveNestedProjects(rojoResult.tree, path$1.dirname(rojoProjectPath));
|
|
2570
2623
|
const resolver = createSnapshotPathResolver({
|
|
2571
2624
|
mappings: tsconfigMappings,
|
|
2572
|
-
rojoProject:
|
|
2625
|
+
rojoProject: {
|
|
2626
|
+
...rojoResult,
|
|
2627
|
+
tree: resolvedTree
|
|
2628
|
+
}
|
|
2573
2629
|
});
|
|
2574
2630
|
let written = 0;
|
|
2575
2631
|
for (const [virtualPath, content] of Object.entries(snapshotWrites)) {
|
|
@@ -3027,4 +3083,4 @@ function writeGameOutput(filePath, entries) {
|
|
|
3027
3083
|
fs$1.writeFileSync(absolutePath, JSON.stringify(entries, null, 2));
|
|
3028
3084
|
}
|
|
3029
3085
|
//#endregion
|
|
3030
|
-
export {
|
|
3086
|
+
export { hashBuffer as A, extractJsonFromOutput as B, resolveNestedProjects as C, createStudioBackend as D, StudioBackend as E, VALID_BACKENDS as F, defineConfig as I, defineProject as L, generateTestScript as M, DEFAULT_CONFIG as N, OpenCloudBackend as O, ROOT_ONLY_KEYS as P, isValidBackend as R, collectPaths as S, resolveConfig as T, parseJestOutput as V, formatResult as _, formatAnnotations as a, formatBanner as b, execute as c, findFormatterOptions as d, formatJson as f, formatMultiProjectResult as g, formatFailure as h, runTypecheck as i, buildJestArgv as j, createOpenCloudBackend as k, formatExecuteOutput as l, formatCompactMultiProject as m, parseGameOutput as n, formatJobSummary as o, writeJsonFile as p, writeGameOutput as r, resolveGitHubActionsOptions as s, formatGameOutputNotice as t, loadCoverageManifest as u, formatTestSummary as v, loadConfig$1 as w, rojoProjectSchema as x, formatTypecheckSummary as y, LuauScriptError as z };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as defineConfig, C as FormatterEntry, D as ROOT_ONLY_KEYS, E as ProjectTestConfig, M as Argv, O as ResolvedConfig, S as DisplayName, T as ProjectEntry, _ as ResolvedProjectConfig, a as formatExecuteOutput, b as ConfigInput, c as Backend, d as extractJsonFromOutput, f as parseJestOutput, g as TestStatus, h as TestFileResult, i as execute, j as defineProject, k as SnapshotFormatOptions, l as BackendOptions, m as TestCaseResult, n as ExecuteResult, o as TimingResult, p as JestResult, r as FormatOutputOptions, s as SourceMapper, t as ExecuteOptions, u as BackendResult, v as CliOptions, w as InlineProjectConfig, x as DEFAULT_CONFIG, y as Config } from "./executor-
|
|
1
|
+
import { A as defineConfig, C as FormatterEntry, D as ROOT_ONLY_KEYS, E as ProjectTestConfig, M as Argv, O as ResolvedConfig, S as DisplayName, T as ProjectEntry, _ as ResolvedProjectConfig, a as formatExecuteOutput, b as ConfigInput, c as Backend, d as extractJsonFromOutput, f as parseJestOutput, g as TestStatus, h as TestFileResult, i as execute, j as defineProject, k as SnapshotFormatOptions, l as BackendOptions, m as TestCaseResult, n as ExecuteResult, o as TimingResult, p as JestResult, r as FormatOutputOptions, s as SourceMapper, t as ExecuteOptions, u as BackendResult, v as CliOptions, w as InlineProjectConfig, x as DEFAULT_CONFIG, y as Config } from "./executor-D6BzDfQ_.mjs";
|
|
2
2
|
import { WebSocket, WebSocketServer } from "ws";
|
|
3
3
|
import buffer from "node:buffer";
|
|
4
4
|
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { B as extractJsonFromOutput, D as createStudioBackend, E as StudioBackend, I as defineConfig, L as defineProject, M as generateTestScript, N as DEFAULT_CONFIG, O as OpenCloudBackend, P as ROOT_ONLY_KEYS, T as resolveConfig, V as parseJestOutput, _ as formatResult, a as formatAnnotations, c as execute, f as formatJson, h as formatFailure, i as runTypecheck, j as buildJestArgv, k as createOpenCloudBackend, l as formatExecuteOutput, n as parseGameOutput, o as formatJobSummary, p as writeJsonFile, r as writeGameOutput, t as formatGameOutputNotice, v as formatTestSummary, w as loadConfig } from "./game-output-BU-9pJ93.mjs";
|
|
2
2
|
export { DEFAULT_CONFIG, OpenCloudBackend, ROOT_ONLY_KEYS, StudioBackend, buildJestArgv, createOpenCloudBackend, createStudioBackend, defineConfig, defineProject, execute, extractJsonFromOutput, formatAnnotations, formatExecuteOutput, formatFailure, formatGameOutputNotice, formatJobSummary, formatJson, formatResult, formatTestSummary, generateTestScript, loadConfig, parseGameOutput, parseJestOutput, resolveConfig, runTypecheck, writeGameOutput, writeJsonFile };
|
package/dist/sea/jest-roblox
CHANGED
|
Binary file
|
package/dist/sea-entry.cjs
CHANGED
|
@@ -7640,7 +7640,7 @@ function f$9() {
|
|
|
7640
7640
|
var C$5 = f$9();
|
|
7641
7641
|
//#endregion
|
|
7642
7642
|
//#region package.json
|
|
7643
|
-
var version = "0.1.
|
|
7643
|
+
var version = "0.1.3";
|
|
7644
7644
|
//#endregion
|
|
7645
7645
|
//#region node_modules/.pnpm/ws@8.18.0/node_modules/ws/lib/stream.js
|
|
7646
7646
|
var require_stream = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
@@ -11358,7 +11358,7 @@ function normalizeString(path, allowAboveRoot) {
|
|
|
11358
11358
|
}
|
|
11359
11359
|
return res;
|
|
11360
11360
|
}
|
|
11361
|
-
var _DRIVE_LETTER_START_RE, _UNC_REGEX, _IS_ABSOLUTE_RE, _DRIVE_LETTER_RE, _ROOT_FOLDER_RE, _EXTNAME_RE, normalize, join$2, resolve$2, isAbsolute$1, extname$2, relative, dirname$1, basename;
|
|
11361
|
+
var _DRIVE_LETTER_START_RE, _UNC_REGEX, _IS_ABSOLUTE_RE, _DRIVE_LETTER_RE, _ROOT_FOLDER_RE, _EXTNAME_RE, normalize, join$2, resolve$2, isAbsolute$1, extname$2, relative$1, dirname$1, basename;
|
|
11362
11362
|
var init_pathe_M_eThtNZ = __esmMin((() => {
|
|
11363
11363
|
_DRIVE_LETTER_START_RE = /^[A-Za-z]:\//;
|
|
11364
11364
|
_UNC_REGEX = /^[/\\]{2}/;
|
|
@@ -11420,7 +11420,7 @@ var init_pathe_M_eThtNZ = __esmMin((() => {
|
|
|
11420
11420
|
const match = _EXTNAME_RE.exec(normalizeWindowsPath$1(p));
|
|
11421
11421
|
return match && match[1] || "";
|
|
11422
11422
|
};
|
|
11423
|
-
relative = function(from, to) {
|
|
11423
|
+
relative$1 = function(from, to) {
|
|
11424
11424
|
const _from = resolve$2(from).replace(_ROOT_FOLDER_RE, "$1").split("/");
|
|
11425
11425
|
const _to = resolve$2(to).replace(_ROOT_FOLDER_RE, "$1").split("/");
|
|
11426
11426
|
if (_to[0][1] === ":" && _from[0][1] === ":" && _from[0] !== _to[0]) return _to.join("/");
|
|
@@ -35720,7 +35720,7 @@ function currentShell() {
|
|
|
35720
35720
|
function startShell(cwd) {
|
|
35721
35721
|
cwd = resolve$2(cwd);
|
|
35722
35722
|
const shell = currentShell();
|
|
35723
|
-
console.info(`(experimental) Opening shell in ${relative(process.cwd(), cwd)}...`);
|
|
35723
|
+
console.info(`(experimental) Opening shell in ${relative$1(process.cwd(), cwd)}...`);
|
|
35724
35724
|
(0, node_child_process.spawnSync)(shell, [], {
|
|
35725
35725
|
cwd,
|
|
35726
35726
|
shell: true,
|
|
@@ -41031,6 +41031,7 @@ const DEFAULT_CONFIG = {
|
|
|
41031
41031
|
"**/rbxts_include/**"
|
|
41032
41032
|
],
|
|
41033
41033
|
coverageReporters: ["text", "lcov"],
|
|
41034
|
+
passWithNoTests: false,
|
|
41034
41035
|
placeFile: "./game.rbxl",
|
|
41035
41036
|
pollInterval: 500,
|
|
41036
41037
|
port: 3001,
|
|
@@ -41149,6 +41150,7 @@ const configSchema = type({
|
|
|
41149
41150
|
"maxWorkers?": type("number").or(type("string")),
|
|
41150
41151
|
"noStackTrace?": "boolean",
|
|
41151
41152
|
"outputFile?": "string",
|
|
41153
|
+
"passWithNoTests?": "boolean",
|
|
41152
41154
|
"placeFile?": "string",
|
|
41153
41155
|
"pollInterval?": "number",
|
|
41154
41156
|
"port?": "number",
|
|
@@ -41703,39 +41705,141 @@ function stripTsExtension(pattern) {
|
|
|
41703
41705
|
//#endregion
|
|
41704
41706
|
//#region src/utils/rojo-tree.ts
|
|
41705
41707
|
function resolveNestedProjects(tree, rootDirectory) {
|
|
41706
|
-
return resolveTree(tree, rootDirectory, /* @__PURE__ */ new Set());
|
|
41708
|
+
return resolveTree(tree, rootDirectory, rootDirectory, /* @__PURE__ */ new Set());
|
|
41707
41709
|
}
|
|
41708
41710
|
function collectPaths(node, result) {
|
|
41709
41711
|
for (const [key, value] of Object.entries(node)) if (key === "$path" && typeof value === "string") result.push(value.replaceAll("\\", "/"));
|
|
41710
41712
|
else if (typeof value === "object" && !Array.isArray(value) && !key.startsWith("$")) collectPaths(value, result);
|
|
41711
41713
|
}
|
|
41712
|
-
function
|
|
41714
|
+
function inlineNestedProject(projectPath, currentDirectory, originalRoot, visited) {
|
|
41715
|
+
const chain = new Set(visited);
|
|
41716
|
+
chain.add(projectPath);
|
|
41717
|
+
let content;
|
|
41718
|
+
try {
|
|
41719
|
+
content = (0, node_fs.readFileSync)(projectPath, "utf-8");
|
|
41720
|
+
} catch (err) {
|
|
41721
|
+
const relativePath = (0, node_path.relative)(currentDirectory, projectPath);
|
|
41722
|
+
throw new Error(`Could not read nested Rojo project: ${relativePath}`, { cause: err });
|
|
41723
|
+
}
|
|
41724
|
+
return resolveTree(JSON.parse(content).tree, (0, node_path.dirname)(projectPath), originalRoot, chain);
|
|
41725
|
+
}
|
|
41726
|
+
function resolveRootRelativePath(currentDirectory, value, originalRoot) {
|
|
41727
|
+
return (0, node_path.relative)(originalRoot, (0, node_path.join)(currentDirectory, value)).replaceAll("\\", "/");
|
|
41728
|
+
}
|
|
41729
|
+
function resolveTree(node, currentDirectory, originalRoot, visited) {
|
|
41713
41730
|
const resolved = {};
|
|
41714
41731
|
for (const [key, value] of Object.entries(node)) {
|
|
41715
41732
|
if (key === "$path" && typeof value === "string" && value.endsWith(".project.json")) {
|
|
41716
|
-
const projectPath = (0, node_path.join)(
|
|
41733
|
+
const projectPath = (0, node_path.join)(currentDirectory, value);
|
|
41717
41734
|
if (visited.has(projectPath)) throw new Error(`Circular project reference: ${value}`);
|
|
41718
|
-
const
|
|
41719
|
-
chain.add(projectPath);
|
|
41720
|
-
let content;
|
|
41721
|
-
try {
|
|
41722
|
-
content = (0, node_fs.readFileSync)(projectPath, "utf-8");
|
|
41723
|
-
} catch (err) {
|
|
41724
|
-
throw new Error(`Could not read nested Rojo project: ${value}`, { cause: err });
|
|
41725
|
-
}
|
|
41726
|
-
const innerTree = resolveTree(JSON.parse(content).tree, (0, node_path.dirname)(projectPath), chain);
|
|
41735
|
+
const innerTree = inlineNestedProject(projectPath, currentDirectory, originalRoot, visited);
|
|
41727
41736
|
for (const [innerKey, innerValue] of Object.entries(innerTree)) resolved[innerKey] = innerValue;
|
|
41728
41737
|
continue;
|
|
41729
41738
|
}
|
|
41739
|
+
if (key === "$path" && typeof value === "string") {
|
|
41740
|
+
resolved[key] = resolveRootRelativePath(currentDirectory, value, originalRoot);
|
|
41741
|
+
continue;
|
|
41742
|
+
}
|
|
41730
41743
|
if (key.startsWith("$") || typeof value !== "object" || Array.isArray(value)) {
|
|
41731
41744
|
resolved[key] = value;
|
|
41732
41745
|
continue;
|
|
41733
41746
|
}
|
|
41734
|
-
resolved[key] = resolveTree(value,
|
|
41747
|
+
resolved[key] = resolveTree(value, currentDirectory, originalRoot, visited);
|
|
41735
41748
|
}
|
|
41736
41749
|
return resolved;
|
|
41737
41750
|
}
|
|
41738
41751
|
//#endregion
|
|
41752
|
+
//#region src/luau/eval-literals.ts
|
|
41753
|
+
/**
|
|
41754
|
+
* Evaluate the first return expression in a Lute-stripped AST root block,
|
|
41755
|
+
* supporting only literal values (string, boolean, number, nil, table, cast).
|
|
41756
|
+
*
|
|
41757
|
+
* Accepts `unknown` and narrows safely — no type casts on JSON.parse needed.
|
|
41758
|
+
*/
|
|
41759
|
+
function evalLuauReturnLiterals(root) {
|
|
41760
|
+
if (!isObject(root) || !Array.isArray(root["statements"])) throw new Error("Config file has no return statement");
|
|
41761
|
+
const returnStat = root["statements"].find((stat) => isObject(stat) && stat["tag"] === "return");
|
|
41762
|
+
if (!isObject(returnStat) || !Array.isArray(returnStat["expressions"])) throw new Error("Config file has no return statement");
|
|
41763
|
+
const first = returnStat["expressions"][0];
|
|
41764
|
+
if (!isObject(first) || !("node" in first)) throw new Error("Return statement has no expressions");
|
|
41765
|
+
return evalExpr(first["node"]);
|
|
41766
|
+
}
|
|
41767
|
+
function isObject(value) {
|
|
41768
|
+
return typeof value === "object" && value !== null;
|
|
41769
|
+
}
|
|
41770
|
+
function evalExpr(node) {
|
|
41771
|
+
if (!isObject(node)) return;
|
|
41772
|
+
let current = node;
|
|
41773
|
+
while (current["tag"] === "cast" && isObject(current["operand"])) current = current["operand"];
|
|
41774
|
+
const { tag } = current;
|
|
41775
|
+
if (tag === "boolean" || tag === "number") return current["value"];
|
|
41776
|
+
if (tag === "string") return current["text"];
|
|
41777
|
+
if (tag === "table" && Array.isArray(current["entries"])) return evalTable(current["entries"]);
|
|
41778
|
+
}
|
|
41779
|
+
function evalTable(entries) {
|
|
41780
|
+
if (entries.length === 0) return {};
|
|
41781
|
+
const first = entries[0];
|
|
41782
|
+
if (isObject(first) && first["kind"] === "list") return entries.map((entry) => isObject(entry) ? evalExpr(entry["value"]) : void 0);
|
|
41783
|
+
const result = {};
|
|
41784
|
+
for (const entry of entries) {
|
|
41785
|
+
if (!isObject(entry) || entry["kind"] !== "record") continue;
|
|
41786
|
+
const { key, value } = entry;
|
|
41787
|
+
if (isObject(key) && typeof key["text"] === "string") result[key["text"]] = evalExpr(value);
|
|
41788
|
+
}
|
|
41789
|
+
return result;
|
|
41790
|
+
}
|
|
41791
|
+
//#endregion
|
|
41792
|
+
//#region src/luau/parse-ast.luau
|
|
41793
|
+
var parse_ast_default = "local fs = require(\"@std/fs\")\nlocal json = require(\"@std/json\")\nlocal process = require(\"@std/process\")\nlocal syntax = require(\"@std/syntax\")\n\nlocal rawArgs = process.args\nlocal userArgs: { string } = {}\nlocal pastSeparator = false\n\nfor _, arg in rawArgs do\n if pastSeparator then\n table.insert(userArgs, arg)\n elseif arg == \"--\" then\n pastSeparator = true\n end\nend\n\nlocal luauRoot = userArgs[1]\nif not luauRoot then\n error(\"Usage: lute run parse-ast.luau -- <file.luau | luau-root> [output-dir]\")\nend\n\nluauRoot = string.gsub(luauRoot, \"\\\\\", \"/\")\n\n-- Fields to keep per AST tag (beyond tag/kind/location which are always kept).\n-- Tags shared by stat/expr variants are merged — nil fields are harmless.\nlocal KEEP: { [string]: { string } } = {\n assign = { \"values\", \"variables\" },\n binary = { \"lhsoperand\", \"rhsoperand\" },\n block = { \"statements\" },\n boolean = { \"value\" },\n call = { \"arguments\", \"func\" },\n cast = { \"operand\" },\n compoundassign = { \"value\", \"variable\" },\n conditional = { \"condition\", \"thenblock\", \"elseifs\", \"elseblock\", \"thenexpr\", \"elseexpr\" },\n [\"do\"] = { \"body\" },\n expression = { \"expression\" },\n [\"for\"] = { \"body\", \"from\", \"to\", \"step\" },\n forin = { \"body\", \"values\" },\n [\"function\"] = { \"body\", \"name\", \"func\" },\n global = { \"name\" },\n group = { \"expression\" },\n index = { \"expression\", \"index\" },\n indexname = { \"expression\", \"accessor\", \"index\" },\n instantiate = { \"expr\" },\n interpolatedstring = { \"expressions\" },\n [\"local\"] = { \"values\", \"variables\" },\n localfunction = { \"name\", \"func\" },\n number = { \"value\" },\n [\"repeat\"] = { \"body\", \"condition\" },\n [\"return\"] = { \"expressions\" },\n string = { \"text\" },\n table = { \"entries\" },\n unary = { \"operand\" },\n [\"while\"] = { \"body\", \"condition\" },\n}\n\nlocal function strip(value: any): any\n if type(value) ~= \"table\" then\n return value\n end\n\n -- LuauSpan — has beginline, no tag\n if value.beginline ~= nil then\n return value\n end\n\n -- Token — has text but no tag, reduce to {text}\n if value.text ~= nil and value.tag == nil then\n return { text = value.text }\n end\n\n -- AST node — has tag, keep only allowlisted fields\n if value.tag ~= nil then\n local result = { tag = value.tag, kind = value.kind, location = value.location }\n local fields = KEEP[value.tag :: string]\n if fields then\n for _, field in fields do\n if value[field] ~= nil then\n result[field] = strip(value[field])\n end\n end\n end\n\n return result\n end\n\n -- Other tables (arrays, Pairs, ElseIf structs) — recurse all fields\n local result: any = {}\n for k, v in value do\n result[k] = strip(v)\n end\n\n return result\nend\n\n-- Single-file mode: parse one file, print stripped AST to stdout\nif string.sub(luauRoot, -5) == \".luau\" or string.sub(luauRoot, -4) == \".lua\" then\n local source = fs.readfiletostring(luauRoot)\n local parseResult = syntax.parse(source)\n print(json.serialize(strip(parseResult.root)))\n return\nend\n\nlocal outputDir = userArgs[2]\nif not outputDir then\n error(\"Usage: lute run parse-ast.luau -- <luau-root> <output-dir>\")\nend\n\noutputDir = string.gsub(outputDir, \"\\\\\", \"/\")\n\n-- Discover .luau files recursively, skipping node_modules, dot dirs, spec/test files\nlocal function discoverFiles(directory: string, relativeTo: string, results: { string })\n local entries = fs.listdirectory(directory)\n for _, entry in entries do\n local fullPath = directory .. \"/\" .. entry.name\n if entry.type == \"dir\" then\n if entry.name == \"node_modules\" or entry.name == \".jest-roblox-coverage\" then\n continue\n end\n\n if string.sub(entry.name, 1, 1) == \".\" then\n continue\n end\n\n discoverFiles(fullPath, relativeTo, results)\n elseif\n entry.type == \"file\"\n and (string.sub(entry.name, -5) == \".luau\" or string.sub(entry.name, -4) == \".lua\")\n then\n if\n string.sub(entry.name, -10) == \".spec.luau\"\n or string.sub(entry.name, -10) == \".test.luau\"\n or string.sub(entry.name, -9) == \".spec.lua\"\n or string.sub(entry.name, -9) == \".test.lua\"\n or string.sub(entry.name, -10) == \".snap.luau\"\n or string.sub(entry.name, -9) == \".snap.lua\"\n then\n continue\n end\n\n -- Compute relative path\n local relative = string.sub(fullPath, #relativeTo + 2)\n table.insert(results, relative)\n end\n end\nend\n\nlocal function dirname(filepath: string): string\n local pos = string.find(filepath, \"/[^/]*$\")\n if pos then\n return string.sub(filepath, 1, pos - 1)\n end\n\n return \"\"\nend\n\nlocal files: { string } = {}\ndiscoverFiles(luauRoot, luauRoot, files)\n\n-- Parse, strip, and write per-file AST JSON\nfor _, relativePath in files do\n local fullPath = luauRoot .. \"/\" .. relativePath\n local source = fs.readfiletostring(fullPath)\n local parseResult = syntax.parse(source)\n local stripped = strip(parseResult.root)\n\n local outPath = outputDir .. \"/\" .. relativePath .. \".json\"\n local dir = dirname(outPath)\n if dir ~= \"\" then\n fs.createdirectory(dir, { makeparents = true })\n end\n\n fs.writestringtofile(outPath, json.serialize(stripped))\nend\n\n-- Print file list to stdout (tiny — just paths)\nprint(json.serialize(files :: json.array))\n";
|
|
41794
|
+
//#endregion
|
|
41795
|
+
//#region src/config/luau-config-loader.ts
|
|
41796
|
+
let cachedTemporaryDirectory$1;
|
|
41797
|
+
/**
|
|
41798
|
+
* Parse a .luau config file via Lute and evaluate its return expression.
|
|
41799
|
+
*/
|
|
41800
|
+
function loadLuauConfig(filePath) {
|
|
41801
|
+
const temporaryDirectory = getTemporaryDirectory$1();
|
|
41802
|
+
const scriptPath = node_path.join(temporaryDirectory, "parse-ast.luau");
|
|
41803
|
+
node_fs.writeFileSync(scriptPath, parse_ast_default);
|
|
41804
|
+
let stdout;
|
|
41805
|
+
try {
|
|
41806
|
+
stdout = node_child_process.execFileSync("lute", [
|
|
41807
|
+
"run",
|
|
41808
|
+
scriptPath,
|
|
41809
|
+
"--",
|
|
41810
|
+
node_path.resolve(filePath)
|
|
41811
|
+
], {
|
|
41812
|
+
encoding: "utf-8",
|
|
41813
|
+
maxBuffer: 1024 * 1024
|
|
41814
|
+
});
|
|
41815
|
+
} catch (err) {
|
|
41816
|
+
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");
|
|
41817
|
+
throw new Error(`Failed to evaluate Luau config ${filePath}`, { cause: err });
|
|
41818
|
+
}
|
|
41819
|
+
let ast;
|
|
41820
|
+
try {
|
|
41821
|
+
ast = JSON.parse(stdout);
|
|
41822
|
+
} catch (err) {
|
|
41823
|
+
throw new Error(`Failed to parse AST JSON from Luau config ${filePath}`, { cause: err });
|
|
41824
|
+
}
|
|
41825
|
+
const result = evalLuauReturnLiterals(ast);
|
|
41826
|
+
if (typeof result !== "object" || result === null) throw new Error(`Luau config ${filePath} must return a table`);
|
|
41827
|
+
return result;
|
|
41828
|
+
}
|
|
41829
|
+
/**
|
|
41830
|
+
* Check if `<cwd>/<directoryOrFile>/jest.config.luau` exists. Returns the
|
|
41831
|
+
* resolved path if found, undefined otherwise.
|
|
41832
|
+
*/
|
|
41833
|
+
function findLuauConfigFile(directoryOrFile, cwd) {
|
|
41834
|
+
const resolved = node_path.resolve(cwd, directoryOrFile, "jest.config.luau");
|
|
41835
|
+
if (node_fs.existsSync(resolved)) return resolved;
|
|
41836
|
+
}
|
|
41837
|
+
function getTemporaryDirectory$1() {
|
|
41838
|
+
if (cachedTemporaryDirectory$1 !== void 0 && node_fs.existsSync(cachedTemporaryDirectory$1)) return cachedTemporaryDirectory$1;
|
|
41839
|
+
cachedTemporaryDirectory$1 = node_fs.mkdtempSync(node_path.join(node_os.tmpdir(), "jest-roblox-luau-config-"));
|
|
41840
|
+
return cachedTemporaryDirectory$1;
|
|
41841
|
+
}
|
|
41842
|
+
//#endregion
|
|
41739
41843
|
//#region src/config/projects.ts
|
|
41740
41844
|
function extractStaticRoot(pattern) {
|
|
41741
41845
|
const globChars = new Set([
|
|
@@ -41835,6 +41939,8 @@ function resolveProjectConfig(project, rootConfig, rojoTree) {
|
|
|
41835
41939
|
};
|
|
41836
41940
|
}
|
|
41837
41941
|
async function loadProjectConfigFile(filePath, cwd) {
|
|
41942
|
+
const luauConfigPath = findLuauConfigFile(filePath, cwd);
|
|
41943
|
+
if (luauConfigPath !== void 0) return buildProjectConfigFromLuau(luauConfigPath, filePath);
|
|
41838
41944
|
let result;
|
|
41839
41945
|
try {
|
|
41840
41946
|
result = await loadConfig$1({
|
|
@@ -41876,6 +41982,38 @@ function mergeProjectConfig(rootConfig, project) {
|
|
|
41876
41982
|
for (const [key, value] of Object.entries(project)) if (!PROJECT_ONLY_KEYS.has(key) && value !== void 0) merged[key] = value;
|
|
41877
41983
|
return merged;
|
|
41878
41984
|
}
|
|
41985
|
+
const LUAU_BOOLEAN_KEYS = [
|
|
41986
|
+
"automock",
|
|
41987
|
+
"clearMocks",
|
|
41988
|
+
"injectGlobals",
|
|
41989
|
+
"mockDataModel",
|
|
41990
|
+
"resetMocks",
|
|
41991
|
+
"resetModules",
|
|
41992
|
+
"restoreMocks"
|
|
41993
|
+
];
|
|
41994
|
+
const LUAU_NUMBER_KEYS = ["slowTestThreshold", "testTimeout"];
|
|
41995
|
+
const LUAU_STRING_KEYS = ["testEnvironment"];
|
|
41996
|
+
const LUAU_STRING_ARRAY_KEYS = ["setupFiles", "setupFilesAfterEnv"];
|
|
41997
|
+
function copyLuauOptionalFields(raw, config) {
|
|
41998
|
+
const record = config;
|
|
41999
|
+
for (const key of LUAU_BOOLEAN_KEYS) if (typeof raw[key] === "boolean") record[key] = raw[key];
|
|
42000
|
+
for (const key of LUAU_NUMBER_KEYS) if (typeof raw[key] === "number") record[key] = raw[key];
|
|
42001
|
+
for (const key of LUAU_STRING_KEYS) if (typeof raw[key] === "string") record[key] = raw[key];
|
|
42002
|
+
for (const key of LUAU_STRING_ARRAY_KEYS) if (Array.isArray(raw[key])) record[key] = raw[key];
|
|
42003
|
+
}
|
|
42004
|
+
function buildProjectConfigFromLuau(luauConfigPath, directoryPath) {
|
|
42005
|
+
const raw = loadLuauConfig(luauConfigPath);
|
|
42006
|
+
const { displayName } = raw;
|
|
42007
|
+
if (typeof displayName !== "string" || displayName === "") throw new Error(`Luau config file "${luauConfigPath}" must have a displayName string`);
|
|
42008
|
+
const testMatch = Array.isArray(raw["testMatch"]) ? raw["testMatch"] : void 0;
|
|
42009
|
+
const config = {
|
|
42010
|
+
displayName,
|
|
42011
|
+
include: testMatch !== void 0 ? testMatch.map((pattern) => node_path.posix.join(directoryPath, `${pattern}.luau`)) : [node_path.posix.join(directoryPath, "**/*.spec.luau")]
|
|
42012
|
+
};
|
|
42013
|
+
if (testMatch !== void 0) config.testMatch = testMatch;
|
|
42014
|
+
copyLuauOptionalFields(raw, config);
|
|
42015
|
+
return config;
|
|
42016
|
+
}
|
|
41879
42017
|
function matchNodePath(childNode, targetPath, childDataModelPath) {
|
|
41880
42018
|
const nodePath = childNode.$path;
|
|
41881
42019
|
if (typeof nodePath !== "string") return;
|
|
@@ -49913,6 +50051,8 @@ function serializeToLuau(config) {
|
|
|
49913
50051
|
}
|
|
49914
50052
|
function generateProjectConfigs(projects) {
|
|
49915
50053
|
for (const project of projects) {
|
|
50054
|
+
const luauConfigPath = project.outputPath.replace(/\.lua$/, ".luau");
|
|
50055
|
+
if (node_fs.default.existsSync(luauConfigPath)) continue;
|
|
49916
50056
|
const content = serializeToLuau(project.config);
|
|
49917
50057
|
const directory = node_path.default.dirname(project.outputPath);
|
|
49918
50058
|
node_fs.default.mkdirSync(directory, { recursive: true });
|
|
@@ -50452,27 +50592,133 @@ function mapCoverageToTypeScript(coverageData, manifest) {
|
|
|
50452
50592
|
if (record === void 0) continue;
|
|
50453
50593
|
const resources = loadFileResources(record);
|
|
50454
50594
|
if (resources === void 0) continue;
|
|
50455
|
-
|
|
50456
|
-
|
|
50595
|
+
if (resources.traceMap === void 0) {
|
|
50596
|
+
passthroughFileStatements(resources, fileCoverage, pendingStatements);
|
|
50597
|
+
passthroughFileFunctions(resources, fileCoverage, pendingFunctions);
|
|
50598
|
+
passthroughFileBranches(resources, fileCoverage, pendingBranches);
|
|
50599
|
+
} else {
|
|
50600
|
+
const mapped = {
|
|
50601
|
+
coverageMap: resources.coverageMap,
|
|
50602
|
+
traceMap: resources.traceMap
|
|
50603
|
+
};
|
|
50604
|
+
mapFileFunctions(mapped, fileCoverage, pendingFunctions, mapFileStatements(mapped, fileCoverage, pendingStatements));
|
|
50605
|
+
mapFileBranches(mapped, fileCoverage, pendingBranches);
|
|
50606
|
+
}
|
|
50457
50607
|
}
|
|
50458
50608
|
return buildResult(pendingStatements, pendingFunctions, pendingBranches);
|
|
50459
50609
|
}
|
|
50460
50610
|
function loadFileResources(record) {
|
|
50461
50611
|
let coverageMapRaw;
|
|
50462
|
-
let sourceMapRaw;
|
|
50463
50612
|
try {
|
|
50464
50613
|
coverageMapRaw = node_fs.readFileSync(record.coverageMapPath, "utf-8");
|
|
50465
|
-
sourceMapRaw = node_fs.readFileSync(record.sourceMapPath, "utf-8");
|
|
50466
50614
|
} catch {
|
|
50467
50615
|
return;
|
|
50468
50616
|
}
|
|
50469
50617
|
const parsed = coverageMapSchema(JSON.parse(coverageMapRaw));
|
|
50470
50618
|
if (parsed instanceof type.errors) return;
|
|
50619
|
+
let traceMap;
|
|
50620
|
+
try {
|
|
50621
|
+
traceMap = new TraceMap(node_fs.readFileSync(record.sourceMapPath, "utf-8"));
|
|
50622
|
+
} catch {}
|
|
50471
50623
|
return {
|
|
50472
50624
|
coverageMap: parsed,
|
|
50473
|
-
|
|
50625
|
+
sourceKey: record.key,
|
|
50626
|
+
traceMap
|
|
50474
50627
|
};
|
|
50475
50628
|
}
|
|
50629
|
+
function toIstanbulColumn(luauColumn) {
|
|
50630
|
+
return Math.max(0, luauColumn - 1);
|
|
50631
|
+
}
|
|
50632
|
+
function passthroughFileStatements(resources, fileCoverage, pending) {
|
|
50633
|
+
let fileStatements = pending.get(resources.sourceKey);
|
|
50634
|
+
if (fileStatements === void 0) {
|
|
50635
|
+
fileStatements = /* @__PURE__ */ new Map();
|
|
50636
|
+
pending.set(resources.sourceKey, fileStatements);
|
|
50637
|
+
}
|
|
50638
|
+
for (const [statementId, rawSpan] of Object.entries(resources.coverageMap.statementMap)) {
|
|
50639
|
+
const span = spanSchema(rawSpan);
|
|
50640
|
+
if (span instanceof type.errors) continue;
|
|
50641
|
+
const hitCount = fileCoverage.s[statementId] ?? 0;
|
|
50642
|
+
fileStatements.set(statementId, {
|
|
50643
|
+
end: {
|
|
50644
|
+
column: toIstanbulColumn(span.end.column),
|
|
50645
|
+
line: span.end.line
|
|
50646
|
+
},
|
|
50647
|
+
hitCount,
|
|
50648
|
+
start: {
|
|
50649
|
+
column: toIstanbulColumn(span.start.column),
|
|
50650
|
+
line: span.start.line
|
|
50651
|
+
}
|
|
50652
|
+
});
|
|
50653
|
+
}
|
|
50654
|
+
}
|
|
50655
|
+
function passthroughFileFunctions(resources, fileCoverage, pendingFunctions) {
|
|
50656
|
+
if (resources.coverageMap.functionMap === void 0) return;
|
|
50657
|
+
let fileFunctions = pendingFunctions.get(resources.sourceKey);
|
|
50658
|
+
if (fileFunctions === void 0) {
|
|
50659
|
+
fileFunctions = [];
|
|
50660
|
+
pendingFunctions.set(resources.sourceKey, fileFunctions);
|
|
50661
|
+
}
|
|
50662
|
+
for (const [functionId, rawEntry] of Object.entries(resources.coverageMap.functionMap)) {
|
|
50663
|
+
const entry = functionEntrySchema(rawEntry);
|
|
50664
|
+
if (entry instanceof type.errors) continue;
|
|
50665
|
+
fileFunctions.push({
|
|
50666
|
+
name: entry.name,
|
|
50667
|
+
hitCount: fileCoverage.f?.[functionId] ?? 0,
|
|
50668
|
+
loc: {
|
|
50669
|
+
end: {
|
|
50670
|
+
column: toIstanbulColumn(entry.location.end.column),
|
|
50671
|
+
line: entry.location.end.line
|
|
50672
|
+
},
|
|
50673
|
+
start: {
|
|
50674
|
+
column: toIstanbulColumn(entry.location.start.column),
|
|
50675
|
+
line: entry.location.start.line
|
|
50676
|
+
}
|
|
50677
|
+
}
|
|
50678
|
+
});
|
|
50679
|
+
}
|
|
50680
|
+
}
|
|
50681
|
+
function passthroughFileBranches(resources, fileCoverage, pendingBranches) {
|
|
50682
|
+
if (resources.coverageMap.branchMap === void 0) return;
|
|
50683
|
+
let fileBranches = pendingBranches.get(resources.sourceKey);
|
|
50684
|
+
if (fileBranches === void 0) {
|
|
50685
|
+
fileBranches = [];
|
|
50686
|
+
pendingBranches.set(resources.sourceKey, fileBranches);
|
|
50687
|
+
}
|
|
50688
|
+
for (const [branchId, rawEntry] of Object.entries(resources.coverageMap.branchMap)) {
|
|
50689
|
+
const entry = branchEntrySchema(rawEntry);
|
|
50690
|
+
if (entry instanceof type.errors) continue;
|
|
50691
|
+
const armHitCounts = fileCoverage.b?.[branchId] ?? [];
|
|
50692
|
+
const locations = [];
|
|
50693
|
+
for (const rawLocation of entry.locations) {
|
|
50694
|
+
const location = spanSchema(rawLocation);
|
|
50695
|
+
if (location instanceof type.errors) continue;
|
|
50696
|
+
locations.push({
|
|
50697
|
+
end: {
|
|
50698
|
+
column: toIstanbulColumn(location.end.column),
|
|
50699
|
+
line: location.end.line
|
|
50700
|
+
},
|
|
50701
|
+
start: {
|
|
50702
|
+
column: toIstanbulColumn(location.start.column),
|
|
50703
|
+
line: location.start.line
|
|
50704
|
+
}
|
|
50705
|
+
});
|
|
50706
|
+
}
|
|
50707
|
+
if (locations.length === 0) continue;
|
|
50708
|
+
const firstLocation = locations[0];
|
|
50709
|
+
const lastLocation = locations[locations.length - 1];
|
|
50710
|
+
(0, node_assert.default)(firstLocation !== void 0 && lastLocation !== void 0, "Branch locations must not be empty after filtering");
|
|
50711
|
+
fileBranches.push({
|
|
50712
|
+
armHitCounts: entry.locations.map((_, index) => armHitCounts[index] ?? 0),
|
|
50713
|
+
loc: {
|
|
50714
|
+
end: lastLocation.end,
|
|
50715
|
+
start: firstLocation.start
|
|
50716
|
+
},
|
|
50717
|
+
locations,
|
|
50718
|
+
type: entry.type
|
|
50719
|
+
});
|
|
50720
|
+
}
|
|
50721
|
+
}
|
|
50476
50722
|
function mapStatement(traceMap, span) {
|
|
50477
50723
|
const mappedStart = originalPositionFor(traceMap, {
|
|
50478
50724
|
column: Math.max(0, span.start.column - 1),
|
|
@@ -53219,26 +53465,7 @@ const rojoProjectSchema = type({
|
|
|
53219
53465
|
"tree": "object"
|
|
53220
53466
|
}).as();
|
|
53221
53467
|
//#endregion
|
|
53222
|
-
//#region src/
|
|
53223
|
-
const INSTRUMENTABLE_STATEMENT_TAGS = new Set([
|
|
53224
|
-
"assign",
|
|
53225
|
-
"break",
|
|
53226
|
-
"compoundassign",
|
|
53227
|
-
"conditional",
|
|
53228
|
-
"continue",
|
|
53229
|
-
"do",
|
|
53230
|
-
"expression",
|
|
53231
|
-
"for",
|
|
53232
|
-
"forin",
|
|
53233
|
-
"function",
|
|
53234
|
-
"local",
|
|
53235
|
-
"localfunction",
|
|
53236
|
-
"repeat",
|
|
53237
|
-
"return",
|
|
53238
|
-
"while"
|
|
53239
|
-
]);
|
|
53240
|
-
//#endregion
|
|
53241
|
-
//#region src/coverage/luau-visitor.ts
|
|
53468
|
+
//#region src/luau/visitor.ts
|
|
53242
53469
|
function visitExpression(expression, visitor) {
|
|
53243
53470
|
if (visitor.visitExpr?.(expression) === false) return;
|
|
53244
53471
|
const { tag } = expression;
|
|
@@ -53509,6 +53736,23 @@ function visitExprInstantiate(node, visitor) {
|
|
|
53509
53736
|
}
|
|
53510
53737
|
//#endregion
|
|
53511
53738
|
//#region src/coverage/coverage-collector.ts
|
|
53739
|
+
const INSTRUMENTABLE_STATEMENT_TAGS = new Set([
|
|
53740
|
+
"assign",
|
|
53741
|
+
"break",
|
|
53742
|
+
"compoundassign",
|
|
53743
|
+
"conditional",
|
|
53744
|
+
"continue",
|
|
53745
|
+
"do",
|
|
53746
|
+
"expression",
|
|
53747
|
+
"for",
|
|
53748
|
+
"forin",
|
|
53749
|
+
"function",
|
|
53750
|
+
"local",
|
|
53751
|
+
"localfunction",
|
|
53752
|
+
"repeat",
|
|
53753
|
+
"return",
|
|
53754
|
+
"while"
|
|
53755
|
+
]);
|
|
53512
53756
|
const END_KEYWORD_LENGTH = 3;
|
|
53513
53757
|
function collectCoverage(root) {
|
|
53514
53758
|
let statementIndex = 1;
|
|
@@ -53748,9 +53992,6 @@ function buildCoverageMap$1(result) {
|
|
|
53748
53992
|
};
|
|
53749
53993
|
}
|
|
53750
53994
|
//#endregion
|
|
53751
|
-
//#region src/coverage/parse-ast.luau
|
|
53752
|
-
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";
|
|
53753
|
-
//#endregion
|
|
53754
53995
|
//#region src/coverage/probe-inserter.ts
|
|
53755
53996
|
function insertProbes(source, result, fileKey) {
|
|
53756
53997
|
const lines = splitLines(source);
|
|
@@ -53805,7 +54046,7 @@ function applyProbes(mutableLines, probes) {
|
|
|
53805
54046
|
(0, node_assert.default)(line !== void 0, `Invalid probe line number: ${probeLine}`);
|
|
53806
54047
|
const before = line.slice(0, column - 1);
|
|
53807
54048
|
const after = line.slice(column - 1);
|
|
53808
|
-
mutableLines[lineIndex] = before + text + after;
|
|
54049
|
+
mutableLines[lineIndex] = before + (before.length > 0 && !/\s$/.test(before) && /^[a-zA-Z_]/.test(text) ? " " : "") + text + after;
|
|
53809
54050
|
}
|
|
53810
54051
|
}
|
|
53811
54052
|
function extractModeDirective(lines) {
|
|
@@ -60302,9 +60543,13 @@ function buildSourceMapper(config, tsconfigMappings) {
|
|
|
60302
60543
|
try {
|
|
60303
60544
|
const rojoResult = rojoProjectSchema(JSON.parse(node_fs.readFileSync(rojoProjectPath, "utf-8")));
|
|
60304
60545
|
if (rojoResult instanceof type.errors) return;
|
|
60546
|
+
const resolvedTree = resolveNestedProjects(rojoResult.tree, node_path.dirname(rojoProjectPath));
|
|
60305
60547
|
return createSourceMapper({
|
|
60306
60548
|
mappings: tsconfigMappings,
|
|
60307
|
-
rojoProject:
|
|
60549
|
+
rojoProject: {
|
|
60550
|
+
...rojoResult,
|
|
60551
|
+
tree: resolvedTree
|
|
60552
|
+
}
|
|
60308
60553
|
});
|
|
60309
60554
|
} catch {
|
|
60310
60555
|
return;
|
|
@@ -60382,9 +60627,13 @@ function writeSnapshots(snapshotWrites, config, tsconfigMappings) {
|
|
|
60382
60627
|
node_process.default.stderr.write("Warning: Cannot write snapshots - invalid rojo project\n");
|
|
60383
60628
|
return;
|
|
60384
60629
|
}
|
|
60630
|
+
const resolvedTree = resolveNestedProjects(rojoResult.tree, node_path.dirname(rojoProjectPath));
|
|
60385
60631
|
const resolver = createSnapshotPathResolver({
|
|
60386
60632
|
mappings: tsconfigMappings,
|
|
60387
|
-
rojoProject:
|
|
60633
|
+
rojoProject: {
|
|
60634
|
+
...rojoResult,
|
|
60635
|
+
tree: resolvedTree
|
|
60636
|
+
}
|
|
60388
60637
|
});
|
|
60389
60638
|
let written = 0;
|
|
60390
60639
|
for (const [virtualPath, content] of Object.entries(snapshotWrites)) {
|
|
@@ -60886,6 +61135,7 @@ Options:
|
|
|
60886
61135
|
--gameOutput <path> Write game output (print/warn/error) to file
|
|
60887
61136
|
--sourceMap Map Luau stack traces to TypeScript source
|
|
60888
61137
|
--rojoProject <path> Path to rojo project file (auto-detected if not set)
|
|
61138
|
+
--passWithNoTests Exit with 0 when no test files are found
|
|
60889
61139
|
--verbose Show individual test results
|
|
60890
61140
|
--silent Suppress output
|
|
60891
61141
|
--no-color Disable colored output
|
|
@@ -60947,6 +61197,7 @@ function parseArgs(args) {
|
|
|
60947
61197
|
"no-color": { type: "boolean" },
|
|
60948
61198
|
"no-show-luau": { type: "boolean" },
|
|
60949
61199
|
"outputFile": { type: "string" },
|
|
61200
|
+
"passWithNoTests": { type: "boolean" },
|
|
60950
61201
|
"pollInterval": { type: "string" },
|
|
60951
61202
|
"port": { type: "string" },
|
|
60952
61203
|
"project": {
|
|
@@ -61002,6 +61253,7 @@ function parseArgs(args) {
|
|
|
61002
61253
|
gameOutput: values.gameOutput,
|
|
61003
61254
|
help: values.help,
|
|
61004
61255
|
outputFile: values.outputFile,
|
|
61256
|
+
passWithNoTests: values.passWithNoTests,
|
|
61005
61257
|
pollInterval,
|
|
61006
61258
|
port,
|
|
61007
61259
|
project: values.project,
|
|
@@ -61443,6 +61695,7 @@ async function runMultiProject(cli, rootConfig, projectEntries) {
|
|
|
61443
61695
|
tsconfig: rootConfig.typecheckTsconfig
|
|
61444
61696
|
}) : void 0;
|
|
61445
61697
|
if (projectResults.length === 0 && typecheckResult === void 0) {
|
|
61698
|
+
if (rootConfig.passWithNoTests) return 0;
|
|
61446
61699
|
console.error("No test files found in any project");
|
|
61447
61700
|
return 2;
|
|
61448
61701
|
}
|
|
@@ -61471,12 +61724,14 @@ async function runSingleProject(config, cliFiles) {
|
|
|
61471
61724
|
resolveSetupFilePaths(config);
|
|
61472
61725
|
const discovery = discoverTestFiles(config, cliFiles);
|
|
61473
61726
|
if (discovery.files.length === 0) {
|
|
61727
|
+
if (config.passWithNoTests) return 0;
|
|
61474
61728
|
console.error("No test files found");
|
|
61475
61729
|
return 2;
|
|
61476
61730
|
}
|
|
61477
61731
|
const typeTestFiles = config.typecheck ? discovery.files.filter((file) => TYPE_TEST_PATTERN.test(file)) : [];
|
|
61478
61732
|
const runtimeTestFiles = config.typecheckOnly ? [] : discovery.files.filter((file) => !TYPE_TEST_PATTERN.test(file));
|
|
61479
61733
|
if (typeTestFiles.length === 0 && runtimeTestFiles.length === 0) {
|
|
61734
|
+
if (config.passWithNoTests) return 0;
|
|
61480
61735
|
console.error("No test files found for the selected mode");
|
|
61481
61736
|
return 2;
|
|
61482
61737
|
}
|
|
@@ -61588,6 +61843,7 @@ function mergeCliWithConfig(cli, config) {
|
|
|
61588
61843
|
formatters: resolveFormatters(cli, config),
|
|
61589
61844
|
gameOutput: cli.gameOutput ?? config.gameOutput,
|
|
61590
61845
|
outputFile: cli.outputFile ?? config.outputFile,
|
|
61846
|
+
passWithNoTests: cli.passWithNoTests ?? config.passWithNoTests,
|
|
61591
61847
|
pollInterval: cli.pollInterval ?? config.pollInterval,
|
|
61592
61848
|
port: cli.port ?? config.port,
|
|
61593
61849
|
rojoProject: cli.rojoProject ?? config.rojoProject,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@isentinel/jest-roblox",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Jest-compatible CLI for running roblox-ts tests via Roblox Open Cloud",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jest",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"@isentinel/tsconfig": "1.2.0",
|
|
61
61
|
"@oxc-project/types": "0.120.0",
|
|
62
62
|
"@rbxts/jest": "3.13.3-ts.1",
|
|
63
|
-
"@rbxts/types": "1.0.
|
|
63
|
+
"@rbxts/types": "1.0.912",
|
|
64
64
|
"@total-typescript/shoehorn": "0.1.2",
|
|
65
65
|
"@tsdown/exe": "0.21.4",
|
|
66
66
|
"@types/istanbul-lib-coverage": "2.0.6",
|