@isentinel/jest-roblox 0.2.0 → 0.2.2

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.
@@ -4,16 +4,15 @@ import assert from "node:assert";
4
4
  import * as fs$1 from "node:fs";
5
5
  import { existsSync, readFileSync } from "node:fs";
6
6
  import * as path$1 from "node:path";
7
- import path, { dirname, join, relative } from "node:path";
7
+ import path from "node:path";
8
8
  import process from "node:process";
9
9
  import color from "tinyrainbow";
10
10
  import { WebSocketServer } from "ws";
11
+ import { createFetchClient, getCacheDirectory, getCacheKey, hashBuffer, isUploaded, markUploaded, readCache, writeCache } from "@isentinel/roblox-runner";
11
12
  import { createDefineConfig, loadConfig } from "c12";
12
- import { homedir, tmpdir } from "node:os";
13
- import * as crypto from "node:crypto";
14
13
  import { randomUUID } from "node:crypto";
15
- import buffer from "node:buffer";
16
14
  import { defuFn } from "defu";
15
+ import { collectPaths as collectPaths$1, resolveNestedProjects } from "@isentinel/rojo-utils";
17
16
  import { getTsconfig } from "get-tsconfig";
18
17
  import { TraceMap, originalPositionFor, sourceContentFor } from "@jridgewell/trace-mapping";
19
18
  import hljs from "highlight.js/lib/core";
@@ -64,6 +63,7 @@ function parseJestOutput(output) {
64
63
  const parsed = JSON.parse(trimmed);
65
64
  const coverageData = extractCoverageData(parsed);
66
65
  const luauTiming = extractLuauTiming(parsed);
66
+ const setupSeconds = extractSetupSeconds(parsed);
67
67
  const snapshotWrites = extractSnapshotWrites(parsed);
68
68
  const unwrapped = unwrapResult(parsed);
69
69
  if (unwrapped["kind"] === "ExecutionError") {
@@ -74,6 +74,7 @@ function parseJestOutput(output) {
74
74
  coverageData,
75
75
  luauTiming,
76
76
  result: validateJestResult(unwrapped["results"]),
77
+ setupSeconds,
77
78
  snapshotWrites
78
79
  };
79
80
  try {
@@ -81,6 +82,7 @@ function parseJestOutput(output) {
81
82
  coverageData,
82
83
  luauTiming,
83
84
  result: validateJestResult(unwrapped),
85
+ setupSeconds,
84
86
  snapshotWrites
85
87
  };
86
88
  } catch {}
@@ -193,6 +195,11 @@ function validateJestResult(value) {
193
195
  if (result instanceof type.errors) throw new Error(`Invalid Jest result: ${result.summary}`);
194
196
  return result;
195
197
  }
198
+ function extractSetupSeconds(parsed) {
199
+ const setup = parsed["_setup"];
200
+ if (typeof setup !== "number") return;
201
+ return setup;
202
+ }
196
203
  //#endregion
197
204
  //#region src/config/schema.ts
198
205
  const ROOT_ONLY_KEYS = new Set([
@@ -208,6 +215,7 @@ const ROOT_ONLY_KEYS = new Set([
208
215
  "gameOutput",
209
216
  "jestPath",
210
217
  "luauRoots",
218
+ "parallel",
211
219
  "placeFile",
212
220
  "pollInterval",
213
221
  "port",
@@ -362,6 +370,7 @@ const configSchema = type({
362
370
  "maxWorkers?": type("number").or(type("string")),
363
371
  "noStackTrace?": "boolean",
364
372
  "outputFile?": "string",
373
+ "parallel?": type("'auto'").or("number.integer >= 1"),
365
374
  "passWithNoTests?": "boolean",
366
375
  "placeFile?": "string",
367
376
  "pollInterval?": "number",
@@ -412,7 +421,7 @@ const defineConfig = createDefineConfig();
412
421
  const defineProject = createDefineConfig();
413
422
  //#endregion
414
423
  //#region src/test-runner.bundled.luau
415
- var test_runner_bundled_default = "type PatchState__DARKLUA_TYPE_a = {\n robloxSharedExports: any,\n originalGetDataModelService: any,\n Runtime: any,\n originalRequireInternalModule: any,\n}\n\ntype Config__DARKLUA_TYPE_b = {\n jestPath: string?,\n projects: { string }?,\n setupFiles: { string }?,\n setupFilesAfterEnv: { string }?,\n _coverage: boolean?,\n _timing: boolean?,\n}\n\ntype CapturedMessage__DARKLUA_TYPE_c = { message: string, messageType: number, timestamp: number }\nlocal __JEST_MODULES={cache={}::any}do do local function __modImpl()--!strict\n\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal module = {}\n\nfunction 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\nend\n\nfunction module.getJest(config: { jestPath: string? }): ModuleScript\n local jestPath = config.jestPath\n if jestPath then\n local instance = 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 = 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\nend\n\nfunction 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)\nend\n\nfunction 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\nend\n\nreturn module\nend function __JEST_MODULES.a():typeof(__modImpl())local v=__JEST_MODULES.cache.a if not v then v={c=__modImpl()}__JEST_MODULES.cache.a=v end return v.c end end do local function __modImpl()--!strict\n\nlocal function 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, \"/\")\nend\n\nlocal CoreScriptSyncService = {}\n\nfunction CoreScriptSyncService:GetScriptFilePath(instance: Instance): string\n return getInstancePath(instance)\nend\n\nreturn CoreScriptSyncService\nend function __JEST_MODULES.b():typeof(__modImpl())local v=__JEST_MODULES.cache.b if not v then v={c=__modImpl()}__JEST_MODULES.cache.b=v end return v.c end end do local function __modImpl()--!strict\n\nlocal function normalizeSnapPath(path: string): string\n return (string.gsub(path, \"%.snap%.lua$\", \".snap.luau\"))\nend\n\nlocal function create(snapshotWrites: { [string]: string })\n local FileSystemService = {}\n\n function FileSystemService:WriteFile(path: string, contents: string)\n snapshotWrites[normalizeSnapPath(path)] = contents\n end\n\n function FileSystemService:CreateDirectories(_path: string) end\n\n function FileSystemService:Exists(path: string): boolean\n return snapshotWrites[normalizeSnapPath(path)] ~= nil\n end\n\n function FileSystemService:Remove(path: string)\n snapshotWrites[normalizeSnapPath(path)] = nil\n end\n\n function FileSystemService:IsRegularFile(path: string): boolean\n return snapshotWrites[normalizeSnapPath(path)] ~= nil\n end\n\n return FileSystemService\nend\n\nreturn create\nend function __JEST_MODULES.c():typeof(__modImpl())local v=__JEST_MODULES.cache.c if not v then v={c=__modImpl()}__JEST_MODULES.cache.c=v end return v.c end end do local function __modImpl()--!strict\n\nlocal CoreScriptSyncService = __JEST_MODULES.b()\nlocal InstanceResolver = __JEST_MODULES.a()\n\nlocal module = {}\n\nfunction module.createMockGetDataModelService(snapshotWrites: { [string]: string })\n local FileSystemService = __JEST_MODULES.c()(snapshotWrites)\n\n return function(service: string): any\n if service == \"FileSystemService\" then\n return FileSystemService\n elseif service == \"CoreScriptSyncService\" then\n return 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\nend\n\n\n\n\n\n\n\n\nfunction module.patch(jestModule: ModuleScript, snapshotWrites: { [string]: string }): PatchState__DARKLUA_TYPE_a?\n local mockGetDataModelService = module.createMockGetDataModelService(snapshotWrites)\n\n local robloxSharedInstance = 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 local getDataModelServiceChild = robloxSharedInstance:FindFirstChild(\"getDataModelService\")\n\n local jestRuntimeModule = InstanceResolver.findSiblingPackage(jestModule, \"JestRuntime\", \"jest-runtime\")\n if not jestRuntimeModule then\n warn(\"Could not find JestRuntime; snapshot interception unavailable\")\n return {\n robloxSharedExports = robloxSharedExports,\n originalGetDataModelService = originalGetDataModelService,\n Runtime = nil,\n originalRequireInternalModule = nil,\n }\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 return {\n robloxSharedExports = robloxSharedExports,\n originalGetDataModelService = originalGetDataModelService,\n Runtime = Runtime,\n originalRequireInternalModule = originalRequireInternalModule,\n }\nend\n\nfunction module.unpatch(state: PatchState__DARKLUA_TYPE_a?)\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\nend\n\nreturn module\nend function __JEST_MODULES.d():typeof(__modImpl())local v=__JEST_MODULES.cache.d if not v then v={c=__modImpl()}__JEST_MODULES.cache.d=v end return v.c end end do local function __modImpl()--!strict\n\nlocal HttpService = game:GetService(\"HttpService\")\nlocal LogService = game:GetService(\"LogService\")\n\nlocal InstanceResolver = __JEST_MODULES.a()\nlocal SnapshotPatch = __JEST_MODULES.d()\n\n\n\n\n\n\n\n\n\n\nlocal function fail(err: string)\n return {\n success = false,\n err = err,\n }\nend\n\n\n\nlocal function interceptWriteable(writeable: any, buffer: { CapturedMessage__DARKLUA_TYPE_c }, messageType: number)\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\nend\n\nlocal module = {}\n\nfunction module.run(callingScript: LuaSourceContainer, config: Config__DARKLUA_TYPE_b): (string, string)\n local t0 = os.clock()\n local timingEnabled = config._timing\n local coverageEnabled = config._coverage\n\n local t_findJest0 = os.clock()\n local findSuccess, findValue = pcall(InstanceResolver.getJest, config)\n local t_findJest = os.clock()\n\n if not findSuccess then\n local logSuccess, logHistory = pcall(function()\n return HttpService:JSONEncode(LogService:GetLogHistory())\n end)\n\n return HttpService:JSONEncode(fail(findValue :: any)), if logSuccess then logHistory else \"[]\"\n end\n\n LogService:ClearOutput()\n\n local snapshotWrites: { [string]: string } = {}\n\n local t_patchSnapshot0 = os.clock()\n local patchState = 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.\n -- Jest writes via process.stdout/stderr (Writeable objects whose _writeFn\n -- defaults to print). Wrapping _writeFn captures messages like\n -- \"No tests found\" that are printed just before exit(1) throws.\n local capturedMessages: { CapturedMessage__DARKLUA_TYPE_c } = {}\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 = interceptWriteable(process.stdout, capturedMessages, 0)\n restoreStderr = interceptWriteable(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 or pass --projects.\"\n )\n\n for _, projectPath in config.projects do\n table.insert(projects, 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, 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, InstanceResolver.findInstance(setupPath))\n end\n\n config.setupFilesAfterEnv = resolved :: any\n end\n local t_resolveSetupFiles = os.clock()\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\n if coverageEnabled then\n _G.__jest_roblox_cov = {}\n end\n\n local t_jestRunCLI0 = os.clock()\n local jestResult = Jest.runCLI(callingScript, config, projects):expect()\n local t_jestRunCLI = os.clock()\n\n local result: { [string]: any } = {\n success = true,\n value = jestResult,\n }\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 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 = 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 SnapshotPatch.unpatch(patchState)\n\n local jestResult\n if not runSuccess then\n jestResult = HttpService:JSONEncode(fail(runValue :: any))\n else\n jestResult = HttpService:JSONEncode(runValue)\n end\n\n local logSuccess, logHistory = pcall(function()\n return HttpService:JSONEncode(capturedMessages)\n end)\n\n return jestResult, if logSuccess then logHistory else \"[]\"\nend\n\nreturn module\nend function __JEST_MODULES.e():typeof(__modImpl())local v=__JEST_MODULES.cache.e if not v then v={c=__modImpl()}__JEST_MODULES.cache.e=v end return v.c end end end--!strict\n\nlocal HttpService = game:GetService(\"HttpService\")\n\nlocal Runner = __JEST_MODULES.e()\n\nlocal config = HttpService:JSONDecode([=[__CONFIG_JSON__]=])\n\nreturn Runner.run(script, config)\n";
424
+ var test_runner_bundled_default = "type PatchState__DARKLUA_TYPE_a = {\n Runtime: any,\n originalRequireModule: any,\n accumulatedSeconds: { value: number },\n}\n\ntype PatchState__DARKLUA_TYPE_b = {\n robloxSharedExports: any,\n originalGetDataModelService: any,\n Runtime: any,\n originalRequireInternalModule: any,\n}\n\ntype Config__DARKLUA_TYPE_c = {\n jestPath: string?,\n projects: { string }?,\n setupFiles: { string }?,\n setupFilesAfterEnv: { string }?,\n _coverage: boolean?,\n _timing: boolean?,\n}\n\ntype CapturedMessage__DARKLUA_TYPE_d = { message: string, messageType: number, timestamp: number }\n\ntype ProjectEntry__DARKLUA_TYPE_e = {\n jestOutput: string,\n gameOutput: string,\n elapsedMs: number,\n}\nlocal __JEST_MODULES={cache={}::any}do do local function __modImpl()--!strict\n\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal module = {}\n\nfunction 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\nend\n\nfunction module.getJest(config: { jestPath: string? }): ModuleScript\n local jestPath = config.jestPath\n if jestPath then\n local instance = 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 = 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\nend\n\nfunction 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)\nend\n\nfunction 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\nend\n\nreturn module\nend function __JEST_MODULES.a():typeof(__modImpl())local v=__JEST_MODULES.cache.a if not v then v={c=__modImpl()}__JEST_MODULES.cache.a=v end return v.c end end do local function __modImpl()--!strict\n\nlocal InstanceResolver = __JEST_MODULES.a()\n\nlocal module = {}\n\n\n\n\n\n\n\nfunction module.patch(\n jestModule: ModuleScript,\n setupFiles: { Instance }?,\n setupFilesAfterEnv: { Instance }?\n): PatchState__DARKLUA_TYPE_a?\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 = 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 }\nend\n\nfunction module.unpatch(state: PatchState__DARKLUA_TYPE_a?)\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\nend\n\nfunction module.getSeconds(state: PatchState__DARKLUA_TYPE_a?): number\n if not state then\n return 0\n end\n\n return state.accumulatedSeconds.value\nend\n\nreturn module\nend function __JEST_MODULES.b():typeof(__modImpl())local v=__JEST_MODULES.cache.b if not v then v={c=__modImpl()}__JEST_MODULES.cache.b=v end return v.c end end do local function __modImpl()--!strict\n\nlocal function 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, \"/\")\nend\n\nlocal CoreScriptSyncService = {}\n\nfunction CoreScriptSyncService:GetScriptFilePath(instance: Instance): string\n return getInstancePath(instance)\nend\n\nreturn CoreScriptSyncService\nend function __JEST_MODULES.c():typeof(__modImpl())local v=__JEST_MODULES.cache.c if not v then v={c=__modImpl()}__JEST_MODULES.cache.c=v end return v.c end end do local function __modImpl()--!strict\n\nlocal function normalizeSnapPath(path: string): string\n return (string.gsub(path, \"%.snap%.lua$\", \".snap.luau\"))\nend\n\nlocal function create(snapshotWrites: { [string]: string })\n local FileSystemService = {}\n\n function FileSystemService:WriteFile(path: string, contents: string)\n snapshotWrites[normalizeSnapPath(path)] = contents\n end\n\n function FileSystemService:CreateDirectories(_path: string) end\n\n function FileSystemService:Exists(path: string): boolean\n return snapshotWrites[normalizeSnapPath(path)] ~= nil\n end\n\n function FileSystemService:Remove(path: string)\n snapshotWrites[normalizeSnapPath(path)] = nil\n end\n\n function FileSystemService:IsRegularFile(path: string): boolean\n return snapshotWrites[normalizeSnapPath(path)] ~= nil\n end\n\n return FileSystemService\nend\n\nreturn create\nend function __JEST_MODULES.d():typeof(__modImpl())local v=__JEST_MODULES.cache.d if not v then v={c=__modImpl()}__JEST_MODULES.cache.d=v end return v.c end end do local function __modImpl()--!strict\n\nlocal CoreScriptSyncService = __JEST_MODULES.c()\nlocal InstanceResolver = __JEST_MODULES.a()\n\nlocal module = {}\n\nfunction module.createMockGetDataModelService(snapshotWrites: { [string]: string })\n local FileSystemService = __JEST_MODULES.d()(snapshotWrites)\n\n return function(service: string): any\n if service == \"FileSystemService\" then\n return FileSystemService\n elseif service == \"CoreScriptSyncService\" then\n return 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\nend\n\n\n\n\n\n\n\n\nfunction module.patch(jestModule: ModuleScript, snapshotWrites: { [string]: string }): PatchState__DARKLUA_TYPE_b?\n local mockGetDataModelService = module.createMockGetDataModelService(snapshotWrites)\n\n local robloxSharedInstance = 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 local getDataModelServiceChild = robloxSharedInstance:FindFirstChild(\"getDataModelService\")\n\n local jestRuntimeModule = InstanceResolver.findSiblingPackage(jestModule, \"JestRuntime\", \"jest-runtime\")\n if not jestRuntimeModule then\n warn(\"Could not find JestRuntime; snapshot interception unavailable\")\n return {\n robloxSharedExports = robloxSharedExports,\n originalGetDataModelService = originalGetDataModelService,\n Runtime = nil,\n originalRequireInternalModule = nil,\n }\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 return {\n robloxSharedExports = robloxSharedExports,\n originalGetDataModelService = originalGetDataModelService,\n Runtime = Runtime,\n originalRequireInternalModule = originalRequireInternalModule,\n }\nend\n\nfunction module.unpatch(state: PatchState__DARKLUA_TYPE_b?)\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\nend\n\nreturn module\nend function __JEST_MODULES.e():typeof(__modImpl())local v=__JEST_MODULES.cache.e if not v then v={c=__modImpl()}__JEST_MODULES.cache.e=v end return v.c end end do local function __modImpl()--!strict\n\nlocal HttpService = game:GetService(\"HttpService\")\nlocal LogService = game:GetService(\"LogService\")\n\nlocal InstanceResolver = __JEST_MODULES.a()\nlocal SetupTiming = __JEST_MODULES.b()\nlocal SnapshotPatch = __JEST_MODULES.e()\n\n\n\n\n\n\n\n\n\n\nlocal function fail(err: string)\n return {\n success = false,\n err = err,\n }\nend\n\n\n\nlocal function interceptWriteable(writeable: any, buffer: { CapturedMessage__DARKLUA_TYPE_d }, messageType: number)\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\nend\n\nlocal module = {}\n\nfunction module.run(callingScript: LuaSourceContainer, config: Config__DARKLUA_TYPE_c): (string, string)\n local t0 = os.clock()\n local timingEnabled = config._timing\n local coverageEnabled = config._coverage\n\n local t_findJest0 = os.clock()\n local findSuccess, findValue = pcall(InstanceResolver.getJest, config)\n local t_findJest = os.clock()\n\n if not findSuccess then\n local logSuccess, logHistory = pcall(function()\n return HttpService:JSONEncode(LogService:GetLogHistory())\n end)\n\n return HttpService:JSONEncode(fail(findValue :: any)), if logSuccess then logHistory else \"[]\"\n end\n\n LogService:ClearOutput()\n\n local snapshotWrites: { [string]: string } = {}\n\n local t_patchSnapshot0 = os.clock()\n local patchState = 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.\n -- Jest writes via process.stdout/stderr (Writeable objects whose _writeFn\n -- defaults to print). Wrapping _writeFn captures messages like\n -- \"No tests found\" that are printed just before exit(1) throws.\n local capturedMessages: { CapturedMessage__DARKLUA_TYPE_d } = {}\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 = interceptWriteable(process.stdout, capturedMessages, 0)\n restoreStderr = interceptWriteable(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 or pass --projects.\"\n )\n\n for _, projectPath in config.projects do\n table.insert(projects, 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, 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, InstanceResolver.findInstance(setupPath))\n end\n\n config.setupFilesAfterEnv = resolved :: any\n end\n local t_resolveSetupFiles = os.clock()\n\n local setupTimingState = 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\n if coverageEnabled then\n _G.__jest_roblox_cov = {}\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 = SetupTiming.getSeconds(setupTimingState)\n SetupTiming.unpatch(setupTimingState)\n\n if not runCLIOk then\n error(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 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 = 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 SnapshotPatch.unpatch(patchState)\n\n local jestResult\n if not runSuccess then\n jestResult = HttpService:JSONEncode(fail(runValue :: any))\n else\n jestResult = HttpService:JSONEncode(runValue)\n end\n\n local logSuccess, logHistory = pcall(function()\n return HttpService:JSONEncode(capturedMessages)\n end)\n\n return jestResult, if logSuccess then logHistory else \"[]\"\nend\n\n\n\n\n\n\n\nlocal function encodeExecutionError(err: any): string\n return HttpService:JSONEncode({\n success = true,\n value = {\n kind = \"ExecutionError\",\n error = tostring(err),\n },\n })\nend\n\n-- TODO(runner-tests): dogfood harness for Runner.runProjects\nfunction module.runProjects(\n callingScript: LuaSourceContainer,\n configs: { Config__DARKLUA_TYPE_c }\n): { ProjectEntry__DARKLUA_TYPE_e }\n local entries: { ProjectEntry__DARKLUA_TYPE_e } = {}\n\n for index, cfg in configs do\n local start = os.clock()\n local ok, jestOutput, gameOutput = pcall(module.run, callingScript, cfg)\n local elapsedMs = math.floor((os.clock() - start) * 1000)\n\n if ok then\n entries[index] = {\n jestOutput = jestOutput :: string,\n gameOutput = gameOutput :: string,\n elapsedMs = elapsedMs,\n }\n else\n entries[index] = {\n jestOutput = encodeExecutionError(jestOutput),\n gameOutput = \"[]\",\n elapsedMs = elapsedMs,\n }\n end\n end\n\n return entries\nend\n\nreturn module\nend function __JEST_MODULES.f():typeof(__modImpl())local v=__JEST_MODULES.cache.f if not v then v={c=__modImpl()}__JEST_MODULES.cache.f=v end return v.c end end end--!strict\n\nlocal HttpService = game:GetService(\"HttpService\")\n\nlocal Runner = __JEST_MODULES.f()\n\nlocal payload = HttpService:JSONDecode([=[__CONFIG_JSON__]=])\nlocal entries = Runner.runProjects(script, payload.configs)\n\nreturn HttpService:JSONEncode({ entries = entries }), \"[]\"\n";
416
425
  //#endregion
417
426
  //#region src/test-script.ts
418
427
  function buildJestArgv(options) {
@@ -428,80 +437,12 @@ function buildJestArgv(options) {
428
437
  };
429
438
  }
430
439
  function generateTestScript(options) {
431
- const config = buildJestArgv(options);
432
- return test_runner_bundled_default.replace("__CONFIG_JSON__", () => JSON.stringify(config));
433
- }
434
- //#endregion
435
- //#region src/utils/cache.ts
436
- const CACHE_DIR_NAME = "jest-roblox";
437
- function getCacheDirectory() {
438
- const xdgCacheHome = process.env["XDG_CACHE_HOME"];
439
- if (xdgCacheHome !== void 0 && xdgCacheHome !== "") return path$1.join(xdgCacheHome, CACHE_DIR_NAME);
440
- if (process.platform === "win32") {
441
- const localAppData = process.env["LOCALAPPDATA"];
442
- if (localAppData !== void 0 && localAppData !== "") return path$1.join(localAppData, CACHE_DIR_NAME);
443
- return path$1.join(tmpdir(), CACHE_DIR_NAME);
444
- }
445
- return path$1.join(homedir(), ".cache", CACHE_DIR_NAME);
446
- }
447
- function getCacheKey(universeId, placeId) {
448
- return `${universeId}:${placeId}`;
449
- }
450
- function isUploaded(cache, key, fileHash) {
451
- return cache[key]?.fileHash === fileHash;
452
- }
453
- function markUploaded(cache, key, fileHash) {
454
- cache[key] = {
455
- fileHash,
456
- uploadedAt: Date.now()
457
- };
458
- }
459
- function readCache(cacheFilePath) {
460
- try {
461
- const data = fs$1.readFileSync(cacheFilePath, "utf-8");
462
- return JSON.parse(data);
463
- } catch {
464
- return {};
465
- }
466
- }
467
- function writeCache(cacheFilePath, cache) {
468
- const cacheDirectory = path$1.dirname(cacheFilePath);
469
- fs$1.mkdirSync(cacheDirectory, { recursive: true });
470
- fs$1.writeFileSync(cacheFilePath, JSON.stringify(cache, null, 2));
471
- }
472
- //#endregion
473
- //#region src/utils/hash.ts
474
- function hashBuffer(data) {
475
- return crypto.createHash("sha256").update(data).digest("hex");
476
- }
477
- //#endregion
478
- //#region src/backends/http-client.ts
479
- function createFetchClient(defaultHeaders) {
480
- return { async request(method, url, options) {
481
- const headers = {
482
- ...defaultHeaders,
483
- ...options?.headers
484
- };
485
- const fetchOptions = {
486
- headers,
487
- method
488
- };
489
- if (options?.body !== void 0) if (options.body instanceof buffer.Buffer) fetchOptions.body = options.body;
490
- else {
491
- fetchOptions.body = JSON.stringify(options.body);
492
- headers["Content-Type"] = "application/json";
493
- }
494
- const response = await fetch(url, fetchOptions);
495
- return {
496
- body: await (response.headers.get("content-type")?.includes("application/json") ?? false ? response.json() : response.text()),
497
- headers: { "retry-after": response.headers.get("retry-after") ?? void 0 },
498
- ok: response.ok,
499
- status: response.status
500
- };
501
- } };
440
+ const configs = (Array.isArray(options) ? options : [options]).map((input) => buildJestArgv(input));
441
+ return test_runner_bundled_default.replace("__CONFIG_JSON__", () => JSON.stringify({ configs }));
502
442
  }
503
443
  //#endregion
504
444
  //#region src/backends/open-cloud.ts
445
+ const PARALLEL_AUTO_CAP = 3;
505
446
  const OPEN_CLOUD_BASE_URL = "https://apis.roblox.com";
506
447
  const RATE_LIMIT_DEFAULT_WAIT_MS = 5e3;
507
448
  const MAX_RATE_LIMIT_RETRIES = 5;
@@ -511,11 +452,17 @@ const taskStatusResponse = type({
511
452
  "output?": { "results?": "string[]" },
512
453
  "state": "'CANCELLED' | 'COMPLETE' | 'FAILED' | 'PROCESSING'"
513
454
  });
455
+ const envelopeSchema$1 = type({ entries: type({
456
+ "elapsedMs?": "number",
457
+ "gameOutput?": "string",
458
+ "jestOutput": "string"
459
+ }).array() });
514
460
  var OpenCloudBackend = class {
515
461
  credentials;
516
462
  http;
517
463
  readFile;
518
464
  sleepFn;
465
+ kind = "open-cloud";
519
466
  constructor(credentials, options) {
520
467
  this.credentials = credentials;
521
468
  this.http = options?.http ?? createFetchClient({ "x-api-key": credentials.apiKey });
@@ -527,7 +474,10 @@ var OpenCloudBackend = class {
527
474
  });
528
475
  }
529
476
  async runTests(options) {
530
- const placeFilePath = path$1.resolve(options.config.rootDir, options.config.placeFile);
477
+ const { jobs, parallel } = options;
478
+ if (jobs.length === 0) throw new Error("OpenCloudBackend requires at least one job");
479
+ const primary = jobs[0];
480
+ const placeFilePath = path$1.resolve(primary.config.rootDir, primary.config.placeFile);
531
481
  const cacheDirectory = getCacheDirectory();
532
482
  const cacheFilePath = path$1.join(cacheDirectory, "upload-cache.json");
533
483
  const uploadStart = Date.now();
@@ -537,30 +487,21 @@ var OpenCloudBackend = class {
537
487
  const cache = readCache(cacheFilePath);
538
488
  const uploadCached = await this.uploadOrReuseCached({
539
489
  cache,
540
- cacheEnabled: options.config.cache,
490
+ cacheEnabled: primary.config.cache,
541
491
  cacheFilePath,
542
492
  cacheKey,
543
493
  fileHash,
544
494
  placeData
545
495
  });
546
496
  const uploadMs = Date.now() - uploadStart;
497
+ const buckets = bucketJobs(jobs, parallel);
547
498
  const executionStart = Date.now();
548
- const taskPath = await this.createExecutionTask(options);
549
- const { gameOutput, jestOutput } = await this.pollForCompletion(taskPath, options.config.timeout, options.config.pollInterval);
499
+ const bucketResults = await Promise.all(buckets.map(async (bucket) => this.runBucket(bucket)));
550
500
  const executionMs = Date.now() - executionStart;
551
- let parsed;
552
- try {
553
- parsed = parseJestOutput(jestOutput);
554
- } catch (err) {
555
- if (err instanceof LuauScriptError) err.gameOutput = gameOutput;
556
- throw err;
557
- }
501
+ const flattened = Array.from({ length: jobs.length });
502
+ for (const { indices, results } of bucketResults) for (const [positionInBucket, originalIndex] of indices.entries()) flattened[originalIndex] = results[positionInBucket];
558
503
  return {
559
- coverageData: parsed.coverageData,
560
- gameOutput,
561
- luauTiming: parsed.luauTiming,
562
- result: parsed.result,
563
- snapshotWrites: parsed.snapshotWrites,
504
+ results: flattened,
564
505
  timing: {
565
506
  executionMs,
566
507
  uploadCached,
@@ -568,12 +509,12 @@ var OpenCloudBackend = class {
568
509
  }
569
510
  };
570
511
  }
571
- async createExecutionTask(options) {
512
+ async createExecutionTask(inputs, timeoutMs) {
572
513
  const url = `${OPEN_CLOUD_BASE_URL}/cloud/v2/universes/${this.credentials.universeId}/places/${this.credentials.placeId}/luau-execution-session-tasks`;
573
- const script = generateTestScript(options);
514
+ const script = generateTestScript(inputs);
574
515
  const response = await this.http.request("POST", url, { body: {
575
516
  script,
576
- timeout: `${Math.floor(options.config.timeout / 1e3)}s`
517
+ timeout: `${Math.floor(timeoutMs / 1e3)}s`
577
518
  } });
578
519
  if (!response.ok) throw new Error(`Failed to create execution task: ${response.status}`);
579
520
  return taskResponse.assert(response.body).path;
@@ -611,6 +552,26 @@ var OpenCloudBackend = class {
611
552
  }
612
553
  throw new Error("Execution timed out");
613
554
  }
555
+ async runBucket(bucket) {
556
+ const { indices, jobs } = bucket;
557
+ const primary = jobs[0];
558
+ const inputs = jobs.map((job) => {
559
+ return {
560
+ config: job.config,
561
+ testFiles: job.testFiles
562
+ };
563
+ });
564
+ const taskPath = await this.createExecutionTask(inputs, primary.config.timeout);
565
+ const { gameOutput, jestOutput } = await this.pollForCompletion(taskPath, primary.config.timeout, primary.config.pollInterval);
566
+ const entries = parseEnvelope(jestOutput);
567
+ if (entries.length !== jobs.length) throw new Error(`Open Cloud backend returned ${entries.length.toString()} entries but bucket had ${jobs.length.toString()} jobs`);
568
+ return {
569
+ indices,
570
+ results: entries.map((entry, index) => {
571
+ return buildProjectResult(entry, jobs[index], gameOutput);
572
+ })
573
+ };
574
+ }
614
575
  async uploadOrReuseCached({ cache, cacheEnabled, cacheFilePath, cacheKey, fileHash, placeData }) {
615
576
  if (cacheEnabled && isUploaded(cache, cacheKey, fileHash)) return true;
616
577
  await this.uploadPlaceData(placeData);
@@ -640,6 +601,55 @@ function createOpenCloudBackend() {
640
601
  universeId
641
602
  });
642
603
  }
604
+ function resolveBucketCount(parallel, jobCount) {
605
+ if (parallel === void 0) return 1;
606
+ if (parallel === "auto") return Math.min(jobCount, PARALLEL_AUTO_CAP);
607
+ if (parallel < 1) throw new Error(`--parallel must be >= 1, got ${parallel.toString()}`);
608
+ return Math.min(Math.floor(parallel), jobCount);
609
+ }
610
+ function bucketJobs(jobs, parallel) {
611
+ const bucketCount = resolveBucketCount(parallel, jobs.length);
612
+ const buckets = [];
613
+ for (let index = 0; index < bucketCount; index++) buckets.push({
614
+ indices: [],
615
+ jobs: []
616
+ });
617
+ for (const [originalIndex, job] of jobs.entries()) {
618
+ const bucket = buckets[originalIndex % bucketCount];
619
+ bucket.indices.push(originalIndex);
620
+ bucket.jobs.push(job);
621
+ }
622
+ return buckets;
623
+ }
624
+ function parseEnvelope(jestOutput) {
625
+ const envelope = envelopeSchema$1(JSON.parse(jestOutput));
626
+ if (envelope instanceof type.errors) return [{
627
+ elapsedMs: 0,
628
+ jestOutput
629
+ }];
630
+ return envelope.entries;
631
+ }
632
+ function buildProjectResult(entry, job, fallbackGameOutput) {
633
+ const gameOutput = entry.gameOutput ?? fallbackGameOutput;
634
+ let parsed;
635
+ try {
636
+ parsed = parseJestOutput(entry.jestOutput);
637
+ } catch (err) {
638
+ if (err instanceof LuauScriptError) err.gameOutput = gameOutput;
639
+ throw err;
640
+ }
641
+ return {
642
+ coverageData: parsed.coverageData,
643
+ displayColor: job.displayColor,
644
+ displayName: job.displayName,
645
+ elapsedMs: entry.elapsedMs ?? 0,
646
+ gameOutput,
647
+ luauTiming: parsed.luauTiming,
648
+ result: parsed.result,
649
+ setupMs: parsed.setupSeconds !== void 0 ? Math.round(parsed.setupSeconds * 1e3) : void 0,
650
+ snapshotWrites: parsed.snapshotWrites
651
+ };
652
+ }
643
653
  function parseRetryAfter(headers) {
644
654
  const value = headers?.["retry-after"];
645
655
  if (value === void 0) return RATE_LIMIT_DEFAULT_WAIT_MS;
@@ -650,6 +660,11 @@ function parseRetryAfter(headers) {
650
660
  //#endregion
651
661
  //#region src/backends/studio.ts
652
662
  const DEFAULT_STUDIO_TIMEOUT = 3e5;
663
+ const envelopeSchema = type({ entries: type({
664
+ "elapsedMs?": "number",
665
+ "gameOutput?": "string",
666
+ "jestOutput": "string"
667
+ }).array() });
653
668
  const pluginMessageSchema = type({
654
669
  "gameOutput?": "string",
655
670
  "jestOutput": "string",
@@ -661,45 +676,72 @@ var StudioBackend = class {
661
676
  port;
662
677
  timeout;
663
678
  preConnected;
679
+ wss;
680
+ kind = "studio";
664
681
  constructor(options) {
665
682
  this.port = options.port;
666
683
  this.timeout = options.timeout ?? DEFAULT_STUDIO_TIMEOUT;
667
684
  this.createServer = options.createServer ?? ((port) => new WebSocketServer({ port }));
668
685
  this.preConnected = options.preConnected;
669
686
  }
687
+ close() {
688
+ const server = this.wss;
689
+ this.wss = void 0;
690
+ if (server === void 0) return;
691
+ server.close();
692
+ }
670
693
  async runTests(options) {
671
694
  const pre = this.preConnected;
672
695
  this.preConnected = void 0;
673
- const wss = pre?.server ?? this.createServer(this.port);
674
- try {
675
- return await this.executeViaPlugin(wss, options, pre?.socket);
676
- } finally {
677
- wss.close();
678
- }
696
+ this.wss ??= pre?.server ?? this.createServer(this.port);
697
+ return this.executeViaPlugin(this.wss, options.jobs, pre?.socket);
679
698
  }
680
- async executeViaPlugin(wss, options, existingSocket) {
681
- const requestId = randomUUID();
682
- const config = buildJestArgv(options);
683
- const executionStart = Date.now();
684
- const message = await this.waitForResult(wss, requestId, config, existingSocket);
685
- const executionMs = Date.now() - executionStart;
699
+ buildProjectResult(entry, job, fallbackGameOutput) {
700
+ const gameOutput = entry.gameOutput ?? fallbackGameOutput;
686
701
  let parsed;
687
702
  try {
688
- parsed = parseJestOutput(message.jestOutput);
703
+ parsed = parseJestOutput(entry.jestOutput);
689
704
  } catch (err) {
690
- if (err instanceof LuauScriptError) err.gameOutput = message.gameOutput;
705
+ if (err instanceof LuauScriptError) err.gameOutput = gameOutput;
691
706
  throw err;
692
707
  }
693
708
  return {
694
709
  coverageData: parsed.coverageData,
695
- gameOutput: message.gameOutput,
710
+ displayColor: job.displayColor,
711
+ displayName: job.displayName,
712
+ elapsedMs: entry.elapsedMs ?? 0,
713
+ gameOutput,
696
714
  luauTiming: parsed.luauTiming,
697
715
  result: parsed.result,
698
- snapshotWrites: parsed.snapshotWrites,
716
+ setupMs: parsed.setupSeconds !== void 0 ? Math.round(parsed.setupSeconds * 1e3) : void 0,
717
+ snapshotWrites: parsed.snapshotWrites
718
+ };
719
+ }
720
+ async executeViaPlugin(wss, jobs, existingSocket) {
721
+ const requestId = randomUUID();
722
+ const configs = jobs.map((job) => buildJestArgv(job));
723
+ const executionStart = Date.now();
724
+ const message = await this.waitForResult(wss, requestId, configs, existingSocket);
725
+ const executionMs = Date.now() - executionStart;
726
+ const entries = this.parseEnvelope(message.jestOutput);
727
+ if (entries.length !== jobs.length) throw new Error(`Studio backend returned ${entries.length.toString()} entries but request had ${jobs.length.toString()} jobs`);
728
+ return {
729
+ results: entries.map((entry, index) => {
730
+ const matched = jobs[index];
731
+ return this.buildProjectResult(entry, matched, message.gameOutput);
732
+ }),
699
733
  timing: { executionMs }
700
734
  };
701
735
  }
702
- async waitForResult(wss, requestId, config, existingSocket) {
736
+ parseEnvelope(jestOutput) {
737
+ const envelope = envelopeSchema(JSON.parse(jestOutput));
738
+ if (envelope instanceof type.errors) return [{
739
+ elapsedMs: 0,
740
+ jestOutput
741
+ }];
742
+ return envelope.entries;
743
+ }
744
+ async waitForResult(wss, requestId, configs, existingSocket) {
703
745
  return new Promise((resolve, reject) => {
704
746
  const timer = setTimeout(() => {
705
747
  reject(/* @__PURE__ */ new Error("Timed out waiting for Studio plugin connection"));
@@ -707,7 +749,7 @@ var StudioBackend = class {
707
749
  function attachSocket(ws) {
708
750
  ws.send(JSON.stringify({
709
751
  action: "run_tests",
710
- config,
752
+ config: { configs },
711
753
  request_id: requestId
712
754
  }));
713
755
  ws.on("message", (data) => {
@@ -1058,6 +1100,48 @@ function createSourceMapper(config) {
1058
1100
  }
1059
1101
  };
1060
1102
  }
1103
+ /**
1104
+ * Compose multiple `SourceMapper`s into one that tries every child in order.
1105
+ * Used by the multi-project CLI path so that failure messages and GitHub
1106
+ * annotations can resolve frames from any project's TS/Luau mapping.
1107
+ *
1108
+ * Each child mapper only rewrites frames it can resolve, leaving the rest
1109
+ * untouched. Chaining `mapFailureMessage` / `mapFailureWithLocations` calls
1110
+ * through every child is therefore safe: later mappers see the partially
1111
+ * rewritten string and still parse any remaining `[string "..."]` frames.
1112
+ * Locations accumulate across mappers; `resolveTestFilePath` returns the
1113
+ * first child's hit.
1114
+ */
1115
+ function combineSourceMappers(mappers) {
1116
+ if (mappers.length === 0) return;
1117
+ if (mappers.length === 1) return mappers[0];
1118
+ return {
1119
+ mapFailureMessage(message) {
1120
+ let result = message;
1121
+ for (const mapper of mappers) result = mapper.mapFailureMessage(result);
1122
+ return result;
1123
+ },
1124
+ mapFailureWithLocations(message) {
1125
+ let mappedMessage = message;
1126
+ const locations = [];
1127
+ for (const mapper of mappers) {
1128
+ const partial = mapper.mapFailureWithLocations(mappedMessage);
1129
+ mappedMessage = partial.message;
1130
+ locations.push(...partial.locations);
1131
+ }
1132
+ return {
1133
+ locations,
1134
+ message: mappedMessage
1135
+ };
1136
+ },
1137
+ resolveTestFilePath(testFilePath) {
1138
+ for (const mapper of mappers) {
1139
+ const resolved = mapper.resolveTestFilePath(testFilePath);
1140
+ if (resolved !== void 0) return resolved;
1141
+ }
1142
+ }
1143
+ };
1144
+ }
1061
1145
  function getSourceSnippet({ column, context = 2, filePath, line, sourceContent }) {
1062
1146
  const content = sourceContent ?? (fs$1.existsSync(filePath) ? fs$1.readFileSync(filePath, "utf-8") : void 0);
1063
1147
  if (content === void 0) return;
@@ -1410,13 +1494,16 @@ function formatTestSummary(result, timing, styles, options) {
1410
1494
  }
1411
1495
  const startAtStr = new Date(timing.startTime).toLocaleTimeString("en-GB", { hour12: false });
1412
1496
  lines.push(`${st.dim(" Start at")} ${startAtStr}`);
1413
- const environmentMs = timing.executionMs - timing.testsMs;
1497
+ const setupMs = timing.setupMs ?? 0;
1498
+ const environmentMs = Math.max(0, timing.executionMs - timing.testsMs - setupMs);
1414
1499
  const uploadMs = timing.uploadMs ?? 0;
1415
1500
  const coverageMs = timing.coverageMs ?? 0;
1416
1501
  const cliMs = Math.max(0, timing.totalMs - uploadMs - timing.executionMs - coverageMs);
1417
1502
  const breakdownParts = [];
1418
1503
  if (timing.uploadMs !== void 0) breakdownParts.push(timing.uploadCached === true ? `upload ${timing.uploadMs}ms (cached)` : `upload ${timing.uploadMs}ms`);
1419
- breakdownParts.push(`environment ${environmentMs}ms`, `tests ${timing.testsMs}ms`, `cli ${cliMs}ms`);
1504
+ breakdownParts.push(`environment ${environmentMs}ms`);
1505
+ if (setupMs > 0) breakdownParts.push(`setup ${setupMs}ms`);
1506
+ breakdownParts.push(`tests ${timing.testsMs}ms`, `cli ${cliMs}ms`);
1420
1507
  if (coverageMs > 0) breakdownParts.push(`coverage ${coverageMs}ms`);
1421
1508
  const breakdown = st.dim(`(${breakdownParts.join(", ")})`);
1422
1509
  lines.push(`${st.dim(" Duration")} ${timing.totalMs}ms ${breakdown}`);
@@ -2359,52 +2446,6 @@ const rojoProjectSchema = type({
2359
2446
  "tree": "object"
2360
2447
  }).as();
2361
2448
  //#endregion
2362
- //#region src/utils/rojo-tree.ts
2363
- function resolveNestedProjects(tree, rootDirectory) {
2364
- return resolveTree(tree, rootDirectory, rootDirectory, /* @__PURE__ */ new Set());
2365
- }
2366
- function collectPaths(node, result) {
2367
- for (const [key, value] of Object.entries(node)) if (key === "$path" && typeof value === "string") result.push(value.replaceAll("\\", "/"));
2368
- else if (typeof value === "object" && !Array.isArray(value) && !key.startsWith("$")) collectPaths(value, result);
2369
- }
2370
- function inlineNestedProject(projectPath, currentDirectory, originalRoot, visited) {
2371
- const chain = new Set(visited);
2372
- chain.add(projectPath);
2373
- let content;
2374
- try {
2375
- content = readFileSync(projectPath, "utf-8");
2376
- } catch (err) {
2377
- const relativePath = relative(currentDirectory, projectPath);
2378
- throw new Error(`Could not read nested Rojo project: ${relativePath}`, { cause: err });
2379
- }
2380
- return resolveTree(JSON.parse(content).tree, dirname(projectPath), originalRoot, chain);
2381
- }
2382
- function resolveRootRelativePath(currentDirectory, value, originalRoot) {
2383
- return relative(originalRoot, join(currentDirectory, value)).replaceAll("\\", "/");
2384
- }
2385
- function resolveTree(node, currentDirectory, originalRoot, visited) {
2386
- const resolved = {};
2387
- for (const [key, value] of Object.entries(node)) {
2388
- if (key === "$path" && typeof value === "string" && value.endsWith(".project.json")) {
2389
- const projectPath = join(currentDirectory, value);
2390
- if (visited.has(projectPath)) throw new Error(`Circular project reference: ${value}`);
2391
- const innerTree = inlineNestedProject(projectPath, currentDirectory, originalRoot, visited);
2392
- for (const [innerKey, innerValue] of Object.entries(innerTree)) resolved[innerKey] = innerValue;
2393
- continue;
2394
- }
2395
- if (key === "$path" && typeof value === "string") {
2396
- resolved[key] = resolveRootRelativePath(currentDirectory, value, originalRoot);
2397
- continue;
2398
- }
2399
- if (key.startsWith("$") || typeof value !== "object" || Array.isArray(value)) {
2400
- resolved[key] = value;
2401
- continue;
2402
- }
2403
- resolved[key] = resolveTree(value, currentDirectory, originalRoot, visited);
2404
- }
2405
- return resolved;
2406
- }
2407
- //#endregion
2408
2449
  //#region src/executor.ts
2409
2450
  function isLuauProject(testFiles, tsconfigMappings) {
2410
2451
  if (tsconfigMappings.length > 0) return false;
@@ -2477,15 +2518,40 @@ function formatExecuteOutput(options) {
2477
2518
  version
2478
2519
  });
2479
2520
  }
2480
- async function execute(options) {
2481
- const startTime = Date.now();
2482
- const tsconfigMappings = resolveAllTsconfigMappings(options.config.rootDir);
2483
- const luauProject = isLuauProject(options.testFiles, tsconfigMappings);
2484
- const config = applySnapshotFormatDefaults(options.config, luauProject);
2485
- const { coverageData, gameOutput, luauTiming, result, snapshotWrites, timing: backendTiming } = await options.backend.runTests({
2486
- config,
2487
- testFiles: options.testFiles
2521
+ /**
2522
+ * Build a `ProjectJob` with `snapshotFormat` resolved per-project. Each job
2523
+ * carries its own config so the Luau runner never re-resolves or shares format
2524
+ * state across projects (fixes the spike's snapshot-diff regression — C1).
2525
+ */
2526
+ function buildProjectJob(parameters) {
2527
+ const tsconfigMappings = resolveAllTsconfigMappings(parameters.config.rootDir);
2528
+ const luauProject = isLuauProject(parameters.testFiles, tsconfigMappings);
2529
+ return {
2530
+ config: applySnapshotFormatDefaults(parameters.config, luauProject),
2531
+ displayColor: parameters.displayColor,
2532
+ displayName: parameters.displayName ?? "",
2533
+ testFiles: parameters.testFiles
2534
+ };
2535
+ }
2536
+ /**
2537
+ * Thin wrapper over `backend.runTests`. Fires exactly once per CLI invocation
2538
+ * with a full `ProjectJob[]` envelope. Returns the raw `BackendResult`.
2539
+ */
2540
+ async function executeBackend(backend, jobs, parallel) {
2541
+ return backend.runTests({
2542
+ jobs,
2543
+ parallel
2488
2544
  });
2545
+ }
2546
+ /**
2547
+ * Process a single `ProjectBackendResult` into an `ExecuteResult`: writes
2548
+ * snapshots, builds the source mapper, resolves test-file paths, and renders
2549
+ * formatter output. Called once per job.
2550
+ */
2551
+ function processProjectResult(entry, options) {
2552
+ const { backendTiming, config, deferFormatting, startTime, version } = options;
2553
+ const { coverageData, gameOutput, luauTiming, result, setupMs, snapshotWrites } = entry;
2554
+ const tsconfigMappings = resolveAllTsconfigMappings(config.rootDir);
2489
2555
  if (snapshotWrites !== void 0) writeSnapshots(snapshotWrites, config, tsconfigMappings);
2490
2556
  const testsMs = calculateTestsMs(result.testResults);
2491
2557
  const sourceMapper = config.sourceMap ? buildSourceMapper(config, tsconfigMappings) : void 0;
@@ -2493,18 +2559,19 @@ async function execute(options) {
2493
2559
  const totalMs = Date.now() - startTime;
2494
2560
  const timing = {
2495
2561
  executionMs: backendTiming.executionMs,
2562
+ setupMs,
2496
2563
  startTime,
2497
2564
  testsMs,
2498
2565
  totalMs,
2499
2566
  uploadCached: backendTiming.uploadCached,
2500
2567
  uploadMs: backendTiming.uploadMs
2501
2568
  };
2502
- const output = options.deferFormatting !== true ? formatExecuteOutput({
2569
+ const output = deferFormatting !== true ? formatExecuteOutput({
2503
2570
  config,
2504
2571
  result,
2505
2572
  sourceMapper,
2506
2573
  timing,
2507
- version: options.version
2574
+ version
2508
2575
  }) : "";
2509
2576
  if (luauTiming !== void 0) printLuauTiming(luauTiming);
2510
2577
  return {
@@ -2517,6 +2584,28 @@ async function execute(options) {
2517
2584
  timing
2518
2585
  };
2519
2586
  }
2587
+ /**
2588
+ * Single-project convenience wrapper: builds a length-1 jobs array, fires
2589
+ * `executeBackend` once, and maps the single entry through
2590
+ * `processProjectResult`. Multi-project callers drive `executeBackend` +
2591
+ * `processProjectResult` directly from `cli.ts`.
2592
+ */
2593
+ async function execute(options) {
2594
+ const startTime = Date.now();
2595
+ const job = buildProjectJob({
2596
+ config: options.config,
2597
+ testFiles: options.testFiles
2598
+ });
2599
+ const { results, timing: backendTiming } = await executeBackend(options.backend, [job]);
2600
+ const first = results[0];
2601
+ return processProjectResult(first, {
2602
+ backendTiming,
2603
+ config: job.config,
2604
+ deferFormatting: options.deferFormatting,
2605
+ startTime,
2606
+ version: options.version
2607
+ });
2608
+ }
2520
2609
  function normalizeDirectoryPath(directory) {
2521
2610
  return path$1.normalize(directory).replaceAll("\\", "/");
2522
2611
  }
@@ -2672,6 +2761,276 @@ function writeSnapshots(snapshotWrites, config, tsconfigMappings) {
2672
2761
  }
2673
2762
  }
2674
2763
  //#endregion
2764
+ //#region packages/luau-ast/dist/index.mjs
2765
+ function visitExpression(expression, visitor) {
2766
+ if (visitor.visitExpr?.(expression) === false) return;
2767
+ const { tag } = expression;
2768
+ switch (tag) {
2769
+ case "binary":
2770
+ visitExprBinary(expression, visitor);
2771
+ break;
2772
+ case "boolean":
2773
+ visitor.visitExprConstantBool?.(expression);
2774
+ break;
2775
+ case "call":
2776
+ visitExprCall(expression, visitor);
2777
+ break;
2778
+ case "cast":
2779
+ visitExprTypeAssertion(expression, visitor);
2780
+ break;
2781
+ case "conditional":
2782
+ visitExprIfElse(expression, visitor);
2783
+ break;
2784
+ case "function":
2785
+ visitExprFunction(expression, visitor);
2786
+ break;
2787
+ case "global":
2788
+ visitor.visitExprGlobal?.(expression);
2789
+ break;
2790
+ case "group":
2791
+ visitExprGroup(expression, visitor);
2792
+ break;
2793
+ case "index":
2794
+ visitExprIndexExpr(expression, visitor);
2795
+ break;
2796
+ case "indexname":
2797
+ visitExprIndexName(expression, visitor);
2798
+ break;
2799
+ case "instantiate":
2800
+ visitExprInstantiate(expression, visitor);
2801
+ break;
2802
+ case "interpolatedstring":
2803
+ visitExprInterpString(expression, visitor);
2804
+ break;
2805
+ case "local":
2806
+ visitor.visitExprLocal?.(expression);
2807
+ break;
2808
+ case "nil":
2809
+ visitor.visitExprConstantNil?.(expression);
2810
+ break;
2811
+ case "number":
2812
+ visitor.visitExprConstantNumber?.(expression);
2813
+ break;
2814
+ case "string":
2815
+ visitor.visitExprConstantString?.(expression);
2816
+ break;
2817
+ case "table":
2818
+ visitExprTable(expression, visitor);
2819
+ break;
2820
+ case "unary":
2821
+ visitExprUnary(expression, visitor);
2822
+ break;
2823
+ case "vararg":
2824
+ visitor.visitExprVarargs?.(expression);
2825
+ break;
2826
+ default: break;
2827
+ }
2828
+ visitor.visitExprEnd?.(expression);
2829
+ }
2830
+ function visitStatement(statement, visitor) {
2831
+ const { tag } = statement;
2832
+ switch (tag) {
2833
+ case "assign":
2834
+ visitStatAssign(statement, visitor);
2835
+ break;
2836
+ case "block":
2837
+ visitStatBlock(statement, visitor);
2838
+ break;
2839
+ case "break":
2840
+ visitor.visitStatBreak?.(statement);
2841
+ break;
2842
+ case "compoundassign":
2843
+ visitStatCompoundAssign(statement, visitor);
2844
+ break;
2845
+ case "conditional":
2846
+ visitStatIf(statement, visitor);
2847
+ break;
2848
+ case "continue":
2849
+ visitor.visitStatContinue?.(statement);
2850
+ break;
2851
+ case "do":
2852
+ visitStatDo(statement, visitor);
2853
+ break;
2854
+ case "expression":
2855
+ visitStatExpr(statement, visitor);
2856
+ break;
2857
+ case "for":
2858
+ visitStatFor(statement, visitor);
2859
+ break;
2860
+ case "forin":
2861
+ visitStatForIn(statement, visitor);
2862
+ break;
2863
+ case "function":
2864
+ visitStatFunction(statement, visitor);
2865
+ break;
2866
+ case "local":
2867
+ visitStatLocal(statement, visitor);
2868
+ break;
2869
+ case "localfunction":
2870
+ visitStatLocalFunction(statement, visitor);
2871
+ break;
2872
+ case "repeat":
2873
+ visitStatRepeat(statement, visitor);
2874
+ break;
2875
+ case "return":
2876
+ visitStatReturn(statement, visitor);
2877
+ break;
2878
+ case "typealias":
2879
+ visitor.visitStatTypeAlias?.(statement);
2880
+ break;
2881
+ case "typefunction":
2882
+ visitor.visitStatTypeFunction?.(statement);
2883
+ break;
2884
+ case "while":
2885
+ visitStatWhile(statement, visitor);
2886
+ break;
2887
+ default: break;
2888
+ }
2889
+ }
2890
+ function visitBlock(block, visitor) {
2891
+ visitStatBlock(block, visitor);
2892
+ }
2893
+ function visitPunctuated(list, visitor, apply) {
2894
+ for (const item of list) apply(item.node, visitor);
2895
+ }
2896
+ function visitStatBlock(block, visitor) {
2897
+ if (visitor.visitStatBlock?.(block) === false) return;
2898
+ for (const statement of block.statements) visitStatement(statement, visitor);
2899
+ visitor.visitStatBlockEnd?.(block);
2900
+ }
2901
+ function visitStatDo(node, visitor) {
2902
+ if (visitor.visitStatDo?.(node) === false) return;
2903
+ visitStatBlock(node.body, visitor);
2904
+ }
2905
+ function visitStatIf(node, visitor) {
2906
+ if (visitor.visitStatIf?.(node) === false) return;
2907
+ visitExpression(node.condition, visitor);
2908
+ visitStatBlock(node.thenBlock, visitor);
2909
+ for (const elseif of node.elseifs) visitElseIfStat(elseif, visitor);
2910
+ if (node.elseBlock) visitStatBlock(node.elseBlock, visitor);
2911
+ }
2912
+ function visitElseIfStat(node, visitor) {
2913
+ visitExpression(node.condition, visitor);
2914
+ visitStatBlock(node.thenBlock, visitor);
2915
+ }
2916
+ function visitStatWhile(node, visitor) {
2917
+ if (visitor.visitStatWhile?.(node) === false) return;
2918
+ visitExpression(node.condition, visitor);
2919
+ visitStatBlock(node.body, visitor);
2920
+ }
2921
+ function visitStatRepeat(node, visitor) {
2922
+ if (visitor.visitStatRepeat?.(node) === false) return;
2923
+ visitStatBlock(node.body, visitor);
2924
+ visitExpression(node.condition, visitor);
2925
+ }
2926
+ function visitStatReturn(node, visitor) {
2927
+ if (visitor.visitStatReturn?.(node) === false) return;
2928
+ visitPunctuated(node.expressions, visitor, visitExpression);
2929
+ }
2930
+ function visitStatLocal(node, visitor) {
2931
+ if (visitor.visitStatLocal?.(node) === false) return;
2932
+ visitPunctuated(node.values, visitor, visitExpression);
2933
+ }
2934
+ function visitStatFor(node, visitor) {
2935
+ if (visitor.visitStatFor?.(node) === false) return;
2936
+ visitExpression(node.from, visitor);
2937
+ visitExpression(node.to, visitor);
2938
+ if (node.step) visitExpression(node.step, visitor);
2939
+ visitStatBlock(node.body, visitor);
2940
+ }
2941
+ function visitStatForIn(node, visitor) {
2942
+ if (visitor.visitStatForIn?.(node) === false) return;
2943
+ visitPunctuated(node.values, visitor, visitExpression);
2944
+ visitStatBlock(node.body, visitor);
2945
+ }
2946
+ function visitStatAssign(node, visitor) {
2947
+ if (visitor.visitStatAssign?.(node) === false) return;
2948
+ visitPunctuated(node.variables, visitor, visitExpression);
2949
+ visitPunctuated(node.values, visitor, visitExpression);
2950
+ }
2951
+ function visitStatCompoundAssign(node, visitor) {
2952
+ if (visitor.visitStatCompoundAssign?.(node) === false) return;
2953
+ visitExpression(node.variable, visitor);
2954
+ visitExpression(node.value, visitor);
2955
+ }
2956
+ function visitStatExpr(node, visitor) {
2957
+ if (visitor.visitStatExpr?.(node) === false) return;
2958
+ visitExpression(node.expression, visitor);
2959
+ }
2960
+ function visitStatFunction(node, visitor) {
2961
+ if (visitor.visitStatFunction?.(node) === false) return;
2962
+ visitExpression(node.name, visitor);
2963
+ visitExprFunction(node.func, visitor);
2964
+ }
2965
+ function visitStatLocalFunction(node, visitor) {
2966
+ if (visitor.visitStatLocalFunction?.(node) === false) return;
2967
+ visitExprFunction(node.func, visitor);
2968
+ }
2969
+ function visitExprFunction(node, visitor) {
2970
+ if (visitor.visitExprFunction?.(node) === false) return;
2971
+ visitStatBlock(node.body, visitor);
2972
+ visitor.visitExprFunctionEnd?.(node);
2973
+ }
2974
+ function visitExprCall(node, visitor) {
2975
+ if (visitor.visitExprCall?.(node) === false) return;
2976
+ visitExpression(node.func, visitor);
2977
+ visitPunctuated(node.arguments, visitor, visitExpression);
2978
+ }
2979
+ function visitExprUnary(node, visitor) {
2980
+ if (visitor.visitExprUnary?.(node) === false) return;
2981
+ visitExpression(node.operand, visitor);
2982
+ }
2983
+ function visitExprBinary(node, visitor) {
2984
+ if (visitor.visitExprBinary?.(node) === false) return;
2985
+ visitExpression(node.lhsOperand, visitor);
2986
+ visitExpression(node.rhsOperand, visitor);
2987
+ }
2988
+ function visitExprTable(node, visitor) {
2989
+ if (visitor.visitExprTable?.(node) === false) return;
2990
+ for (const item of node.entries) visitTableExprItem(item, visitor);
2991
+ }
2992
+ function visitTableExprItem(node, visitor) {
2993
+ if (visitor.visitTableExprItem?.(node) === false) return;
2994
+ visitExpression(node.value, visitor);
2995
+ if (node.kind === "general") visitExpression(node.key, visitor);
2996
+ }
2997
+ function visitExprIndexName(node, visitor) {
2998
+ if (visitor.visitExprIndexName?.(node) === false) return;
2999
+ visitExpression(node.expression, visitor);
3000
+ }
3001
+ function visitExprIndexExpr(node, visitor) {
3002
+ if (visitor.visitExprIndexExpr?.(node) === false) return;
3003
+ visitExpression(node.expression, visitor);
3004
+ visitExpression(node.index, visitor);
3005
+ }
3006
+ function visitExprGroup(node, visitor) {
3007
+ if (visitor.visitExprGroup?.(node) === false) return;
3008
+ visitExpression(node.expression, visitor);
3009
+ }
3010
+ function visitExprInterpString(node, visitor) {
3011
+ if (visitor.visitExprInterpString?.(node) === false) return;
3012
+ for (const expr of node.expressions) visitExpression(expr, visitor);
3013
+ }
3014
+ function visitExprTypeAssertion(node, visitor) {
3015
+ if (visitor.visitExprTypeAssertion?.(node) === false) return;
3016
+ visitExpression(node.operand, visitor);
3017
+ }
3018
+ function visitExprIfElse(node, visitor) {
3019
+ if (visitor.visitExprIfElse?.(node) === false) return;
3020
+ visitExpression(node.condition, visitor);
3021
+ visitExpression(node.thenExpr, visitor);
3022
+ for (const elseif of node.elseifs) visitElseIfExpr(elseif, visitor);
3023
+ visitExpression(node.elseExpr, visitor);
3024
+ }
3025
+ function visitElseIfExpr(node, visitor) {
3026
+ visitExpression(node.condition, visitor);
3027
+ visitExpression(node.thenExpr, visitor);
3028
+ }
3029
+ function visitExprInstantiate(node, visitor) {
3030
+ if (visitor.visitExprInstantiate?.(node) === false) return;
3031
+ visitExpression(node.expr, visitor);
3032
+ }
3033
+ //#endregion
2675
3034
  //#region src/formatters/github-actions.ts
2676
3035
  const SEPARATOR = " · ";
2677
3036
  function escapeData(value) {
@@ -3097,4 +3456,4 @@ function writeGameOutput(filePath, entries) {
3097
3456
  fs$1.writeFileSync(absolutePath, JSON.stringify(entries, null, 2));
3098
3457
  }
3099
3458
  //#endregion
3100
- export { createOpenCloudBackend as A, LuauScriptError as B, formatTypecheckSummary as C, StudioBackend as D, resolveConfig as E, ROOT_ONLY_KEYS as F, parseJestOutput as H, VALID_BACKENDS as I, defineConfig as L, buildJestArgv as M, generateTestScript as N, createStudioBackend as O, DEFAULT_CONFIG as P, defineProject as R, formatTestSummary as S, loadConfig$1 as T, extractJsonFromOutput as V, writeJsonFile as _, formatAnnotations as a, formatMultiProjectResult as b, execute as c, resolveTsconfigDirectories as d, collectPaths as f, formatJson as g, findFormatterOptions as h, runTypecheck as i, hashBuffer as j, OpenCloudBackend as k, formatExecuteOutput as l, rojoProjectSchema as m, parseGameOutput as n, formatJobSummary as o, resolveNestedProjects as p, writeGameOutput as r, resolveGitHubActionsOptions as s, formatGameOutputNotice as t, loadCoverageManifest as u, formatAgentMultiProject as v, formatBanner as w, formatResult as x, formatFailure as y, isValidBackend as z };
3459
+ export { formatBanner as A, DEFAULT_CONFIG as B, writeJsonFile as C, formatResult as D, formatMultiProjectResult as E, createStudioBackend as F, isValidBackend as G, VALID_BACKENDS as H, OpenCloudBackend as I, parseJestOutput as J, LuauScriptError as K, createOpenCloudBackend as L, loadConfig$1 as M, resolveConfig as N, formatTestSummary as O, StudioBackend as P, buildJestArgv as R, formatJson as S, formatFailure as T, defineConfig as U, ROOT_ONLY_KEYS as V, defineProject as W, resolveTsconfigDirectories as _, formatAnnotations as a, rojoProjectSchema as b, visitBlock as c, buildProjectJob as d, execute as f, processProjectResult as g, loadCoverageManifest as h, runTypecheck as i, combineSourceMappers as j, formatTypecheckSummary as k, visitExpression as l, formatExecuteOutput as m, parseGameOutput as n, formatJobSummary as o, executeBackend as p, extractJsonFromOutput as q, writeGameOutput as r, resolveGitHubActionsOptions as s, formatGameOutputNotice as t, visitStatement as u, collectPaths$1 as v, formatAgentMultiProject as w, findFormatterOptions as x, resolveNestedProjects as y, generateTestScript as z };