@isentinel/jest-roblox 0.3.8 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/jest-roblox.js +4 -7
- package/dist/cli.mjs +3 -2
- package/dist/index.d.mts +73 -1
- package/dist/index.mjs +52 -2
- package/dist/{run-Cirdb_CB.mjs → run-BM5sqclu.mjs} +381 -237
- package/package.json +6 -6
|
@@ -36,7 +36,7 @@ import { once } from "node:events";
|
|
|
36
36
|
import { parseJSONC, parseYAML } from "confbox";
|
|
37
37
|
import { Visitor, parseSync } from "oxc-parser";
|
|
38
38
|
//#region package.json
|
|
39
|
-
var version = "0.3.
|
|
39
|
+
var version = "0.3.9";
|
|
40
40
|
//#endregion
|
|
41
41
|
//#region src/config/errors.ts
|
|
42
42
|
var ConfigError = class extends Error {
|
|
@@ -438,7 +438,11 @@ function resolveConfig(config) {
|
|
|
438
438
|
const { test, ...rest } = config;
|
|
439
439
|
const definedRest = Object.fromEntries(Object.entries(rest).filter(([, value]) => value !== void 0));
|
|
440
440
|
const definedTest = test === void 0 ? {} : Object.fromEntries(Object.entries(test).filter(([, value]) => value !== void 0));
|
|
441
|
-
const resolved =
|
|
441
|
+
const resolved = {
|
|
442
|
+
...DEFAULT_CONFIG,
|
|
443
|
+
...definedTest,
|
|
444
|
+
...definedRest
|
|
445
|
+
};
|
|
442
446
|
if (config.gameOutput === true) resolved.gameOutput = path$1.join(resolved.rootDir, "game-output.log");
|
|
443
447
|
if (config.outputFile === true) resolved.outputFile = path$1.join(resolved.rootDir, "jest-output.log");
|
|
444
448
|
return resolved;
|
|
@@ -820,7 +824,7 @@ function passthroughFileBranches(resources, fileCoverage, pendingBranches) {
|
|
|
820
824
|
});
|
|
821
825
|
if (locations.length === 0) continue;
|
|
822
826
|
const firstLocation = locations[0];
|
|
823
|
-
const lastLocation = locations
|
|
827
|
+
const lastLocation = locations.at(-1);
|
|
824
828
|
assert(firstLocation !== void 0 && lastLocation !== void 0, "Branch locations must not be empty after filtering");
|
|
825
829
|
fileBranches.push({
|
|
826
830
|
armHitCounts: entry.locations.map((_, index) => armHitCounts[index] ?? 0),
|
|
@@ -1023,7 +1027,7 @@ function mapFileBranches(resources, fileCoverage, pendingBranches) {
|
|
|
1023
1027
|
pendingBranches.set(result.tsPath, fileBranches);
|
|
1024
1028
|
}
|
|
1025
1029
|
const firstLocation = result.locations[0];
|
|
1026
|
-
const lastLocation = result.locations
|
|
1030
|
+
const lastLocation = result.locations.at(-1);
|
|
1027
1031
|
assert(firstLocation !== void 0 && lastLocation !== void 0, "Branch locations must not be empty after successful mapping");
|
|
1028
1032
|
fileBranches.push({
|
|
1029
1033
|
armHitCounts: entry.locations.map((_, index) => armHitCounts[index] ?? 0),
|
|
@@ -1530,19 +1534,22 @@ function loadRojoProject(projectPath) {
|
|
|
1530
1534
|
tree
|
|
1531
1535
|
};
|
|
1532
1536
|
}
|
|
1537
|
+
const TrailingSlashPattern = /\/$/;
|
|
1533
1538
|
function collectMounts(node, currentDataModelPath, classify) {
|
|
1534
1539
|
const result = [];
|
|
1535
1540
|
walk(node, currentDataModelPath, classify, result);
|
|
1536
1541
|
return result;
|
|
1537
1542
|
}
|
|
1538
1543
|
function pruneAncestors(paths) {
|
|
1539
|
-
return paths.filter((candidate) =>
|
|
1544
|
+
return paths.filter((candidate) => {
|
|
1545
|
+
return paths.every((other) => other === candidate || !candidate.startsWith(`${other}/`));
|
|
1546
|
+
});
|
|
1540
1547
|
}
|
|
1541
1548
|
function addDirectoryMount(node, dataModelPath, classify, result) {
|
|
1542
1549
|
const rawPath = node.$path;
|
|
1543
1550
|
if (typeof rawPath !== "string") return;
|
|
1544
1551
|
if (rawPath.endsWith(".project.json")) return;
|
|
1545
|
-
const fsPath = rawPath.replace(
|
|
1552
|
+
const fsPath = rawPath.replace(TrailingSlashPattern, "");
|
|
1546
1553
|
if (classify(fsPath) === "directory") result.push({
|
|
1547
1554
|
dataModelPath,
|
|
1548
1555
|
fsPath
|
|
@@ -1559,10 +1566,11 @@ function walk(node, currentDataModelPath, classify, result) {
|
|
|
1559
1566
|
walk(value, childDataModelPath, classify, result);
|
|
1560
1567
|
}
|
|
1561
1568
|
}
|
|
1569
|
+
const TRAILING_SLASH$1$1 = /\/$/;
|
|
1562
1570
|
function matchNodePath(childNode, targetPath, childDataModelPath) {
|
|
1563
1571
|
const nodePath = childNode.$path;
|
|
1564
1572
|
if (typeof nodePath !== "string") return;
|
|
1565
|
-
const normalizedNodePath = nodePath.replace(
|
|
1573
|
+
const normalizedNodePath = nodePath.replace(TRAILING_SLASH$1$1, "");
|
|
1566
1574
|
if (normalizedNodePath === targetPath) return childDataModelPath;
|
|
1567
1575
|
if (targetPath.startsWith(`${normalizedNodePath}/`)) return `${childDataModelPath}/${targetPath.slice(normalizedNodePath.length + 1)}`;
|
|
1568
1576
|
}
|
|
@@ -1750,7 +1758,7 @@ var RojoResolver = class RojoResolver {
|
|
|
1750
1758
|
const relativePath = path.relative(partition.fsPath, stripped);
|
|
1751
1759
|
const relativeParts = relativePath === "" ? [] : relativePath.split(path.sep);
|
|
1752
1760
|
if (ROJO_SCRIPT_EXTS.has(extension) && relativeParts.at(-1) === INIT_NAME) relativeParts.pop();
|
|
1753
|
-
return partition.rbxPath
|
|
1761
|
+
return [...partition.rbxPath, ...relativeParts];
|
|
1754
1762
|
}
|
|
1755
1763
|
}
|
|
1756
1764
|
getRbxTypeFromFilePath(filePath) {
|
|
@@ -1777,8 +1785,8 @@ var RojoResolver = class RojoResolver {
|
|
|
1777
1785
|
rbxPath: partition.rbxPath.slice()
|
|
1778
1786
|
};
|
|
1779
1787
|
}),
|
|
1780
|
-
walkedConfigFiles:
|
|
1781
|
-
walkedDirs:
|
|
1788
|
+
walkedConfigFiles: [...this.walkedConfigFilesInternal],
|
|
1789
|
+
walkedDirs: [...this.walkedDirectoriesInternal],
|
|
1782
1790
|
warnings: this.warnings.slice()
|
|
1783
1791
|
};
|
|
1784
1792
|
}
|
|
@@ -1870,7 +1878,8 @@ var RojoResolver = class RojoResolver {
|
|
|
1870
1878
|
if (!doNotPush) this.rbxPath.push(name);
|
|
1871
1879
|
if (tree.$path !== void 0) this.parsePath(path.resolve(basePath, typeof tree.$path === "string" ? tree.$path : tree.$path.optional));
|
|
1872
1880
|
if (tree.$className === "DataModel") this.isGame = true;
|
|
1873
|
-
|
|
1881
|
+
const childNames = Object.keys(tree).filter((value) => !value.startsWith("$"));
|
|
1882
|
+
for (const childName of childNames) this.parseTree(basePath, childName, tree[childName]);
|
|
1874
1883
|
if (!doNotPush) this.rbxPath.pop();
|
|
1875
1884
|
}
|
|
1876
1885
|
searchChildren(directory, directoryEntries) {
|
|
@@ -1914,6 +1923,91 @@ var RojoResolver = class RojoResolver {
|
|
|
1914
1923
|
}
|
|
1915
1924
|
};
|
|
1916
1925
|
//#endregion
|
|
1926
|
+
//#region src/coverage-pipeline/raw-coverage.ts
|
|
1927
|
+
/**
|
|
1928
|
+
* Normalize a raw coverage table into typed {@link RawCoverageData}. The input
|
|
1929
|
+
* is the per-file hit table the coverage probes accumulate at runtime — the
|
|
1930
|
+
* `_G.__jest_roblox_cov` global, or the `_coverage` field of a run envelope —
|
|
1931
|
+
* keyed by the stable per-file join key (`fileKey`). Luau serializes the
|
|
1932
|
+
* `s`/`f` counters as 1-based arrays and `b` as an array of arrays; this
|
|
1933
|
+
* canonicalizes them to string-keyed records while leaving the fileKey verbatim
|
|
1934
|
+
* (it is the byte-identical join key the static maps are also keyed to). Returns
|
|
1935
|
+
* `undefined` when the input is not an object or carries no file with a
|
|
1936
|
+
* statement map.
|
|
1937
|
+
*/
|
|
1938
|
+
function normalizeRawCoverage(coverage) {
|
|
1939
|
+
if (coverage === void 0 || coverage === null || typeof coverage !== "object") return;
|
|
1940
|
+
const record = {};
|
|
1941
|
+
for (const [key, value] of Object.entries(coverage)) {
|
|
1942
|
+
if (typeof value !== "object" || value === null || !("s" in value)) continue;
|
|
1943
|
+
const raw = value;
|
|
1944
|
+
const file = { s: normalizeHitCounts(raw.s) };
|
|
1945
|
+
if (raw.f !== void 0) file.f = normalizeHitCounts(raw.f);
|
|
1946
|
+
if (raw.b !== void 0) file.b = normalizeBranchCounts(raw.b);
|
|
1947
|
+
record[key] = file;
|
|
1948
|
+
}
|
|
1949
|
+
return Object.keys(record).length > 0 ? record : void 0;
|
|
1950
|
+
}
|
|
1951
|
+
/**
|
|
1952
|
+
* Extract raw coverage from a completed run's result envelope — the companion
|
|
1953
|
+
* seam for a run this CLI did not launch. Accepts the plugin's `jestOutput`
|
|
1954
|
+
* (a JSON string or an already-parsed object), or the bare `_G.__jest_roblox_cov`
|
|
1955
|
+
* table read straight off the run. When an object carries a `_coverage` field it
|
|
1956
|
+
* is used; otherwise the object is treated as the hit table itself. Returns
|
|
1957
|
+
* `undefined` for malformed JSON or an envelope with no coverage.
|
|
1958
|
+
*
|
|
1959
|
+
* A multi-project result (`{ entries: [{ jestOutput }, …] }`) carries one
|
|
1960
|
+
* envelope per project; parse each `entries[i].jestOutput` and combine with
|
|
1961
|
+
* `mergeRawCoverage`.
|
|
1962
|
+
*/
|
|
1963
|
+
function parseCoverageEnvelope(output) {
|
|
1964
|
+
let parsed = output;
|
|
1965
|
+
if (typeof output === "string") try {
|
|
1966
|
+
parsed = JSON.parse(output);
|
|
1967
|
+
} catch {
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
if (parsed === null || typeof parsed !== "object") return;
|
|
1971
|
+
return normalizeRawCoverage("_coverage" in parsed ? parsed._coverage : parsed);
|
|
1972
|
+
}
|
|
1973
|
+
function coerceCount(value) {
|
|
1974
|
+
return typeof value === "number" ? value : 0;
|
|
1975
|
+
}
|
|
1976
|
+
function normalizeHitCounts(data) {
|
|
1977
|
+
if (Array.isArray(data)) {
|
|
1978
|
+
const result = {};
|
|
1979
|
+
for (const [index, element] of data.entries()) result[String(index + 1)] = coerceCount(element);
|
|
1980
|
+
return result;
|
|
1981
|
+
}
|
|
1982
|
+
if (typeof data === "object" && data !== null) {
|
|
1983
|
+
const result = {};
|
|
1984
|
+
for (const [key, value] of Object.entries(data)) result[key] = coerceCount(value);
|
|
1985
|
+
return result;
|
|
1986
|
+
}
|
|
1987
|
+
return {};
|
|
1988
|
+
}
|
|
1989
|
+
function coerceArms(value) {
|
|
1990
|
+
return Array.isArray(value) ? value.map(coerceCount) : [];
|
|
1991
|
+
}
|
|
1992
|
+
/**
|
|
1993
|
+
* Normalize branch hit counts from Luau's nested array format. Luau serializes
|
|
1994
|
+
* `__cov_b` as an array of arrays: `[[0,0,0], [0,0]]`. Convert the outer array
|
|
1995
|
+
* to a string-keyed Record with 1-based keys, coercing each arm.
|
|
1996
|
+
*/
|
|
1997
|
+
function normalizeBranchCounts(data) {
|
|
1998
|
+
if (Array.isArray(data)) {
|
|
1999
|
+
const result = {};
|
|
2000
|
+
for (const [index, inner] of data.entries()) result[String(index + 1)] = coerceArms(inner);
|
|
2001
|
+
return result;
|
|
2002
|
+
}
|
|
2003
|
+
if (typeof data === "object" && data !== null) {
|
|
2004
|
+
const result = {};
|
|
2005
|
+
for (const [key, value] of Object.entries(data)) result[key] = coerceArms(value);
|
|
2006
|
+
return result;
|
|
2007
|
+
}
|
|
2008
|
+
return {};
|
|
2009
|
+
}
|
|
2010
|
+
//#endregion
|
|
1917
2011
|
//#region src/reporter/parser.ts
|
|
1918
2012
|
const TASK_SCRIPT_PREFIX = /^TaskScript:\d+:\s*/;
|
|
1919
2013
|
var LuauScriptError = class extends Error {
|
|
@@ -1976,10 +2070,8 @@ function parseJestOutput(output) {
|
|
|
1976
2070
|
}
|
|
1977
2071
|
function countBraces(line) {
|
|
1978
2072
|
let count = 0;
|
|
1979
|
-
for (const character of line) {
|
|
1980
|
-
|
|
1981
|
-
if (character === "}") count--;
|
|
1982
|
-
}
|
|
2073
|
+
for (const character of line) if (character === "{") count++;
|
|
2074
|
+
else if (character === "}") count--;
|
|
1983
2075
|
return count;
|
|
1984
2076
|
}
|
|
1985
2077
|
function isValidJson(text) {
|
|
@@ -2030,56 +2122,8 @@ function extractLuauTiming(parsed) {
|
|
|
2030
2122
|
for (const [key, value] of Object.entries(timing)) if (typeof value === "number") record[key] = value;
|
|
2031
2123
|
return Object.keys(record).length > 0 ? record : void 0;
|
|
2032
2124
|
}
|
|
2033
|
-
/**
|
|
2034
|
-
* Luau 1-based integer-keyed tables serialize as JSON arrays.
|
|
2035
|
-
* Convert arrays to string-keyed Records with 1-based keys to match cov-map format.
|
|
2036
|
-
*/
|
|
2037
|
-
function normalizeHitCounts(data) {
|
|
2038
|
-
if (Array.isArray(data)) {
|
|
2039
|
-
const result = {};
|
|
2040
|
-
let index = 0;
|
|
2041
|
-
for (const element of data) {
|
|
2042
|
-
result[String(index + 1)] = typeof element === "number" ? element : 0;
|
|
2043
|
-
index++;
|
|
2044
|
-
}
|
|
2045
|
-
return result;
|
|
2046
|
-
}
|
|
2047
|
-
if (typeof data === "object" && data !== null) return data;
|
|
2048
|
-
return {};
|
|
2049
|
-
}
|
|
2050
|
-
/**
|
|
2051
|
-
* Normalize branch hit counts from Luau's nested array format.
|
|
2052
|
-
* Luau serializes `__cov_b` as an array of arrays: `[[0,0,0], [0,0]]`.
|
|
2053
|
-
* Convert outer array to string-keyed Record with 1-based keys,
|
|
2054
|
-
* keeping inner arrays as-is.
|
|
2055
|
-
*/
|
|
2056
|
-
function normalizeBranchCounts(data) {
|
|
2057
|
-
if (Array.isArray(data)) {
|
|
2058
|
-
const result = {};
|
|
2059
|
-
let index = 0;
|
|
2060
|
-
for (const inner of data) {
|
|
2061
|
-
if (Array.isArray(inner)) result[String(index + 1)] = inner.map((value) => typeof value === "number" ? value : 0);
|
|
2062
|
-
else result[String(index + 1)] = [];
|
|
2063
|
-
index++;
|
|
2064
|
-
}
|
|
2065
|
-
return result;
|
|
2066
|
-
}
|
|
2067
|
-
if (typeof data === "object" && data !== null) return data;
|
|
2068
|
-
return {};
|
|
2069
|
-
}
|
|
2070
2125
|
function extractCoverageData(parsed) {
|
|
2071
|
-
|
|
2072
|
-
if (coverage === void 0 || coverage === null || typeof coverage !== "object") return;
|
|
2073
|
-
const record = {};
|
|
2074
|
-
for (const [key, value] of Object.entries(coverage)) if (typeof value === "object" && value !== null && "s" in value) {
|
|
2075
|
-
const raw = value;
|
|
2076
|
-
record[key] = {
|
|
2077
|
-
b: raw.b !== void 0 ? normalizeBranchCounts(raw.b) : void 0,
|
|
2078
|
-
f: raw.f !== void 0 ? normalizeHitCounts(raw.f) : void 0,
|
|
2079
|
-
s: normalizeHitCounts(raw.s)
|
|
2080
|
-
};
|
|
2081
|
-
}
|
|
2082
|
-
return Object.keys(record).length > 0 ? record : void 0;
|
|
2126
|
+
return normalizeRawCoverage(parsed["_coverage"]);
|
|
2083
2127
|
}
|
|
2084
2128
|
function extractPerTestCoverage(parsed) {
|
|
2085
2129
|
const raw = parsed["_perTestCoverage"];
|
|
@@ -2507,13 +2551,14 @@ function replacePrefix(filePath, from, to) {
|
|
|
2507
2551
|
}
|
|
2508
2552
|
//#endregion
|
|
2509
2553
|
//#region src/source-mapper/column-finder.ts
|
|
2554
|
+
const EXPECT_CALL = /\bexpect\s*[.(]/;
|
|
2510
2555
|
/**
|
|
2511
2556
|
* Finds the column position of the failing matcher in an expect() call. Returns
|
|
2512
2557
|
* 1-indexed column position, or undefined if no expect is found.
|
|
2513
2558
|
*/
|
|
2514
2559
|
function findExpectationColumn(lineText) {
|
|
2515
2560
|
if (!lineText) return;
|
|
2516
|
-
const expectIndex = lineText.search(
|
|
2561
|
+
const expectIndex = lineText.search(EXPECT_CALL);
|
|
2517
2562
|
if (expectIndex === -1) return;
|
|
2518
2563
|
const afterExpect = lineText.slice(expectIndex);
|
|
2519
2564
|
const matcherRegex = /[.:]\s*([A-Za-z_$][\w$]*)\s*(?=\()/g;
|
|
@@ -2525,9 +2570,11 @@ function findExpectationColumn(lineText) {
|
|
|
2525
2570
|
}
|
|
2526
2571
|
//#endregion
|
|
2527
2572
|
//#region src/source-mapper/path-resolver.ts
|
|
2573
|
+
const INIT_SEGMENT = /(^|\/)(init)(\.|\/)/;
|
|
2574
|
+
const LEADING_DOT_SLASH = /^\.\//;
|
|
2528
2575
|
/** roblox-ts compiles index.ts → init.luau; reverse the rename for TS paths. */
|
|
2529
2576
|
function luauInitToIndex(filePath) {
|
|
2530
|
-
return filePath.replace(
|
|
2577
|
+
return filePath.replace(INIT_SEGMENT, "$1index$3");
|
|
2531
2578
|
}
|
|
2532
2579
|
function createPathResolver(rojoProject, config) {
|
|
2533
2580
|
const rojoMappings = /* @__PURE__ */ new Map();
|
|
@@ -2542,14 +2589,14 @@ function createPathResolver(rojoProject, config) {
|
|
|
2542
2589
|
}
|
|
2543
2590
|
walkTree(rojoProject.tree, "");
|
|
2544
2591
|
const tsconfigMappings = config?.mappings ?? [];
|
|
2545
|
-
const sortedRojoMappings = [...rojoMappings
|
|
2592
|
+
const sortedRojoMappings = [...rojoMappings].sort(([a], [b]) => b.length - a.length);
|
|
2546
2593
|
return { resolve(dataModelPath) {
|
|
2547
2594
|
for (const [prefix, basePath] of sortedRojoMappings) {
|
|
2548
2595
|
if (dataModelPath !== prefix && !dataModelPath.startsWith(`${prefix}.`)) continue;
|
|
2549
2596
|
const result = `${basePath}/${convertToFilePath(dataModelPath.slice(prefix.length + 1))}`;
|
|
2550
2597
|
const mapping = findMapping(result, tsconfigMappings);
|
|
2551
2598
|
if (mapping !== void 0) return {
|
|
2552
|
-
filePath: `${luauInitToIndex(replacePrefix(result, mapping.outDir, mapping.rootDir).replace(
|
|
2599
|
+
filePath: `${luauInitToIndex(replacePrefix(result, mapping.outDir, mapping.rootDir).replace(LEADING_DOT_SLASH, ""))}.ts`,
|
|
2553
2600
|
mapping
|
|
2554
2601
|
};
|
|
2555
2602
|
return { filePath: findLuaFile(result) };
|
|
@@ -2624,6 +2671,8 @@ function getTraceMap(luauPath) {
|
|
|
2624
2671
|
}
|
|
2625
2672
|
//#endregion
|
|
2626
2673
|
//#region src/source-mapper/index.ts
|
|
2674
|
+
const TS_EXTENSION = /\.ts$/;
|
|
2675
|
+
const LEADING_SLASH = /^\//;
|
|
2627
2676
|
function createSourceMapper(config) {
|
|
2628
2677
|
const pathResolver = createPathResolver(config.rojoProject, { mappings: config.mappings });
|
|
2629
2678
|
function mapFrame(frame) {
|
|
@@ -2634,7 +2683,7 @@ function createSourceMapper(config) {
|
|
|
2634
2683
|
mapped: void 0
|
|
2635
2684
|
};
|
|
2636
2685
|
const { filePath, mapping } = resolved;
|
|
2637
|
-
const luauPath = replacePrefix(filePath, mapping.rootDir, mapping.outDir).replace(
|
|
2686
|
+
const luauPath = replacePrefix(filePath, mapping.rootDir, mapping.outDir).replace(TS_EXTENSION, ".luau");
|
|
2638
2687
|
const v3Result = mapFromSourceMap(luauPath, frame.line, frame.column);
|
|
2639
2688
|
if (v3Result !== void 0 && v3Result.source !== null && v3Result.line !== null) {
|
|
2640
2689
|
const mapDirectory = path$1.dirname(luauPath);
|
|
@@ -2715,7 +2764,7 @@ function createSourceMapper(config) {
|
|
|
2715
2764
|
resolveTestFilePath
|
|
2716
2765
|
};
|
|
2717
2766
|
function resolveTestFilePath(testFilePath) {
|
|
2718
|
-
const dataModelPath = testFilePath.replace(
|
|
2767
|
+
const dataModelPath = testFilePath.replace(LEADING_SLASH, "").replaceAll("/", ".");
|
|
2719
2768
|
return pathResolver.resolve(dataModelPath)?.filePath;
|
|
2720
2769
|
}
|
|
2721
2770
|
}
|
|
@@ -2960,6 +3009,11 @@ function highlightTypeScript(source) {
|
|
|
2960
3009
|
//#endregion
|
|
2961
3010
|
//#region src/formatters/formatter.ts
|
|
2962
3011
|
const DEFAULT_SLOW_TEST_THRESHOLD_MS = 300;
|
|
3012
|
+
const SNAPSHOT_HEADER = /^- Snapshot\s+- \d+/;
|
|
3013
|
+
const EXPECTED_VALUE = /Expected\b.*?:\s*(.+)/;
|
|
3014
|
+
const RECEIVED_VALUE = /Received\b.*?:\s*(.+)/;
|
|
3015
|
+
const ROBLOX_PATH_CHAIN = /^(?:[A-Za-z][\w.@-]*:\d+:\s*)+/;
|
|
3016
|
+
const SOURCE_LOCATION = /([^\s:]+\.(?:tsx?|luau?)):(\d+)(?::(\d+))?/;
|
|
2963
3017
|
const EXEC_ERROR_HINTS = [[/loadstring\(\) is not available/, "loadstring() must be enabled for Jest to run. Add to your project.json:\n\n \"ServerScriptService\": {\n \"$properties\": {\n \"LoadStringEnabled\": true\n }\n }"]];
|
|
2964
3018
|
function getExecErrorHint(message) {
|
|
2965
3019
|
for (const [pattern, hint] of EXEC_ERROR_HINTS) if (pattern.test(message)) return hint;
|
|
@@ -2975,7 +3029,7 @@ function parseErrorMessage(message) {
|
|
|
2975
3029
|
const lines = message.split("\n");
|
|
2976
3030
|
const firstLine = lines[0];
|
|
2977
3031
|
assert(firstLine !== void 0, "split always returns ≥1 element");
|
|
2978
|
-
const snapshotHeaderIndex = lines.findIndex((line) =>
|
|
3032
|
+
const snapshotHeaderIndex = lines.findIndex((line) => SNAPSHOT_HEADER.test(line));
|
|
2979
3033
|
if (snapshotHeaderIndex !== -1) {
|
|
2980
3034
|
const diffLines = [];
|
|
2981
3035
|
for (let index = snapshotHeaderIndex; index < lines.length; index++) {
|
|
@@ -2988,8 +3042,8 @@ function parseErrorMessage(message) {
|
|
|
2988
3042
|
snapshotDiff: diffLines.join("\n").trimEnd()
|
|
2989
3043
|
};
|
|
2990
3044
|
}
|
|
2991
|
-
const expectedMatch = message.match(
|
|
2992
|
-
const receivedMatch = message.match(
|
|
3045
|
+
const expectedMatch = message.match(EXPECTED_VALUE);
|
|
3046
|
+
const receivedMatch = message.match(RECEIVED_VALUE);
|
|
2993
3047
|
return {
|
|
2994
3048
|
expected: expectedMatch?.[1],
|
|
2995
3049
|
message: firstLine,
|
|
@@ -3018,7 +3072,7 @@ function cleanExecErrorMessage(raw) {
|
|
|
3018
3072
|
}
|
|
3019
3073
|
}
|
|
3020
3074
|
if (contentLine === void 0) return raw.trim();
|
|
3021
|
-
return contentLine.replace(
|
|
3075
|
+
return contentLine.replace(ROBLOX_PATH_CHAIN, "");
|
|
3022
3076
|
}
|
|
3023
3077
|
function formatSourceSnippet(snippet, filePath, options) {
|
|
3024
3078
|
const useColor = options?.useColor ?? true;
|
|
@@ -3047,7 +3101,7 @@ function formatSourceSnippet(snippet, filePath, options) {
|
|
|
3047
3101
|
return lines.join("\n");
|
|
3048
3102
|
}
|
|
3049
3103
|
function parseSourceLocation(message) {
|
|
3050
|
-
const match = message.match(
|
|
3104
|
+
const match = message.match(SOURCE_LOCATION);
|
|
3051
3105
|
if (match === null) return;
|
|
3052
3106
|
const [, filePath, lineStr, columnStr] = match;
|
|
3053
3107
|
assert(filePath !== void 0, "regex group 1 matched");
|
|
@@ -3181,15 +3235,20 @@ function formatTypecheckSummary(result, useColor = true) {
|
|
|
3181
3235
|
parts.push(`\n${label} ${failedPart}${passedPart}, ${String(total)} total\n`);
|
|
3182
3236
|
return parts.join("\n");
|
|
3183
3237
|
}
|
|
3184
|
-
function
|
|
3185
|
-
const styles = createStyles(useColor);
|
|
3238
|
+
function formatTypecheckFileFailures(file, styles) {
|
|
3186
3239
|
const lines = [];
|
|
3187
|
-
for (const
|
|
3240
|
+
for (const test of file.testResults) {
|
|
3188
3241
|
if (test.status !== "failed") continue;
|
|
3189
3242
|
const badge = styles.failBadge(" FAIL ");
|
|
3190
3243
|
lines.push(` ${badge} ${styles.status.fail(test.fullName)}`);
|
|
3191
3244
|
for (const message of test.failureMessages) lines.push(` ${styles.dim(message)}`);
|
|
3192
3245
|
}
|
|
3246
|
+
return lines;
|
|
3247
|
+
}
|
|
3248
|
+
function formatTypecheckFailures(result, useColor = true) {
|
|
3249
|
+
const styles = createStyles(useColor);
|
|
3250
|
+
const lines = [];
|
|
3251
|
+
for (const file of result.testResults) lines.push(...formatTypecheckFileFailures(file, styles));
|
|
3193
3252
|
return lines.join("\n");
|
|
3194
3253
|
}
|
|
3195
3254
|
const PROJECT_BADGE_COLORS = [
|
|
@@ -3433,7 +3492,8 @@ function computeProjectStats(result) {
|
|
|
3433
3492
|
function formatFileFailures(file, options, styles, failureCtx) {
|
|
3434
3493
|
const lines = [];
|
|
3435
3494
|
const displayPath = resolveDisplayPath(file.testFilePath, options.sourceMapper);
|
|
3436
|
-
for (const testCase of file.testResults)
|
|
3495
|
+
for (const testCase of file.testResults) {
|
|
3496
|
+
if (testCase.status !== "failed") continue;
|
|
3437
3497
|
const index = failureCtx.currentIndex;
|
|
3438
3498
|
failureCtx.currentIndex++;
|
|
3439
3499
|
lines.push(formatFailure({
|
|
@@ -3844,7 +3904,8 @@ function formatFileHeaderFailures(file, options) {
|
|
|
3844
3904
|
const totalTests = file.numFailingTests + file.numPassingTests + file.numPendingTests;
|
|
3845
3905
|
const testWord = totalTests === 1 ? "test" : "tests";
|
|
3846
3906
|
lines.push(` ❯ ${relativePath} (${totalTests} ${testWord} | ${file.numFailingTests} failed)`);
|
|
3847
|
-
for (const test of file.testResults)
|
|
3907
|
+
for (const test of file.testResults) {
|
|
3908
|
+
if (test.status !== "failed") continue;
|
|
3848
3909
|
const duration = test.duration !== void 0 ? ` ${String(test.duration)}ms` : "";
|
|
3849
3910
|
lines.push(` × ${test.title}${duration}`);
|
|
3850
3911
|
}
|
|
@@ -4211,7 +4272,8 @@ function createTimingCollector(options = {}) {
|
|
|
4211
4272
|
node.elapsedMs += elapsedMs;
|
|
4212
4273
|
}
|
|
4213
4274
|
function emit(node, depth) {
|
|
4214
|
-
|
|
4275
|
+
const indent = " ".repeat(depth);
|
|
4276
|
+
sink(`[TIMING] ${indent}${node.name}: ${String(Math.round(node.elapsedMs))}ms`);
|
|
4215
4277
|
for (const child of node.children.values()) emit(child, depth + 1);
|
|
4216
4278
|
}
|
|
4217
4279
|
function flushTimingReport() {
|
|
@@ -4310,16 +4372,18 @@ function writeJsonFile(filePath, value) {
|
|
|
4310
4372
|
}
|
|
4311
4373
|
//#endregion
|
|
4312
4374
|
//#region src/executor.ts
|
|
4375
|
+
const TS_SOURCE_EXTENSION$1 = /\.tsx?$/;
|
|
4376
|
+
const TSCONFIG_FILENAME = /^tsconfig.*\.json$/i;
|
|
4313
4377
|
function isLuauProject(testFiles, tsconfigMappings) {
|
|
4314
4378
|
if (tsconfigMappings.length > 0) return false;
|
|
4315
|
-
if (testFiles.some((file) =>
|
|
4379
|
+
if (testFiles.some((file) => TS_SOURCE_EXTENSION$1.test(file))) return false;
|
|
4316
4380
|
return true;
|
|
4317
4381
|
}
|
|
4318
4382
|
function resolveAllTsconfigMappings(projectRoot) {
|
|
4319
4383
|
const resolvedRoot = path$1.resolve(projectRoot);
|
|
4320
4384
|
let files;
|
|
4321
4385
|
try {
|
|
4322
|
-
files = fs$1.readdirSync(resolvedRoot).filter((file) =>
|
|
4386
|
+
files = fs$1.readdirSync(resolvedRoot).filter((file) => TSCONFIG_FILENAME.test(file));
|
|
4323
4387
|
} catch {
|
|
4324
4388
|
return [];
|
|
4325
4389
|
}
|
|
@@ -4785,6 +4849,7 @@ function buildProjectJob(parameters, timing) {
|
|
|
4785
4849
|
//#endregion
|
|
4786
4850
|
//#region src/formatters/github-actions.ts
|
|
4787
4851
|
const SEPARATOR = " · ";
|
|
4852
|
+
const TRAILING_SLASH$4 = /\/$/;
|
|
4788
4853
|
function escapeData(value) {
|
|
4789
4854
|
return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
|
|
4790
4855
|
}
|
|
@@ -4826,13 +4891,7 @@ function formatJobSummary(result, options) {
|
|
|
4826
4891
|
});
|
|
4827
4892
|
continue;
|
|
4828
4893
|
}
|
|
4829
|
-
|
|
4830
|
-
if (test.status !== "failed") continue;
|
|
4831
|
-
failures.push({
|
|
4832
|
-
file: makeRelative(file.testFilePath, options.workspace),
|
|
4833
|
-
title: test.fullName
|
|
4834
|
-
});
|
|
4835
|
-
}
|
|
4894
|
+
failures.push(...collectFileFailures(file, options));
|
|
4836
4895
|
}
|
|
4837
4896
|
if (failures.length > 0) {
|
|
4838
4897
|
lines.push("### Failures\n");
|
|
@@ -4857,7 +4916,7 @@ function resolveGitHubActionsOptions(userOptions, sourceMapper, environment = pr
|
|
|
4857
4916
|
function makeRelative(filePath, workspace) {
|
|
4858
4917
|
if (workspace === void 0) return filePath;
|
|
4859
4918
|
const normalized = normalizeWindowsPath(filePath);
|
|
4860
|
-
const normalizedWorkspace = normalizeWindowsPath(workspace).replace(
|
|
4919
|
+
const normalizedWorkspace = normalizeWindowsPath(workspace).replace(TRAILING_SLASH$4, "");
|
|
4861
4920
|
if (normalized.startsWith(`${normalizedWorkspace}/`)) return normalized.slice(normalizedWorkspace.length + 1);
|
|
4862
4921
|
return filePath;
|
|
4863
4922
|
}
|
|
@@ -4895,6 +4954,17 @@ function collectTestFailureAnnotations(annotations, file, options) {
|
|
|
4895
4954
|
});
|
|
4896
4955
|
}
|
|
4897
4956
|
}
|
|
4957
|
+
function collectFileFailures(file, options) {
|
|
4958
|
+
const failures = [];
|
|
4959
|
+
for (const test of file.testResults) {
|
|
4960
|
+
if (test.status !== "failed") continue;
|
|
4961
|
+
failures.push({
|
|
4962
|
+
file: makeRelative(file.testFilePath, options.workspace),
|
|
4963
|
+
title: test.fullName
|
|
4964
|
+
});
|
|
4965
|
+
}
|
|
4966
|
+
return failures;
|
|
4967
|
+
}
|
|
4898
4968
|
function noun(count, singular, plural) {
|
|
4899
4969
|
return count === 1 ? singular : plural;
|
|
4900
4970
|
}
|
|
@@ -4961,11 +5031,12 @@ async function outputSingleResult(config, result) {
|
|
|
4961
5031
|
coverageEnabled: config.collectCoverage,
|
|
4962
5032
|
printResults: () => {
|
|
4963
5033
|
if (config.silent) return;
|
|
5034
|
+
const timing = runtimeResult !== void 0 ? addCoverageTiming(runtimeResult.timing, preCoverageMs) : void 0;
|
|
4964
5035
|
printFormattedOutput({
|
|
4965
5036
|
config,
|
|
4966
5037
|
mergedResult,
|
|
4967
5038
|
runtimeResult,
|
|
4968
|
-
timing
|
|
5039
|
+
timing,
|
|
4969
5040
|
typecheckResult
|
|
4970
5041
|
});
|
|
4971
5042
|
},
|
|
@@ -5488,6 +5559,7 @@ function redirectPathToShadow(target, coverageRoots) {
|
|
|
5488
5559
|
}
|
|
5489
5560
|
//#endregion
|
|
5490
5561
|
//#region src/staging/synthesizer.ts
|
|
5562
|
+
const TRAILING_SLASH$3 = /\/$/;
|
|
5491
5563
|
const STUB_INJECTION_KEY = "jest.config";
|
|
5492
5564
|
const COLLIDING_SOURCE_FILES = ["jest.config.lua", "jest.config.luau"];
|
|
5493
5565
|
const SERVICE_CLASSES = /* @__PURE__ */ new Set([
|
|
@@ -5565,7 +5637,7 @@ function resolveDollarPath(value, treeBase, coverageBase, coverageRoots) {
|
|
|
5565
5637
|
if (coverageRoots === void 0) return absoluteTarget;
|
|
5566
5638
|
return redirectPathToShadow(absoluteTarget, coverageRoots.map((root) => {
|
|
5567
5639
|
return {
|
|
5568
|
-
luauRoot: normalizeWindowsPath(path$1.resolve(coverageBase, root.luauRoot)).replace(
|
|
5640
|
+
luauRoot: normalizeWindowsPath(path$1.resolve(coverageBase, root.luauRoot)).replace(TRAILING_SLASH$3, ""),
|
|
5569
5641
|
shadowDir: normalizeWindowsPath(root.shadowDir)
|
|
5570
5642
|
};
|
|
5571
5643
|
})) ?? absoluteTarget;
|
|
@@ -6124,7 +6196,8 @@ function collectCoverage(root) {
|
|
|
6124
6196
|
return true;
|
|
6125
6197
|
},
|
|
6126
6198
|
visitStatBlock(block) {
|
|
6127
|
-
for (const stmt of block.statements)
|
|
6199
|
+
for (const stmt of block.statements) {
|
|
6200
|
+
if (!INSTRUMENTABLE_STATEMENT_TAGS.has(stmt.tag)) continue;
|
|
6128
6201
|
statements.push({
|
|
6129
6202
|
index: statementIndex,
|
|
6130
6203
|
location: { ...stmt.location }
|
|
@@ -6168,16 +6241,14 @@ function collectCoverage(root) {
|
|
|
6168
6241
|
location: { ...elseif.thenBlock.location }
|
|
6169
6242
|
});
|
|
6170
6243
|
}
|
|
6171
|
-
|
|
6172
|
-
if (hasExplicitElse) {
|
|
6244
|
+
if (elseBlock !== void 0 && elseBlock.statements.length > 0) {
|
|
6173
6245
|
const elseFirst = getBodyFirstStatement(elseBlock);
|
|
6174
6246
|
branch.arms.push({
|
|
6175
6247
|
bodyFirstColumn: elseFirst.column,
|
|
6176
6248
|
bodyFirstLine: elseFirst.line,
|
|
6177
6249
|
location: { ...elseBlock.location }
|
|
6178
6250
|
});
|
|
6179
|
-
}
|
|
6180
|
-
if (!hasExplicitElse) {
|
|
6251
|
+
} else {
|
|
6181
6252
|
branch.arms.push({
|
|
6182
6253
|
bodyFirstColumn: 0,
|
|
6183
6254
|
bodyFirstLine: 0,
|
|
@@ -6300,6 +6371,9 @@ const KIND_RANK = {
|
|
|
6300
6371
|
open: 0,
|
|
6301
6372
|
point: 1
|
|
6302
6373
|
};
|
|
6374
|
+
const TRAILING_WHITESPACE = /\s$/;
|
|
6375
|
+
const IDENTIFIER_START = /^[a-zA-Z_]/;
|
|
6376
|
+
const MODE_DIRECTIVE = /^--![a-z]+/;
|
|
6303
6377
|
function insertProbes(source, result, fileKey) {
|
|
6304
6378
|
const lines = splitLines(source);
|
|
6305
6379
|
applyProbes(lines, collectProbes(result));
|
|
@@ -6370,11 +6444,11 @@ function applyProbes(mutableLines, probes) {
|
|
|
6370
6444
|
assert(line !== void 0, `Invalid probe line number: ${probeLine}`);
|
|
6371
6445
|
const before = line.slice(0, column - 1);
|
|
6372
6446
|
const after = line.slice(column - 1);
|
|
6373
|
-
mutableLines[lineIndex] = before + (before.length > 0 &&
|
|
6447
|
+
mutableLines[lineIndex] = before + (before.length > 0 && !TRAILING_WHITESPACE.test(before) && IDENTIFIER_START.test(text) ? " " : "") + text + after;
|
|
6374
6448
|
}
|
|
6375
6449
|
}
|
|
6376
6450
|
function extractModeDirective(lines) {
|
|
6377
|
-
if (lines.length > 0 && lines[0] !== void 0 &&
|
|
6451
|
+
if (lines.length > 0 && lines[0] !== void 0 && MODE_DIRECTIVE.test(lines[0])) {
|
|
6378
6452
|
const directive = `${lines[0]}\n`;
|
|
6379
6453
|
lines.splice(0, 1);
|
|
6380
6454
|
return directive;
|
|
@@ -6426,6 +6500,7 @@ function buildPreamble(modeDirective, fileKey, result) {
|
|
|
6426
6500
|
}
|
|
6427
6501
|
//#endregion
|
|
6428
6502
|
//#region src/coverage-pipeline/instrumenter.ts
|
|
6503
|
+
const LUAU_EXTENSION = /\.luau?$/;
|
|
6429
6504
|
let cachedTemporaryDirectory$1;
|
|
6430
6505
|
/**
|
|
6431
6506
|
* Instrument a single luauRoot directory. Returns the files map without
|
|
@@ -6470,7 +6545,7 @@ function instrumentRoot(options) {
|
|
|
6470
6545
|
} catch (err) {
|
|
6471
6546
|
throw new Error("Failed to parse file list from lute", { cause: err });
|
|
6472
6547
|
}
|
|
6473
|
-
if (!
|
|
6548
|
+
if (!isStringArray(parsed)) throw new Error("Expected file list array from lute");
|
|
6474
6549
|
const fileList = parsed;
|
|
6475
6550
|
const files = {};
|
|
6476
6551
|
const posixLuauRoot = normalizeWindowsPath(luauRoot);
|
|
@@ -6487,7 +6562,7 @@ function instrumentRoot(options) {
|
|
|
6487
6562
|
const fileKey = normalizeWindowsPath(path$1.join(posixLuauRoot, relativePath));
|
|
6488
6563
|
const originalLuauPath = fileKey;
|
|
6489
6564
|
const instrumentedLuauPath = normalizeWindowsPath(path$1.join(shadowDir, relativePath));
|
|
6490
|
-
const coverageMapOutputPath = path$1.join(shadowDir, relativePath.replace(
|
|
6565
|
+
const coverageMapOutputPath = path$1.join(shadowDir, relativePath.replace(LUAU_EXTENSION, ".cov-map.json"));
|
|
6491
6566
|
const sourceMapPath = `${originalLuauPath}.map`;
|
|
6492
6567
|
const outputDirectory = path$1.dirname(path$1.join(shadowDir, relativePath));
|
|
6493
6568
|
fs$1.mkdirSync(outputDirectory, { recursive: true });
|
|
@@ -6514,6 +6589,9 @@ function instrumentRoot(options) {
|
|
|
6514
6589
|
}
|
|
6515
6590
|
return files;
|
|
6516
6591
|
}
|
|
6592
|
+
function isStringArray(value) {
|
|
6593
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
6594
|
+
}
|
|
6517
6595
|
function shouldSkipFile(relativePath, skipFiles) {
|
|
6518
6596
|
if (relativePath.endsWith(".snap.luau") || relativePath.endsWith(".snap.lua")) return true;
|
|
6519
6597
|
return skipFiles?.has(relativePath) === true;
|
|
@@ -6587,7 +6665,8 @@ function walkDirectory$1(directory, visitedDirectories, files) {
|
|
|
6587
6665
|
const real = toKey(fs$1.realpathSync(directory));
|
|
6588
6666
|
if (visitedDirectories.has(real)) return;
|
|
6589
6667
|
visitedDirectories.add(real);
|
|
6590
|
-
|
|
6668
|
+
const entries = fs$1.readdirSync(directory, { withFileTypes: true });
|
|
6669
|
+
for (const entry of entries) {
|
|
6591
6670
|
if (entry.name.startsWith(".")) continue;
|
|
6592
6671
|
collectInputFiles(path$1.join(directory, entry.name), visitedDirectories, files);
|
|
6593
6672
|
}
|
|
@@ -7256,18 +7335,18 @@ const DEFAULT_BACKOFF_MS = 5e3;
|
|
|
7256
7335
|
*/
|
|
7257
7336
|
async function runTaskPool(options) {
|
|
7258
7337
|
const { concurrency, isDone, onError, onResult, places } = options;
|
|
7338
|
+
if (concurrency < 1) throw new Error(`runTaskPool concurrency must be >= 1, got ${String(concurrency)}`);
|
|
7339
|
+
if (places.length === 0) throw new Error("runTaskPool requires at least one place");
|
|
7259
7340
|
const now = options.now ?? Date.now;
|
|
7260
7341
|
const sleep = options.sleep ?? setTimeout$1;
|
|
7261
|
-
const
|
|
7342
|
+
const allocations = distributeSlots(places, concurrency, options.warn ?? ((message) => {
|
|
7262
7343
|
console.warn(message);
|
|
7263
|
-
});
|
|
7264
|
-
if (concurrency < 1) throw new Error(`runTaskPool concurrency must be >= 1, got ${String(concurrency)}`);
|
|
7265
|
-
if (places.length === 0) throw new Error("runTaskPool requires at least one place");
|
|
7266
|
-
const allocations = distributeSlots(places, concurrency, warn);
|
|
7344
|
+
}));
|
|
7267
7345
|
async function backoff(state, retryAfterMs) {
|
|
7268
7346
|
const genuineMs = retryAfterMs !== void 0 && retryAfterMs > 0 ? retryAfterMs : DEFAULT_BACKOFF_MS;
|
|
7269
7347
|
const sinceCompletion = now() - state.lastCompletionMs;
|
|
7270
|
-
|
|
7348
|
+
const waitMs = sinceCompletion < RECYCLE_LAG_MS ? RECYCLE_LAG_MS - sinceCompletion : genuineMs;
|
|
7349
|
+
await sleep(waitMs);
|
|
7271
7350
|
}
|
|
7272
7351
|
async function worker(state) {
|
|
7273
7352
|
while (!isDone()) {
|
|
@@ -7426,6 +7505,7 @@ function isShardedParallel(parallel) {
|
|
|
7426
7505
|
var test_runner_bundled_default = "local __WELD_MODULES = {\n cache = {} :: any,\n}\n\ndo\n --!strict\n local b_ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n \n local b_module = {}\n \n function b_module.findInstance(path: string): Instance\n local parts = string.split(path, \"/\")\n \n local success, current = pcall(function()\n return game:FindService(parts[1])\n end)\n assert(success, `Failed to find service {parts[1]}: {current}`)\n \n for i = 2, #parts do\n assert(current, `Failed to find '{parts[i - 1]}' in path {path}`)\n current = current:FindFirstChild(parts[i])\n end\n \n assert(current, `Failed to find instance at path {path}`)\n \n return current\n end\n \n function b_module.getJest(config: { jestPath: string? }): ModuleScript\n local jestPath = config.jestPath\n if jestPath then\n local instance = b_module.findInstance(jestPath)\n assert(instance, `Failed to find Jest instance at path {jestPath}`)\n assert(instance:IsA(\"ModuleScript\"), `Instance at path {jestPath} is not a ModuleScript`)\n return instance :: ModuleScript\n end\n \n local jestInstance = b_ReplicatedStorage:FindFirstChild(\"Jest\", true)\n assert(jestInstance, \"Failed to find Jest instance in ReplicatedStorage\")\n assert(jestInstance:IsA(\"ModuleScript\"), \"Jest instance in ReplicatedStorage is not a ModuleScript\")\n return jestInstance\n end\n \n function b_module.findRobloxShared(jestModule: ModuleScript): Instance?\n local parent = jestModule.Parent\n if parent then\n local found = parent:FindFirstChild(\"RobloxShared\") or parent:FindFirstChild(\"jest-roblox-shared\")\n if found then\n return found\n end\n \n if parent.Parent then\n found = parent.Parent:FindFirstChild(\"RobloxShared\") or parent.Parent:FindFirstChild(\"jest-roblox-shared\")\n if found then\n return found\n end\n end\n end\n \n return game:FindFirstChild(\"RobloxShared\", true)\n end\n \n function b_module.findSiblingPackage(jestModule: ModuleScript, ...: string): Instance?\n local parent = jestModule.Parent\n if parent then\n for _, name in { ... } do\n local found = parent:FindFirstChild(name)\n if found then\n return found\n end\n end\n \n if parent.Parent then\n for _, name in { ... } do\n local found = parent.Parent:FindFirstChild(name)\n if found then\n return found\n end\n end\n end\n end\n \n for _, name in { ... } do\n local found = game:FindFirstChild(name, true)\n if found then\n return found\n end\n end\n \n return nil\n end\n \n local _b = b_module\n \n --!strict\n -- Patches a Writeable's `_writeFn` to push every write into `buffer` (tagged\n -- with `messageType` and a monotonic timestamp) before delegating to the\n -- original. Returns a restore thunk; callers must invoke it to undo the patch,\n -- otherwise the buffer leaks across runs and the original write side-effect\n -- stays hidden behind our tap.\n --\n -- Used by both runner.luau (single-pkg) and staging/entry.luau (workspace) to\n -- capture per-task stdout/stderr around Jest.runCLI so failure messages like\n -- \"No tests found, exiting with code 1\" can be surfaced in the CLI banner.\n \n local i_module = {}\n \n export type CapturedMessage = { message: string, messageType: number, timestamp: number }\n \n function i_module.intercept(\n writeable: any,\n buffer: { CapturedMessage },\n messageType: number\n ): () -> ()\n local original = writeable._writeFn\n if typeof(original) ~= \"function\" then\n return function() end\n end\n \n writeable._writeFn = function(data: string)\n table.insert(buffer, {\n message = data,\n messageType = messageType,\n timestamp = os.clock(),\n })\n original(data)\n end\n \n return function()\n writeable._writeFn = original\n end\n end\n \n local _i = i_module\n \n --!strict\n -- Per-test coverage attribution: pure snapshot-and-diff over the runtime hit\n -- table (`_G.__jest_roblox_cov`). The runner snapshots before each Jest test\n -- case and diffs after, attributing the newly-covered statements to that test.\n --\n -- Coverage-primitive-agnostic: the input is whatever accumulated hit table the\n -- collector exposes (Lute probes today, native ScriptContext stats later), so\n -- the diff is identical regardless of how the counts were produced.\n \n -- `s` is the per-statement hit-count map, keyed by statement id. The single\n -- letter is the wire key the coverage probes emit (`__cov_s`, serialized as `s`\n -- under `_G.__jest_roblox_cov[file]`), matching `RawFileCoverage` on the\n -- TypeScript side.\n type FileHitCounts = { s: { number } }\n type HitTableByFile = { [string]: FileHitCounts }\n \n -- A test's delta: the ids of the statements it newly covered, per file. `s`\n -- again mirrors the serialized envelope the TypeScript parser validates.\n type FileStatementDelta = { s: { number } }\n type CoverageDeltaByFile = { [string]: FileStatementDelta }\n \n local d_module = {}\n \n -- Copy the statement hit counts so a later mutation of the live table does not\n -- alias the snapshot. Probes only ever increment, so the copied counts are the\n -- pre-test baseline the diff subtracts against.\n function d_module.snapshot(coverage: HitTableByFile): HitTableByFile\n local baseline: HitTableByFile = {}\n for fileKey, fileHitCounts in coverage do\n local statementCounts: { number } = {}\n for statementId, hitCount in fileHitCounts.s do\n statementCounts[statementId] = hitCount\n end\n \n baseline[fileKey] = { s = statementCounts }\n end\n \n return baseline\n end\n \n -- Statements whose count rose between `before` and `after` were executed during\n -- the test, so they are attributed to it. Files with no newly-covered statement\n -- are omitted.\n function d_module.diff(before: HitTableByFile, after: HitTableByFile): CoverageDeltaByFile\n local delta: CoverageDeltaByFile = {}\n for fileKey, afterCounts in after do\n local beforeCounts = before[fileKey]\n local coveredStatementIds: { number } = {}\n \n for statementId, afterHitCount in afterCounts.s do\n local beforeHitCount = if beforeCounts then beforeCounts.s[statementId] or 0 else 0\n if afterHitCount > beforeHitCount then\n table.insert(coveredStatementIds, statementId)\n end\n end\n \n if #coveredStatementIds > 0 then\n delta[fileKey] = { s = coveredStatementIds }\n end\n end\n \n return delta\n end\n \n local _d = d_module\n \n --!strict\n -- Per-test coverage hook. Installs a single jest-circus event handler that\n -- snapshots the runtime hit table on `test_start` and diffs it on `test_done`,\n -- attributing the newly-covered statements to the test that just ran. The test's\n -- identity (file + full name) is read from the Expect matcher state, which jest\n -- sets per-file (`testPath`) and per-test (`currentTestName`).\n --\n -- `addEventHandler`, `getMatcherState`, and `getCoverage` are injected so the\n -- orchestration is testable under lute without a real circus or Roblox\n -- DataModel. The runner passes `_G.__jest_roblox_cov` as the coverage source.\n \n local h_Attribution = _d\n \n type MatcherState = { currentTestName: string?, testPath: Instance? }\n type Record = { delta: any, testCaseId: string, testFilePath: string }\n \n local h_module = {}\n \n -- Service-rooted DataModel path (the `game` root, whose parent is nil, is\n -- excluded), slash-joined to match the form jest-roblox-cli's source mapper\n -- resolves.\n local function h_dataModelPath(instance: Instance): string\n local parts: { string } = {}\n local current: Instance? = instance\n while current and current.Parent ~= nil do\n table.insert(parts, 1, current.Name)\n current = current.Parent\n end\n \n return table.concat(parts, \"/\")\n end\n \n function h_module.install(\n addEventHandler: (handler: any) -> (),\n getMatcherState: () -> MatcherState,\n getCoverage: () -> any\n ): { Record }\n local records: { Record } = {}\n local before: any = nil\n \n -- Circus does not guard event handlers (a throw rejects the run's dispatch\n -- chain), so the whole body is pcall-wrapped: attribution is best-effort and\n -- must never break the coverage run it observes.\n addEventHandler(function(_self: any, event: { name: string })\n pcall(function()\n if event.name == \"test_start\" then\n before = h_Attribution.snapshot(getCoverage())\n elseif event.name == \"test_done\" then\n if before == nil then\n return\n end\n \n local delta = h_Attribution.diff(before, getCoverage())\n before = nil\n if next(delta) == nil then\n return\n end\n \n local state = getMatcherState()\n table.insert(records, {\n delta = delta,\n testCaseId = state.currentTestName or \"\",\n testFilePath = if state.testPath then dataModelPath(state.testPath) else \"\",\n })\n end\n end)\n end)\n \n return records\n end\n \n local _h = h_module\n \n --!strict\n -- Mirrors @rbxts-js/roblox-shared/src/normalizePromiseError.lua: when Jest's\n -- internal Promise chain rejects (e.g. process.exit fires via nodeUtils:25),\n -- the rejection value is a Promise.Error table whose `.error` field at every\n -- non-leaf level is `tostring(parent)` — the upstream Promise.Error\n -- __tostring blob. Walking to the deepest parent recovers the original cause\n -- string (\"Exited with code: 1\") instead of a multi-frame trace. Depth-capped\n -- to defend against future cycles (a Promise.Error pointing at itself would\n -- otherwise hang the test task until the infinite-yield watchdog fires).\n \n local g_module = {}\n \n local g_MAX_PROMISE_ERROR_DEPTH = 64\n \n -- Single-line Luau path:line prefix, e.g. `Module.Path:25: <msg>`. We strip\n -- this so the CLI banner's exit-code branch (^Exited with code: \\d+$) can\n -- match the surfaced message and demote the transport line to a footer.\n local g_PATH_LINE_PREFIX = \"^[%w_%.@%-/]+:%d+:%s*\"\n \n local function g_stripPathLinePrefix(message: string): string\n -- Multi-line strings keep their full content — only strip when the\n -- entire string is the single `<path>:<line>: <msg>` shape so we don't\n -- swallow useful context from layered messages.\n if string.find(message, \"\\n\", 1, true) ~= nil then\n return message\n end\n \n local stripped, replacements = string.gsub(message, g_PATH_LINE_PREFIX, \"\", 1)\n if replacements > 0 then\n return stripped\n end\n \n return message\n end\n \n function g_module.normalize(err: any): string\n if type(err) ~= \"table\" then\n return g_stripPathLinePrefix(tostring(err))\n end\n \n local current: any = err\n for _ = 1, g_MAX_PROMISE_ERROR_DEPTH do\n if type(current.parent) ~= \"table\" then\n break\n end\n \n current = current.parent\n end\n \n if type(current.error) == \"string\" then\n return g_stripPathLinePrefix(current.error)\n end\n \n return g_stripPathLinePrefix(tostring(err))\n end\n \n local _g = g_module\n \n --!strict\n local f_InstanceResolver = _b\n \n local f_module = {}\n \n export type PatchState = {\n Runtime: any,\n originalRequireModule: any,\n accumulatedSeconds: { value: number },\n }\n \n function f_module.patch(\n jestModule: ModuleScript,\n setupFiles: { Instance }?,\n setupFilesAfterEnv: { Instance }?\n ): PatchState?\n local setupSet: { [Instance]: boolean } = {}\n \n if setupFiles then\n for _, inst in setupFiles do\n setupSet[inst] = true\n end\n end\n \n if setupFilesAfterEnv then\n for _, inst in setupFilesAfterEnv do\n setupSet[inst] = true\n end\n end\n \n if not next(setupSet) then\n return nil\n end\n \n local jestRuntimeModule = f_InstanceResolver.findSiblingPackage(jestModule, \"JestRuntime\", \"jest-runtime\")\n if not jestRuntimeModule then\n warn(\"Could not find JestRuntime; setup timing unavailable\")\n return nil\n end\n \n local Runtime = (require :: any)(jestRuntimeModule)\n local originalRequireModule = Runtime.requireModule\n local accumulated = { value = 0 }\n local insideSetupRequire = false\n \n Runtime.requireModule = function(self: any, moduleName: any, ...): any\n if not insideSetupRequire and typeof(moduleName) == \"Instance\" and setupSet[moduleName] then\n insideSetupRequire = true\n local t0 = os.clock()\n local results = table.pack(pcall(originalRequireModule, self, moduleName, ...))\n accumulated.value += os.clock() - t0\n insideSetupRequire = false\n \n if not results[1] then\n error(results[2], 0)\n end\n \n return table.unpack(results, 2, results.n)\n end\n \n return originalRequireModule(self, moduleName, ...)\n end\n \n return {\n Runtime = Runtime,\n originalRequireModule = originalRequireModule,\n accumulatedSeconds = accumulated,\n }\n end\n \n function f_module.unpatch(state: PatchState?)\n if not state then\n return\n end\n \n if state.Runtime and state.originalRequireModule then\n state.Runtime.requireModule = state.originalRequireModule\n end\n end\n \n function f_module.getSeconds(state: PatchState?): number\n if not state then\n return 0\n end\n \n return state.accumulatedSeconds.value\n end\n \n local _f = f_module\n \n --!strict\n local function c_getInstancePath(instance: Instance): string\n local parts = {} :: { string }\n local current: Instance? = instance\n while current and current ~= game do\n table.insert(parts, 1, current.Name)\n current = current.Parent\n end\n \n return table.concat(parts, \"/\")\n end\n \n local c_CoreScriptSyncService = {}\n \n function c_CoreScriptSyncService:GetScriptFilePath(instance: Instance): string\n return c_getInstancePath(instance)\n end\n \n local _c = c_CoreScriptSyncService\n \n --!strict\n local function a_normalizeSnapPath(path: string): string\n return (string.gsub(path, \"%.snap%.lua$\", \".snap.luau\"))\n end\n \n local function a_create(snapshotWrites: { [string]: string })\n local FileSystemService = {}\n \n function FileSystemService:WriteFile(path: string, contents: string)\n snapshotWrites[a_normalizeSnapPath(path)] = contents\n end\n \n function FileSystemService:CreateDirectories(_path: string) end\n \n function FileSystemService:Exists(path: string): boolean\n return snapshotWrites[a_normalizeSnapPath(path)] ~= nil\n end\n \n function FileSystemService:Remove(path: string)\n snapshotWrites[a_normalizeSnapPath(path)] = nil\n end\n \n function FileSystemService:IsRegularFile(path: string): boolean\n return snapshotWrites[a_normalizeSnapPath(path)] ~= nil\n end\n \n return FileSystemService\n end\n \n local _a = a_create\n \n --!strict\n local e_CoreScriptSyncService = _c\n local e_InstanceResolver = _b\n \n local e_module = {}\n \n function e_module.createMockGetDataModelService(snapshotWrites: { [string]: string })\n local FileSystemService = _a(snapshotWrites)\n \n return function(service: string): any\n if service == \"FileSystemService\" then\n return FileSystemService\n elseif service == \"CoreScriptSyncService\" then\n return e_CoreScriptSyncService\n end\n \n local success, result = pcall(function()\n local service_ = game:GetService(service)\n local _ = service_.Name\n return service_\n end)\n \n return if success then result else nil\n end\n end\n \n export type PatchState = {\n robloxSharedExports: any,\n originalGetDataModelService: any,\n Runtime: any,\n originalRequireInternalModule: any,\n }\n \n function e_module.patch(jestModule: ModuleScript, snapshotWrites: { [string]: string }): PatchState?\n local mockGetDataModelService = e_module.createMockGetDataModelService(snapshotWrites)\n \n local robloxSharedInstance = e_InstanceResolver.findRobloxShared(jestModule)\n if not robloxSharedInstance then\n warn(\"Could not find RobloxShared; snapshot support unavailable\")\n return nil\n end\n \n local robloxSharedExports = (require :: any)(robloxSharedInstance)\n local originalGetDataModelService = robloxSharedExports.getDataModelService\n robloxSharedExports.getDataModelService = mockGetDataModelService\n \n -- Snapshot the partial state immediately so any subsequent throw can\n -- be rolled back. Without this, workspace mode (where multiple patch\n -- cycles share the same RobloxShared module) would leak the mock if\n -- the JestRuntime acquisition below threw — the next package's patch\n -- would then save the leaked mock as \"original\".\n local partialState: PatchState = {\n robloxSharedExports = robloxSharedExports,\n originalGetDataModelService = originalGetDataModelService,\n Runtime = nil,\n originalRequireInternalModule = nil,\n }\n \n local ok, err = pcall(function()\n local getDataModelServiceChild = robloxSharedInstance:FindFirstChild(\"getDataModelService\")\n \n local jestRuntimeModule = e_InstanceResolver.findSiblingPackage(jestModule, \"JestRuntime\", \"jest-runtime\")\n if not jestRuntimeModule then\n warn(\"Could not find JestRuntime; snapshot interception unavailable\")\n return\n end\n \n local Runtime = (require :: any)(jestRuntimeModule)\n local originalRequireInternalModule = Runtime.requireInternalModule\n \n Runtime.requireInternalModule = function(self: any, from: any, to: any, ...): any\n local target = if to ~= nil then to else from\n if getDataModelServiceChild and typeof(target) == \"Instance\" and target == getDataModelServiceChild then\n return mockGetDataModelService\n end\n \n return originalRequireInternalModule(self, from, to, ...)\n end\n \n partialState.Runtime = Runtime\n partialState.originalRequireInternalModule = originalRequireInternalModule\n end)\n \n if not ok then\n e_module.unpatch(partialState)\n error(err, 0)\n end\n \n return partialState\n end\n \n function e_module.unpatch(state: PatchState?)\n if not state then\n return\n end\n \n if state.robloxSharedExports and state.originalGetDataModelService then\n state.robloxSharedExports.getDataModelService = state.originalGetDataModelService\n end\n \n if state.Runtime and state.originalRequireInternalModule then\n state.Runtime.requireInternalModule = state.originalRequireInternalModule\n end\n end\n \n local _e = e_module\n \n --!strict\n local j_HttpService = game:GetService(\"HttpService\")\n local j_LogService = game:GetService(\"LogService\")\n \n local j_InstanceResolver = _b\n local j_InterceptWriteable = _i\n local j_PerTestCoverage = _h\n local j_PromiseError = _g\n local j_SetupTiming = _f\n local j_SnapshotPatch = _e\n \n type CapturedMessage = InterceptWriteable.CapturedMessage\n \n type Config = {\n jestPath: string?,\n projects: { string }?,\n setupFiles: { string }?,\n setupFilesAfterEnv: { string }?,\n _coverage: boolean?,\n _perTestCoverage: boolean?,\n _timing: boolean?,\n }\n \n local function j_fail(err: string)\n return {\n success = false,\n err = err,\n }\n end\n \n -- Install the per-test coverage hook against the same jest-circus and Expect\n -- instances Jest itself uses. All resolution is pcall-guarded so a missing or\n -- renamed package degrades to no attribution rather than failing the run; the\n -- returned accumulator fills as tests run. The matcher state carries the current\n -- file (`testPath`) and full test name (`currentTestName`).\n local function j_installPerTestCoverage(jestModule: ModuleScript): { any }?\n local ok, recordsOrErr = pcall(function()\n local circusModule = j_InstanceResolver.findSiblingPackage(jestModule, \"JestCircus\", \"jest-circus\")\n local expectModule = j_InstanceResolver.findSiblingPackage(jestModule, \"Expect\", \"expect\")\n assert(circusModule and expectModule, \"could not resolve jest-circus / Expect packages\")\n \n local Circus = (require :: any)(circusModule)\n local Expect = (require :: any)(expectModule)\n \n return j_PerTestCoverage.install(Circus.addEventHandler, Expect.getState, function()\n return _G.__jest_roblox_cov or {}\n end)\n end)\n \n if ok then\n return recordsOrErr\n end\n \n warn(\"Per-test coverage attribution unavailable: \" .. tostring(recordsOrErr))\n return nil\n end\n \n local j_module = {}\n \n function j_module.run(callingScript: LuaSourceContainer, config: Config): (string, string, string)\n local t0 = os.clock()\n local timingEnabled = config._timing\n local coverageEnabled = config._coverage\n local perTestEnabled = config._perTestCoverage\n \n -- Game Output capture: subscribe to LogService.MessageOut at the top of\n -- the run so engine warnings during findJest/require(Jest) (broken\n -- jestPath, missing dep) also land in the user-facing `--gameOutput`\n -- dump. Disconnected last during teardown so late warns that fire after\n -- Promise:expect resumes the coroutine still reach us — GetLogHistory\n -- has a known gap here (plans/SNAPSHOT-SUPPORT.md).\n local logMessages: { CapturedMessage } = {}\n local logConnection = j_LogService.MessageOut:Connect(\n function(message: string, messageType: Enum.MessageType)\n table.insert(logMessages, {\n message = message,\n messageType = messageType.Value,\n timestamp = os.clock(),\n })\n end\n )\n \n local function encodeGameOutput(): string\n local ok, encoded = pcall(function()\n return j_HttpService:JSONEncode(logMessages)\n end)\n return if ok then encoded else \"[]\"\n end\n \n local t_findJest0 = os.clock()\n local findSuccess, findValue = pcall(j_InstanceResolver.getJest, config)\n local t_findJest = os.clock()\n \n if not findSuccess then\n logConnection:Disconnect()\n return j_HttpService:JSONEncode(j_fail(findValue :: any)), encodeGameOutput(), \"[]\"\n end\n \n local snapshotWrites: { [string]: string } = {}\n \n local t_patchSnapshot0 = os.clock()\n local patchState = j_SnapshotPatch.patch(findValue, snapshotWrites)\n local t_patchSnapshot = os.clock()\n \n local t_requireJest0 = os.clock()\n local Jest = (require :: any)(findValue)\n local t_requireJest = os.clock()\n \n -- Intercept Jest's stdout/stderr to capture output synchronously into\n -- the BANNER OUTPUT buffer. Jest writes via process.stdout/stderr\n -- (Writeable objects whose _writeFn defaults to print); wrapping\n -- _writeFn captures messages like \"No tests found\" that are printed\n -- just before exit(1) throws. Distinct from the LogService-sourced\n -- Game Output above: this narrower buffer is what the CLI's error\n -- banner reads on Luau errors (see cli.ts:formatLuauErrorBanner).\n local capturedMessages: { CapturedMessage } = {}\n local restoreStdout: (() -> ())?\n local restoreStderr: (() -> ())?\n \n local interceptOk = pcall(function()\n local nodeModules = findValue.Parent.Parent.Parent :: any\n local RobloxShared = (require :: any)(nodeModules[\"@rbxts-js\"].RobloxShared)\n local process = RobloxShared.nodeUtils.process\n \n restoreStdout = j_InterceptWriteable.intercept(process.stdout, capturedMessages, 0)\n restoreStderr = j_InterceptWriteable.intercept(process.stderr, capturedMessages, 1)\n end)\n \n if not interceptOk then\n restoreStdout = nil\n restoreStderr = nil\n end\n \n local function runTests()\n local t_resolveProjects0 = os.clock()\n local projects = {}\n \n assert(\n config.projects and #config.projects > 0,\n \"No projects configured. Set 'projects' in jest.config.ts.\"\n )\n \n for _, projectPath in config.projects do\n table.insert(projects, j_InstanceResolver.findInstance(projectPath))\n end\n \n config.projects = {}\n local t_resolveProjects = os.clock()\n \n local t_resolveSetupFiles0 = os.clock()\n if config.setupFiles and #config.setupFiles > 0 then\n local resolved = {}\n \n for _, setupPath in config.setupFiles do\n table.insert(resolved, j_InstanceResolver.findInstance(setupPath))\n end\n \n config.setupFiles = resolved :: any\n end\n if config.setupFilesAfterEnv and #config.setupFilesAfterEnv > 0 then\n local resolved = {}\n \n for _, setupPath in config.setupFilesAfterEnv do\n table.insert(resolved, j_InstanceResolver.findInstance(setupPath))\n end\n \n config.setupFilesAfterEnv = resolved :: any\n end\n local t_resolveSetupFiles = os.clock()\n \n local setupTimingState = j_SetupTiming.patch(\n findValue,\n config.setupFiles :: any,\n config.setupFilesAfterEnv :: any\n )\n \n -- Strip private keys before Jest.runCLI (safe: single-task execution per VM)\n config._timing = nil :: any\n config._coverage = nil :: any\n config._perTestCoverage = nil :: any\n \n local perTestRecords: { any }? = nil\n if coverageEnabled then\n _G.__jest_roblox_cov = {}\n if perTestEnabled then\n perTestRecords = j_installPerTestCoverage(findValue)\n end\n end\n \n local t_jestRunCLI0 = os.clock()\n local runCLIOk, runCLIValue = pcall(function()\n return Jest.runCLI(callingScript, config, projects):expect()\n end)\n local t_jestRunCLI = os.clock()\n \n local setupSeconds = j_SetupTiming.getSeconds(setupTimingState)\n j_SetupTiming.unpatch(setupTimingState)\n \n if not runCLIOk then\n error(j_PromiseError.normalize(runCLIValue), 0)\n end\n \n local jestResult = runCLIValue\n \n local result: { [string]: any } = {\n success = true,\n value = jestResult,\n }\n \n if setupSeconds > 0 then\n result._setup = setupSeconds\n end\n \n if timingEnabled then\n result._timing = {\n findJest = t_findJest - t_findJest0,\n patchSnapshot = t_patchSnapshot - t_patchSnapshot0,\n requireJest = t_requireJest - t_requireJest0,\n resolveProjects = t_resolveProjects - t_resolveProjects0,\n resolveSetupFiles = t_resolveSetupFiles - t_resolveSetupFiles0,\n jestRunCLI = t_jestRunCLI - t_jestRunCLI0,\n total = os.clock() - t0,\n }\n end\n \n if next(snapshotWrites) then\n result._snapshotWrites = snapshotWrites\n end\n \n if coverageEnabled then\n result._coverage = _G.__jest_roblox_cov\n if perTestRecords and #perTestRecords > 0 then\n result._perTestCoverage = perTestRecords\n end\n end\n \n return result\n end\n \n local jestDone = false\n local runSuccess = false\n local runValue: any = nil\n \n task.spawn(function()\n local ok, val = pcall(runTests)\n jestDone = true\n runSuccess = ok\n runValue = val\n end)\n \n local infiniteYieldMessage: string? = nil\n local watchdogConnection = j_LogService.MessageOut:Connect(function(message: string, messageType: Enum.MessageType)\n if\n messageType == Enum.MessageType.MessageWarning\n and string.find(message, \"Infinite yield possible\")\n and not infiniteYieldMessage\n then\n infiniteYieldMessage = message\n end\n end)\n \n while not jestDone and not infiniteYieldMessage do\n task.wait(0.1)\n end\n \n watchdogConnection:Disconnect()\n \n if restoreStdout then\n restoreStdout()\n end\n \n if restoreStderr then\n restoreStderr()\n end\n \n if not jestDone and infiniteYieldMessage then\n runSuccess = false\n runValue = \"Infinite yield detected, aborting tests: \" .. infiniteYieldMessage\n end\n \n j_SnapshotPatch.unpatch(patchState)\n \n -- MessageOut listener stays connected through SnapshotPatch.unpatch and\n -- the encode below, so any disconnect-time warns still land in\n -- gameOutput.\n logConnection:Disconnect()\n \n local jestResult\n if not runSuccess then\n jestResult = j_HttpService:JSONEncode(j_fail(runValue :: any))\n else\n jestResult = j_HttpService:JSONEncode(runValue)\n end\n \n local bannerEncodeOk, bannerEncoded = pcall(function()\n return j_HttpService:JSONEncode(capturedMessages)\n end)\n \n return jestResult, encodeGameOutput(), if bannerEncodeOk then bannerEncoded else \"[]\"\n end\n \n type ProjectEntry = {\n jestOutput: string,\n gameOutput: string,\n bannerOutput: string,\n elapsedMs: number,\n }\n \n local function j_encodeExecutionError(err: any): string\n return j_HttpService:JSONEncode({\n success = true,\n value = {\n kind = \"ExecutionError\",\n error = tostring(err),\n },\n })\n end\n \n -- Per-config hooks. Studio passes `beforeConfig` to inject the current\n -- config's `jest.config` ModuleScripts and `afterConfig` to destroy them\n -- before the next config runs. Per-config (not upfront-all) avoids\n -- nested-project DataModel contamination where configs at e.g.\n -- `ReplicatedStorage` and `ReplicatedStorage/Foo` would both have stubs\n -- present simultaneously and Jest's parent-traversal config lookup could\n -- resolve the wrong one. Open-cloud passes no hooks — its stubs are baked\n -- into the place file by the synthesizer.\n type RunHooks = {\n beforeConfig: ((cfg: Config, index: number) -> ())?,\n afterConfig: ((cfg: Config, index: number) -> ())?,\n }\n \n -- TODO(runner-tests): dogfood harness for Runner.runProjects\n function j_module.runProjects(\n callingScript: LuaSourceContainer,\n configs: { Config },\n hooks: RunHooks?\n ): { ProjectEntry }\n local entries: { ProjectEntry } = {}\n \n for index, cfg in configs do\n local start = os.clock()\n local beforeOk: boolean, beforeErr: any = true, nil\n if hooks and hooks.beforeConfig then\n -- beforeConfig runs inside pcall too: a failed injection\n -- mid-mount must still trigger afterConfig so partially-\n -- parented stubs get destroyed. Without pcall, a throw would\n -- propagate out and afterConfig would never fire.\n beforeOk, beforeErr = pcall(hooks.beforeConfig, cfg, index)\n end\n \n local ok: boolean, jestOutput: any, gameOutput: any, bannerOutput: any =\n false, nil, nil, nil\n if beforeOk then\n ok, jestOutput, gameOutput, bannerOutput = pcall(j_module.run, callingScript, cfg)\n else\n jestOutput = beforeErr\n end\n local elapsedMs = math.floor((os.clock() - start) * 1000)\n \n -- Always run afterConfig, even on failure — cleanup must be\n -- unconditional so injected ModuleScripts don't leak into the next\n -- iteration when Jest (or the beforeConfig itself) throws partway\n -- through.\n if hooks and hooks.afterConfig then\n local cleanupOk, cleanupErr = pcall(hooks.afterConfig, cfg, index)\n if not cleanupOk then\n warn(\"Runner afterConfig hook threw: \" .. tostring(cleanupErr))\n end\n end\n \n if ok then\n entries[index] = {\n jestOutput = jestOutput :: string,\n gameOutput = gameOutput :: string,\n bannerOutput = bannerOutput :: string,\n elapsedMs = elapsedMs,\n }\n else\n entries[index] = {\n jestOutput = j_encodeExecutionError(jestOutput),\n gameOutput = \"[]\",\n bannerOutput = \"[]\",\n elapsedMs = elapsedMs,\n }\n end\n end\n \n return entries\n end\n \n local _j = j_module\n \n function __WELD_MODULES.b() return _b end\n function __WELD_MODULES.i() return _i end\n function __WELD_MODULES.d() return _d end\n function __WELD_MODULES.h() return _h end\n function __WELD_MODULES.g() return _g end\n function __WELD_MODULES.f() return _f end\n function __WELD_MODULES.c() return _c end\n function __WELD_MODULES.a() return _a end\n function __WELD_MODULES.e() return _e end\n function __WELD_MODULES.j() return _j end\nend\n\n--!strict\nlocal HttpService = game:GetService(\"HttpService\")\n\nlocal Runner = __WELD_MODULES.j()\n\nlocal payload = HttpService:JSONDecode([=[__CONFIG_JSON__]=])\nlocal entries = Runner.runProjects(script, payload.configs)\n\nreturn HttpService:JSONEncode({ entries = entries }), \"[]\"\n";
|
|
7427
7506
|
//#endregion
|
|
7428
7507
|
//#region src/test-script.ts
|
|
7508
|
+
const TS_OR_LUAU_EXTENSION$2 = /\.(tsx?|luau?)$/;
|
|
7429
7509
|
function buildJestArgv(options) {
|
|
7430
7510
|
const argv = {};
|
|
7431
7511
|
for (const [key, value] of Object.entries(options.config)) if (!JEST_ARGV_EXCLUDED_KEYS.has(key) && value !== void 0) argv[key] = value;
|
|
@@ -7436,7 +7516,7 @@ function buildJestArgv(options) {
|
|
|
7436
7516
|
return {
|
|
7437
7517
|
...argv,
|
|
7438
7518
|
reporters: argv["reporters"] ?? [],
|
|
7439
|
-
testMatch: options.config.testMatch.map((pattern) => pattern.replace(
|
|
7519
|
+
testMatch: options.config.testMatch.map((pattern) => pattern.replace(TS_OR_LUAU_EXTENSION$2, ""))
|
|
7440
7520
|
};
|
|
7441
7521
|
}
|
|
7442
7522
|
function generateTestScript(options) {
|
|
@@ -7478,6 +7558,7 @@ const PARALLEL_AUTO_CAP = 3;
|
|
|
7478
7558
|
const BASE_URL_ENV = "JEST_ROBLOX_OPEN_CLOUD_BASE_URL";
|
|
7479
7559
|
const MAX_RETRIES_ENV = "JEST_ROBLOX_OCALE_MAX_RETRIES";
|
|
7480
7560
|
const DEFAULT_STREAM_POLL_MS = 250;
|
|
7561
|
+
const TrailingSlashesPattern = /\/+$/;
|
|
7481
7562
|
var OpenCloudBackend = class {
|
|
7482
7563
|
runner;
|
|
7483
7564
|
kind = "open-cloud";
|
|
@@ -7611,7 +7692,7 @@ async function pollStreamingResults(hooks, isDone) {
|
|
|
7611
7692
|
function resolveOpenCloudBaseUrl() {
|
|
7612
7693
|
const override = process.env[BASE_URL_ENV]?.trim();
|
|
7613
7694
|
if (override === void 0 || override === "") return;
|
|
7614
|
-
return override.replace(
|
|
7695
|
+
return override.replace(TrailingSlashesPattern, "");
|
|
7615
7696
|
}
|
|
7616
7697
|
/**
|
|
7617
7698
|
* Reads {@link MAX_RETRIES_ENV} for an Open Cloud retry-budget override. Lets
|
|
@@ -7706,15 +7787,19 @@ function parseStealingEnvelope(result) {
|
|
|
7706
7787
|
gameOutput: result.outputs[1]
|
|
7707
7788
|
};
|
|
7708
7789
|
}
|
|
7709
|
-
function
|
|
7710
|
-
const
|
|
7711
|
-
|
|
7790
|
+
function addEntriesToMap(entryByKey, entries, gameOutput) {
|
|
7791
|
+
for (const entry of entries) {
|
|
7792
|
+
if (entry.pkg === void 0) continue;
|
|
7712
7793
|
const key = entryLookupKey(entry.pkg, entry.project);
|
|
7713
7794
|
if (!entryByKey.has(key)) entryByKey.set(key, {
|
|
7714
7795
|
entry,
|
|
7715
7796
|
gameOutput
|
|
7716
7797
|
});
|
|
7717
7798
|
}
|
|
7799
|
+
}
|
|
7800
|
+
function aggregateEntriesByKey(taskEnvelopes) {
|
|
7801
|
+
const entryByKey = /* @__PURE__ */ new Map();
|
|
7802
|
+
for (const { entries, gameOutput } of taskEnvelopes) addEntriesToMap(entryByKey, entries, gameOutput);
|
|
7718
7803
|
return entryByKey;
|
|
7719
7804
|
}
|
|
7720
7805
|
//#endregion
|
|
@@ -8239,8 +8324,8 @@ var StudioBackend = class {
|
|
|
8239
8324
|
const requestMessage = buildRunTestsMessage(jobs, requestId);
|
|
8240
8325
|
const executionStart = Date.now();
|
|
8241
8326
|
const message = await this.waitForResult(wss, requestMessage, requestId, existingSocket);
|
|
8242
|
-
const executionMs = Date.now() - executionStart;
|
|
8243
8327
|
if (message.type === "version_mismatch") throw new Error(`Studio plugin protocol version mismatch: plugin reported v${message.actualVersion.toString()}, CLI expected v${message.expectedVersion.toString()}. Update the jest-roblox Studio plugin to match this CLI version.`);
|
|
8328
|
+
const executionMs = Date.now() - executionStart;
|
|
8244
8329
|
const entries = parseEnvelope(message.jestOutput);
|
|
8245
8330
|
if (entries.length !== jobs.length) throw new Error(`Studio backend returned ${entries.length.toString()} entries but request had ${jobs.length.toString()} jobs`);
|
|
8246
8331
|
return {
|
|
@@ -8261,7 +8346,8 @@ var StudioBackend = class {
|
|
|
8261
8346
|
function attachSocket(ws) {
|
|
8262
8347
|
ws.send(String(JSON.stringify(requestMessage)));
|
|
8263
8348
|
ws.on("message", (data) => {
|
|
8264
|
-
const
|
|
8349
|
+
const raw = JSON.parse(data.toString());
|
|
8350
|
+
const message = pluginMessageSchema(raw);
|
|
8265
8351
|
if (message instanceof type.errors) {
|
|
8266
8352
|
clearTimeout(timer);
|
|
8267
8353
|
reject(/* @__PURE__ */ new Error(`Invalid plugin message: ${message.summary}`));
|
|
@@ -8323,6 +8409,7 @@ function buildRunTestsMessage(jobs, requestId) {
|
|
|
8323
8409
|
//#endregion
|
|
8324
8410
|
//#region src/backends/auto.ts
|
|
8325
8411
|
const ENV_PREFIX = "JEST_";
|
|
8412
|
+
const StudioBusyPattern = /previous call to start play session/i;
|
|
8326
8413
|
var StudioWithFallback = class {
|
|
8327
8414
|
credentials;
|
|
8328
8415
|
studio;
|
|
@@ -8347,7 +8434,7 @@ var StudioWithFallback = class {
|
|
|
8347
8434
|
}
|
|
8348
8435
|
};
|
|
8349
8436
|
function isStudioBusyError(error) {
|
|
8350
|
-
if (error instanceof LuauScriptError) return
|
|
8437
|
+
if (error instanceof LuauScriptError) return StudioBusyPattern.test(error.message);
|
|
8351
8438
|
return typeof error === "object" && error !== null && "code" in error && error.code === "EADDRINUSE";
|
|
8352
8439
|
}
|
|
8353
8440
|
async function probeStudioPlugin(port, timeoutMs, createServer = (wsPort) => {
|
|
@@ -8472,7 +8559,7 @@ function walkDirectory(directoryPath, baseDirectory) {
|
|
|
8472
8559
|
*/
|
|
8473
8560
|
function applyExcludes(files, excludeGlobs) {
|
|
8474
8561
|
if (excludeGlobs === void 0 || excludeGlobs.length === 0) return files;
|
|
8475
|
-
return files.filter((file) =>
|
|
8562
|
+
return files.filter((file) => excludeGlobs.every((pattern) => !matchesGlobPattern(file, pattern)));
|
|
8476
8563
|
}
|
|
8477
8564
|
//#endregion
|
|
8478
8565
|
//#region src/config/derive-typecheck-include.ts
|
|
@@ -8485,16 +8572,19 @@ function applyExcludes(files, excludeGlobs) {
|
|
|
8485
8572
|
* `/\.(test-d|spec-d)\.ts$/`), so patterns ending in any other extension, lacking
|
|
8486
8573
|
* a trailing `.spec.`/`.test.` marker, or already carrying `-d` are dropped.
|
|
8487
8574
|
*/
|
|
8575
|
+
const SpecTsSuffixPattern = /\.spec\.ts$/;
|
|
8576
|
+
const TestTsSuffixPattern = /\.test\.ts$/;
|
|
8488
8577
|
function deriveTypecheckInclude(runtimeInclude) {
|
|
8489
8578
|
const derived = [];
|
|
8490
|
-
for (const pattern of runtimeInclude) if (
|
|
8491
|
-
else if (
|
|
8579
|
+
for (const pattern of runtimeInclude) if (SpecTsSuffixPattern.test(pattern)) derived.push(pattern.replace(SpecTsSuffixPattern, ".spec-d.ts"));
|
|
8580
|
+
else if (TestTsSuffixPattern.test(pattern)) derived.push(pattern.replace(TestTsSuffixPattern, ".test-d.ts"));
|
|
8492
8581
|
return derived;
|
|
8493
8582
|
}
|
|
8494
8583
|
//#endregion
|
|
8495
8584
|
//#region src/utils/extensions.ts
|
|
8585
|
+
const TS_OR_LUAU_EXTENSION$1 = /\.(tsx?|luau?)$/;
|
|
8496
8586
|
function stripTsExtension(pattern) {
|
|
8497
|
-
return pattern.replace(
|
|
8587
|
+
return pattern.replace(TS_OR_LUAU_EXTENSION$1, "");
|
|
8498
8588
|
}
|
|
8499
8589
|
//#endregion
|
|
8500
8590
|
//#region src/luau/eval-literals.ts
|
|
@@ -8603,6 +8693,8 @@ function getTemporaryDirectory() {
|
|
|
8603
8693
|
}
|
|
8604
8694
|
//#endregion
|
|
8605
8695
|
//#region src/config/projects.ts
|
|
8696
|
+
const TRAILING_SLASH$2 = /\/$/;
|
|
8697
|
+
const TS_OR_LUAU_EXTENSION = /\.(tsx?|luau?)$/;
|
|
8606
8698
|
function extractStaticRoot(pattern) {
|
|
8607
8699
|
const globChars = /* @__PURE__ */ new Set([
|
|
8608
8700
|
"*",
|
|
@@ -8630,7 +8722,7 @@ function extractStaticRoot(pattern) {
|
|
|
8630
8722
|
};
|
|
8631
8723
|
}
|
|
8632
8724
|
function mapFsRootToDataModel(outDirectory, rojoTree) {
|
|
8633
|
-
const normalized = outDirectory.replace(
|
|
8725
|
+
const normalized = outDirectory.replace(TRAILING_SLASH$2, "");
|
|
8634
8726
|
const result = findInTree(rojoTree, normalized, "");
|
|
8635
8727
|
if (result === void 0) {
|
|
8636
8728
|
const available = [];
|
|
@@ -8655,7 +8747,7 @@ function extractProjectRoots(include) {
|
|
|
8655
8747
|
}
|
|
8656
8748
|
patterns.push(qualified);
|
|
8657
8749
|
}
|
|
8658
|
-
return
|
|
8750
|
+
return Array.from(rootMap, ([root, testMatch]) => ({
|
|
8659
8751
|
root,
|
|
8660
8752
|
testMatch
|
|
8661
8753
|
}));
|
|
@@ -8692,7 +8784,8 @@ const PROJECT_ONLY_KEYS = /* @__PURE__ */ new Set([
|
|
|
8692
8784
|
function dedupeMounts(mounts) {
|
|
8693
8785
|
const seen = /* @__PURE__ */ new Set();
|
|
8694
8786
|
const result = [];
|
|
8695
|
-
for (const mount of mounts)
|
|
8787
|
+
for (const mount of mounts) {
|
|
8788
|
+
if (seen.has(mount.dataModelPath)) continue;
|
|
8696
8789
|
seen.add(mount.dataModelPath);
|
|
8697
8790
|
result.push(mount);
|
|
8698
8791
|
}
|
|
@@ -8848,7 +8941,7 @@ function deriveIncludeFromTestMatch(config, configDirectory, tsconfig) {
|
|
|
8848
8941
|
if (raw["include"] !== void 0) return;
|
|
8849
8942
|
if (!Array.isArray(raw["testMatch"])) return;
|
|
8850
8943
|
config.include = raw["testMatch"].flatMap((pattern) => {
|
|
8851
|
-
return (
|
|
8944
|
+
return (TS_OR_LUAU_EXTENSION.test(pattern) ? [pattern] : [`${pattern}.ts`, `${pattern}.tsx`]).map((extension) => path$1.posix.join(configDirectory, extension));
|
|
8852
8945
|
});
|
|
8853
8946
|
const { outDir, rootDir } = tsconfig;
|
|
8854
8947
|
if (raw["outDir"] === void 0 && rootDir !== void 0 && outDir !== void 0) {
|
|
@@ -8942,6 +9035,7 @@ function buildNoMatchMessage(files, roots) {
|
|
|
8942
9035
|
const REGEX_METACHARACTERS = /[.*+?^${}()|[\]\\]/g;
|
|
8943
9036
|
const TEST_FILE_EXTENSION = /\.(tsx?|luau?)$/;
|
|
8944
9037
|
const TS_SOURCE_EXTENSION = /\.tsx?$/;
|
|
9038
|
+
const INDEX_STEM = /^index(\.|$)/;
|
|
8945
9039
|
/**
|
|
8946
9040
|
* Translate a list of explicit test files (typically from CLI positional args)
|
|
8947
9041
|
* into a `testPathPattern` regex that constrains Jest on the Luau side. Each
|
|
@@ -8999,7 +9093,7 @@ function narrowForLuauRun(config, runtimeFiles, filterActive) {
|
|
|
8999
9093
|
* rewritten (else a pure-Luau project's positional arg matches zero tests).
|
|
9000
9094
|
*/
|
|
9001
9095
|
function indexStemToInit(basename) {
|
|
9002
|
-
return basename.replace(
|
|
9096
|
+
return basename.replace(INDEX_STEM, "init$1");
|
|
9003
9097
|
}
|
|
9004
9098
|
function toBasenamePattern(file) {
|
|
9005
9099
|
const posix = file.replaceAll("\\", "/");
|
|
@@ -9023,8 +9117,8 @@ function isGeneratedStub(filePath) {
|
|
|
9023
9117
|
try {
|
|
9024
9118
|
const fd = fs.openSync(filePath, "r");
|
|
9025
9119
|
try {
|
|
9026
|
-
const
|
|
9027
|
-
return fs.readSync(fd,
|
|
9120
|
+
const buffer = Buffer.alloc(47);
|
|
9121
|
+
return fs.readSync(fd, buffer, 0, 47, 0) === 47 && buffer.toString("utf8") === HEADER;
|
|
9028
9122
|
} finally {
|
|
9029
9123
|
fs.closeSync(fd);
|
|
9030
9124
|
}
|
|
@@ -9131,17 +9225,7 @@ function cleanLeftoverStubs(projects, rootDirectory) {
|
|
|
9131
9225
|
realRoot ??= fs.realpathSync(path.resolve(rootDirectory));
|
|
9132
9226
|
return realRoot;
|
|
9133
9227
|
}
|
|
9134
|
-
for (const project of projects)
|
|
9135
|
-
assertMountContained(project, mount.fsPath, rootDirectory);
|
|
9136
|
-
const stubPath = path.resolve(rootDirectory, mount.fsPath, STUB_FILENAME);
|
|
9137
|
-
if (!isGeneratedStub(stubPath)) continue;
|
|
9138
|
-
const realStubPath = fs.realpathSync(stubPath);
|
|
9139
|
-
const root = realRootResolved();
|
|
9140
|
-
const relativePath = path.relative(root, realStubPath);
|
|
9141
|
-
if (!(relativePath === "" || !relativePath.startsWith("..") && !path.isAbsolute(relativePath))) throw new Error(`Project "${project.displayName}" mount fsPath resolves outside root via symlink: ${mount.fsPath} → ${realStubPath}`);
|
|
9142
|
-
fs.unlinkSync(stubPath);
|
|
9143
|
-
cleaned.push(stubPath);
|
|
9144
|
-
}
|
|
9228
|
+
for (const project of projects) cleaned.push(...cleanLeftoverStubsForProject(project, rootDirectory, realRootResolved));
|
|
9145
9229
|
return cleaned;
|
|
9146
9230
|
}
|
|
9147
9231
|
function generateProjectStubs(projects, rootDirectory, outputRoot) {
|
|
@@ -9155,15 +9239,7 @@ function generateProjectStubs(projects, rootDirectory, outputRoot) {
|
|
|
9155
9239
|
include: [],
|
|
9156
9240
|
testMatch: project.testMatch
|
|
9157
9241
|
};
|
|
9158
|
-
|
|
9159
|
-
assertMountContained(project, mount.fsPath, writeRoot);
|
|
9160
|
-
if (hasUserAuthoredConfig(path.resolve(rootDirectory, mount.fsPath))) continue;
|
|
9161
|
-
const outputPath = path.resolve(writeRoot, mount.fsPath, STUB_FILENAME);
|
|
9162
|
-
entries.push({
|
|
9163
|
-
config: stubConfig,
|
|
9164
|
-
outputPath
|
|
9165
|
-
});
|
|
9166
|
-
}
|
|
9242
|
+
entries.push(...buildStubEntriesForProject(project, stubConfig, rootDirectory, writeRoot));
|
|
9167
9243
|
}
|
|
9168
9244
|
generateProjectConfigs(entries);
|
|
9169
9245
|
}
|
|
@@ -9184,6 +9260,34 @@ function syncStubsToShadowDirectory(projects, rootDirectory, shadowDirectory) {
|
|
|
9184
9260
|
}
|
|
9185
9261
|
return changed;
|
|
9186
9262
|
}
|
|
9263
|
+
function cleanLeftoverStubsForProject(project, rootDirectory, realRootResolved) {
|
|
9264
|
+
const cleaned = [];
|
|
9265
|
+
for (const mount of project.rojoMounts) {
|
|
9266
|
+
assertMountContained(project, mount.fsPath, rootDirectory);
|
|
9267
|
+
const stubPath = path.resolve(rootDirectory, mount.fsPath, STUB_FILENAME);
|
|
9268
|
+
if (!isGeneratedStub(stubPath)) continue;
|
|
9269
|
+
const realStubPath = fs.realpathSync(stubPath);
|
|
9270
|
+
const root = realRootResolved();
|
|
9271
|
+
const relativePath = path.relative(root, realStubPath);
|
|
9272
|
+
if (!(relativePath === "" || !relativePath.startsWith("..") && !path.isAbsolute(relativePath))) throw new Error(`Project "${project.displayName}" mount fsPath resolves outside root via symlink: ${mount.fsPath} → ${realStubPath}`);
|
|
9273
|
+
fs.unlinkSync(stubPath);
|
|
9274
|
+
cleaned.push(stubPath);
|
|
9275
|
+
}
|
|
9276
|
+
return cleaned;
|
|
9277
|
+
}
|
|
9278
|
+
function buildStubEntriesForProject(project, stubConfig, rootDirectory, writeRoot) {
|
|
9279
|
+
const entries = [];
|
|
9280
|
+
for (const mount of project.rojoMounts) {
|
|
9281
|
+
assertMountContained(project, mount.fsPath, writeRoot);
|
|
9282
|
+
if (hasUserAuthoredConfig(path.resolve(rootDirectory, mount.fsPath))) continue;
|
|
9283
|
+
const outputPath = path.resolve(writeRoot, mount.fsPath, STUB_FILENAME);
|
|
9284
|
+
entries.push({
|
|
9285
|
+
config: stubConfig,
|
|
9286
|
+
outputPath
|
|
9287
|
+
});
|
|
9288
|
+
}
|
|
9289
|
+
return entries;
|
|
9290
|
+
}
|
|
9187
9291
|
function buildStubConfig(config) {
|
|
9188
9292
|
const result = {};
|
|
9189
9293
|
for (const [key, value] of Object.entries(config)) if (PROJECT_TEST_KEYS.has(key) && !STUB_SKIP_KEYS.has(key) && value !== void 0) result[key] = value;
|
|
@@ -9254,6 +9358,7 @@ function serializeLuauValue(value, indent) {
|
|
|
9254
9358
|
}
|
|
9255
9359
|
//#endregion
|
|
9256
9360
|
//#region src/coverage-pipeline/derive-coverage-from.ts
|
|
9361
|
+
const SPEC_OR_TEST_EXTENSION = /\.(?:spec|test)(\.\w+)$/;
|
|
9257
9362
|
/**
|
|
9258
9363
|
* Derives `collectCoverageFrom` glob patterns from project `include` patterns.
|
|
9259
9364
|
*
|
|
@@ -9286,7 +9391,7 @@ function deriveCoverageFromIncludes(projects) {
|
|
|
9286
9391
|
* extension so that misconfigured globs fail loudly.
|
|
9287
9392
|
*/
|
|
9288
9393
|
function inferSourceExtension(pattern) {
|
|
9289
|
-
const match = pattern.match(
|
|
9394
|
+
const match = pattern.match(SPEC_OR_TEST_EXTENSION);
|
|
9290
9395
|
if (!match) throw new Error(`Cannot infer source extension from include pattern "${pattern}". Patterns must end with .spec.<ext> or .test.<ext> (e.g. **/*.spec.ts, **/*.test.luau).`);
|
|
9291
9396
|
const [, extension] = match;
|
|
9292
9397
|
return extension;
|
|
@@ -9428,9 +9533,10 @@ function extractDefinition(node, source) {
|
|
|
9428
9533
|
//#endregion
|
|
9429
9534
|
//#region src/typecheck/parse.ts
|
|
9430
9535
|
const errorCodeRegExp = /error TS(?<errorCode>\d+)/;
|
|
9536
|
+
const LINE_SPLIT = /\r?\n/;
|
|
9431
9537
|
function parseTscOutput(stdout) {
|
|
9432
9538
|
const map = /* @__PURE__ */ new Map();
|
|
9433
|
-
const merged = stdout.split(
|
|
9539
|
+
const merged = stdout.split(LINE_SPLIT).reduce((lines, next) => {
|
|
9434
9540
|
if (!next) return lines;
|
|
9435
9541
|
if (next[0] !== " ") lines.push(next);
|
|
9436
9542
|
else if (lines.length > 0) lines[lines.length - 1] += `\n${next}`;
|
|
@@ -9561,7 +9667,7 @@ async function runTypecheck(options) {
|
|
|
9561
9667
|
function buildFileResult(filePath, fileInfo, errors) {
|
|
9562
9668
|
const indexMap = createLocationsIndexMap(fileInfo.source);
|
|
9563
9669
|
const testDefinitions = fileInfo.definitions.filter((definition) => definition.type === "test");
|
|
9564
|
-
const sortedDefinitions =
|
|
9670
|
+
const sortedDefinitions = testDefinitions.toSorted((a, b) => b.start - a.start);
|
|
9565
9671
|
const errorsByTest = /* @__PURE__ */ new Map();
|
|
9566
9672
|
const fileErrors = [];
|
|
9567
9673
|
for (const error of errors) {
|
|
@@ -9738,7 +9844,7 @@ function discoverTestFiles(config, cliFiles) {
|
|
|
9738
9844
|
}
|
|
9739
9845
|
const ignoredPatterns = config.testPathIgnorePatterns.map((pat) => new RegExp(pat));
|
|
9740
9846
|
const baseFiles = allFiles.filter((file) => {
|
|
9741
|
-
return
|
|
9847
|
+
return ignoredPatterns.every((pattern) => !pattern.test(file));
|
|
9742
9848
|
});
|
|
9743
9849
|
const totalFiles = new Set(baseFiles).size;
|
|
9744
9850
|
let filtered = baseFiles;
|
|
@@ -9854,13 +9960,7 @@ const VERSION$2 = version;
|
|
|
9854
9960
|
*/
|
|
9855
9961
|
function collectStubMounts(projects, rootDirectory, cacheRoot) {
|
|
9856
9962
|
const stubMounts = [];
|
|
9857
|
-
for (const project of projects)
|
|
9858
|
-
if (hasUserAuthoredConfig(path$1.resolve(rootDirectory, mount.fsPath))) continue;
|
|
9859
|
-
stubMounts.push({
|
|
9860
|
-
absStubPath: path$1.resolve(cacheRoot, mount.fsPath, STUB_FILENAME),
|
|
9861
|
-
dataModelPath: mount.dataModelPath
|
|
9862
|
-
});
|
|
9863
|
-
}
|
|
9963
|
+
for (const project of projects) stubMounts.push(...collectStubMountsForProject(project, rootDirectory, cacheRoot));
|
|
9864
9964
|
return stubMounts;
|
|
9865
9965
|
}
|
|
9866
9966
|
function loadRojoTree(config) {
|
|
@@ -9871,6 +9971,26 @@ function loadRojoTree(config) {
|
|
|
9871
9971
|
return resolveNestedProjects(validated.tree, path$1.dirname(rojoPath));
|
|
9872
9972
|
}
|
|
9873
9973
|
/**
|
|
9974
|
+
* Instrument + rojo-build the coverage place and project it to a
|
|
9975
|
+
* `CoverageArtifacts`, optionally baking each project's `jest.config` stub into
|
|
9976
|
+
* the place. The single seam the run path (`prepareMultiProjectCoverage`) and
|
|
9977
|
+
* the offline build path (`buildCoveragePlace`) both drive, so the
|
|
9978
|
+
* prepare-and-bake mechanism can't drift between them. The caller owns the
|
|
9979
|
+
* `bakeStubs` decision (the run path skips baking for studio-cli, which injects
|
|
9980
|
+
* `jest.config` at runtime; the build path always bakes so a place opened by a
|
|
9981
|
+
* foreign runner is self-contained). Stubs must already be generated into
|
|
9982
|
+
* `cacheRoot`. Baking mirrors those cache stubs into the shadow tree — the
|
|
9983
|
+
* source tree is clean (stubs land in `cacheRoot`, not `rootDir`), so without it
|
|
9984
|
+
* the coverage place would build with no `jest.config` ModuleScripts.
|
|
9985
|
+
*/
|
|
9986
|
+
function prepareBakedCoverage(config, projects, cacheRoot, bakeStubs) {
|
|
9987
|
+
const coverage = prepareCoverage(config, bakeStubs ? (shadowDirectory) => syncStubsToShadowDirectory(projects, cacheRoot, shadowDirectory) : void 0);
|
|
9988
|
+
return {
|
|
9989
|
+
artifacts: toCoverageArtifacts(coverage, toBuildManifestProjects(projects)),
|
|
9990
|
+
coverage
|
|
9991
|
+
};
|
|
9992
|
+
}
|
|
9993
|
+
/**
|
|
9874
9994
|
* Multi-project execution core: stub generation, place build, discovery, and
|
|
9875
9995
|
* job dispatch over a set of already-resolved projects. Shared by the
|
|
9876
9996
|
* `projects:`-configured path (`runMultiProject`) and the no-`projects` collapse
|
|
@@ -9999,6 +10119,17 @@ async function runMultiProject(options) {
|
|
|
9999
10119
|
return resolveAllProjects(rawProjects, rootConfig, rojoTree, rootConfig.rootDir);
|
|
10000
10120
|
}), rootConfig, cli, timing);
|
|
10001
10121
|
}
|
|
10122
|
+
function collectStubMountsForProject(project, rootDirectory, cacheRoot) {
|
|
10123
|
+
const stubMounts = [];
|
|
10124
|
+
for (const mount of project.rojoMounts) {
|
|
10125
|
+
if (hasUserAuthoredConfig(path$1.resolve(rootDirectory, mount.fsPath))) continue;
|
|
10126
|
+
stubMounts.push({
|
|
10127
|
+
absStubPath: path$1.resolve(cacheRoot, mount.fsPath, STUB_FILENAME),
|
|
10128
|
+
dataModelPath: mount.dataModelPath
|
|
10129
|
+
});
|
|
10130
|
+
}
|
|
10131
|
+
return stubMounts;
|
|
10132
|
+
}
|
|
10002
10133
|
function buildMultiDisplayFilter(options) {
|
|
10003
10134
|
const { cliFiles, matchedRuntimeFiles, projectNames, projects, rootDir, testPathPattern } = options;
|
|
10004
10135
|
if (cliFiles !== void 0 && cliFiles.length > 0) return sourceTwinFilter(cliFiles, rootDir);
|
|
@@ -10173,11 +10304,9 @@ function prepareMultiProjectCoverage(rootConfig, projects, cacheRoot) {
|
|
|
10173
10304
|
};
|
|
10174
10305
|
const bakeStubs = rootConfig.backend !== "studio-cli";
|
|
10175
10306
|
const start = Date.now();
|
|
10176
|
-
const coverage =
|
|
10177
|
-
return syncStubsToShadowDirectory(projects, cacheRoot, shadowDirectory);
|
|
10178
|
-
} : void 0);
|
|
10307
|
+
const { artifacts, coverage } = prepareBakedCoverage(rootConfig, projects, cacheRoot, bakeStubs);
|
|
10179
10308
|
return {
|
|
10180
|
-
coverageArtifacts:
|
|
10309
|
+
coverageArtifacts: artifacts,
|
|
10181
10310
|
effectiveConfig: {
|
|
10182
10311
|
...rootConfig,
|
|
10183
10312
|
placeFile: coverage.placeFile
|
|
@@ -10224,6 +10353,7 @@ function selectProjects(allProjects, projectNames, cliFiles, rootDirectory) {
|
|
|
10224
10353
|
}
|
|
10225
10354
|
//#endregion
|
|
10226
10355
|
//#region src/run/single-projects.ts
|
|
10356
|
+
const TRAILING_SLASH$1 = /\/$/;
|
|
10227
10357
|
/**
|
|
10228
10358
|
* Map each compiled-Luau root to its Rojo mount (FS path ↔ DataModel path) via
|
|
10229
10359
|
* the Rojo tree. Roots that don't map (a compiled-output dir the Rojo project
|
|
@@ -10232,7 +10362,7 @@ function selectProjects(allProjects, projectNames, cliFiles, rootDirectory) {
|
|
|
10232
10362
|
*/
|
|
10233
10363
|
function deriveProjectMounts(luauRoots, rojoTree) {
|
|
10234
10364
|
return dedupeMounts(luauRoots.flatMap((luauRoot) => {
|
|
10235
|
-
const fsPath = luauRoot.replace(
|
|
10365
|
+
const fsPath = luauRoot.replace(TRAILING_SLASH$1, "");
|
|
10236
10366
|
const dataModelPath = findInTree(rojoTree, fsPath, "");
|
|
10237
10367
|
return dataModelPath !== void 0 ? [{
|
|
10238
10368
|
dataModelPath,
|
|
@@ -10650,10 +10780,11 @@ function prepareWorkspaceCoverage(options) {
|
|
|
10650
10780
|
const timing = options.timing ?? NOOP_TIMING_COLLECTOR;
|
|
10651
10781
|
const defaultMatcher = createIgnoreMatcher(DEFAULT_CONFIG.coveragePathIgnorePatterns);
|
|
10652
10782
|
return packages.map((descriptor) => {
|
|
10653
|
-
|
|
10783
|
+
const ignore = {
|
|
10654
10784
|
matcher: descriptor.coveragePathIgnorePatterns !== void 0 ? createIgnoreMatcher(descriptor.coveragePathIgnorePatterns) : defaultMatcher,
|
|
10655
10785
|
patterns: descriptor.coveragePathIgnorePatterns ?? DEFAULT_CONFIG.coveragePathIgnorePatterns
|
|
10656
|
-
}
|
|
10786
|
+
};
|
|
10787
|
+
return prepareForPackage(descriptor, workspaceRoot, ignore, timing);
|
|
10657
10788
|
});
|
|
10658
10789
|
}
|
|
10659
10790
|
/**
|
|
@@ -10840,10 +10971,11 @@ function prepareForPackage(descriptor, workspaceRoot, ignore, timing) {
|
|
|
10840
10971
|
shadowDir: shadowDirectory
|
|
10841
10972
|
});
|
|
10842
10973
|
}
|
|
10974
|
+
const generatedAtDate = /* @__PURE__ */ new Date();
|
|
10843
10975
|
const manifest = {
|
|
10844
10976
|
buildId: crypto.randomUUID(),
|
|
10845
10977
|
files: allFiles,
|
|
10846
|
-
generatedAt:
|
|
10978
|
+
generatedAt: generatedAtDate.toISOString(),
|
|
10847
10979
|
instrumenterVersion: 2,
|
|
10848
10980
|
luauRoots: coverageRoots.map((entry) => entry.shadowDir),
|
|
10849
10981
|
nonInstrumentedFiles: allNonInstrumented,
|
|
@@ -11204,7 +11336,7 @@ async function runWorkspaceProfiled(options, timing) {
|
|
|
11204
11336
|
const { emptyPackageErrors, filteredContexts, pending, typecheckByDirectory, typeTestEntries, typeTestProjects } = timing.profile("discoverTests", () => {
|
|
11205
11337
|
const discoveredContexts = applyProjectFilter(contexts, cli.project);
|
|
11206
11338
|
const discovered = collectPendingEntries(discoveredContexts, cli);
|
|
11207
|
-
const typeTestPackages = new Set(
|
|
11339
|
+
const typeTestPackages = new Set(Array.from(discovered.typecheckByDirectory.values(), (entry) => entry.pkg));
|
|
11208
11340
|
const policy = applyEmptyPackagePolicy(discovered.pending, discoveredContexts, typeTestPackages);
|
|
11209
11341
|
return {
|
|
11210
11342
|
emptyPackageErrors: policy.emptyPackageErrors,
|
|
@@ -11311,7 +11443,7 @@ async function runWorkspaceProfiled(options, timing) {
|
|
|
11311
11443
|
pending,
|
|
11312
11444
|
results,
|
|
11313
11445
|
runOptions,
|
|
11314
|
-
verbose:
|
|
11446
|
+
verbose: cli.verbose,
|
|
11315
11447
|
workspaceRoot
|
|
11316
11448
|
});
|
|
11317
11449
|
return attachTypecheck(attachCoverageManifests(results, pending, coverageByPackage), typecheckPass.outcome, timing);
|
|
@@ -11541,6 +11673,43 @@ function applyEmptyPackagePolicy(allEntries, contexts, typeTestPackages) {
|
|
|
11541
11673
|
pending
|
|
11542
11674
|
};
|
|
11543
11675
|
}
|
|
11676
|
+
function collectPackageProjectEntries(ctx, cliTypecheck, packageTypecheck, accumulators) {
|
|
11677
|
+
const { pending, typecheckByDirectory, typeTestEntries, typeTestProjects } = accumulators;
|
|
11678
|
+
const { packageDirectory } = ctx.info;
|
|
11679
|
+
for (const project of ctx.projects) {
|
|
11680
|
+
const typecheck = resolveTypecheckConfig({
|
|
11681
|
+
cli: cliTypecheck,
|
|
11682
|
+
project: project.typecheck,
|
|
11683
|
+
root: ctx.pkgConfig.typecheck
|
|
11684
|
+
});
|
|
11685
|
+
const projectConfig = buildProjectExecutionConfig(ctx.pkgConfig, project);
|
|
11686
|
+
const testFiles = typecheck.only ? [] : discoverProjectTestFiles(project, packageDirectory);
|
|
11687
|
+
pending.push({
|
|
11688
|
+
pkg: ctx.info.name,
|
|
11689
|
+
project,
|
|
11690
|
+
projectConfig,
|
|
11691
|
+
testFiles
|
|
11692
|
+
});
|
|
11693
|
+
if (!typecheck.enabled) continue;
|
|
11694
|
+
const typeTestFiles = discoverProjectTypeTests(project, typecheck, packageDirectory);
|
|
11695
|
+
if (typeTestFiles.length === 0) continue;
|
|
11696
|
+
typeTestProjects.push({
|
|
11697
|
+
pkg: ctx.info.name,
|
|
11698
|
+
project: project.displayName
|
|
11699
|
+
});
|
|
11700
|
+
typeTestEntries.push({
|
|
11701
|
+
cwd: packageDirectory,
|
|
11702
|
+
files: typeTestFiles,
|
|
11703
|
+
...typecheck.tsconfig !== void 0 ? { tsconfig: typecheck.tsconfig } : {}
|
|
11704
|
+
});
|
|
11705
|
+
typecheckByDirectory.set(packageDirectory, {
|
|
11706
|
+
ignoreSourceErrors: packageTypecheck.ignoreSourceErrors,
|
|
11707
|
+
pkg: ctx.info.name,
|
|
11708
|
+
spawnTimeout: packageTypecheck.spawnTimeout,
|
|
11709
|
+
timeout: ctx.pkgConfig.timeout
|
|
11710
|
+
});
|
|
11711
|
+
}
|
|
11712
|
+
}
|
|
11544
11713
|
function collectPendingEntries(contexts, cli) {
|
|
11545
11714
|
const cliTypecheck = {
|
|
11546
11715
|
enabled: cli.typecheck,
|
|
@@ -11551,46 +11720,15 @@ function collectPendingEntries(contexts, cli) {
|
|
|
11551
11720
|
const typeTestEntries = [];
|
|
11552
11721
|
const typeTestProjects = [];
|
|
11553
11722
|
const typecheckByDirectory = /* @__PURE__ */ new Map();
|
|
11554
|
-
for (const ctx of contexts) {
|
|
11555
|
-
|
|
11556
|
-
|
|
11557
|
-
|
|
11558
|
-
|
|
11559
|
-
|
|
11560
|
-
|
|
11561
|
-
|
|
11562
|
-
|
|
11563
|
-
project: project.typecheck,
|
|
11564
|
-
root: ctx.pkgConfig.typecheck
|
|
11565
|
-
});
|
|
11566
|
-
const projectConfig = buildProjectExecutionConfig(ctx.pkgConfig, project);
|
|
11567
|
-
const testFiles = typecheck.only ? [] : discoverProjectTestFiles(project, packageDirectory);
|
|
11568
|
-
pending.push({
|
|
11569
|
-
pkg: ctx.info.name,
|
|
11570
|
-
project,
|
|
11571
|
-
projectConfig,
|
|
11572
|
-
testFiles
|
|
11573
|
-
});
|
|
11574
|
-
if (!typecheck.enabled) continue;
|
|
11575
|
-
const typeTestFiles = discoverProjectTypeTests(project, typecheck, packageDirectory);
|
|
11576
|
-
if (typeTestFiles.length === 0) continue;
|
|
11577
|
-
typeTestProjects.push({
|
|
11578
|
-
pkg: ctx.info.name,
|
|
11579
|
-
project: project.displayName
|
|
11580
|
-
});
|
|
11581
|
-
typeTestEntries.push({
|
|
11582
|
-
cwd: packageDirectory,
|
|
11583
|
-
files: typeTestFiles,
|
|
11584
|
-
...typecheck.tsconfig !== void 0 ? { tsconfig: typecheck.tsconfig } : {}
|
|
11585
|
-
});
|
|
11586
|
-
typecheckByDirectory.set(packageDirectory, {
|
|
11587
|
-
ignoreSourceErrors: packageTypecheck.ignoreSourceErrors,
|
|
11588
|
-
pkg: ctx.info.name,
|
|
11589
|
-
spawnTimeout: packageTypecheck.spawnTimeout,
|
|
11590
|
-
timeout: ctx.pkgConfig.timeout
|
|
11591
|
-
});
|
|
11592
|
-
}
|
|
11593
|
-
}
|
|
11723
|
+
for (const ctx of contexts) collectPackageProjectEntries(ctx, cliTypecheck, resolveTypecheckConfig({
|
|
11724
|
+
cli: cliTypecheck,
|
|
11725
|
+
root: ctx.pkgConfig.typecheck
|
|
11726
|
+
}), {
|
|
11727
|
+
pending,
|
|
11728
|
+
typecheckByDirectory,
|
|
11729
|
+
typeTestEntries,
|
|
11730
|
+
typeTestProjects
|
|
11731
|
+
});
|
|
11594
11732
|
return {
|
|
11595
11733
|
pending,
|
|
11596
11734
|
typecheckByDirectory,
|
|
@@ -11746,19 +11884,24 @@ function liveProjectsByPackage(pending) {
|
|
|
11746
11884
|
}
|
|
11747
11885
|
return live;
|
|
11748
11886
|
}
|
|
11887
|
+
function collectLiveProjectStubMounts(project, ctx) {
|
|
11888
|
+
const stubMounts = [];
|
|
11889
|
+
for (const mount of project.rojoMounts) {
|
|
11890
|
+
if (hasUserAuthoredConfig(path$1.resolve(ctx.info.packageDirectory, mount.fsPath))) continue;
|
|
11891
|
+
stubMounts.push({
|
|
11892
|
+
absStubPath: path$1.resolve(ctx.cacheRoot, mount.fsPath, STUB_FILENAME),
|
|
11893
|
+
dataModelPath: mount.dataModelPath
|
|
11894
|
+
});
|
|
11895
|
+
}
|
|
11896
|
+
return stubMounts;
|
|
11897
|
+
}
|
|
11749
11898
|
function writeStubsAndBuildDescriptors(contexts, liveProjects) {
|
|
11750
11899
|
return contexts.map((ctx) => {
|
|
11751
11900
|
const live = liveProjects.get(ctx.info.name) ?? /* @__PURE__ */ new Set();
|
|
11752
11901
|
const liveProjectsForPackage = ctx.projects.filter((project) => live.has(project.displayName));
|
|
11753
11902
|
generateProjectStubs(liveProjectsForPackage, ctx.info.packageDirectory, ctx.cacheRoot);
|
|
11754
11903
|
const stubMounts = [];
|
|
11755
|
-
for (const project of liveProjectsForPackage)
|
|
11756
|
-
if (hasUserAuthoredConfig(path$1.resolve(ctx.info.packageDirectory, mount.fsPath))) continue;
|
|
11757
|
-
stubMounts.push({
|
|
11758
|
-
absStubPath: path$1.resolve(ctx.cacheRoot, mount.fsPath, STUB_FILENAME),
|
|
11759
|
-
dataModelPath: mount.dataModelPath
|
|
11760
|
-
});
|
|
11761
|
-
}
|
|
11904
|
+
for (const project of liveProjectsForPackage) stubMounts.push(...collectLiveProjectStubMounts(project, ctx));
|
|
11762
11905
|
return {
|
|
11763
11906
|
...ctx.descriptor,
|
|
11764
11907
|
stubMounts
|
|
@@ -11782,6 +11925,7 @@ function discoverWorkspaceRoot(cwd) {
|
|
|
11782
11925
|
//#endregion
|
|
11783
11926
|
//#region src/workspace/package-resolver.ts
|
|
11784
11927
|
const JEST_CONFIG_MARKER$1 = /^jest\.config\.[^.]+$/;
|
|
11928
|
+
const TRAILING_SLASH = /\/$/;
|
|
11785
11929
|
function readPackageJsonName(packageJsonPath) {
|
|
11786
11930
|
if (!fs$1.existsSync(packageJsonPath)) return;
|
|
11787
11931
|
const raw = parsePackageJson(packageJsonPath);
|
|
@@ -11819,7 +11963,7 @@ function listPnpmPackages(workspaceRoot) {
|
|
|
11819
11963
|
const patterns = parseYAML(fs$1.readFileSync(yamlPath, "utf-8")).packages ?? [];
|
|
11820
11964
|
const packages = [];
|
|
11821
11965
|
for (const pattern of patterns) {
|
|
11822
|
-
const matches = globSync(`${pattern.replace(
|
|
11966
|
+
const matches = globSync(`${pattern.replace(TRAILING_SLASH, "")}/package.json`, { cwd: workspaceRoot });
|
|
11823
11967
|
for (const match of matches) {
|
|
11824
11968
|
const packageJsonPath = path$1.join(workspaceRoot, match);
|
|
11825
11969
|
const name = readPackageJsonName(packageJsonPath);
|
|
@@ -11831,9 +11975,6 @@ function listPnpmPackages(workspaceRoot) {
|
|
|
11831
11975
|
}
|
|
11832
11976
|
return packages;
|
|
11833
11977
|
}
|
|
11834
|
-
function inferPackageName(packageDirectory) {
|
|
11835
|
-
return readPackageJsonName(path$1.join(packageDirectory, "package.json")) ?? path$1.basename(packageDirectory);
|
|
11836
|
-
}
|
|
11837
11978
|
function assertNoDuplicateNames(packages, workspaceRoot) {
|
|
11838
11979
|
const byName = /* @__PURE__ */ new Map();
|
|
11839
11980
|
for (const package_ of packages) {
|
|
@@ -11843,26 +11984,29 @@ function assertNoDuplicateNames(packages, workspaceRoot) {
|
|
|
11843
11984
|
byName.set(package_.name, list);
|
|
11844
11985
|
}
|
|
11845
11986
|
for (const [name, paths] of byName) if (paths.length > 1) {
|
|
11846
|
-
const sorted =
|
|
11987
|
+
const sorted = paths.toSorted();
|
|
11847
11988
|
throw new Error(`Duplicate package name "${name}" from ${sorted.join(" and ")}. Add a package.json with a unique \`name\`, or rename a directory.`);
|
|
11848
11989
|
}
|
|
11849
11990
|
}
|
|
11991
|
+
function inferPackageName(packageDirectory) {
|
|
11992
|
+
return readPackageJsonName(path$1.join(packageDirectory, "package.json")) ?? path$1.basename(packageDirectory);
|
|
11993
|
+
}
|
|
11994
|
+
function collectPackagesFromMatches(matches, workspaceRoot, seenDirectories, packages) {
|
|
11995
|
+
for (const match of matches) {
|
|
11996
|
+
if (!JEST_CONFIG_MARKER$1.test(path$1.basename(match))) continue;
|
|
11997
|
+
const packageDirectory = path$1.dirname(path$1.join(workspaceRoot, match));
|
|
11998
|
+
if (seenDirectories.has(packageDirectory)) continue;
|
|
11999
|
+
seenDirectories.add(packageDirectory);
|
|
12000
|
+
packages.push({
|
|
12001
|
+
name: inferPackageName(packageDirectory),
|
|
12002
|
+
packageDirectory
|
|
12003
|
+
});
|
|
12004
|
+
}
|
|
12005
|
+
}
|
|
11850
12006
|
function enumerateFromGlobs(workspaceRoot, patterns) {
|
|
11851
12007
|
const seenDirectories = /* @__PURE__ */ new Set();
|
|
11852
12008
|
const packages = [];
|
|
11853
|
-
for (const pattern of patterns) {
|
|
11854
|
-
const matches = globSync(`${pattern.replace(/\/$/, "")}/jest.config.*`, { cwd: workspaceRoot });
|
|
11855
|
-
for (const match of matches) {
|
|
11856
|
-
if (!JEST_CONFIG_MARKER$1.test(path$1.basename(match))) continue;
|
|
11857
|
-
const packageDirectory = path$1.dirname(path$1.join(workspaceRoot, match));
|
|
11858
|
-
if (seenDirectories.has(packageDirectory)) continue;
|
|
11859
|
-
seenDirectories.add(packageDirectory);
|
|
11860
|
-
packages.push({
|
|
11861
|
-
name: inferPackageName(packageDirectory),
|
|
11862
|
-
packageDirectory
|
|
11863
|
-
});
|
|
11864
|
-
}
|
|
11865
|
-
}
|
|
12009
|
+
for (const pattern of patterns) collectPackagesFromMatches(globSync(`${pattern.replace(TRAILING_SLASH, "")}/jest.config.*`, { cwd: workspaceRoot }), workspaceRoot, seenDirectories, packages);
|
|
11866
12010
|
assertNoDuplicateNames(packages, workspaceRoot);
|
|
11867
12011
|
return packages;
|
|
11868
12012
|
}
|
|
@@ -11873,7 +12017,7 @@ function resolvePosixShim(binDirectory, command) {
|
|
|
11873
12017
|
const candidate = path$1.join(binDirectory, command);
|
|
11874
12018
|
return fs$1.existsSync(candidate) ? candidate : command;
|
|
11875
12019
|
}
|
|
11876
|
-
function
|
|
12020
|
+
function buildCommandExeArgs(command, args) {
|
|
11877
12021
|
return [
|
|
11878
12022
|
"/d",
|
|
11879
12023
|
"/s",
|
|
@@ -11935,7 +12079,7 @@ function runTool(command, args, cwd) {
|
|
|
11935
12079
|
PATH: `${binDirectory}${path$1.delimiter}${process.env["PATH"]}`
|
|
11936
12080
|
} : process.env;
|
|
11937
12081
|
const file = isWindows ? "cmd.exe" : resolvePosixShim(binDirectory, command);
|
|
11938
|
-
const spawnArgs = isWindows ?
|
|
12082
|
+
const spawnArgs = isWindows ? buildCommandExeArgs(command, args) : args;
|
|
11939
12083
|
try {
|
|
11940
12084
|
return cp.execFileSync(file, spawnArgs, {
|
|
11941
12085
|
cwd,
|
|
@@ -12329,4 +12473,4 @@ async function runJestRoblox(cli, config) {
|
|
|
12329
12473
|
}
|
|
12330
12474
|
}
|
|
12331
12475
|
//#endregion
|
|
12332
|
-
export {
|
|
12476
|
+
export { writeManifest as $, buildManifestSchema as A, parseGameOutput as B, COVERAGE_MANIFEST_PATH as C, visitStatement as D, visitExpression as E, formatAnnotations as F, formatFailure as G, createTimingCollector as H, formatJobSummary as I, formatBanner as J, formatResult as K, formatExecuteOutput as L, readBuildManifest as M, outputMultiResult as N, buildPlace as O, outputSingleResult as P, readManifest as Q, runProjects as R, COVERAGE_BUILD_MANIFEST_PATH as S, visitBlock as T, formatJson as U, writeGameOutput as V, writeJsonFile$1 as W, MANIFEST_VERSION as X, hashFile as Y, manifestSchema as Z, createOpenCloudBackend as _, defineProject as _t, collectStubMounts as a, parseCoverageEnvelope as at, buildJestArgv as b, version as bt, runTypecheck as c, loadConfig$1 as ct, resolveAllProjects as d, GLOBAL_TEST_KEYS as dt, applyAttribution as et, StudioBackend as f, JEST_ARGV_EXCLUDED_KEYS as ft, OpenCloudBackend as g, defineConfig as gt, createStudioCliBackend as h, VALID_BACKENDS as ht, buildImplicitProject as i, normalizeRawCoverage as it, emitBuildManifest as j, BUILD_MANIFEST_VERSION as k, cleanLeftoverStubs as l, resolveConfig as lt, StudioCliBackend as m, SHARED_TEST_KEYS as mt, runJestRoblox as n, extractJsonFromOutput as nt, loadRojoTree as o, mergeRawCoverage as ot, createStudioBackend as p, ROOT_CLI_KEYS as pt, formatTestSummary as q, runSingleOrMulti as r, parseJestOutput as rt, prepareBakedCoverage as s, mergeCliWithConfig as st, getRawProjects as t, LuauScriptError as tt, generateProjectStubs as u, DEFAULT_CONFIG as ut, formatMissingScopes as v, isValidBackend as vt, findRojoProject as w, generateTestScript as x, walkErrorChain as y, ConfigError as yt, formatGameOutputNotice as z };
|