@isentinel/jest-roblox 0.3.6 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.mts +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +24 -7
- package/dist/index.mjs +1 -1
- package/dist/{run-C4GuftZH.mjs → run-Bh8G33J1.mjs} +479 -102
- package/dist/{schema-BCTnsaiC.d.mts → schema-CCF2E3k_.d.mts} +219 -7
- package/package.json +4 -4
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { PermissionError, PollTimeoutError, TRANSIENT_TRANSPORT_CODES } from "@bedrock-rbx/ocale";
|
|
2
|
+
import { ApiError, PermissionError, PollTimeoutError, RateLimitError, TRANSIENT_TRANSPORT_CODES } from "@bedrock-rbx/ocale";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { isDeepStrictEqual } from "node:util";
|
|
5
5
|
import color from "tinyrainbow";
|
|
@@ -29,12 +29,13 @@ import { execFile } from "node:child_process";
|
|
|
29
29
|
import * as os from "node:os";
|
|
30
30
|
import { LuauExecutionClient } from "@bedrock-rbx/ocale/luau-execution";
|
|
31
31
|
import { PlacesClient } from "@bedrock-rbx/ocale/places";
|
|
32
|
-
import { WebSocketServer } from "ws";
|
|
33
32
|
import { StorageClient } from "@bedrock-rbx/ocale/storage";
|
|
33
|
+
import { WebSocketServer } from "ws";
|
|
34
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
34
35
|
import { parseJSONC, parseYAML } from "confbox";
|
|
35
36
|
import { Visitor, parseSync } from "oxc-parser";
|
|
36
37
|
//#region package.json
|
|
37
|
-
var version = "0.3.
|
|
38
|
+
var version = "0.3.7";
|
|
38
39
|
//#endregion
|
|
39
40
|
//#region src/config/errors.ts
|
|
40
41
|
var ConfigError = class extends Error {
|
|
@@ -385,7 +386,28 @@ function validateConfig(raw) {
|
|
|
385
386
|
if (workspace !== void 0 && workspace.root === void 0 !== (workspace.packages === void 0)) throw new Error("workspace.root and workspace.packages must be declared together.");
|
|
386
387
|
return result;
|
|
387
388
|
}
|
|
389
|
+
/**
|
|
390
|
+
* Identity helper for authoring a typed `jest.config.*` file. Returns its
|
|
391
|
+
* input unchanged; it exists purely to give editors autocompletion and
|
|
392
|
+
* type-checking for the root config shape (`Config` plus the c12 layer props
|
|
393
|
+
* on `ConfigInput`).
|
|
394
|
+
*
|
|
395
|
+
* Use it as the default export of a config file discovered by c12 (`.ts`,
|
|
396
|
+
* `.js`, `.mjs`, `.cjs`, `.json`, `.yaml`, `.toml`). All jest-passthrough
|
|
397
|
+
* options live under the `test:` block; root keys are CLI/runner-level.
|
|
398
|
+
* Configs may extend a shared base via `extends`, and any `Mergeable` array
|
|
399
|
+
* field (e.g. `test.testMatch`) accepts a function that receives the inherited
|
|
400
|
+
* defaults and returns the merged value. Precedence is CLI flags > config file
|
|
401
|
+
* > extended config > defaults.
|
|
402
|
+
*/
|
|
388
403
|
const defineConfig = createDefineConfig();
|
|
404
|
+
/**
|
|
405
|
+
* Identity helper for a single entry inside `test.projects`, mirroring
|
|
406
|
+
* {@link defineConfig} for per-project overrides. Returns its input unchanged
|
|
407
|
+
* and exists only for editor autocompletion and type-checking of the
|
|
408
|
+
* `InlineProjectConfig` shape — a `test:` block carrying the project's
|
|
409
|
+
* `include`/`displayName` plus any shared per-project jest options.
|
|
410
|
+
*/
|
|
389
411
|
const defineProject = createDefineConfig();
|
|
390
412
|
//#endregion
|
|
391
413
|
//#region src/config/loader.ts
|
|
@@ -445,7 +467,7 @@ async function seaImport(id) {
|
|
|
445
467
|
const content = readFileSync(id, "utf-8");
|
|
446
468
|
return JSON.parse(content);
|
|
447
469
|
}
|
|
448
|
-
return
|
|
470
|
+
return createRequire(id)(id);
|
|
449
471
|
}
|
|
450
472
|
function merger(...sources) {
|
|
451
473
|
return defuFn(...sources.filter(Boolean));
|
|
@@ -1156,8 +1178,8 @@ const VALID_REPORTERS = /* @__PURE__ */ new Set([
|
|
|
1156
1178
|
"text-lcov",
|
|
1157
1179
|
"text-summary"
|
|
1158
1180
|
]);
|
|
1159
|
-
function printCoverageHeader() {
|
|
1160
|
-
const header = ` ${color.blue("%")} ${color.dim("Coverage report from")} ${color.yellow("istanbul")}`;
|
|
1181
|
+
function printCoverageHeader(agentMode = false) {
|
|
1182
|
+
const header = agentMode ? " % Coverage report from istanbul" : ` ${color.blue("%")} ${color.dim("Coverage report from")} ${color.yellow("istanbul")}`;
|
|
1161
1183
|
process.stdout.write(`\n${header}\n`);
|
|
1162
1184
|
}
|
|
1163
1185
|
const TEXT_REPORTERS = /* @__PURE__ */ new Set(["text", "text-summary"]);
|
|
@@ -2069,6 +2091,10 @@ function parseParsedOutput(parsed) {
|
|
|
2069
2091
|
}
|
|
2070
2092
|
//#endregion
|
|
2071
2093
|
//#region src/backends/envelope.ts
|
|
2094
|
+
const wholeRunErrorSchema = type({
|
|
2095
|
+
err: "unknown",
|
|
2096
|
+
success: "false"
|
|
2097
|
+
});
|
|
2072
2098
|
const envelopeSchema = type({ entries: type({
|
|
2073
2099
|
"bannerOutput?": "string",
|
|
2074
2100
|
"elapsedMs?": "number",
|
|
@@ -2079,8 +2105,12 @@ const envelopeSchema = type({ entries: type({
|
|
|
2079
2105
|
"snapshotWrites?": { "[string]": "string" }
|
|
2080
2106
|
}).array() });
|
|
2081
2107
|
function parseEnvelope(jestOutput) {
|
|
2082
|
-
const
|
|
2083
|
-
|
|
2108
|
+
const raw = JSON.parse(jestOutput);
|
|
2109
|
+
const envelope = envelopeSchema(raw);
|
|
2110
|
+
if (envelope instanceof type.errors) {
|
|
2111
|
+
if (!(wholeRunErrorSchema(raw) instanceof type.errors)) parseJestOutput(jestOutput);
|
|
2112
|
+
return [{ jestOutput }];
|
|
2113
|
+
}
|
|
2084
2114
|
return envelope.entries;
|
|
2085
2115
|
}
|
|
2086
2116
|
function buildProjectResult(entry, job, fallbackGameOutput) {
|
|
@@ -4820,18 +4850,29 @@ function mergeResults$1(typecheck, runtime) {
|
|
|
4820
4850
|
assert(result !== void 0, "mergeResults requires at least one result");
|
|
4821
4851
|
return result;
|
|
4822
4852
|
}
|
|
4853
|
+
async function writeResultFile(outputFile, typecheck, runtime) {
|
|
4854
|
+
if (outputFile === void 0) return;
|
|
4855
|
+
await writeJsonFile$1(mergeResults$1(typecheck, runtime), outputFile);
|
|
4856
|
+
}
|
|
4823
4857
|
async function outputSingleResult(config, result) {
|
|
4824
4858
|
const { preCoverageMs, runtimeResult, typecheckResult } = result;
|
|
4825
4859
|
const mergedResult = mergeResults$1(typecheckResult, runtimeResult?.result);
|
|
4826
|
-
|
|
4860
|
+
const coveragePassed = emitResultsAndCoverage({
|
|
4827
4861
|
config,
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4862
|
+
coverageEnabled: config.collectCoverage,
|
|
4863
|
+
printResults: () => {
|
|
4864
|
+
if (config.silent) return;
|
|
4865
|
+
printFormattedOutput({
|
|
4866
|
+
config,
|
|
4867
|
+
mergedResult,
|
|
4868
|
+
runtimeResult,
|
|
4869
|
+
timing: runtimeResult !== void 0 ? addCoverageTiming(runtimeResult.timing, preCoverageMs) : void 0,
|
|
4870
|
+
typecheckResult
|
|
4871
|
+
});
|
|
4872
|
+
},
|
|
4873
|
+
runCoverage: () => processCoverage(config, runtimeResult?.coverageData)
|
|
4832
4874
|
});
|
|
4833
|
-
|
|
4834
|
-
if (config.outputFile !== void 0) await writeJsonFile$1(mergedResult, config.outputFile);
|
|
4875
|
+
await writeResultFile(config.outputFile, typecheckResult, runtimeResult?.result);
|
|
4835
4876
|
if (runtimeResult !== void 0) writeGameOutputIfConfigured(config, runtimeResult.gameOutput, { hintsShown: !mergedResult.success });
|
|
4836
4877
|
runGitHubActionsFormatter(config, mergedResult, runtimeResult?.sourceMapper);
|
|
4837
4878
|
const snapshotsPersisted = (runtimeResult?.snapshotWriteFailures ?? 0) === 0;
|
|
@@ -4916,20 +4957,26 @@ async function outputMultiResult(rootConfig, result) {
|
|
|
4916
4957
|
});
|
|
4917
4958
|
const merged = mergeProjectResults(projectResults.map((entry) => entry.result));
|
|
4918
4959
|
const mergedResult = mergeResults$1(typecheckResult, merged.result);
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4960
|
+
const workspaceCoverage = extractWorkspaceCoverageMapped(result);
|
|
4961
|
+
const coveragePassed = emitResultsAndCoverage({
|
|
4962
|
+
config,
|
|
4963
|
+
coverageEnabled: config.collectCoverage || workspaceCoverage !== void 0,
|
|
4964
|
+
printResults: () => {
|
|
4965
|
+
if (config.silent) return;
|
|
4966
|
+
printMultiProjectOutput({
|
|
4967
|
+
config,
|
|
4968
|
+
...resolveSinkHints(result, config),
|
|
4969
|
+
merged,
|
|
4970
|
+
preCoverageMs,
|
|
4971
|
+
projectResults,
|
|
4972
|
+
typecheckResult
|
|
4973
|
+
});
|
|
4974
|
+
if (typecheckResult !== void 0 && !usesDefaultFormatter(config)) process.stderr.write(formatTypecheckSummary(typecheckResult));
|
|
4975
|
+
},
|
|
4976
|
+
runCoverage: () => processCoverage(config, merged.coverageData, workspaceCoverage)
|
|
4977
|
+
});
|
|
4931
4978
|
if (mode === "multi") {
|
|
4932
|
-
|
|
4979
|
+
await writeResultFile(config.outputFile, typecheckResult, merged.result);
|
|
4933
4980
|
writeAggregatedGameOutput(config, projectResults, { hintsShown: !mergedResult.success });
|
|
4934
4981
|
}
|
|
4935
4982
|
runGitHubActionsFormatter(config, mergedResult, merged.sourceMapper);
|
|
@@ -4946,6 +4993,16 @@ function addCoverageTiming(timing, coverageMs) {
|
|
|
4946
4993
|
totalMs: timing.totalMs + coverageMs
|
|
4947
4994
|
};
|
|
4948
4995
|
}
|
|
4996
|
+
function emitResultsAndCoverage(options) {
|
|
4997
|
+
const { config, coverageEnabled, printResults, runCoverage } = options;
|
|
4998
|
+
const deferResults = coverageEnabled && usesAgentFormatter(config.formatters, config.verbose);
|
|
4999
|
+
if (!deferResults) printResults();
|
|
5000
|
+
try {
|
|
5001
|
+
return runCoverage();
|
|
5002
|
+
} finally {
|
|
5003
|
+
if (deferResults) printResults();
|
|
5004
|
+
}
|
|
5005
|
+
}
|
|
4949
5006
|
function usesDefaultFormatter(config) {
|
|
4950
5007
|
return !hasFormatter(config.formatters, "json") && !usesAgentFormatter(config.formatters, config.verbose);
|
|
4951
5008
|
}
|
|
@@ -5014,9 +5071,10 @@ function processCoverage(config, coverageData, preMapped) {
|
|
|
5014
5071
|
if (!config.collectCoverage && preMapped === void 0) return true;
|
|
5015
5072
|
const mapped = resolveMappedCoverage(config, coverageData, preMapped);
|
|
5016
5073
|
if (mapped === void 0) return true;
|
|
5017
|
-
|
|
5074
|
+
const agentMode = usesAgentFormatter(config.formatters, config.verbose);
|
|
5075
|
+
if (!config.silent) printCoverageHeader(agentMode);
|
|
5018
5076
|
generateReports({
|
|
5019
|
-
agentMode
|
|
5077
|
+
agentMode,
|
|
5020
5078
|
collectCoverageFrom: config.collectCoverageFrom,
|
|
5021
5079
|
coverageDirectory: path$1.resolve(config.rootDir, config.coverageDirectory),
|
|
5022
5080
|
coveragePathIgnorePatterns: config.coveragePathIgnorePatterns,
|
|
@@ -5527,7 +5585,7 @@ function buildPlace(options) {
|
|
|
5527
5585
|
}
|
|
5528
5586
|
//#endregion
|
|
5529
5587
|
//#region src/luau/parse-ast.luau
|
|
5530
|
-
var parse_ast_default = "local fs = require(\"@std/fs\")\nlocal json = require(\"@std/json\")\nlocal process = require(\"@std/process\")\nlocal syntax = require(\"@std/syntax\")\n\nlocal rawArgs = process.args\nlocal userArgs: { string } = {}\nlocal pastSeparator = false\n\nfor _, arg in rawArgs do\n if pastSeparator then\n table.insert(userArgs, arg)\n elseif arg == \"--\" then\n pastSeparator = true\n end\nend\n\nlocal luauRoot = userArgs[1]\nif not luauRoot then\n error(\"Usage: lute run parse-ast.luau -- <file.luau | luau-root> [output-dir]\")\nend\n\nluauRoot = string.gsub(luauRoot, \"\\\\\", \"/\")\n\n-- Fields to keep per AST tag (beyond tag/kind/location which are always kept).\n-- Tags shared by stat/expr variants are merged — nil fields are harmless.\nlocal KEEP: { [string]: { string } } = {\n assign = { \"values\", \"variables\" },\n binary = { \"lhsOperand\", \"rhsOperand\" },\n block = { \"statements\" },\n boolean = { \"value\" },\n call = { \"arguments\", \"func\" },\n cast = { \"operand\" },\n compoundassign = { \"value\", \"variable\" },\n conditional = { \"condition\", \"thenBlock\", \"elseifs\", \"elseBlock\", \"thenExpr\", \"elseExpr\" },\n [\"do\"] = { \"body\" },\n expression = { \"expression\" },\n [\"for\"] = { \"body\", \"from\", \"to\", \"step\" },\n forin = { \"body\", \"values\" },\n [\"function\"] = { \"body\", \"name\", \"func\" },\n global = { \"name\" },\n group = { \"expression\" },\n index = { \"expression\", \"index\" },\n indexname = { \"expression\", \"accessor\", \"index\" },\n instantiate = { \"expr\" },\n interpolatedstring = { \"expressions\" },\n [\"local\"] = { \"values\", \"variables\" },\n localfunction = { \"name\", \"func\" },\n number = { \"value\" },\n [\"repeat\"] = { \"body\", \"condition\" },\n [\"return\"] = { \"expressions\" },\n -- `string` is intentionally absent: the literal value lives at\n -- `value.token.text` and is flattened by the special-case branch in\n -- `strip` below. A flat `{ \"text\" }` entry here can't reach it.\n table = { \"entries\" },\n unary = { \"operand\" },\n [\"while\"] = { \"body\", \"condition\" },\n}\n\nlocal function strip(value: any): any\n if type(value) ~= \"table\" then\n return value\n end\n\n -- LuauSpan — has beginLine, no tag\n if value.beginLine ~= nil then\n return value\n end\n\n -- Token — has text but no tag, reduce to {text}\n if value.text ~= nil and value.tag == nil then\n return { text = value.text }\n end\n\n -- AST node — has tag, keep only allowlisted fields\n if value.tag ~= nil then\n local result = { tag = value.tag, kind = value.kind, location = value.location }\n\n -- Lute nests the string literal inside `.token.text`, so the KEEP\n -- entry can't reach it via flat field lookup. Flatten it here so\n -- downstream consumers read `node.text` like every other literal.\n if value.tag == \"string\" and type(value.token) == \"table\" then\n result.text = value.token.text\n end\n\n local fields = KEEP[value.tag :: string]\n if fields then\n for _, field in fields do\n if value[field] ~= nil then\n result[field] = strip(value[field])\n end\n end\n end\n\n return result\n end\n\n -- Other tables (arrays, Pairs, ElseIf structs) — recurse all fields\n local result: any = {}\n for k, v in value do\n result[k] = strip(v)\n end\n\n return result\nend\n\n-- Single-file mode: parse one file, print stripped AST to stdout\nif string.sub(luauRoot, -5) == \".luau\" or string.sub(luauRoot, -4) == \".lua\" then\n local source = fs.readFileToString(luauRoot)\n local parseResult = syntax.parse(source)\n print(json.serialize(strip(parseResult.root)))\n return\nend\n\nlocal outputDir = userArgs[2]\nif not outputDir then\n error(\"Usage: lute run parse-ast.luau -- <luau-root> <output-dir>\")\nend\n\noutputDir = string.gsub(outputDir, \"\\\\\", \"/\")\n\n-- Discover .luau files recursively, skipping node_modules, dot dirs, spec/test files\nlocal function discoverFiles(directory: string, relativeTo: string, results: { string })\n local entries = fs.listDirectory(directory)\n for _, entry in entries do\n local fullPath = directory .. \"/\" .. entry.name\n if entry.type == \"dir\" then\n if entry.name == \"node_modules\" or entry.name == \".jest-roblox\" then\n continue\n end\n\n if string.sub(entry.name, 1, 1) == \".\" then\n continue\n end\n\n discoverFiles(fullPath, relativeTo, results)\n elseif\n entry.type == \"file\"\n and (string.sub(entry.name, -5) == \".luau\" or string.sub(entry.name, -4) == \".lua\")\n then\n if\n string.sub(entry.name, -10) == \".spec.luau\"\n or string.sub(entry.name, -10) == \".test.luau\"\n or string.sub(entry.name, -9) == \".spec.lua\"\n or string.sub(entry.name, -9) == \".test.lua\"\n or string.sub(entry.name, -10) == \".snap.luau\"\n or string.sub(entry.name, -9) == \".snap.lua\"\n then\n continue\n end\n\n -- Compute relative path\n local relative = string.sub(fullPath, #relativeTo + 2)\n table.insert(results, relative)\n end\n end\nend\n\nlocal function dirname(filepath: string): string\n local pos = string.find(filepath, \"/[^/]*$\")\n if pos then\n return string.sub(filepath, 1, pos - 1)\n end\n\n return \"\"\nend\n\n-- Optional 3rd arg: path to skip list JSON file.\n-- Skipped files are still included in the file list but not parsed.\nlocal skipListPath = userArgs[3]\nlocal skipSet: { [string]: boolean } = {}\nif skipListPath then\n local skipJson = fs.readFileToString(skipListPath)\n local skipList = json.deserialize(skipJson) :: { string }\n for _, entry in skipList do\n skipSet[entry] = true\n end\nend\n\nlocal files: { string } = {}\ndiscoverFiles(luauRoot, luauRoot, files)\n\n-- Parse, strip, and write per-file AST JSON\nfor _, relativePath in files do\n if skipSet[relativePath] then\n continue\n end\n\n local fullPath = luauRoot .. \"/\" .. relativePath\n local source = fs.readFileToString(fullPath)\n local parseResult = syntax.parse(source)\n local stripped = strip(parseResult.root)\n\n local outPath = outputDir .. \"/\" .. relativePath .. \".json\"\n local dir = dirname(outPath)\n if dir ~= \"\" then\n fs.createDirectory(dir, { makeParents = true })\n end\n\n fs.writeStringToFile(outPath, json.serialize(stripped))\nend\n\n-- Print file list to stdout (tiny — just paths)\nprint(json.serialize(files :: json.Array))\n";
|
|
5588
|
+
var parse_ast_default = "local fs = require(\"@std/fs\")\nlocal json = require(\"@std/json\")\nlocal process = require(\"@std/process\")\nlocal syntax = require(\"@std/syntax\")\n\nlocal rawArgs = process.args\nlocal userArgs: { string } = {}\nlocal pastSeparator = false\n\nfor _, arg in rawArgs do\n if pastSeparator then\n table.insert(userArgs, arg)\n elseif arg == \"--\" then\n pastSeparator = true\n end\nend\n\nlocal luauRoot = userArgs[1]\nif not luauRoot then\n error(\"Usage: lute run parse-ast.luau -- <file.luau | luau-root> [output-dir]\")\nend\n\nluauRoot = string.gsub(luauRoot, \"\\\\\", \"/\")\n\n-- Fields to keep per AST tag (beyond tag/kind/location which are always kept).\n-- Tags shared by stat/expr variants are merged — nil fields are harmless.\nlocal KEEP: { [string]: { string } } = {\n assign = { \"values\", \"variables\" },\n -- `operator` (reduced to its `.text` by the token branch in `strip`) lets\n -- the collector tell `and`/`or` short-circuit branches from other binaries.\n binary = { \"lhsOperand\", \"rhsOperand\", \"operator\" },\n block = { \"statements\" },\n boolean = { \"value\" },\n call = { \"arguments\", \"func\" },\n cast = { \"operand\" },\n compoundassign = { \"value\", \"variable\" },\n conditional = { \"condition\", \"thenBlock\", \"elseifs\", \"elseBlock\", \"thenExpr\", \"elseExpr\" },\n [\"do\"] = { \"body\" },\n expression = { \"expression\" },\n [\"for\"] = { \"body\", \"from\", \"to\", \"step\" },\n forin = { \"body\", \"values\" },\n [\"function\"] = { \"body\", \"name\", \"func\" },\n global = { \"name\" },\n group = { \"expression\" },\n index = { \"expression\", \"index\" },\n indexname = { \"expression\", \"accessor\", \"index\" },\n instantiate = { \"expr\" },\n interpolatedstring = { \"expressions\" },\n [\"local\"] = { \"values\", \"variables\" },\n localfunction = { \"name\", \"func\" },\n number = { \"value\" },\n [\"repeat\"] = { \"body\", \"condition\" },\n [\"return\"] = { \"expressions\" },\n -- `string` is intentionally absent: the literal value lives at\n -- `value.token.text` and is flattened by the special-case branch in\n -- `strip` below. A flat `{ \"text\" }` entry here can't reach it.\n table = { \"entries\" },\n unary = { \"operand\" },\n [\"while\"] = { \"body\", \"condition\" },\n}\n\nlocal function strip(value: any): any\n if type(value) ~= \"table\" then\n return value\n end\n\n -- LuauSpan — has beginLine, no tag\n if value.beginLine ~= nil then\n return value\n end\n\n -- Token — has text but no tag, reduce to {text}\n if value.text ~= nil and value.tag == nil then\n return { text = value.text }\n end\n\n -- AST node — has tag, keep only allowlisted fields\n if value.tag ~= nil then\n local result = { tag = value.tag, kind = value.kind, location = value.location }\n\n -- Lute nests the string literal inside `.token.text`, so the KEEP\n -- entry can't reach it via flat field lookup. Flatten it here so\n -- downstream consumers read `node.text` like every other literal.\n if value.tag == \"string\" and type(value.token) == \"table\" then\n result.text = value.token.text\n end\n\n local fields = KEEP[value.tag :: string]\n if fields then\n for _, field in fields do\n if value[field] ~= nil then\n result[field] = strip(value[field])\n end\n end\n end\n\n return result\n end\n\n -- Other tables (arrays, Pairs, ElseIf structs) — recurse all fields\n local result: any = {}\n for k, v in value do\n result[k] = strip(v)\n end\n\n return result\nend\n\n-- Single-file mode: parse one file, print stripped AST to stdout\nif string.sub(luauRoot, -5) == \".luau\" or string.sub(luauRoot, -4) == \".lua\" then\n local source = fs.readFileToString(luauRoot)\n local parseResult = syntax.parse(source)\n print(json.serialize(strip(parseResult.root)))\n return\nend\n\nlocal outputDir = userArgs[2]\nif not outputDir then\n error(\"Usage: lute run parse-ast.luau -- <luau-root> <output-dir>\")\nend\n\noutputDir = string.gsub(outputDir, \"\\\\\", \"/\")\n\n-- Discover .luau files recursively, skipping node_modules, dot dirs, spec/test files\nlocal function discoverFiles(directory: string, relativeTo: string, results: { string })\n local entries = fs.listDirectory(directory)\n for _, entry in entries do\n local fullPath = directory .. \"/\" .. entry.name\n if entry.type == \"dir\" then\n if entry.name == \"node_modules\" or entry.name == \".jest-roblox\" then\n continue\n end\n\n if string.sub(entry.name, 1, 1) == \".\" then\n continue\n end\n\n discoverFiles(fullPath, relativeTo, results)\n elseif\n entry.type == \"file\"\n and (string.sub(entry.name, -5) == \".luau\" or string.sub(entry.name, -4) == \".lua\")\n then\n if\n string.sub(entry.name, -10) == \".spec.luau\"\n or string.sub(entry.name, -10) == \".test.luau\"\n or string.sub(entry.name, -9) == \".spec.lua\"\n or string.sub(entry.name, -9) == \".test.lua\"\n or string.sub(entry.name, -10) == \".snap.luau\"\n or string.sub(entry.name, -9) == \".snap.lua\"\n then\n continue\n end\n\n -- Compute relative path\n local relative = string.sub(fullPath, #relativeTo + 2)\n table.insert(results, relative)\n end\n end\nend\n\nlocal function dirname(filepath: string): string\n local pos = string.find(filepath, \"/[^/]*$\")\n if pos then\n return string.sub(filepath, 1, pos - 1)\n end\n\n return \"\"\nend\n\n-- Optional 3rd arg: path to skip list JSON file.\n-- Skipped files are still included in the file list but not parsed.\nlocal skipListPath = userArgs[3]\nlocal skipSet: { [string]: boolean } = {}\nif skipListPath then\n local skipJson = fs.readFileToString(skipListPath)\n local skipList = json.deserialize(skipJson) :: { string }\n for _, entry in skipList do\n skipSet[entry] = true\n end\nend\n\nlocal files: { string } = {}\ndiscoverFiles(luauRoot, luauRoot, files)\n\n-- Parse, strip, and write per-file AST JSON\nfor _, relativePath in files do\n if skipSet[relativePath] then\n continue\n end\n\n local fullPath = luauRoot .. \"/\" .. relativePath\n local source = fs.readFileToString(fullPath)\n local parseResult = syntax.parse(source)\n local stripped = strip(parseResult.root)\n\n local outPath = outputDir .. \"/\" .. relativePath .. \".json\"\n local dir = dirname(outPath)\n if dir ~= \"\" then\n fs.createDirectory(dir, { makeParents = true })\n end\n\n fs.writeStringToFile(outPath, json.serialize(stripped))\nend\n\n-- Print file list to stdout (tiny — just paths)\nprint(json.serialize(files :: json.Array))\n";
|
|
5531
5589
|
//#endregion
|
|
5532
5590
|
//#region packages/luau-ast/dist/index.mjs
|
|
5533
5591
|
function visitExpression(expression, visitor) {
|
|
@@ -5825,9 +5883,37 @@ function collectCoverage(root) {
|
|
|
5825
5883
|
const functions = [];
|
|
5826
5884
|
const branches = [];
|
|
5827
5885
|
const implicitElseProbes = [];
|
|
5828
|
-
const
|
|
5886
|
+
const wrapProbes = [];
|
|
5829
5887
|
const namedFunctions = /* @__PURE__ */ new Set();
|
|
5830
5888
|
visitBlock(root, {
|
|
5889
|
+
visitExprBinary(node) {
|
|
5890
|
+
const operator = node.operator?.text;
|
|
5891
|
+
if (operator !== "and" && operator !== "or") return true;
|
|
5892
|
+
branches.push({
|
|
5893
|
+
arms: [{
|
|
5894
|
+
bodyFirstColumn: 0,
|
|
5895
|
+
bodyFirstLine: 0,
|
|
5896
|
+
location: { ...node.lhsOperand.location }
|
|
5897
|
+
}, {
|
|
5898
|
+
bodyFirstColumn: 0,
|
|
5899
|
+
bodyFirstLine: 0,
|
|
5900
|
+
location: { ...node.rhsOperand.location }
|
|
5901
|
+
}],
|
|
5902
|
+
branchType: "binary-expr",
|
|
5903
|
+
index: branchIndex
|
|
5904
|
+
});
|
|
5905
|
+
wrapProbes.push({
|
|
5906
|
+
armIndex: 1,
|
|
5907
|
+
branchIndex,
|
|
5908
|
+
exprLocation: { ...node.lhsOperand.location }
|
|
5909
|
+
}, {
|
|
5910
|
+
armIndex: 2,
|
|
5911
|
+
branchIndex,
|
|
5912
|
+
exprLocation: { ...node.rhsOperand.location }
|
|
5913
|
+
});
|
|
5914
|
+
branchIndex++;
|
|
5915
|
+
return true;
|
|
5916
|
+
},
|
|
5831
5917
|
visitExprFunction(node) {
|
|
5832
5918
|
if (namedFunctions.has(node)) return true;
|
|
5833
5919
|
const first = getBodyFirstStatement(node.body);
|
|
@@ -5853,7 +5939,7 @@ function collectCoverage(root) {
|
|
|
5853
5939
|
bodyFirstLine: 0,
|
|
5854
5940
|
location: { ...node.thenExpr.location }
|
|
5855
5941
|
});
|
|
5856
|
-
|
|
5942
|
+
wrapProbes.push({
|
|
5857
5943
|
armIndex,
|
|
5858
5944
|
branchIndex,
|
|
5859
5945
|
exprLocation: { ...node.thenExpr.location }
|
|
@@ -5865,7 +5951,7 @@ function collectCoverage(root) {
|
|
|
5865
5951
|
bodyFirstLine: 0,
|
|
5866
5952
|
location: { ...elseif.thenExpr.location }
|
|
5867
5953
|
});
|
|
5868
|
-
|
|
5954
|
+
wrapProbes.push({
|
|
5869
5955
|
armIndex,
|
|
5870
5956
|
branchIndex,
|
|
5871
5957
|
exprLocation: { ...elseif.thenExpr.location }
|
|
@@ -5877,7 +5963,7 @@ function collectCoverage(root) {
|
|
|
5877
5963
|
bodyFirstLine: 0,
|
|
5878
5964
|
location: { ...node.elseExpr.location }
|
|
5879
5965
|
});
|
|
5880
|
-
|
|
5966
|
+
wrapProbes.push({
|
|
5881
5967
|
armIndex,
|
|
5882
5968
|
branchIndex,
|
|
5883
5969
|
exprLocation: { ...node.elseExpr.location }
|
|
@@ -5981,10 +6067,10 @@ function collectCoverage(root) {
|
|
|
5981
6067
|
});
|
|
5982
6068
|
return {
|
|
5983
6069
|
branches,
|
|
5984
|
-
exprIfProbes,
|
|
5985
6070
|
functions,
|
|
5986
6071
|
implicitElseProbes,
|
|
5987
|
-
statements
|
|
6072
|
+
statements,
|
|
6073
|
+
wrapProbes
|
|
5988
6074
|
};
|
|
5989
6075
|
}
|
|
5990
6076
|
function getBodyFirstStatement(block) {
|
|
@@ -6058,6 +6144,11 @@ function buildCoverageMap$1(result) {
|
|
|
6058
6144
|
}
|
|
6059
6145
|
//#endregion
|
|
6060
6146
|
//#region src/coverage-pipeline/probe-inserter.ts
|
|
6147
|
+
const KIND_RANK = {
|
|
6148
|
+
close: 2,
|
|
6149
|
+
open: 0,
|
|
6150
|
+
point: 1
|
|
6151
|
+
};
|
|
6061
6152
|
function insertProbes(source, result, fileKey) {
|
|
6062
6153
|
const lines = splitLines(source);
|
|
6063
6154
|
applyProbes(lines, collectProbes(result));
|
|
@@ -6067,11 +6158,13 @@ function collectProbes(result) {
|
|
|
6067
6158
|
const probes = [];
|
|
6068
6159
|
for (const stmt of result.statements) probes.push({
|
|
6069
6160
|
column: stmt.location.beginColumn,
|
|
6161
|
+
kind: "point",
|
|
6070
6162
|
line: stmt.location.beginLine,
|
|
6071
6163
|
text: `__cov_s[${stmt.index}] += 1; `
|
|
6072
6164
|
});
|
|
6073
6165
|
for (const func of result.functions) if (func.bodyFirstLine > 0) probes.push({
|
|
6074
6166
|
column: func.bodyFirstColumn,
|
|
6167
|
+
kind: "point",
|
|
6075
6168
|
line: func.bodyFirstLine,
|
|
6076
6169
|
text: `__cov_f[${func.index}] += 1; `
|
|
6077
6170
|
});
|
|
@@ -6079,27 +6172,42 @@ function collectProbes(result) {
|
|
|
6079
6172
|
const arm = branch.arms[armIndex];
|
|
6080
6173
|
if (arm !== void 0 && arm.bodyFirstLine > 0) probes.push({
|
|
6081
6174
|
column: arm.bodyFirstColumn,
|
|
6175
|
+
kind: "point",
|
|
6082
6176
|
line: arm.bodyFirstLine,
|
|
6083
6177
|
text: `__cov_b[${branch.index}][${armIndex + 1}] += 1; `
|
|
6084
6178
|
});
|
|
6085
6179
|
}
|
|
6086
6180
|
for (const probe of result.implicitElseProbes) probes.push({
|
|
6087
6181
|
column: probe.endColumn,
|
|
6182
|
+
kind: "point",
|
|
6088
6183
|
line: probe.endLine,
|
|
6089
6184
|
text: `else __cov_b[${probe.branchIndex}][${probe.armIndex}] += 1 `
|
|
6090
6185
|
});
|
|
6091
|
-
for (const probe of result.
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6186
|
+
for (const probe of result.wrapProbes) {
|
|
6187
|
+
const { beginColumn, beginLine, endColumn, endLine } = probe.exprLocation;
|
|
6188
|
+
probes.push({
|
|
6189
|
+
column: beginColumn,
|
|
6190
|
+
kind: "open",
|
|
6191
|
+
line: beginLine,
|
|
6192
|
+
spanColumn: endColumn,
|
|
6193
|
+
spanLine: endLine,
|
|
6194
|
+
text: `__cov_br(${probe.branchIndex}, ${probe.armIndex}, `
|
|
6195
|
+
}, {
|
|
6196
|
+
column: endColumn,
|
|
6197
|
+
kind: "close",
|
|
6198
|
+
line: endLine,
|
|
6199
|
+
spanColumn: beginColumn,
|
|
6200
|
+
spanLine: beginLine,
|
|
6201
|
+
text: ")"
|
|
6202
|
+
});
|
|
6203
|
+
}
|
|
6100
6204
|
probes.sort((a, b) => {
|
|
6101
|
-
if (a.line
|
|
6102
|
-
return b.
|
|
6205
|
+
if (a.line !== b.line) return b.line - a.line;
|
|
6206
|
+
if (a.column !== b.column) return b.column - a.column;
|
|
6207
|
+
if (KIND_RANK[a.kind] !== KIND_RANK[b.kind]) return KIND_RANK[a.kind] - KIND_RANK[b.kind];
|
|
6208
|
+
if (a.spanLine !== void 0 && b.spanLine !== void 0 && a.spanLine !== b.spanLine) return a.spanLine - b.spanLine;
|
|
6209
|
+
if (a.spanColumn !== void 0 && b.spanColumn !== void 0) return a.spanColumn - b.spanColumn;
|
|
6210
|
+
return 0;
|
|
6103
6211
|
});
|
|
6104
6212
|
return probes;
|
|
6105
6213
|
}
|
|
@@ -6161,7 +6269,7 @@ function buildPreamble(modeDirective, fileKey, result) {
|
|
|
6161
6269
|
const zeros = branch.arms.map(() => "0").join(", ");
|
|
6162
6270
|
preamble += `if __cov_b[${branch.index}] == nil then __cov_b[${branch.index}] = {${zeros}} end\n`;
|
|
6163
6271
|
}
|
|
6164
|
-
if (result.
|
|
6272
|
+
if (result.wrapProbes.length > 0) preamble += "local function __cov_br(__bi, __ai, ...) __cov_b[__bi][__ai] += 1; return ... end\n";
|
|
6165
6273
|
}
|
|
6166
6274
|
return preamble;
|
|
6167
6275
|
}
|
|
@@ -6890,6 +6998,7 @@ var OcaleRunner = class {
|
|
|
6890
6998
|
apiKey: credentials.apiKey,
|
|
6891
6999
|
...options?.baseUrl !== void 0 ? { baseUrl: options.baseUrl } : {},
|
|
6892
7000
|
...options?.httpClient !== void 0 ? { httpClient: options.httpClient } : {},
|
|
7001
|
+
...options?.maxRetries !== void 0 ? { maxRetries: options.maxRetries } : {},
|
|
6893
7002
|
...options?.sleep !== void 0 ? { sleep: options.sleep } : {}
|
|
6894
7003
|
};
|
|
6895
7004
|
this.luau = new LuauExecutionClient(clientOptions);
|
|
@@ -6897,7 +7006,7 @@ var OcaleRunner = class {
|
|
|
6897
7006
|
this.readFileFn = options?.readFile ?? ((filePath) => fs$1.readFileSync(filePath));
|
|
6898
7007
|
}
|
|
6899
7008
|
async executeScript(options) {
|
|
6900
|
-
const { script, timeout } = options;
|
|
7009
|
+
const { placeVersion, script, timeout } = options;
|
|
6901
7010
|
if (timeout <= 0) throw new Error("Timeout must be a positive number");
|
|
6902
7011
|
const startTime = Date.now();
|
|
6903
7012
|
const timeoutSeconds = Math.min(Math.floor(timeout / 1e3), MAX_TASK_TIMEOUT_SECONDS);
|
|
@@ -6905,7 +7014,8 @@ var OcaleRunner = class {
|
|
|
6905
7014
|
placeId: this.credentials.placeId,
|
|
6906
7015
|
script,
|
|
6907
7016
|
timeoutSeconds,
|
|
6908
|
-
universeId: this.credentials.universeId
|
|
7017
|
+
universeId: this.credentials.universeId,
|
|
7018
|
+
...placeVersion !== void 0 ? { versionId: String(placeVersion) } : {}
|
|
6909
7019
|
}, {
|
|
6910
7020
|
retryableTransportCodes: TRANSIENT_TRANSPORT_CODES,
|
|
6911
7021
|
timeoutMs: timeout
|
|
@@ -6957,6 +7067,115 @@ type({
|
|
|
6957
7067
|
request_id: "string",
|
|
6958
7068
|
type: "'results'"
|
|
6959
7069
|
});
|
|
7070
|
+
/**
|
|
7071
|
+
* Measured Open Cloud per-place active-task ceiling — a platform constant, not a
|
|
7072
|
+
* tuning knob. `P` places offer `10·P` concurrent task slots, so the pool caps
|
|
7073
|
+
* each place's slot share here and warns when the requested total exceeds the
|
|
7074
|
+
* aggregate ceiling.
|
|
7075
|
+
*/
|
|
7076
|
+
const MAX_TASKS_PER_PLACE = 10;
|
|
7077
|
+
/**
|
|
7078
|
+
* A freed slot needs ~10s before its place will accept a new task. A backoff
|
|
7079
|
+
* signal inside this window after a completion on that place is slot-recycle
|
|
7080
|
+
* lag, not a genuinely-full place, so the pool waits out the remainder of the
|
|
7081
|
+
* window rather than treating it as a hard place-full signal (and burning the
|
|
7082
|
+
* shared creation budget on an immediate retry storm).
|
|
7083
|
+
*/
|
|
7084
|
+
const RECYCLE_LAG_MS = 1e4;
|
|
7085
|
+
/** Backoff when a place pushes back with no server-supplied retry delay. */
|
|
7086
|
+
const DEFAULT_BACKOFF_MS = 5e3;
|
|
7087
|
+
/**
|
|
7088
|
+
* Drive a replenishing pool of long-lived tasks across a configurable list of
|
|
7089
|
+
* places: spread `concurrency` slots across the places (capped at
|
|
7090
|
+
* {@link MAX_TASKS_PER_PLACE} each), keep every slot full, relaunch a slot when
|
|
7091
|
+
* its task returns while work remains (`!isDone()`), and resolve once every slot
|
|
7092
|
+
* has drained and the done-signal has fired. All places drain the consumer's one
|
|
7093
|
+
* shared queue, so a slot that frees on a fast place transparently re-runs a
|
|
7094
|
+
* sibling's lost work. The pool is jest/mutation-agnostic — chunk sizing, result
|
|
7095
|
+
* de-duplication, and bail live in the consumer.
|
|
7096
|
+
*
|
|
7097
|
+
* A task that throws a recognised platform backoff signal (a genuinely-full
|
|
7098
|
+
* place's `RESOURCE_EXHAUSTED`, the ~10s slot-recycle lag, or a rate-limit 429)
|
|
7099
|
+
* waits the slot out and retries without surfacing an error; any other throw
|
|
7100
|
+
* frees the slot (its in-flight work re-surfaces via the queue invisibility
|
|
7101
|
+
* window), reports through `onError`, and is relaunched while work remains — so
|
|
7102
|
+
* neither a transient failure nor platform backoff aborts the run or corrupts
|
|
7103
|
+
* results.
|
|
7104
|
+
*/
|
|
7105
|
+
async function runTaskPool(options) {
|
|
7106
|
+
const { concurrency, isDone, onError, onResult, places } = options;
|
|
7107
|
+
const now = options.now ?? Date.now;
|
|
7108
|
+
const sleep = options.sleep ?? setTimeout$1;
|
|
7109
|
+
const warn = options.warn ?? ((message) => {
|
|
7110
|
+
console.warn(message);
|
|
7111
|
+
});
|
|
7112
|
+
if (concurrency < 1) throw new Error(`runTaskPool concurrency must be >= 1, got ${String(concurrency)}`);
|
|
7113
|
+
if (places.length === 0) throw new Error("runTaskPool requires at least one place");
|
|
7114
|
+
const allocations = distributeSlots(places, concurrency, warn);
|
|
7115
|
+
async function backoff(state, retryAfterMs) {
|
|
7116
|
+
const genuineMs = retryAfterMs !== void 0 && retryAfterMs > 0 ? retryAfterMs : DEFAULT_BACKOFF_MS;
|
|
7117
|
+
const sinceCompletion = now() - state.lastCompletionMs;
|
|
7118
|
+
await sleep(sinceCompletion < RECYCLE_LAG_MS ? RECYCLE_LAG_MS - sinceCompletion : genuineMs);
|
|
7119
|
+
}
|
|
7120
|
+
async function worker(state) {
|
|
7121
|
+
while (!isDone()) {
|
|
7122
|
+
let result;
|
|
7123
|
+
try {
|
|
7124
|
+
result = await state.place.runTask();
|
|
7125
|
+
} catch (err) {
|
|
7126
|
+
const signal = classifyBackoff(err);
|
|
7127
|
+
if (signal !== void 0) {
|
|
7128
|
+
await backoff(state, signal.retryAfterMs);
|
|
7129
|
+
continue;
|
|
7130
|
+
}
|
|
7131
|
+
onError?.(err);
|
|
7132
|
+
continue;
|
|
7133
|
+
}
|
|
7134
|
+
state.lastCompletionMs = now();
|
|
7135
|
+
onResult(result);
|
|
7136
|
+
}
|
|
7137
|
+
}
|
|
7138
|
+
const workers = [];
|
|
7139
|
+
for (const { place, slots } of allocations) {
|
|
7140
|
+
const state = {
|
|
7141
|
+
lastCompletionMs: Number.NEGATIVE_INFINITY,
|
|
7142
|
+
place
|
|
7143
|
+
};
|
|
7144
|
+
for (let slot = 0; slot < slots; slot += 1) workers.push(worker(state));
|
|
7145
|
+
}
|
|
7146
|
+
await Promise.all(workers);
|
|
7147
|
+
}
|
|
7148
|
+
/**
|
|
7149
|
+
* Spread `concurrency` task-slots across the places as evenly as possible, capped
|
|
7150
|
+
* at {@link MAX_TASKS_PER_PLACE} per place. A total above the aggregate ceiling is
|
|
7151
|
+
* clamped and reported once through `warn` so the run still proceeds at the real
|
|
7152
|
+
* capacity instead of silently over-subscribing.
|
|
7153
|
+
*/
|
|
7154
|
+
function distributeSlots(places, concurrency, warn) {
|
|
7155
|
+
const placeCount = places.length;
|
|
7156
|
+
const capacity = placeCount * MAX_TASKS_PER_PLACE;
|
|
7157
|
+
if (concurrency > capacity) warn(`runTaskPool concurrency ${String(concurrency)} exceeds ${String(placeCount)} place(s) × ${String(MAX_TASKS_PER_PLACE)} = ${String(capacity)}; clamping to ${String(capacity)}`);
|
|
7158
|
+
const effective = Math.min(concurrency, capacity);
|
|
7159
|
+
const base = Math.floor(effective / placeCount);
|
|
7160
|
+
const remainder = effective % placeCount;
|
|
7161
|
+
return places.map((place, index) => ({
|
|
7162
|
+
place,
|
|
7163
|
+
slots: base + (index < remainder ? 1 : 0)
|
|
7164
|
+
}));
|
|
7165
|
+
}
|
|
7166
|
+
/**
|
|
7167
|
+
* Classify the platform backoff signal the pool waits out rather than failing:
|
|
7168
|
+
* a rate-limit 429 ({@link RateLimitError}, carrying the server retry delay) or a
|
|
7169
|
+
* genuinely-full place ({@link ApiError} with the `RESOURCE_EXHAUSTED` code).
|
|
7170
|
+
* Walks the `cause` chain because {@link RemoteRunner.executeScript} wraps the
|
|
7171
|
+
* transport error in a plain `Error`. Any other error is not a backoff signal.
|
|
7172
|
+
*/
|
|
7173
|
+
function classifyBackoff(error) {
|
|
7174
|
+
for (let current = error; current instanceof Error; current = current.cause) {
|
|
7175
|
+
if (current instanceof RateLimitError) return { retryAfterMs: current.retryAfterSeconds * 1e3 };
|
|
7176
|
+
if (current instanceof ApiError && current.code === "RESOURCE_EXHAUSTED") return {};
|
|
7177
|
+
}
|
|
7178
|
+
}
|
|
6960
7179
|
var WorkQueue = class {
|
|
6961
7180
|
decode;
|
|
6962
7181
|
encode;
|
|
@@ -7083,6 +7302,7 @@ function readStringProperty(err, key) {
|
|
|
7083
7302
|
//#region src/backends/open-cloud.ts
|
|
7084
7303
|
const PARALLEL_AUTO_CAP = 3;
|
|
7085
7304
|
const BASE_URL_ENV = "JEST_ROBLOX_OPEN_CLOUD_BASE_URL";
|
|
7305
|
+
const MAX_RETRIES_ENV = "JEST_ROBLOX_OCALE_MAX_RETRIES";
|
|
7086
7306
|
const DEFAULT_STREAM_POLL_MS = 250;
|
|
7087
7307
|
var OpenCloudBackend = class {
|
|
7088
7308
|
runner;
|
|
@@ -7102,17 +7322,18 @@ var OpenCloudBackend = class {
|
|
|
7102
7322
|
rawResults: workStealing === true ? await this.runWorkStealing({
|
|
7103
7323
|
jobs,
|
|
7104
7324
|
parallel,
|
|
7325
|
+
placeVersion: upload.versionNumber,
|
|
7105
7326
|
primaryConfig: primary.config,
|
|
7106
7327
|
scriptOverride,
|
|
7107
7328
|
streaming
|
|
7108
|
-
}) : await this.runStaticBuckets(jobs, parallel, scriptOverride),
|
|
7329
|
+
}) : await this.runStaticBuckets(jobs, parallel, upload.versionNumber, scriptOverride),
|
|
7109
7330
|
timing: {
|
|
7110
7331
|
executionMs: Date.now() - executionStart,
|
|
7111
7332
|
uploadMs: upload.uploadMs
|
|
7112
7333
|
}
|
|
7113
7334
|
};
|
|
7114
7335
|
}
|
|
7115
|
-
async runBucket(bucket, scriptOverride) {
|
|
7336
|
+
async runBucket(bucket, placeVersion, scriptOverride) {
|
|
7116
7337
|
const { indices, jobs } = bucket;
|
|
7117
7338
|
const primary = jobs[0];
|
|
7118
7339
|
const inputs = jobs.map((job) => {
|
|
@@ -7123,6 +7344,7 @@ var OpenCloudBackend = class {
|
|
|
7123
7344
|
});
|
|
7124
7345
|
const script = scriptOverride ?? generateTestScript(inputs);
|
|
7125
7346
|
const scriptResult = await this.runner.executeScript({
|
|
7347
|
+
placeVersion,
|
|
7126
7348
|
script,
|
|
7127
7349
|
timeout: primary.config.timeout
|
|
7128
7350
|
});
|
|
@@ -7141,37 +7363,44 @@ var OpenCloudBackend = class {
|
|
|
7141
7363
|
})
|
|
7142
7364
|
};
|
|
7143
7365
|
}
|
|
7144
|
-
async runStaticBuckets(jobs, parallel, scriptOverride) {
|
|
7366
|
+
async runStaticBuckets(jobs, parallel, placeVersion, scriptOverride) {
|
|
7145
7367
|
const buckets = bucketJobs(jobs, parallel);
|
|
7146
|
-
const bucketResults = await Promise.all(buckets.map(async (bucket) => this.runBucket(bucket, scriptOverride)));
|
|
7368
|
+
const bucketResults = await Promise.all(buckets.map(async (bucket) => this.runBucket(bucket, placeVersion, scriptOverride)));
|
|
7147
7369
|
const flattened = Array.from({ length: jobs.length });
|
|
7148
7370
|
for (const { indices, rawResults } of bucketResults) for (const [positionInBucket, originalIndex] of indices.entries()) flattened[originalIndex] = rawResults[positionInBucket];
|
|
7149
7371
|
return flattened;
|
|
7150
7372
|
}
|
|
7151
|
-
async runStealingTask(script, primaryConfig) {
|
|
7152
|
-
const result = await this.runner.executeScript({
|
|
7153
|
-
script,
|
|
7154
|
-
timeout: primaryConfig.timeout
|
|
7155
|
-
});
|
|
7156
|
-
const jestOutput = result.outputs[0];
|
|
7157
|
-
if (jestOutput === void 0) throw new Error(`No test results in output. Got: ${JSON.stringify(result.outputs)}`);
|
|
7158
|
-
return {
|
|
7159
|
-
entries: parseEnvelope(jestOutput),
|
|
7160
|
-
gameOutput: result.outputs[1]
|
|
7161
|
-
};
|
|
7162
|
-
}
|
|
7163
7373
|
async runWorkStealing(args) {
|
|
7164
|
-
const { jobs, parallel, primaryConfig, scriptOverride, streaming } = args;
|
|
7374
|
+
const { jobs, parallel, placeVersion, primaryConfig, scriptOverride, streaming } = args;
|
|
7165
7375
|
const taskCount = resolveBucketCount(parallel, jobs.length);
|
|
7166
7376
|
const tasksDone = { value: false };
|
|
7167
|
-
const
|
|
7377
|
+
const taskResults = [];
|
|
7378
|
+
let launched = 0;
|
|
7379
|
+
let taskFailure;
|
|
7380
|
+
const poolPromise = runTaskPool({
|
|
7381
|
+
concurrency: taskCount,
|
|
7382
|
+
isDone: () => launched >= taskCount,
|
|
7383
|
+
onError: (error) => {
|
|
7384
|
+
taskFailure = { error };
|
|
7385
|
+
},
|
|
7386
|
+
onResult: (result) => {
|
|
7387
|
+
taskResults.push(result);
|
|
7388
|
+
},
|
|
7389
|
+
places: [{ runTask: async () => {
|
|
7390
|
+
launched += 1;
|
|
7391
|
+
return this.runner.executeScript({
|
|
7392
|
+
placeVersion,
|
|
7393
|
+
script: scriptOverride,
|
|
7394
|
+
timeout: primaryConfig.timeout
|
|
7395
|
+
});
|
|
7396
|
+
} }]
|
|
7397
|
+
}).finally(() => {
|
|
7168
7398
|
tasksDone.value = true;
|
|
7169
7399
|
});
|
|
7170
7400
|
const pollPromise = streaming !== void 0 ? pollStreamingResults(streaming, () => tasksDone.value) : Promise.resolve();
|
|
7171
|
-
|
|
7172
|
-
if (
|
|
7173
|
-
const
|
|
7174
|
-
const entryByKey = aggregateEntriesByKey(taskEnvelopes);
|
|
7401
|
+
await Promise.all([poolPromise, pollPromise]);
|
|
7402
|
+
if (taskFailure !== void 0) throw taskFailure.error;
|
|
7403
|
+
const entryByKey = aggregateEntriesByKey(taskResults.map(parseStealingEnvelope));
|
|
7175
7404
|
const missing = [];
|
|
7176
7405
|
const rawResults = [];
|
|
7177
7406
|
for (const job of jobs) {
|
|
@@ -7210,6 +7439,19 @@ function resolveOpenCloudBaseUrl() {
|
|
|
7210
7439
|
if (override === void 0 || override === "") return;
|
|
7211
7440
|
return override.replace(/\/+$/, "");
|
|
7212
7441
|
}
|
|
7442
|
+
/**
|
|
7443
|
+
* Reads {@link MAX_RETRIES_ENV} for an Open Cloud retry-budget override. Lets
|
|
7444
|
+
* the live e2e suite raise the per-request retry count so concurrent place
|
|
7445
|
+
* uploads (which share one per-minute quota across processes) ride out a
|
|
7446
|
+
* transient 429 instead of failing. Returns undefined for unset, empty, or
|
|
7447
|
+
* non-integer values so the client keeps its own default.
|
|
7448
|
+
*/
|
|
7449
|
+
function resolveOcaleMaxRetries() {
|
|
7450
|
+
const raw = process.env[MAX_RETRIES_ENV]?.trim();
|
|
7451
|
+
if (raw === void 0 || raw === "") return;
|
|
7452
|
+
const parsed = Number(raw);
|
|
7453
|
+
return Number.isInteger(parsed) && parsed >= 0 ? parsed : void 0;
|
|
7454
|
+
}
|
|
7213
7455
|
function createOpenCloudBackend(credentials) {
|
|
7214
7456
|
return new OpenCloudBackend(credentials);
|
|
7215
7457
|
}
|
|
@@ -7248,7 +7490,11 @@ async function sleep(ms) {
|
|
|
7248
7490
|
}
|
|
7249
7491
|
function resolveRunnerOptions() {
|
|
7250
7492
|
const baseUrl = resolveOpenCloudBaseUrl();
|
|
7251
|
-
|
|
7493
|
+
const maxRetries = resolveOcaleMaxRetries();
|
|
7494
|
+
return {
|
|
7495
|
+
...baseUrl === void 0 ? {} : { baseUrl },
|
|
7496
|
+
...maxRetries === void 0 ? {} : { maxRetries }
|
|
7497
|
+
};
|
|
7252
7498
|
}
|
|
7253
7499
|
function resolveBucketCount(parallel, jobCount) {
|
|
7254
7500
|
if (parallel === void 0) return 1;
|
|
@@ -7273,6 +7519,19 @@ function bucketJobs(jobs, parallel) {
|
|
|
7273
7519
|
function entryLookupKey(package_, project) {
|
|
7274
7520
|
return project === void 0 || project === package_ ? package_ : `${package_}::${project}`;
|
|
7275
7521
|
}
|
|
7522
|
+
/**
|
|
7523
|
+
* Decode one work-stealing task's return envelope. Throws when the task
|
|
7524
|
+
* produced no Jest output so a broken task surfaces as a run failure rather
|
|
7525
|
+
* than a silently-missing package.
|
|
7526
|
+
*/
|
|
7527
|
+
function parseStealingEnvelope(result) {
|
|
7528
|
+
const jestOutput = result.outputs[0];
|
|
7529
|
+
if (jestOutput === void 0) throw new Error(`No test results in output. Got: ${JSON.stringify(result.outputs)}`);
|
|
7530
|
+
return {
|
|
7531
|
+
entries: parseEnvelope(jestOutput),
|
|
7532
|
+
gameOutput: result.outputs[1]
|
|
7533
|
+
};
|
|
7534
|
+
}
|
|
7276
7535
|
function aggregateEntriesByKey(taskEnvelopes) {
|
|
7277
7536
|
const entryByKey = /* @__PURE__ */ new Map();
|
|
7278
7537
|
for (const { entries, gameOutput } of taskEnvelopes) for (const entry of entries) if (entry.pkg !== void 0) {
|
|
@@ -8578,8 +8837,19 @@ function parseTscErrorLine(line) {
|
|
|
8578
8837
|
}
|
|
8579
8838
|
//#endregion
|
|
8580
8839
|
//#region src/typecheck/runner.ts
|
|
8581
|
-
/**
|
|
8840
|
+
/**
|
|
8841
|
+
* Milliseconds to wait for tsgo to launch (the child `spawn` event) before the
|
|
8842
|
+
* pass fails. Bounds only process startup, so a slow *compile* never trips it;
|
|
8843
|
+
* the compile itself is governed by {@link TypecheckOptions.timeout}.
|
|
8844
|
+
*/
|
|
8582
8845
|
const DEFAULT_SPAWN_TIMEOUT = 1e4;
|
|
8846
|
+
/**
|
|
8847
|
+
* Milliseconds the tsgo compile may run before it is killed and the pass
|
|
8848
|
+
* throws. Mirrors the run-level `timeout` default (`config.timeout`); callers
|
|
8849
|
+
* pass the resolved run timeout so a wedged compile dies on the same deadline
|
|
8850
|
+
* as the Roblox run instead of a tight typecheck-only number.
|
|
8851
|
+
*/
|
|
8852
|
+
const DEFAULT_RUN_TIMEOUT = 3e5;
|
|
8583
8853
|
function createLocationsIndexMap(source) {
|
|
8584
8854
|
const map = /* @__PURE__ */ new Map();
|
|
8585
8855
|
let index = 0;
|
|
@@ -8729,27 +8999,53 @@ async function spawnTsgo(options) {
|
|
|
8729
8999
|
}
|
|
8730
9000
|
const tsgoScript = resolveTsgoScript();
|
|
8731
9001
|
const spawnTimeout = options.spawnTimeout ?? DEFAULT_SPAWN_TIMEOUT;
|
|
9002
|
+
const runTimeout = options.timeout ?? DEFAULT_RUN_TIMEOUT;
|
|
8732
9003
|
return new Promise((resolve, reject) => {
|
|
8733
|
-
|
|
9004
|
+
let launchTimer;
|
|
9005
|
+
const state = { settled: false };
|
|
9006
|
+
function finish(action) {
|
|
9007
|
+
if (state.settled) return;
|
|
9008
|
+
state.settled = true;
|
|
9009
|
+
if (launchTimer !== void 0) clearTimeout(launchTimer);
|
|
9010
|
+
action();
|
|
9011
|
+
}
|
|
9012
|
+
const child = execFile(process.execPath, [tsgoScript, ...args], {
|
|
8734
9013
|
cwd: options.rootDir,
|
|
8735
9014
|
encoding: "utf-8",
|
|
8736
|
-
timeout:
|
|
9015
|
+
timeout: runTimeout,
|
|
8737
9016
|
windowsHide: true
|
|
8738
9017
|
}, (error, stdout, stderr) => {
|
|
8739
9018
|
if (error === null) {
|
|
8740
|
-
|
|
9019
|
+
finish(() => {
|
|
9020
|
+
resolve(stdout);
|
|
9021
|
+
});
|
|
8741
9022
|
return;
|
|
8742
9023
|
}
|
|
8743
9024
|
if (error.killed === true) {
|
|
8744
|
-
|
|
9025
|
+
finish(() => {
|
|
9026
|
+
reject(/* @__PURE__ */ new Error(`tsgo typecheck timed out after ${String(runTimeout)}ms (timeout)`));
|
|
9027
|
+
});
|
|
8745
9028
|
return;
|
|
8746
9029
|
}
|
|
8747
9030
|
if (typeof error.code === "number") {
|
|
8748
|
-
|
|
9031
|
+
finish(() => {
|
|
9032
|
+
resolve(stdout !== "" ? stdout : stderr);
|
|
9033
|
+
});
|
|
8749
9034
|
return;
|
|
8750
9035
|
}
|
|
8751
|
-
|
|
9036
|
+
finish(() => {
|
|
9037
|
+
reject(new Error(error.message, { cause: error }));
|
|
9038
|
+
});
|
|
9039
|
+
});
|
|
9040
|
+
child.once("spawn", () => {
|
|
9041
|
+
clearTimeout(launchTimer);
|
|
8752
9042
|
});
|
|
9043
|
+
if (!state.settled) launchTimer = setTimeout(() => {
|
|
9044
|
+
child.kill();
|
|
9045
|
+
finish(() => {
|
|
9046
|
+
reject(/* @__PURE__ */ new Error(`tsgo spawn timed out after ${String(spawnTimeout)}ms (spawnTimeout)`));
|
|
9047
|
+
});
|
|
9048
|
+
}, spawnTimeout);
|
|
8753
9049
|
});
|
|
8754
9050
|
}
|
|
8755
9051
|
//#endregion
|
|
@@ -8959,6 +9255,20 @@ async function runMultiProject(options) {
|
|
|
8959
9255
|
const { filesByProject, projects } = timing.profile("selectProjects", () => {
|
|
8960
9256
|
return selectProjects(allProjects, cli.project, cli.files, rootConfig.rootDir);
|
|
8961
9257
|
});
|
|
9258
|
+
if (projects.every((project) => {
|
|
9259
|
+
return resolveTypecheckConfig({
|
|
9260
|
+
cli: cliTypecheck,
|
|
9261
|
+
project: project.typecheck,
|
|
9262
|
+
root: rootConfig.typecheck
|
|
9263
|
+
}).only;
|
|
9264
|
+
})) return runMultiTypecheckOnly({
|
|
9265
|
+
cliFiles: cli.files,
|
|
9266
|
+
cliTypecheck,
|
|
9267
|
+
filesByProject,
|
|
9268
|
+
projects,
|
|
9269
|
+
rootConfig,
|
|
9270
|
+
timing
|
|
9271
|
+
});
|
|
8962
9272
|
const cacheRoot = path$1.resolve(rootConfig.rootDir, ".jest-roblox", "cache");
|
|
8963
9273
|
const cleaned = timing.profile("cleanLeftoverStubs", () => {
|
|
8964
9274
|
return cleanLeftoverStubs(projects, rootConfig.rootDir);
|
|
@@ -9004,6 +9314,7 @@ async function runMultiProject(options) {
|
|
|
9004
9314
|
ignoreSourceErrors: rootTypecheck.ignoreSourceErrors,
|
|
9005
9315
|
rootDir: group.cwd,
|
|
9006
9316
|
spawnTimeout: rootTypecheck.spawnTimeout,
|
|
9317
|
+
timeout: rootConfig.timeout,
|
|
9007
9318
|
tsconfig: group.tsconfig
|
|
9008
9319
|
});
|
|
9009
9320
|
})]);
|
|
@@ -9106,6 +9417,56 @@ function collectPendingJobs(arguments_) {
|
|
|
9106
9417
|
typeTestEntries
|
|
9107
9418
|
};
|
|
9108
9419
|
}
|
|
9420
|
+
async function runMultiTypecheckOnly(arguments_) {
|
|
9421
|
+
const { cliFiles, cliTypecheck, filesByProject, projects, rootConfig, timing } = arguments_;
|
|
9422
|
+
const { typeTestEntries } = timing.profile("collectPendingJobs", () => {
|
|
9423
|
+
return collectPendingJobs({
|
|
9424
|
+
cliFiles,
|
|
9425
|
+
cliTypecheck,
|
|
9426
|
+
effectivePlaceFile: rootConfig.placeFile,
|
|
9427
|
+
filesByProject,
|
|
9428
|
+
projects,
|
|
9429
|
+
rootConfig
|
|
9430
|
+
});
|
|
9431
|
+
});
|
|
9432
|
+
if (typeTestEntries.length === 0) {
|
|
9433
|
+
if (rootConfig.passWithNoTests) return {
|
|
9434
|
+
merged: {},
|
|
9435
|
+
mode: "multi",
|
|
9436
|
+
preCoverageMs: 0,
|
|
9437
|
+
projectResults: []
|
|
9438
|
+
};
|
|
9439
|
+
return {
|
|
9440
|
+
merged: {},
|
|
9441
|
+
mode: "multi",
|
|
9442
|
+
preCoverageMs: 0,
|
|
9443
|
+
projectResults: [],
|
|
9444
|
+
validationExitCode: 2,
|
|
9445
|
+
validationMessage: "No test files found in any project\n"
|
|
9446
|
+
};
|
|
9447
|
+
}
|
|
9448
|
+
const rootTypecheck = resolveTypecheckConfig({
|
|
9449
|
+
cli: cliTypecheck,
|
|
9450
|
+
root: rootConfig.typecheck
|
|
9451
|
+
});
|
|
9452
|
+
const typecheckPass = await runTypecheckPass(typeTestEntries, async (group) => {
|
|
9453
|
+
return runTypecheck({
|
|
9454
|
+
files: group.files,
|
|
9455
|
+
ignoreSourceErrors: rootTypecheck.ignoreSourceErrors,
|
|
9456
|
+
rootDir: group.cwd,
|
|
9457
|
+
spawnTimeout: rootTypecheck.spawnTimeout,
|
|
9458
|
+
tsconfig: group.tsconfig
|
|
9459
|
+
});
|
|
9460
|
+
});
|
|
9461
|
+
timing.record("runTypecheck", typecheckPass.elapsedMs);
|
|
9462
|
+
return {
|
|
9463
|
+
merged: {},
|
|
9464
|
+
mode: "multi",
|
|
9465
|
+
preCoverageMs: 0,
|
|
9466
|
+
projectResults: [],
|
|
9467
|
+
typecheckResult: typecheckPass.result
|
|
9468
|
+
};
|
|
9469
|
+
}
|
|
9109
9470
|
async function runJobs(backend, pendingJobs, parallel, timing) {
|
|
9110
9471
|
if (pendingJobs.length === 0) {
|
|
9111
9472
|
await backend.close?.();
|
|
@@ -9274,6 +9635,7 @@ async function runSingleProject(options) {
|
|
|
9274
9635
|
ignoreSourceErrors: typecheck.ignoreSourceErrors,
|
|
9275
9636
|
rootDir: effectiveConfig.rootDir,
|
|
9276
9637
|
spawnTimeout: typecheck.spawnTimeout,
|
|
9638
|
+
timeout: effectiveConfig.timeout,
|
|
9277
9639
|
tsconfig: typecheck.tsconfig
|
|
9278
9640
|
});
|
|
9279
9641
|
} finally {
|
|
@@ -10218,7 +10580,7 @@ async function runWorkspaceProfiled(options, timing) {
|
|
|
10218
10580
|
});
|
|
10219
10581
|
}), runWorkspaceTypecheckPass(typeTestEntries, typecheckByDirectory)]);
|
|
10220
10582
|
if (runOptions.workspaceOutputFile) writePerPackageOutputFiles(workspaceRoot, buildPerPackageResults(pending, results, typecheckPass.byPackage, typeTestProjects));
|
|
10221
|
-
|
|
10583
|
+
await writeResultFile(runOptions.outputFile, typecheckPass.outcome.result, runOptions.outputFile !== void 0 ? mergeProjectResults(results).result : void 0);
|
|
10222
10584
|
emitWorkspaceGameOutput({
|
|
10223
10585
|
pending,
|
|
10224
10586
|
results,
|
|
@@ -10498,7 +10860,8 @@ function collectPendingEntries(contexts, cli) {
|
|
|
10498
10860
|
typecheckByDirectory.set(packageDirectory, {
|
|
10499
10861
|
ignoreSourceErrors: packageTypecheck.ignoreSourceErrors,
|
|
10500
10862
|
pkg: ctx.info.name,
|
|
10501
|
-
spawnTimeout: packageTypecheck.spawnTimeout
|
|
10863
|
+
spawnTimeout: packageTypecheck.spawnTimeout,
|
|
10864
|
+
timeout: ctx.pkgConfig.timeout
|
|
10502
10865
|
});
|
|
10503
10866
|
}
|
|
10504
10867
|
}
|
|
@@ -10531,6 +10894,7 @@ async function runWorkspaceTypecheckPass(entries, typecheckByDirectory) {
|
|
|
10531
10894
|
ignoreSourceErrors: policy.ignoreSourceErrors,
|
|
10532
10895
|
rootDir: group.cwd,
|
|
10533
10896
|
spawnTimeout: policy.spawnTimeout,
|
|
10897
|
+
timeout: policy.timeout,
|
|
10534
10898
|
...group.tsconfig !== void 0 ? { tsconfig: group.tsconfig } : {}
|
|
10535
10899
|
}), policy.pkg);
|
|
10536
10900
|
byPackage.set(policy.pkg, mergeResults$1(stamped, byPackage.get(policy.pkg)));
|
|
@@ -10545,12 +10909,9 @@ function attachTypecheck(results, pass, timing) {
|
|
|
10545
10909
|
...pass.result !== void 0 ? { typecheckResult: pass.result } : {}
|
|
10546
10910
|
};
|
|
10547
10911
|
}
|
|
10548
|
-
async function writeWorkspaceOutputFile(outputFile, runtime, typecheck) {
|
|
10549
|
-
await writeJsonFile$1(mergeResults$1(typecheck, runtime), outputFile);
|
|
10550
|
-
}
|
|
10551
10912
|
async function runTypecheckOnlyWorkspace(input) {
|
|
10552
10913
|
const typecheckPass = await runWorkspaceTypecheckPass(input.typeTestEntries, input.typecheckByDirectory);
|
|
10553
|
-
|
|
10914
|
+
await writeResultFile(input.runOptions.outputFile, typecheckPass.outcome.result, void 0);
|
|
10554
10915
|
if (input.runOptions.workspaceOutputFile) writePerPackageOutputFiles(input.workspaceRoot, buildPerPackageResults([], [], typecheckPass.byPackage, input.typeTestProjects));
|
|
10555
10916
|
return attachTypecheck([], typecheckPass.outcome, input.timing);
|
|
10556
10917
|
}
|
|
@@ -10695,6 +11056,13 @@ function discoverWorkspaceRoot(cwd) {
|
|
|
10695
11056
|
//#endregion
|
|
10696
11057
|
//#region src/workspace/package-resolver.ts
|
|
10697
11058
|
const JEST_CONFIG_MARKER$1 = /^jest\.config\.[^.]+$/;
|
|
11059
|
+
function readPackageJsonName(packageJsonPath) {
|
|
11060
|
+
if (!fs$1.existsSync(packageJsonPath)) return;
|
|
11061
|
+
const raw = parsePackageJson(packageJsonPath);
|
|
11062
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return;
|
|
11063
|
+
const nameValue = Reflect.get(raw, "name");
|
|
11064
|
+
return typeof nameValue === "string" ? nameValue : void 0;
|
|
11065
|
+
}
|
|
10698
11066
|
/**
|
|
10699
11067
|
* Enumerate workspace packages. With `patterns` (from `workspace.packages`),
|
|
10700
11068
|
* resolve directories by globbing for a `jest.config.*` — works in any repo,
|
|
@@ -10719,13 +11087,6 @@ function parsePackageJson(packageJsonPath) {
|
|
|
10719
11087
|
throw new Error(`Failed to parse ${packageJsonPath}.`, { cause: err });
|
|
10720
11088
|
}
|
|
10721
11089
|
}
|
|
10722
|
-
function readPackageJsonName(packageJsonPath) {
|
|
10723
|
-
if (!fs$1.existsSync(packageJsonPath)) return;
|
|
10724
|
-
const raw = parsePackageJson(packageJsonPath);
|
|
10725
|
-
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return;
|
|
10726
|
-
const nameValue = Reflect.get(raw, "name");
|
|
10727
|
-
return typeof nameValue === "string" ? nameValue : void 0;
|
|
10728
|
-
}
|
|
10729
11090
|
function listPnpmPackages(workspaceRoot) {
|
|
10730
11091
|
const yamlPath = path$1.join(workspaceRoot, "pnpm-workspace.yaml");
|
|
10731
11092
|
if (!fs$1.existsSync(yamlPath)) throw new Error("Workspace mode requires either a `workspace.packages` glob list in your jest config or a pnpm-workspace.yaml at the workspace root. Use `workspace.packages` (with `--workspace-root` to run from outside a package) for Luau-only, npm, or yarn repos.");
|
|
@@ -10807,15 +11168,25 @@ function getAffectedPackages(workspaceRoot, ref) {
|
|
|
10807
11168
|
"ls",
|
|
10808
11169
|
`--filter=...[${ref}]`,
|
|
10809
11170
|
"--output=json"
|
|
10810
|
-
], workspaceRoot)).filter((item) => hasJestConfig(path$1.join(workspaceRoot, item.relativePath))).map((item) =>
|
|
11171
|
+
], workspaceRoot)).filter((item) => hasJestConfig(path$1.join(workspaceRoot, item.relativePath))).map((item) => {
|
|
11172
|
+
return {
|
|
11173
|
+
name: item.name,
|
|
11174
|
+
packageDirectory: path$1.join(workspaceRoot, item.relativePath)
|
|
11175
|
+
};
|
|
11176
|
+
});
|
|
10811
11177
|
if (fs$1.existsSync(path$1.join(workspaceRoot, "nx.json"))) return parseNxOutput(runTool("nx", [
|
|
10812
11178
|
"show",
|
|
10813
11179
|
"projects",
|
|
10814
11180
|
"--affected",
|
|
10815
11181
|
`--base=${ref}`,
|
|
10816
11182
|
"--json"
|
|
10817
|
-
], workspaceRoot)).
|
|
10818
|
-
|
|
11183
|
+
], workspaceRoot)).flatMap((nxName) => {
|
|
11184
|
+
const packageDirectory = path$1.join(workspaceRoot, nxProjectRoot(workspaceRoot, nxName));
|
|
11185
|
+
if (!hasJestConfig(packageDirectory)) return [];
|
|
11186
|
+
return [{
|
|
11187
|
+
name: readPackageJsonName(path$1.join(packageDirectory, "package.json")) ?? nxName,
|
|
11188
|
+
packageDirectory
|
|
11189
|
+
}];
|
|
10819
11190
|
});
|
|
10820
11191
|
throw new Error("--affected-since requires turbo or nx at the workspace root. Use --packages to specify packages explicitly.");
|
|
10821
11192
|
}
|
|
@@ -10931,9 +11302,15 @@ function assertWorkspaceRunOptions(runOptions) {
|
|
|
10931
11302
|
};
|
|
10932
11303
|
return { ok: true };
|
|
10933
11304
|
}
|
|
10934
|
-
|
|
11305
|
+
/**
|
|
11306
|
+
* Resolve the affected/requested packages to full `PackageInfo`. The
|
|
11307
|
+
* `--affected-since` branch already carries directory + `package.json#name`
|
|
11308
|
+
* from turbo/nx, so it skips the `resolvePackage` round-trip; the `--packages`
|
|
11309
|
+
* branch resolves each comma-separated name against the workspace.
|
|
11310
|
+
*/
|
|
11311
|
+
function resolveWorkspacePackages(cli, workspaceRoot, patterns) {
|
|
10935
11312
|
if (cli.affectedSince !== void 0) return getAffectedPackages(workspaceRoot, cli.affectedSince);
|
|
10936
|
-
return cli.packages.split(",").map((name) => name.trim()).filter((name) => name.length > 0);
|
|
11313
|
+
return cli.packages.split(",").map((name) => name.trim()).filter((name) => name.length > 0).map((name) => resolvePackage(workspaceRoot, name, patterns));
|
|
10937
11314
|
}
|
|
10938
11315
|
function buildWorkspaceCredentials(cli, runOptions) {
|
|
10939
11316
|
return resolveCredentials({
|
|
@@ -11131,10 +11508,10 @@ function resolveEnumerationRoot(workspace) {
|
|
|
11131
11508
|
function resolvePackages(cli, workspace) {
|
|
11132
11509
|
try {
|
|
11133
11510
|
const { patterns, workspaceRoot } = resolveEnumerationRoot(workspace);
|
|
11134
|
-
const
|
|
11135
|
-
if (
|
|
11511
|
+
const packageInfos = resolveWorkspacePackages(cli, workspaceRoot, patterns);
|
|
11512
|
+
if (packageInfos.length === 0) return { noAffected: true };
|
|
11136
11513
|
return {
|
|
11137
|
-
packageInfos
|
|
11514
|
+
packageInfos,
|
|
11138
11515
|
workspaceRoot
|
|
11139
11516
|
};
|
|
11140
11517
|
} catch (err) {
|