@isentinel/jest-roblox 0.0.3 → 0.0.4

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.
@@ -10,9 +10,13 @@ import * as crypto from "node:crypto";
10
10
  import { randomUUID } from "node:crypto";
11
11
  import buffer from "node:buffer";
12
12
  import { createJiti } from "jiti";
13
+ import { getTsconfig } from "get-tsconfig";
14
+ import { TraceMap, originalPositionFor, sourceContentFor } from "@jridgewell/trace-mapping";
13
15
  import color from "tinyrainbow";
14
16
  import hljs from "highlight.js/lib/core";
15
17
  import typescript from "highlight.js/lib/languages/typescript";
18
+ import { execSync } from "node:child_process";
19
+ import { Visitor, parseSync } from "oxc-parser";
16
20
 
17
21
  //#region src/reporter/parser.ts
18
22
  const TASK_SCRIPT_PREFIX = /^TaskScript:\d+:\s*/;
@@ -86,6 +90,14 @@ function countBraces(line) {
86
90
  }
87
91
  return count;
88
92
  }
93
+ function isValidJson(text) {
94
+ try {
95
+ JSON.parse(text);
96
+ return true;
97
+ } catch {
98
+ return false;
99
+ }
100
+ }
89
101
  function extractExecutionError(object) {
90
102
  let current = object;
91
103
  while (current["parent"] !== void 0 && typeof current["parent"] === "object") current = current["parent"];
@@ -105,16 +117,13 @@ function extractSnapshotWrites(parsed) {
105
117
  for (const [key, value] of Object.entries(writes)) if (typeof value === "string") record[key] = value;
106
118
  return Object.keys(record).length > 0 ? record : void 0;
107
119
  }
108
- function isValidJson(text) {
109
- try {
110
- JSON.parse(text);
111
- return true;
112
- } catch {
113
- return false;
114
- }
120
+ function stringifyError(err) {
121
+ if (typeof err === "string") return err;
122
+ if (typeof err === "object" && err !== null && "message" in err && typeof err.message === "string") return err.message;
123
+ return JSON.stringify(err) ?? String(err);
115
124
  }
116
125
  function unwrapResult(parsed) {
117
- if ("err" in parsed && parsed["success"] === false) throw new LuauScriptError(String(parsed["err"]));
126
+ if ("err" in parsed && parsed["success"] === false) throw new LuauScriptError(stringifyError(parsed["err"]));
118
127
  if ("value" in parsed && parsed["success"] === true) return parsed["value"];
119
128
  return parsed;
120
129
  }
@@ -125,8 +134,8 @@ function validateJestResult(value) {
125
134
  }
126
135
 
127
136
  //#endregion
128
- //#region src/test-runner.luau
129
- var test_runner_default = "local HttpService = game:GetService(\"HttpService\")\nlocal LogService = game:GetService(\"LogService\")\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\n\nlocal t0 = os.clock()\nlocal baseConfig = HttpService:JSONDecode([[__CONFIG_JSON__]])\nlocal t_configDecode = os.clock()\n\ntype Config = {\n jestPath: string?,\n projects: { string }?,\n setupFiles: { string }?,\n setupFilesAfterEnv: { string }?,\n _timing: boolean?,\n}\n\nlocal function fail(err: string)\n return {\n success = false,\n err = err,\n }\nend\n\nlocal config = table.clone(baseConfig) :: Config\nlocal timingEnabled = config._timing\n\nlocal function findInstance(path: string, class: string?): Instance\n local parts = string.split(path, \"/\")\n\n local result, current: Instance? = pcall(function()\n return game:FindService(parts[1])\n end)\n assert(result, `Failed to find service {parts[1]}: {current}`)\n\n for i = 2, #parts do\n assert(current, `Failed to find instance at {parts[i]}`)\n current = current:FindFirstChild(parts[i])\n end\n\n assert(current, `Failed to find instance at path {path}: {current}`)\n\n return current\nend\n\nlocal function getJest(): ModuleScript\n local jestPath = config.jestPath\n if jestPath then\n local instance = 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\n end\n\n local jestInstance = ReplicatedStorage:FindFirstChild(\"Jest\", true)\n assert(jestInstance, \"Failed to find Jest instance in ReplicatedStorage\")\n assert(\n jestInstance:IsA(\"ModuleScript\"),\n \"Jest instance in ReplicatedStorage is not a ModuleScript\"\n )\n return jestInstance\nend\n\nlocal t_findJest0 = os.clock()\nlocal findSuccess, findValue = pcall(getJest)\nlocal t_findJest = os.clock()\n\nif 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 \"[]\"\nend\n\nLogService:ClearOutput()\n\n-- Snapshot support: patch jest-roblox-shared before requiring Jest\nlocal snapshotWrites: { [string]: string } = {}\n\nlocal function getInstancePath(instance: Instance): string\n local parts = {} :: { string }\n local curr: Instance? = instance\n while curr and curr ~= game do\n table.insert(parts, 1, curr.Name)\n curr = curr.Parent\n end\n\n return table.concat(parts, \"/\")\nend\n\nlocal mockFileSystemService = {\n WriteFile = function(self: any, path: string, contents: string)\n -- Rewrite .snap.lua → .snap.luau\n snapshotWrites[string.gsub(path, \"%.snap%.lua$\", \".snap.luau\")] = contents\n end,\n CreateDirectories = function(self: any, _path: string) end,\n Exists = function(self: any, path: string): boolean\n return snapshotWrites[string.gsub(path, \"%.snap%.lua$\", \".snap.luau\")] ~= nil\n end,\n Remove = function(self: any, path: string)\n snapshotWrites[string.gsub(path, \"%.snap%.lua$\", \".snap.luau\")] = nil\n end,\n IsRegularFile = function(self: any, path: string): boolean\n return snapshotWrites[string.gsub(path, \"%.snap%.lua$\", \".snap.luau\")] ~= nil\n end,\n}\n\nlocal mockCoreScriptSyncService = {}\nfunction mockCoreScriptSyncService:GetScriptFilePath(script_: Instance): string\n return getInstancePath(script_)\nend\n\n-- Use _G so the mock survives across Jest's separate module loader context.\n-- Requires getDataModelService.lua to be patched (jest-roblox patch).\n_G.__mockGetDataModelService = function(service: string): any\n if service == \"FileSystemService\" then\n return mockFileSystemService\n elseif service == \"CoreScriptSyncService\" then\n return mockCoreScriptSyncService\n end\n\n local success, result = pcall(function()\n local svc = game:GetService(service)\n local _ = svc.Name\n return svc\n end)\n\n return if success then result else nil\nend\n\nlocal t_requireJest0 = os.clock()\nlocal Jest = (require :: any)(findValue)\nlocal t_requireJest = os.clock()\n\nlocal 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, 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, 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, findInstance(setupPath))\n end\n\n config.setupFilesAfterEnv = resolved :: any\n end\n local t_resolveSetupFiles = os.clock()\n\n config._timing = nil :: any\n\n local t_jestRunCLI0 = os.clock()\n local jestResult = Jest.runCLI(script, 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 configDecode = t_configDecode - t0,\n findJest = t_findJest - t_findJest0,\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 }\n end\n\n if next(snapshotWrites) then\n result._snapshotWrites = snapshotWrites\n end\n\n return result\nend\n\nlocal runSuccess, runValue = pcall(runTests)\n\n_G.__mockGetDataModelService = nil\n\nlocal jestResult\nif not runSuccess then\n jestResult = HttpService:JSONEncode(fail(runValue :: any))\nelse\n jestResult = HttpService:JSONEncode(runValue)\nend\n\nlocal logSuccess, logHistory = pcall(function()\n return HttpService:JSONEncode(LogService:GetLogHistory())\nend)\n\nreturn jestResult, if logSuccess then logHistory else \"[]\"\n";
137
+ //#region src/test-runner.bundled.luau
138
+ 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 _timing: boolean?,\n}\nlocal __JEST_MODULES __JEST_MODULES={cache={}, load=function(m)if not __JEST_MODULES.cache[m]then __JEST_MODULES.cache[m]={c=__JEST_MODULES[m]()}end return __JEST_MODULES.cache[m].c end}do function __JEST_MODULES.a()--!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(\n jestInstance:IsA(\"ModuleScript\"),\n \"Jest instance in ReplicatedStorage is not a ModuleScript\"\n )\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\")\n 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\")\n 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.b()--!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()--!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()--!strict\n\nlocal CoreScriptSyncService = __JEST_MODULES.load('b')\nlocal InstanceResolver = __JEST_MODULES.load('a')\n\nlocal module = {}\n\nfunction module.createMockGetDataModelService(snapshotWrites: { [string]: string })\n local FileSystemService = __JEST_MODULES.load('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 =\n 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\n getDataModelServiceChild\n and typeof(target) == \"Instance\"\n and target == getDataModelServiceChild\n 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.e()--!strict\n\nlocal HttpService = game:GetService(\"HttpService\")\nlocal LogService = game:GetService(\"LogService\")\n\nlocal InstanceResolver = __JEST_MODULES.load('a')\nlocal SnapshotPatch = __JEST_MODULES.load('d')\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\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\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)),\n 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 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 config._timing = nil :: any\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 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(\n 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\n while not jestDone and not infiniteYieldMessage do\n task.wait(0.1)\n end\n\n watchdogConnection:Disconnect()\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(LogService:GetLogHistory())\n end)\n\n return jestResult, if logSuccess then logHistory else \"[]\"\nend\n\nreturn module\nend end--!strict\n\nlocal HttpService = game:GetService(\"HttpService\")\n\nlocal Runner = __JEST_MODULES.load('e')\n\nlocal config = HttpService:JSONDecode([=[__CONFIG_JSON__]=])\n\nreturn Runner.run(script, config)\n";
130
139
 
131
140
  //#endregion
132
141
  //#region src/test-script.ts
@@ -158,7 +167,7 @@ function buildJestArgv(options) {
158
167
  }
159
168
  function generateTestScript(options) {
160
169
  const config = buildJestArgv(options);
161
- return test_runner_default.replace("__CONFIG_JSON__", JSON.stringify(config));
170
+ return test_runner_bundled_default.replace("__CONFIG_JSON__", () => JSON.stringify(config));
162
171
  }
163
172
 
164
173
  //#endregion
@@ -247,7 +256,11 @@ var OpenCloudBackend = class {
247
256
  this.credentials = credentials;
248
257
  this.http = options?.http ?? createFetchClient({ "x-api-key": credentials.apiKey });
249
258
  this.readFile = options?.readFile ?? ((fp) => fs.readFileSync(fp));
250
- this.sleepFn = options?.sleep ?? (async (ms) => new Promise((resolve) => setTimeout(resolve, ms)));
259
+ this.sleepFn = options?.sleep ?? (async (ms) => {
260
+ return new Promise((resolve) => {
261
+ setTimeout(resolve, ms);
262
+ });
263
+ });
251
264
  }
252
265
  async runTests(options) {
253
266
  const placeFilePath = path$1.resolve(options.config.rootDir, options.config.placeFile);
@@ -368,24 +381,28 @@ var StudioBackend = class {
368
381
  createServer;
369
382
  port;
370
383
  timeout;
384
+ preConnected;
371
385
  constructor(options) {
372
386
  this.port = options.port;
373
387
  this.timeout = options.timeout ?? 3e5;
374
388
  this.createServer = options.createServer ?? ((port) => new WebSocketServer({ port }));
389
+ this.preConnected = options.preConnected;
375
390
  }
376
391
  async runTests(options) {
377
- const wss = this.createServer(this.port);
392
+ const pre = this.preConnected;
393
+ this.preConnected = void 0;
394
+ const wss = pre?.server ?? this.createServer(this.port);
378
395
  try {
379
- return await this.executeViaPlugin(wss, options);
396
+ return await this.executeViaPlugin(wss, options, pre?.socket);
380
397
  } finally {
381
398
  wss.close();
382
399
  }
383
400
  }
384
- async executeViaPlugin(wss, options) {
401
+ async executeViaPlugin(wss, options, existingSocket) {
385
402
  const requestId = randomUUID();
386
403
  const config = buildJestArgv(options);
387
404
  const executionStart = Date.now();
388
- const message = await this.waitForResult(wss, requestId, config);
405
+ const message = await this.waitForResult(wss, requestId, config, existingSocket);
389
406
  const executionMs = Date.now() - executionStart;
390
407
  const parsed = parseJestOutput(message.jestOutput);
391
408
  return {
@@ -396,12 +413,12 @@ var StudioBackend = class {
396
413
  timing: { executionMs }
397
414
  };
398
415
  }
399
- async waitForResult(wss, requestId, config) {
416
+ async waitForResult(wss, requestId, config, existingSocket) {
400
417
  return new Promise((resolve, reject) => {
401
418
  const timer = setTimeout(() => {
402
419
  reject(/* @__PURE__ */ new Error("Timed out waiting for Studio plugin connection"));
403
420
  }, this.timeout);
404
- wss.on("connection", (ws) => {
421
+ function attachSocket(ws) {
405
422
  ws.send(JSON.stringify({
406
423
  action: "run_tests",
407
424
  config,
@@ -424,6 +441,10 @@ var StudioBackend = class {
424
441
  clearTimeout(timer);
425
442
  reject(err);
426
443
  });
444
+ }
445
+ if (existingSocket) attachSocket(existingSocket);
446
+ wss.on("connection", (ws) => {
447
+ attachSocket(ws);
427
448
  });
428
449
  wss.on("error", (err) => {
429
450
  clearTimeout(timer);
@@ -438,6 +459,14 @@ function createStudioBackend(options) {
438
459
 
439
460
  //#endregion
440
461
  //#region src/config/schema.ts
462
+ const VALID_BACKENDS = new Set([
463
+ "auto",
464
+ "open-cloud",
465
+ "studio"
466
+ ]);
467
+ function isValidBackend(value) {
468
+ return VALID_BACKENDS.has(value);
469
+ }
441
470
  const DEFAULT_CONFIG = {
442
471
  backend: "auto",
443
472
  cache: true,
@@ -456,10 +485,14 @@ const DEFAULT_CONFIG = {
456
485
  "**/*.spec.ts",
457
486
  "**/*.spec.tsx",
458
487
  "**/*.test.ts",
459
- "**/*.test.tsx"
488
+ "**/*.test.tsx",
489
+ "**/*.spec-d.ts",
490
+ "**/*.test-d.ts"
460
491
  ],
461
492
  testPathIgnorePatterns: ["/node_modules/", "/dist/"],
462
493
  timeout: 3e5,
494
+ typecheck: false,
495
+ typecheckOnly: false,
463
496
  verbose: false
464
497
  };
465
498
 
@@ -475,76 +508,54 @@ async function findConfigFile(cwd) {
475
508
  if (fs.existsSync(configPath)) return configPath;
476
509
  }
477
510
  }
478
- async function loadConfig(configPath, cwd = process.cwd()) {
479
- let config = {};
480
- const resolvedPath = configPath ?? await findConfigFile(cwd);
481
- if (resolvedPath !== void 0) config = await loadConfigFile(resolvedPath);
482
- config.rootDir ??= cwd;
483
- return resolveConfig(config);
484
- }
485
511
  async function loadConfigFile(configPath) {
486
512
  const absolutePath = path$1.resolve(configPath);
487
513
  if (!fs.existsSync(absolutePath)) throw new Error(`Config file not found: ${absolutePath}`);
488
514
  return createJiti(import.meta.url).import(absolutePath, { default: true });
489
515
  }
490
516
  function resolveConfig(config) {
517
+ if (config.backend !== void 0 && !isValidBackend(config.backend)) {
518
+ const valid = [...VALID_BACKENDS].join(", ");
519
+ throw new Error(`Invalid backend "${config.backend}" in config file. Must be one of: ${valid}`);
520
+ }
491
521
  const defined = Object.fromEntries(Object.entries(config).filter(([, value]) => value !== void 0));
492
522
  return Object.assign({}, DEFAULT_CONFIG, defined);
493
523
  }
524
+ async function loadConfig(configPath, cwd = process.cwd()) {
525
+ let config = {};
526
+ const resolvedPath = configPath ?? await findConfigFile(cwd);
527
+ if (resolvedPath !== void 0) config = await loadConfigFile(resolvedPath);
528
+ config.rootDir ??= cwd;
529
+ return resolveConfig(config);
530
+ }
531
+
532
+ //#endregion
533
+ //#region src/utils/normalize-windows-path.ts
534
+ const DRIVE_LETTER_START_REGEX = /^[A-Za-z]:\//;
535
+ function normalizeWindowsPath(input = "") {
536
+ if (!input) return input;
537
+ return input.replace(/\\/g, "/").replace(DRIVE_LETTER_START_REGEX, (driveLetterMatch) => driveLetterMatch.toUpperCase());
538
+ }
494
539
 
495
540
  //#endregion
496
541
  //#region src/source-mapper/column-finder.ts
497
542
  /**
498
543
  * Finds the column position of the failing matcher in an expect() call. Returns
499
- * 1-indexed column position.
544
+ * 1-indexed column position, or undefined if no expect is found.
500
545
  */
501
546
  function findExpectationColumn(lineText) {
502
- if (!lineText) return 1;
503
- const expectIndex = lineText.search(/\bexpect\s*\(/);
504
- if (expectIndex === -1) return 1;
547
+ if (!lineText) return;
548
+ const expectIndex = lineText.search(/\bexpect\s*[.(]/);
549
+ if (expectIndex === -1) return;
505
550
  const afterExpect = lineText.slice(expectIndex);
506
551
  const matcherRegex = /[.:]\s*([A-Za-z_$][\w$]*)\s*(?=\()/g;
507
552
  let lastMatcher = null;
508
553
  for (const match of afterExpect.matchAll(matcherRegex)) lastMatcher = match;
509
554
  const matcherName = lastMatcher?.[1];
510
- if (lastMatcher === null || matcherName === void 0) return 1;
555
+ if (lastMatcher === null || matcherName === void 0) return;
511
556
  return expectIndex + (lastMatcher.index + lastMatcher[0].indexOf(matcherName)) + 1;
512
557
  }
513
558
 
514
- //#endregion
515
- //#region src/source-mapper/line-mapper.ts
516
- function mapLine(luauLineNumber, luauLines, tsLines) {
517
- const luauLine = normalize(luauLines[luauLineNumber - 1]);
518
- if (luauLine === void 0 || luauLine === "") return;
519
- for (const [index, tsLine] of tsLines.entries()) if (normalize(tsLine) === luauLine) return index + 1;
520
- const stripped = luauLine.replace(/\s/g, "");
521
- for (const [index, tsLine] of tsLines.entries()) {
522
- const tsNormalized = normalize(tsLine);
523
- if (tsNormalized === void 0 || tsNormalized === "") continue;
524
- const tsStripped = tsNormalized.replace(/\s/g, "");
525
- if (tsStripped.includes(stripped) || stripped.includes(tsStripped)) return index + 1;
526
- }
527
- const temporaryPattern = buildTemporaryVariablePattern(luauLine);
528
- if (temporaryPattern !== void 0) for (const [index, tsLine] of tsLines.entries()) {
529
- const normalized = normalize(tsLine);
530
- if (normalized !== void 0 && temporaryPattern.test(normalized)) return index + 1;
531
- }
532
- }
533
- const TEMP_VAR_RE = /_[a-z]\w*/g;
534
- function buildTemporaryVariablePattern(line) {
535
- const parts = line.split(TEMP_VAR_RE);
536
- if (parts.length < 2) return;
537
- const pattern = parts.map(escapeRegExp).join(".+?");
538
- return new RegExp(`^${pattern}$`);
539
- }
540
- function escapeRegExp(text) {
541
- return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
542
- }
543
- function normalize(line) {
544
- if (line === void 0) return;
545
- return line.trim().replace(/;$/, "").replace(/\s+/g, " ").replace(/\(\s+/g, "(").replace(/\s+\)/g, ")").replace(/\.(\w+)\(/g, ":$1(");
546
- }
547
-
548
559
  //#endregion
549
560
  //#region src/source-mapper/path-resolver.ts
550
561
  function createPathResolver(rojoProject, config) {
@@ -611,6 +622,34 @@ function parseStack(input) {
611
622
  };
612
623
  }
613
624
 
625
+ //#endregion
626
+ //#region src/source-mapper/v3-mapper.ts
627
+ const mapCache = /* @__PURE__ */ new Map();
628
+ function getSourceContent(luauPath, source) {
629
+ const traced = getTraceMap(luauPath);
630
+ if (traced === void 0) return;
631
+ return sourceContentFor(traced, source);
632
+ }
633
+ function mapFromSourceMap(luauPath, luauLine, luauColumn = 0) {
634
+ const traced = getTraceMap(luauPath);
635
+ if (traced === void 0) return;
636
+ const result = originalPositionFor(traced, {
637
+ column: luauColumn,
638
+ line: luauLine
639
+ });
640
+ if (result.line === null) return;
641
+ return result;
642
+ }
643
+ function getTraceMap(luauPath) {
644
+ let traced = mapCache.get(luauPath);
645
+ if (traced !== void 0) return traced;
646
+ const mapPath = `${luauPath}.map`;
647
+ if (!fs.existsSync(mapPath)) return;
648
+ traced = new TraceMap(fs.readFileSync(mapPath, "utf-8"));
649
+ mapCache.set(luauPath, traced);
650
+ return traced;
651
+ }
652
+
614
653
  //#endregion
615
654
  //#region src/source-mapper/index.ts
616
655
  function createSourceMapper(config) {
@@ -622,17 +661,26 @@ function createSourceMapper(config) {
622
661
  const tsPath = pathResolver.resolve(frame.dataModelPath);
623
662
  if (tsPath === void 0) return;
624
663
  const luauPath = tsPath.replace(config.rootDir, config.outDir).replace(/\.ts$/, ".luau");
625
- if (!fs.existsSync(luauPath) || !fs.existsSync(tsPath)) return;
626
- const luauLines = fs.readFileSync(luauPath, "utf-8").split("\n");
627
- const tsLines = fs.readFileSync(tsPath, "utf-8").split("\n");
628
- const tsLine = mapLine(frame.line, luauLines, tsLines) ?? frame.line;
664
+ const v3Result = mapFromSourceMap(luauPath, frame.line, frame.column);
665
+ if (v3Result !== void 0 && v3Result.source !== null && v3Result.line !== null) {
666
+ const mapDirectory = path$1.dirname(luauPath);
667
+ const resolvedTsPath = normalizeWindowsPath(path$1.resolve(mapDirectory, v3Result.source));
668
+ const tsLine = v3Result.line;
669
+ const embeddedContent = getSourceContent(luauPath, v3Result.source) ?? void 0;
670
+ const tsContent = embeddedContent ?? (fs.existsSync(resolvedTsPath) ? fs.readFileSync(resolvedTsPath, "utf-8") : void 0);
671
+ return {
672
+ luauPath,
673
+ mapped: {
674
+ column: tsContent !== void 0 ? findExpectationColumn(tsContent.split("\n")[tsLine - 1] ?? "") : void 0,
675
+ line: tsLine,
676
+ path: resolvedTsPath,
677
+ sourceContent: embeddedContent
678
+ }
679
+ };
680
+ }
629
681
  return {
630
682
  luauPath,
631
- mapped: {
632
- column: findExpectationColumn(tsLines[tsLine - 1] ?? ""),
633
- line: tsLine,
634
- path: tsPath
635
- }
683
+ mapped: void 0
636
684
  };
637
685
  }
638
686
  return {
@@ -661,6 +709,7 @@ function createSourceMapper(config) {
661
709
  locations.push({
662
710
  luauLine: frame.line,
663
711
  luauPath: frameResult.luauPath,
712
+ sourceContent: frameResult.mapped.sourceContent,
664
713
  tsColumn: frameResult.mapped.column,
665
714
  tsLine: frameResult.mapped.line,
666
715
  tsPath: frameResult.mapped.path
@@ -670,12 +719,17 @@ function createSourceMapper(config) {
670
719
  locations,
671
720
  message: mappedMessage
672
721
  };
722
+ },
723
+ resolveTestFilePath(testFilePath) {
724
+ const dataModelPath = testFilePath.replace(/^\//, "").replaceAll("/", ".");
725
+ return pathResolver.resolve(dataModelPath);
673
726
  }
674
727
  };
675
728
  }
676
- function getSourceSnippet(filePath, line, context = 2, column) {
677
- if (!fs.existsSync(filePath)) return;
678
- const allLines = fs.readFileSync(filePath, "utf-8").split("\n");
729
+ function getSourceSnippet({ column, context = 2, filePath, line, sourceContent }) {
730
+ const content = sourceContent ?? (fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8") : void 0);
731
+ if (content === void 0) return;
732
+ const allLines = content.split("\n");
679
733
  const startLine = Math.max(1, line - context);
680
734
  const endLine = Math.min(allLines.length, line + context);
681
735
  const lines = [];
@@ -691,6 +745,12 @@ function getSourceSnippet(filePath, line, context = 2, column) {
691
745
  };
692
746
  }
693
747
 
748
+ //#endregion
749
+ //#region src/types/jest-result.ts
750
+ function hasExecError(file) {
751
+ return file.failureMessage !== void 0 && file.failureMessage !== "" && file.testResults.length === 0;
752
+ }
753
+
694
754
  //#endregion
695
755
  //#region src/highlighter/luau-grammar.ts
696
756
  const OPENING_LONG_BRACKET = "\\[=*\\[";
@@ -756,14 +816,6 @@ function luauGrammar(hljs) {
756
816
  };
757
817
  }
758
818
 
759
- //#endregion
760
- //#region src/utils/normalize-windows-path.ts
761
- const DRIVE_LETTER_START_REGEX = /^[A-Za-z]:\//;
762
- function normalizeWindowsPath(input = "") {
763
- if (!input) return input;
764
- return input.replace(/\\/g, "/").replace(DRIVE_LETTER_START_REGEX, (driveLetterMatch) => driveLetterMatch.toUpperCase());
765
- }
766
-
767
819
  //#endregion
768
820
  //#region src/utils/colors.ts
769
821
  hljs.registerLanguage("luau", luauGrammar);
@@ -832,51 +884,59 @@ function highlightTypeScript(source) {
832
884
  //#region src/formatters/formatter.ts
833
885
  function formatFailedTestsHeader(failCount, styles) {
834
886
  const st = styles ?? createStyles(true);
835
- const line = "⎯".repeat(7);
836
- const badge = st.failBadge(` Failed Tests ${failCount} `);
837
- return `${st.status.fail(line)} ${badge} ${st.status.fail(line)}`;
887
+ const badgeText = ` Failed Tests ${failCount} `;
888
+ const badge = st.failBadge(badgeText);
889
+ const remaining = getTerminalWidth() - badgeText.length;
890
+ const leftWidth = Math.max(1, Math.floor(remaining / 2));
891
+ const rightWidth = Math.max(1, remaining - leftWidth);
892
+ return `${st.status.fail("⎯".repeat(leftWidth))}${badge}${st.status.fail("⎯".repeat(rightWidth))}`;
838
893
  }
839
- function formatFailure({ failureIndex, filePath, showLuau = false, sourceMapper, styles, test, totalFailures, useColor = true }) {
840
- const st = styles ?? createStyles(useColor);
841
- const lines = [];
842
- const pathParts = filePath !== void 0 ? [filePath] : [];
843
- pathParts.push(...test.ancestorTitles, test.title);
844
- const testPath = pathParts.join(" > ");
845
- lines.push(`${st.failBadge(" FAIL ")} ${st.status.fail(testPath)}`);
846
- for (const originalMessage of test.failureMessages) lines.push(...formatFailureMessage(originalMessage, {
847
- showLuau,
848
- sourceMapper,
849
- styles: st,
850
- useColor
851
- }));
852
- if (failureIndex !== void 0 && totalFailures !== void 0) {
853
- const separatorLine = "⎯".repeat(24);
854
- const counter = `[${failureIndex}/${totalFailures}]`;
855
- lines.push("", st.status.fail(`${separatorLine}${counter}⎯`));
894
+ function parseErrorMessage(message) {
895
+ const lines = message.split("\n");
896
+ const snapshotHeaderIndex = lines.findIndex((line) => /^- Snapshot\s+- \d+/.test(line));
897
+ if (snapshotHeaderIndex !== -1) {
898
+ const diffLines = [];
899
+ for (let index = snapshotHeaderIndex; index < lines.length; index++) {
900
+ const line = lines[index];
901
+ if (line.startsWith("[string ")) break;
902
+ diffLines.push(line);
903
+ }
904
+ return {
905
+ message: lines[0] ?? message,
906
+ snapshotDiff: diffLines.join("\n").trimEnd()
907
+ };
856
908
  }
857
- return lines.map((line) => ` ${line}`).join("\n");
909
+ const expectedMatch = message.match(/Expected\b.*?:\s*(.+)/);
910
+ const receivedMatch = message.match(/Received\b.*?:\s*(.+)/);
911
+ return {
912
+ expected: expectedMatch?.[1],
913
+ message: lines[0] ?? message,
914
+ received: receivedMatch?.[1]
915
+ };
858
916
  }
859
- function formatResult(result, timing, options) {
860
- const styles = createStyles(options.color);
861
- const lines = [formatRunHeader(options, styles)];
862
- for (const file of result.testResults) lines.push(formatFileSummary(file, options, styles));
863
- if (result.numFailedTests > 0) {
864
- lines.push("", formatFailedTestsHeader(result.numFailedTests, styles));
865
- const failureCtx = {
866
- currentIndex: 1,
867
- totalFailures: result.numFailedTests
868
- };
869
- for (const file of result.testResults) {
870
- const failures = formatFileFailures(file, options, styles, failureCtx);
871
- if (failures !== "") lines.push(failures);
917
+ /**
918
+ * Extracts the meaningful error message from a Jest `failureMessage` string.
919
+ * Strips the "● Test suite failed to run" header, Roblox DataModel path chains,
920
+ * and stack trace lines.
921
+ */
922
+ function cleanExecErrorMessage(raw) {
923
+ if (raw === "") return "";
924
+ const lines = raw.split("\n");
925
+ let contentLine;
926
+ let pastHeader = false;
927
+ for (const line of lines) {
928
+ const trimmed = line.trim();
929
+ if (trimmed.startsWith("")) {
930
+ pastHeader = true;
931
+ continue;
932
+ }
933
+ if (pastHeader && trimmed !== "") {
934
+ contentLine = trimmed;
935
+ break;
872
936
  }
873
937
  }
874
- lines.push("", formatTestSummary(result, timing, styles));
875
- return lines.join("\n");
876
- }
877
- function formatRunHeader(options, styles) {
878
- const st = styles ?? createStyles(options.color);
879
- return `\n${st.runBadge(" RUN ")} ${st.location(`v${options.version}`)} ${st.lineNumber(options.rootDir)}\n`;
938
+ if (contentLine === void 0) return raw.trim();
939
+ return contentLine.replace(/^(?:[A-Za-z][\w.@-]*:\d+:\s*)+/, "");
880
940
  }
881
941
  function formatSourceSnippet(snippet, filePath, options) {
882
942
  const useColor = options?.useColor ?? true;
@@ -891,45 +951,78 @@ function formatSourceSnippet(snippet, filePath, options) {
891
951
  const padding = String(maxLineNumber).length;
892
952
  for (const line of snippet.lines) {
893
953
  const prefix = `${String(line.num).padStart(padding)}|`;
894
- const highlighted = highlightSyntax(filePath, line.content, useColor);
954
+ const highlighted = highlightSyntax(filePath, expandTabs(line.content), useColor);
895
955
  if (line.num === snippet.failureLine) {
896
956
  lines.push(`${indent}${st.lineNumber(prefix)} ${highlighted}`);
897
957
  if (snippet.column !== void 0) {
898
- const visualPad = line.content.slice(0, snippet.column - 1).replace(/[^\t]/g, " ");
899
- const gutterPad = " ".repeat(padding + 1);
900
- lines.push(`${indent}${st.lineNumber("|")} ${gutterPad}${visualPad}${st.status.fail("^")}`);
958
+ const beforeColumn = expandTabs(line.content.slice(0, snippet.column - 1));
959
+ const caretGutter = `${" ".repeat(padding)}|`;
960
+ const gutterPrefix = st.lineNumber(caretGutter);
961
+ lines.push(`${indent}${gutterPrefix} ${" ".repeat(beforeColumn.length)}${st.status.fail("^")}`);
901
962
  }
902
963
  } else lines.push(`${indent}${st.lineNumber(prefix)} ${highlighted}`);
903
964
  }
904
965
  return lines.join("\n");
905
966
  }
967
+ function parseSourceLocation(message) {
968
+ const match = message.match(/([^\s:]+\.tsx?):(\d+)(?::(\d+))?/);
969
+ if (match === null) return;
970
+ return {
971
+ column: match[3] !== void 0 ? Number.parseInt(match[3], 10) : void 0,
972
+ line: Number.parseInt(match[2] ?? "", 10),
973
+ path: match[1] ?? ""
974
+ };
975
+ }
976
+ function formatFailure({ failureIndex, filePath, showLuau = false, sourceMapper, styles, test, totalFailures, useColor = true }) {
977
+ const st = styles ?? createStyles(useColor);
978
+ const lines = [];
979
+ const pathParts = filePath !== void 0 ? [filePath] : [];
980
+ pathParts.push(...test.ancestorTitles, test.title);
981
+ const testPath = pathParts.join(" > ");
982
+ lines.push("", `${st.failBadge(" FAIL ")} ${st.status.fail(testPath)}`);
983
+ for (const originalMessage of test.failureMessages) lines.push(...formatFailureMessage(originalMessage, {
984
+ filePath,
985
+ showLuau,
986
+ sourceMapper,
987
+ styles: st,
988
+ useColor
989
+ }));
990
+ if (failureIndex !== void 0 && totalFailures !== void 0) {
991
+ const counter = `[${failureIndex}/${totalFailures}]`;
992
+ const termWidth = getTerminalWidth();
993
+ const fillWidth = Math.max(1, termWidth - counter.length - 3);
994
+ lines.push("", st.dim(st.status.fail(`${"⎯".repeat(fillWidth)}${counter}⎯`)));
995
+ }
996
+ return lines.map((line) => ` ${line}`).join("\n");
997
+ }
998
+ function formatRunHeader(options, styles) {
999
+ const st = styles ?? createStyles(options.color);
1000
+ return `\n${st.runBadge(" RUN ")} ${st.location(`v${options.version}`)} ${st.lineNumber(options.rootDir)}\n`;
1001
+ }
906
1002
  function formatTestSummary(result, timing, styles) {
907
1003
  const st = styles ?? createStyles(true);
908
1004
  const lines = [];
909
- const hasFailed = result.numFailedTests > 0;
910
- const labelStyle = hasFailed ? st.status.fail : st.dim;
1005
+ const execErrorFiles = result.testResults.filter(hasExecError).length;
911
1006
  const totalFiles = result.testResults.length;
912
- const failedFiles = result.testResults.filter((fr) => fr.numFailingTests > 0).length;
913
- const skippedFiles = result.testResults.filter((fr) => fr.numFailingTests === 0 && fr.numPassingTests === 0).length;
1007
+ const failedFiles = result.testResults.filter((fr) => fr.numFailingTests > 0).length + execErrorFiles;
1008
+ const skippedFiles = result.testResults.filter((fr) => fr.numFailingTests === 0 && fr.numPassingTests === 0 && !hasExecError(fr)).length;
914
1009
  const passedFiles = totalFiles - failedFiles - skippedFiles;
915
1010
  const fileParts = [];
916
1011
  if (passedFiles > 0) fileParts.push(st.summary.passed(`${passedFiles} passed`));
917
1012
  if (failedFiles > 0) fileParts.push(st.summary.failed(`${failedFiles} failed`));
918
1013
  if (skippedFiles > 0) fileParts.push(st.summary.pending(`${skippedFiles} skipped`));
1014
+ const snapshotLine = formatSnapshotLine(result.snapshot, st);
1015
+ if (snapshotLine !== void 0) lines.push(snapshotLine);
919
1016
  const fileTotalLabel = st.dim(`(${totalFiles})`);
920
- const fileLabel = labelStyle(" Test Files");
921
- lines.push(`${fileLabel} ${fileParts.join(" | ")} ${fileTotalLabel}`);
1017
+ lines.push(`${st.dim(" Test Files")} ${fileParts.join(" | ")} ${fileTotalLabel}`);
922
1018
  const testParts = [];
923
1019
  if (result.numPassedTests > 0) testParts.push(st.summary.passed(`${result.numPassedTests} passed`));
924
1020
  if (result.numFailedTests > 0) testParts.push(st.summary.failed(`${result.numFailedTests} failed`));
925
1021
  if (result.numPendingTests > 0) testParts.push(st.summary.pending(`${result.numPendingTests} skipped`));
926
1022
  const testTotalLabel = st.dim(`(${result.numTotalTests})`);
927
- const testsLabel = labelStyle(" Tests");
928
- lines.push(`${testsLabel} ${testParts.join(" | ")} ${testTotalLabel}`);
1023
+ lines.push(`${st.dim(" Tests")} ${testParts.join(" | ")} ${testTotalLabel}`);
929
1024
  const startAtStr = new Date(timing.startTime).toLocaleTimeString("en-GB", { hour12: false });
930
- const startAtLabel = labelStyle(" Start at");
931
- const startAtValue = hasFailed ? st.status.fail(startAtStr) : startAtStr;
932
- lines.push(`${startAtLabel} ${startAtValue}`);
1025
+ lines.push(`${st.dim(" Start at")} ${startAtStr}`);
933
1026
  const environmentMs = timing.executionMs - timing.testsMs;
934
1027
  const uploadMs = timing.uploadMs ?? 0;
935
1028
  const cliMs = timing.totalMs - uploadMs - timing.executionMs;
@@ -937,29 +1030,39 @@ function formatTestSummary(result, timing, styles) {
937
1030
  if (timing.uploadMs !== void 0) breakdownParts.push(timing.uploadCached === true ? `upload ${timing.uploadMs}ms (cached)` : `upload ${timing.uploadMs}ms`);
938
1031
  breakdownParts.push(`environment ${environmentMs}ms`, `tests ${timing.testsMs}ms`, `cli ${cliMs}ms`);
939
1032
  const breakdown = st.dim(`(${breakdownParts.join(", ")})`);
940
- const durationLabel = labelStyle(" Duration");
941
- const durationValue = hasFailed ? st.status.fail(`${timing.totalMs}ms`) : `${timing.totalMs}ms`;
942
- lines.push(`${durationLabel} ${durationValue} ${breakdown}`);
1033
+ lines.push(`${st.dim(" Duration")} ${timing.totalMs}ms ${breakdown}`);
943
1034
  return lines.join("\n");
944
1035
  }
945
- function parseErrorMessage(message) {
946
- const lines = message.split("\n");
947
- const expectedMatch = message.match(/Expected\b.*?:\s*(.+)/);
948
- const receivedMatch = message.match(/Received\b.*?:\s*(.+)/);
949
- return {
950
- expected: expectedMatch?.[1],
951
- message: lines[0] ?? message,
952
- received: receivedMatch?.[1]
953
- };
1036
+ function formatResult(result, timing, options) {
1037
+ const styles = createStyles(options.color);
1038
+ const lines = [formatRunHeader(options, styles)];
1039
+ for (const file of result.testResults) lines.push(formatFileSummary(file, options, styles));
1040
+ const execErrors = result.testResults.filter(hasExecError);
1041
+ const totalDetailedFailures = result.numFailedTests + execErrors.length;
1042
+ if (totalDetailedFailures > 0) {
1043
+ lines.push("", formatFailedTestsHeader(totalDetailedFailures, styles));
1044
+ const failureCtx = {
1045
+ currentIndex: 1,
1046
+ totalFailures: totalDetailedFailures
1047
+ };
1048
+ for (const file of result.testResults) {
1049
+ const failures = formatFileFailures(file, options, styles, failureCtx);
1050
+ if (failures !== "") lines.push(failures);
1051
+ }
1052
+ for (const file of execErrors) lines.push(formatExecErrorDetail(file, styles, failureCtx));
1053
+ }
1054
+ lines.push("", formatTestSummary(result, timing, styles));
1055
+ if (!result.success) {
1056
+ const hints = formatLogHints(options, styles);
1057
+ if (hints !== "") lines.push("", hints);
1058
+ }
1059
+ return lines.join("\n");
954
1060
  }
955
- function parseSourceLocation(message) {
956
- const match = message.match(/([^\s:]+\.tsx?):(\d+)(?::(\d+))?/);
957
- if (match === null) return;
958
- return {
959
- column: match[3] !== void 0 ? Number.parseInt(match[3], 10) : void 0,
960
- line: Number.parseInt(match[2] ?? "", 10),
961
- path: match[1] ?? ""
962
- };
1061
+ function getTerminalWidth() {
1062
+ return "columns" in process.stdout && process.stdout.columns || 80;
1063
+ }
1064
+ function identity(text) {
1065
+ return text;
963
1066
  }
964
1067
  function createStyles(useColor) {
965
1068
  if (!useColor) return {
@@ -1013,59 +1116,139 @@ function createStyles(useColor) {
1013
1116
  }
1014
1117
  };
1015
1118
  }
1016
- function formatDescribeGroup(describeName, tests, styles) {
1017
- const lines = [];
1018
- const groupHasFailure = tests.some((tc) => tc.status === "failed");
1019
- const groupTestCount = tests.length;
1020
- const groupDuration = tests.reduce((sum, tc) => sum + (tc.duration ?? 0), 0);
1021
- const groupDurationStr = styles.lineNumber(` ${groupDuration}ms`);
1022
- if (groupHasFailure) {
1023
- const failedCount = tests.filter((tc) => tc.status === "failed").length;
1024
- const groupMeta = styles.dim(`(${groupTestCount} tests | `) + styles.summary.failed(`${failedCount} failed`) + styles.dim(")");
1025
- const header = styles.status.fail(` ❯ ${describeName}`);
1026
- lines.push(`${header} ${groupMeta}${groupDurationStr}`);
1027
- for (const tc of tests) lines.push(formatTestInGroup(tc, styles));
1028
- } else {
1029
- const groupMeta = styles.dim(`(${groupTestCount} tests)`);
1030
- const marker = styles.status.pass(" ✓");
1031
- const name = styles.status.fail(` ${describeName}`);
1032
- lines.push(`${marker}${name} ${groupMeta}${groupDurationStr}`);
1033
- }
1034
- return lines;
1119
+ function expandTabs(text, tabWidth = 4) {
1120
+ let result = "";
1121
+ for (const char of text) if (char === " ") {
1122
+ const spaces = tabWidth - result.length % tabWidth;
1123
+ result += " ".repeat(spaces);
1124
+ } else result += char;
1125
+ return result;
1035
1126
  }
1036
- function formatFailedFileSummary(file, testCount, styles) {
1037
- const lines = [];
1038
- const failedMeta = styles.summary.failed(`${file.numFailingTests} failed`);
1039
- const meta = `${styles.dim(`(${testCount} tests | `)}${failedMeta}${styles.dim(")")}`;
1040
- const header = styles.status.fail(` ❯ ${file.testFilePath}`);
1041
- lines.push(`${header} ${meta}`);
1042
- const groups = groupByDescribe(file.testResults);
1043
- for (const [describeName, tests] of groups) lines.push(...formatDescribeGroup(describeName, tests, styles));
1044
- return lines;
1127
+ function highlightSyntax(filePath, code, useColor) {
1128
+ if (!useColor) return code;
1129
+ return highlightCode(filePath, code);
1045
1130
  }
1046
- function formatFailureMessage(originalMessage, options) {
1047
- const { showLuau, sourceMapper, styles: st, useColor } = options;
1048
- const lines = [];
1049
- let mappedLocations = [];
1050
- let message = originalMessage;
1051
- if (sourceMapper !== void 0) ({locations: mappedLocations, message} = sourceMapper.mapFailureWithLocations(originalMessage));
1052
- const parsed = parseErrorMessage(message);
1053
- lines.push(st.status.fail(parsed.message));
1054
- if (parsed.expected !== void 0 && parsed.received !== void 0) lines.push("", st.diff.expected("- Expected"), st.diff.received("+ Received"), "", st.diff.expected(`- ${parsed.expected}`), st.diff.received(`+ ${parsed.received}`));
1055
- if (mappedLocations.length > 0) for (const loc of mappedLocations) lines.push(...formatMappedLocationSnippets(loc, showLuau, st, useColor));
1056
- else lines.push(...formatFallbackSnippet(message, st, useColor));
1057
- return lines;
1131
+ function formatDiffBlock(parsed, st) {
1132
+ if (parsed.snapshotDiff !== void 0) {
1133
+ const lines = [""];
1134
+ for (const diffLine of parsed.snapshotDiff.split("\n")) if (diffLine.startsWith("- ")) lines.push(st.diff.expected(diffLine));
1135
+ else if (diffLine.startsWith("+ ")) lines.push(st.diff.received(diffLine));
1136
+ else lines.push(st.dim(diffLine));
1137
+ return lines;
1138
+ }
1139
+ if (parsed.expected !== void 0 && parsed.received !== void 0) return [
1140
+ "",
1141
+ st.diff.expected("- Expected"),
1142
+ st.diff.received("+ Received"),
1143
+ "",
1144
+ st.diff.expected(`- ${parsed.expected}`),
1145
+ st.diff.received(`+ ${parsed.received}`)
1146
+ ];
1147
+ return [];
1148
+ }
1149
+ function formatErrorLine(parsed, st, useColor) {
1150
+ if (useColor && parsed.message.startsWith("Error:")) return st.status.fail(color.bold("Error:") + parsed.message.slice(6));
1151
+ return st.status.fail(parsed.message);
1058
1152
  }
1059
1153
  function formatFallbackSnippet(message, styles, useColor) {
1060
1154
  const location = parseSourceLocation(message);
1061
1155
  if (location === void 0) return [];
1062
- const snippet = getSourceSnippet(location.path, location.line, 2, location.column);
1156
+ const snippet = getSourceSnippet({
1157
+ column: location.column,
1158
+ context: 2,
1159
+ filePath: location.path,
1160
+ line: location.line
1161
+ });
1063
1162
  if (snippet === void 0) return [];
1064
1163
  return ["", formatSourceSnippet(snippet, location.path, {
1065
1164
  styles,
1066
1165
  useColor
1067
1166
  })];
1068
1167
  }
1168
+ function formatMappedLocationSnippets(loc, showLuau, st, useColor) {
1169
+ const snippets = [];
1170
+ const tsSnippet = getSourceSnippet({
1171
+ column: loc.tsColumn,
1172
+ context: 2,
1173
+ filePath: loc.tsPath,
1174
+ line: loc.tsLine,
1175
+ sourceContent: loc.sourceContent
1176
+ });
1177
+ if (tsSnippet !== void 0) {
1178
+ const label = showLuau ? "TypeScript" : void 0;
1179
+ snippets.push("", formatSourceSnippet(tsSnippet, loc.tsPath, {
1180
+ language: label,
1181
+ styles: st,
1182
+ useColor
1183
+ }));
1184
+ }
1185
+ if (showLuau) {
1186
+ const luauSnippet = getSourceSnippet({
1187
+ context: 2,
1188
+ filePath: loc.luauPath,
1189
+ line: loc.luauLine
1190
+ });
1191
+ if (luauSnippet !== void 0) snippets.push("", formatSourceSnippet(luauSnippet, loc.luauPath, {
1192
+ language: "Luau",
1193
+ styles: st,
1194
+ useColor
1195
+ }));
1196
+ }
1197
+ return snippets;
1198
+ }
1199
+ function formatSnapshotCallSnippet(filePath, styles, useColor) {
1200
+ if (!fs.existsSync(filePath)) return [];
1201
+ const content = fs.readFileSync(filePath, "utf-8");
1202
+ const snapshotIndices = content.split("\n").reduce((acc, fileLine, index) => {
1203
+ if (fileLine.includes("toMatchSnapshot")) acc.push(index);
1204
+ return acc;
1205
+ }, []);
1206
+ if (snapshotIndices.length !== 1) return [];
1207
+ const snippet = getSourceSnippet({
1208
+ context: 2,
1209
+ filePath,
1210
+ line: snapshotIndices[0] + 1,
1211
+ sourceContent: content
1212
+ });
1213
+ if (snippet === void 0) return [];
1214
+ return ["", formatSourceSnippet(snippet, filePath, {
1215
+ styles,
1216
+ useColor
1217
+ })];
1218
+ }
1219
+ function resolveSourceSnippets(options) {
1220
+ const { filePath, hasSnapshotDiff, mappedLocations, message, showLuau, sourceMapper, styles: st, useColor } = options;
1221
+ if (mappedLocations.length > 0) return mappedLocations.flatMap((loc) => formatMappedLocationSnippets(loc, showLuau, st, useColor));
1222
+ const fallback = formatFallbackSnippet(message, st, useColor);
1223
+ if (fallback.length > 0) return fallback;
1224
+ if (hasSnapshotDiff && filePath !== void 0) return formatSnapshotCallSnippet(sourceMapper?.resolveTestFilePath(filePath) ?? filePath, st, useColor);
1225
+ return [];
1226
+ }
1227
+ function formatFailureMessage(originalMessage, options) {
1228
+ const { filePath, showLuau, sourceMapper, styles: st, useColor } = options;
1229
+ let mappedLocations = [];
1230
+ let message = originalMessage;
1231
+ if (sourceMapper !== void 0) ({locations: mappedLocations, message} = sourceMapper.mapFailureWithLocations(originalMessage));
1232
+ const parsed = parseErrorMessage(originalMessage);
1233
+ return [
1234
+ formatErrorLine(parsed, st, useColor),
1235
+ ...formatDiffBlock(parsed, st),
1236
+ ...resolveSourceSnippets({
1237
+ filePath,
1238
+ hasSnapshotDiff: parsed.snapshotDiff !== void 0,
1239
+ mappedLocations,
1240
+ message,
1241
+ showLuau,
1242
+ sourceMapper,
1243
+ styles: st,
1244
+ useColor
1245
+ })
1246
+ ];
1247
+ }
1248
+ function formatSnapshotLine(snapshot, st) {
1249
+ if (snapshot === void 0 || snapshot.unmatched === 0) return;
1250
+ return `${st.dim(" Snapshots")} ${st.summary.failed(`${snapshot.unmatched} failed`)}`;
1251
+ }
1069
1252
  function formatFileFailures(file, options, styles, failureCtx) {
1070
1253
  const lines = [];
1071
1254
  for (const tc of file.testResults) if (tc.status === "failed") {
@@ -1084,58 +1267,23 @@ function formatFileFailures(file, options, styles, failureCtx) {
1084
1267
  }
1085
1268
  return lines.join("\n");
1086
1269
  }
1087
- function formatFilePath(filePath, styles) {
1088
- const directory = path.dirname(filePath);
1089
- const base = path.basename(filePath);
1090
- const directoryWithSlash = styles.path.dir(`${directory}/`);
1091
- const fileName = styles.path.file(base);
1092
- return directory && directory !== "." ? directoryWithSlash + fileName : fileName;
1093
- }
1094
- function formatFileSummary(file, options, styles) {
1270
+ function formatExecErrorDetail(file, styles, failureCtx) {
1095
1271
  const lines = [];
1096
- const formattedPath = formatFilePath(file.testFilePath, styles);
1097
- const testCount = file.numPassingTests + file.numFailingTests + file.numPendingTests;
1098
- if (file.numFailingTests > 0) lines.push(...formatFailedFileSummary(file, testCount, styles));
1099
- else if (file.numPassingTests === 0 && file.numPendingTests > 0) {
1100
- const symbol = styles.status.pending("↓");
1101
- const meta = styles.dim(`(${testCount} tests)`);
1102
- lines.push(` ${symbol} ${formattedPath} ${meta}`);
1103
- } else {
1104
- const fileMs = sumFileDuration(file);
1105
- const symbol = styles.status.pass("✓");
1106
- const duration = fileMs > 0 ? ` - ${fileMs}ms` : "";
1107
- const meta = styles.dim(`(${testCount} tests${duration})`);
1108
- lines.push(` ${symbol} ${formattedPath} ${meta}`);
1109
- if (options.verbose) {
1110
- for (const tc of file.testResults) if (tc.status === "passed") lines.push(formatPass(tc, styles));
1111
- }
1112
- }
1272
+ const index = failureCtx.currentIndex;
1273
+ failureCtx.currentIndex++;
1274
+ const errorMessage = cleanExecErrorMessage(file.failureMessage ?? "");
1275
+ const counter = `[${index}/${failureCtx.totalFailures}]`;
1276
+ const termWidth = getTerminalWidth();
1277
+ const fillWidth = Math.max(1, termWidth - counter.length - 3);
1278
+ const separator = styles.dim(styles.status.fail(`${"⎯".repeat(fillWidth)}${counter}\u23af`));
1279
+ lines.push(` ${styles.failBadge(" FAIL ")} ${styles.status.fail(file.testFilePath)}`, ` ${styles.status.fail("Test suite failed to run")}`, "", ` ${styles.status.fail(errorMessage)}`, "", ` ${separator}`);
1113
1280
  return lines.join("\n");
1114
1281
  }
1115
- function formatMappedLocationSnippets(loc, showLuau, st, useColor) {
1116
- const snippets = [];
1117
- const tsSnippet = getSourceSnippet(loc.tsPath, loc.tsLine, 2, loc.tsColumn);
1118
- if (tsSnippet !== void 0) {
1119
- const label = showLuau ? "TypeScript" : void 0;
1120
- snippets.push("", formatSourceSnippet(tsSnippet, loc.tsPath, {
1121
- language: label,
1122
- styles: st,
1123
- useColor
1124
- }));
1125
- }
1126
- if (showLuau) {
1127
- const luauSnippet = getSourceSnippet(loc.luauPath, loc.luauLine, 2);
1128
- if (luauSnippet !== void 0) snippets.push("", formatSourceSnippet(luauSnippet, loc.luauPath, {
1129
- language: "Luau",
1130
- styles: st,
1131
- useColor
1132
- }));
1133
- }
1134
- return snippets;
1135
- }
1136
- function formatPass(test, styles) {
1137
- const duration = test.duration !== void 0 ? styles.dim(` ${test.duration}ms`) : "";
1138
- return styles.status.pass(` ✓ ${test.fullName}`) + duration;
1282
+ function formatLogHints(options, styles) {
1283
+ const lines = [];
1284
+ if (options.outputFile !== void 0) lines.push(styles.dim(` View ${options.outputFile} for full Jest output`));
1285
+ if (options.gameOutput !== void 0) lines.push(styles.dim(` View ${options.gameOutput} for Roblox game logs`));
1286
+ return lines.join("\n");
1139
1287
  }
1140
1288
  function formatTestInGroup(tc, styles) {
1141
1289
  const duration = tc.duration !== void 0 ? styles.lineNumber(` ${tc.duration}ms`) : "";
@@ -1143,6 +1291,26 @@ function formatTestInGroup(tc, styles) {
1143
1291
  const failedText = ` × ${tc.title}`;
1144
1292
  return `${styles.status.fail(failedText)}${duration}`;
1145
1293
  }
1294
+ function formatDescribeGroup(describeName, tests, styles) {
1295
+ const lines = [];
1296
+ const groupHasFailure = tests.some((tc) => tc.status === "failed");
1297
+ const groupTestCount = tests.length;
1298
+ const groupDuration = tests.reduce((sum, tc) => sum + (tc.duration ?? 0), 0);
1299
+ const groupDurationStr = styles.lineNumber(` ${groupDuration}ms`);
1300
+ if (groupHasFailure) {
1301
+ const failedCount = tests.filter((tc) => tc.status === "failed").length;
1302
+ const groupMeta = styles.dim(`(${groupTestCount} tests | `) + styles.summary.failed(`${failedCount} failed`) + styles.dim(")");
1303
+ const header = styles.status.fail(` ❯ ${describeName}`);
1304
+ lines.push(`${header} ${groupMeta}${groupDurationStr}`);
1305
+ for (const tc of tests) lines.push(formatTestInGroup(tc, styles));
1306
+ } else {
1307
+ const groupMeta = styles.dim(`(${groupTestCount} tests)`);
1308
+ const marker = styles.status.pass(" ✓");
1309
+ const name = styles.status.fail(` ${describeName}`);
1310
+ lines.push(`${marker}${name} ${groupMeta}${groupDurationStr}`);
1311
+ }
1312
+ return lines;
1313
+ }
1146
1314
  function groupByDescribe(tests) {
1147
1315
  const groups = /* @__PURE__ */ new Map();
1148
1316
  for (const test of tests) {
@@ -1153,36 +1321,102 @@ function groupByDescribe(tests) {
1153
1321
  }
1154
1322
  return groups;
1155
1323
  }
1156
- function highlightSyntax(filePath, code, useColor) {
1157
- if (!useColor) return code;
1158
- return highlightCode(filePath, code);
1324
+ function formatFailedFileSummary(file, testCount, styles) {
1325
+ const lines = [];
1326
+ const failedMeta = styles.summary.failed(`${file.numFailingTests} failed`);
1327
+ const meta = `${styles.dim(`(${testCount} tests | `)}${failedMeta}${styles.dim(")")}`;
1328
+ const header = styles.status.fail(` ❯ ${file.testFilePath}`);
1329
+ lines.push(`${header} ${meta}`);
1330
+ const groups = groupByDescribe(file.testResults);
1331
+ for (const [describeName, tests] of groups) lines.push(...formatDescribeGroup(describeName, tests, styles));
1332
+ return lines;
1159
1333
  }
1160
- function identity(text) {
1161
- return text;
1334
+ function formatFilePath(filePath, styles) {
1335
+ const directory = path.dirname(filePath);
1336
+ const base = path.basename(filePath);
1337
+ const directoryWithSlash = styles.path.dir(`${directory}/`);
1338
+ const fileName = styles.path.file(base);
1339
+ return directory && directory !== "." ? directoryWithSlash + fileName : fileName;
1340
+ }
1341
+ function formatExecErrorFileSummary(file, formattedPath, styles) {
1342
+ const symbol = styles.status.fail("✗");
1343
+ const errorMessage = cleanExecErrorMessage(file.failureMessage ?? "");
1344
+ return [` ${symbol} ${formattedPath}`, ` ${styles.status.fail(errorMessage)}`];
1345
+ }
1346
+ function formatPass(test, styles) {
1347
+ const duration = test.duration !== void 0 ? styles.dim(` ${test.duration}ms`) : "";
1348
+ return styles.status.pass(` ✓ ${test.fullName}`) + duration;
1162
1349
  }
1163
1350
  function sumFileDuration(file) {
1164
1351
  let total = 0;
1165
1352
  for (const test of file.testResults) if (test.duration !== void 0) total += test.duration;
1166
1353
  return total;
1167
1354
  }
1355
+ function formatPassedFileSummary(file, ctx) {
1356
+ const lines = [];
1357
+ const fileMs = sumFileDuration(file);
1358
+ const symbol = ctx.styles.status.pass("✓");
1359
+ const duration = fileMs > 0 ? ` - ${fileMs}ms` : "";
1360
+ const meta = ctx.styles.dim(`(${ctx.testCount} tests${duration})`);
1361
+ lines.push(` ${symbol} ${ctx.formattedPath} ${meta}`);
1362
+ if (ctx.verbose) {
1363
+ for (const tc of file.testResults) if (tc.status === "passed") lines.push(formatPass(tc, ctx.styles));
1364
+ }
1365
+ return lines;
1366
+ }
1367
+ function formatFileSummary(file, options, styles) {
1368
+ const formattedPath = formatFilePath(file.testFilePath, styles);
1369
+ const testCount = file.numPassingTests + file.numFailingTests + file.numPendingTests;
1370
+ if (file.numFailingTests > 0) return formatFailedFileSummary(file, testCount, styles).join("\n");
1371
+ if (hasExecError(file)) return formatExecErrorFileSummary(file, formattedPath, styles).join("\n");
1372
+ if (file.numPassingTests === 0 && file.numPendingTests > 0) return ` ${styles.status.pending("↓")} ${formattedPath} ${styles.dim(`(${testCount} tests)`)}`;
1373
+ return formatPassedFileSummary(file, {
1374
+ formattedPath,
1375
+ styles,
1376
+ testCount,
1377
+ verbose: options.verbose
1378
+ }).join("\n");
1379
+ }
1168
1380
 
1169
1381
  //#endregion
1170
1382
  //#region src/formatters/compact.ts
1383
+ function formatCompactSummary(result) {
1384
+ const parts = [];
1385
+ const execErrorCount = result.testResults.filter(hasExecError).length;
1386
+ const failCount = result.numFailedTests + execErrorCount;
1387
+ if (result.numPassedTests > 0) parts.push(`PASS ${result.numPassedTests}`);
1388
+ if (failCount > 0) parts.push(`FAIL ${failCount}`);
1389
+ if (result.numPendingTests > 0) parts.push(`SKIP ${result.numPendingTests}`);
1390
+ return parts.join(" | ");
1391
+ }
1171
1392
  function formatCompact(result, options) {
1172
1393
  const lines = [formatCompactSummary(result)];
1173
- if (result.numFailedTests > 0) {
1394
+ const execErrors = result.testResults.filter(hasExecError);
1395
+ if (result.numFailedTests > 0 || execErrors.length > 0) {
1174
1396
  lines.push("");
1175
- const failureLines = formatFailures(result, options);
1176
- lines.push(...failureLines);
1397
+ if (result.numFailedTests > 0) {
1398
+ const failureLines = formatFailures(result, options);
1399
+ lines.push(...failureLines);
1400
+ }
1401
+ for (const file of execErrors) {
1402
+ const relativePath = makeRelative(file.testFilePath, options.rootDir);
1403
+ const errorMessage = cleanExecErrorMessage(file.failureMessage ?? "");
1404
+ lines.push(`[FAIL] ${relativePath} - suite failed to run`, errorMessage, "");
1405
+ }
1406
+ const hints = formatCompactLogHints(options);
1407
+ if (hints !== "") lines.push(hints);
1177
1408
  }
1178
1409
  return lines.join("\n");
1179
1410
  }
1180
- function formatCompactSummary(result) {
1181
- const parts = [];
1182
- if (result.numPassedTests > 0) parts.push(`PASS ${result.numPassedTests}`);
1183
- if (result.numFailedTests > 0) parts.push(`FAIL ${result.numFailedTests}`);
1184
- if (result.numPendingTests > 0) parts.push(`SKIP ${result.numPendingTests}`);
1185
- return parts.join(" | ");
1411
+ function formatCompactLogHints(options) {
1412
+ const lines = [];
1413
+ if (options.outputFile !== void 0) lines.push(`View ${options.outputFile} for full Jest output`);
1414
+ if (options.gameOutput !== void 0) lines.push(`View ${options.gameOutput} for Roblox game logs`);
1415
+ return lines.join("\n");
1416
+ }
1417
+ function makeRelative(filePath, rootDirectory) {
1418
+ if (filePath.startsWith(rootDirectory)) return path.relative(rootDirectory, filePath);
1419
+ return filePath;
1186
1420
  }
1187
1421
  function collectFailedTests(result) {
1188
1422
  const failures = [];
@@ -1202,18 +1436,43 @@ function findFailureLocation(mappedLocations, message) {
1202
1436
  }
1203
1437
  return parseSourceLocation(message);
1204
1438
  }
1439
+ function getFailureSnippet(mappedLocations, location) {
1440
+ let snippet;
1441
+ if (mappedLocations.length > 0) {
1442
+ const loc = mappedLocations[0];
1443
+ if (loc !== void 0) snippet = getSourceSnippet({
1444
+ column: loc.tsColumn,
1445
+ context: 1,
1446
+ filePath: loc.tsPath,
1447
+ line: loc.tsLine,
1448
+ sourceContent: loc.sourceContent
1449
+ });
1450
+ } else if (location !== void 0) snippet = getSourceSnippet({
1451
+ context: 1,
1452
+ filePath: location.path,
1453
+ line: location.line
1454
+ });
1455
+ if (snippet === void 0) return;
1456
+ const snippetLines = [];
1457
+ for (const line of snippet.lines) {
1458
+ const prefix = line.num === snippet.failureLine ? ">" : " ";
1459
+ snippetLines.push(`${prefix} ${line.num}| ${line.content}`);
1460
+ }
1461
+ return snippetLines.join("\n");
1462
+ }
1205
1463
  function formatCompactFailure(test, filePath, options) {
1206
1464
  const lines = [];
1207
1465
  for (const originalMessage of test.failureMessages) {
1208
1466
  let mappedLocations = [];
1209
1467
  let message = originalMessage;
1210
1468
  if (options.sourceMapper !== void 0) ({locations: mappedLocations, message} = options.sourceMapper.mapFailureWithLocations(originalMessage));
1211
- const parsed = parseErrorMessage(message);
1469
+ const parsed = parseErrorMessage(originalMessage);
1212
1470
  const location = findFailureLocation(mappedLocations, message);
1213
1471
  const relativePath = makeRelative(location?.path ?? filePath, options.rootDir);
1214
1472
  const lineInfo = location?.line !== void 0 ? `:${location.line}` : "";
1215
1473
  lines.push(`[FAIL] ${relativePath}${lineInfo} - ${test.title}`);
1216
- if (parsed.expected !== void 0 && parsed.received !== void 0) lines.push(`expect: ${parsed.expected}`, `actual: ${parsed.received}`);
1474
+ if (parsed.snapshotDiff !== void 0) lines.push(parsed.snapshotDiff);
1475
+ else if (parsed.expected !== void 0 && parsed.received !== void 0) lines.push(`expect: ${parsed.expected}`, `actual: ${parsed.received}`);
1217
1476
  const snippet = getFailureSnippet(mappedLocations, location);
1218
1477
  if (snippet !== void 0) lines.push(snippet);
1219
1478
  lines.push("");
@@ -1232,24 +1491,6 @@ function formatFailures(result, options) {
1232
1491
  }
1233
1492
  return lines;
1234
1493
  }
1235
- function getFailureSnippet(mappedLocations, location) {
1236
- let snippet;
1237
- if (mappedLocations.length > 0) {
1238
- const loc = mappedLocations[0];
1239
- if (loc !== void 0) snippet = getSourceSnippet(loc.tsPath, loc.tsLine, 1, loc.tsColumn);
1240
- } else if (location !== void 0) snippet = getSourceSnippet(location.path, location.line, 1);
1241
- if (snippet === void 0) return;
1242
- const snippetLines = [];
1243
- for (const line of snippet.lines) {
1244
- const prefix = line.num === snippet.failureLine ? ">" : " ";
1245
- snippetLines.push(`${prefix} ${line.num}| ${line.content}`);
1246
- }
1247
- return snippetLines.join("\n");
1248
- }
1249
- function makeRelative(filePath, rootDirectory) {
1250
- if (filePath.startsWith(rootDirectory)) return path.relative(rootDirectory, filePath);
1251
- return filePath;
1252
- }
1253
1494
 
1254
1495
  //#endregion
1255
1496
  //#region src/formatters/json.ts
@@ -1292,19 +1533,16 @@ function buildMappings(tree, prefix) {
1292
1533
  //#endregion
1293
1534
  //#region src/executor.ts
1294
1535
  const rojoProjectSchema = type({ tree: "object" });
1295
- const tsconfigSchema = type({ "compilerOptions?": {
1296
- "outDir?": "string",
1297
- "rootDir?": "string"
1298
- } });
1299
1536
  async function execute(options) {
1300
1537
  const startTime = Date.now();
1301
1538
  const { gameOutput, luauTiming, result, snapshotWrites, timing: backendTiming } = await options.backend.runTests({
1302
1539
  config: options.config,
1303
1540
  testFiles: options.testFiles
1304
1541
  });
1305
- if (snapshotWrites !== void 0) writeSnapshots(snapshotWrites, options.config);
1542
+ const tsconfigDirectories = resolveTsconfigDirectories(options.config.rootDir);
1543
+ if (snapshotWrites !== void 0) writeSnapshots(snapshotWrites, options.config, tsconfigDirectories);
1306
1544
  const testsMs = calculateTestsMs(result.testResults);
1307
- const sourceMapper = options.config.sourceMap ? buildSourceMapper(options.config) : void 0;
1545
+ const sourceMapper = options.config.sourceMap ? buildSourceMapper(options.config, tsconfigDirectories) : void 0;
1308
1546
  const totalMs = Date.now() - startTime;
1309
1547
  const timing = {
1310
1548
  executionMs: backendTiming.executionMs,
@@ -1314,15 +1552,21 @@ async function execute(options) {
1314
1552
  uploadCached: backendTiming.uploadCached,
1315
1553
  uploadMs: backendTiming.uploadMs
1316
1554
  };
1555
+ const resolvedOutputFile = options.config.outputFile !== void 0 ? path$1.resolve(options.config.outputFile) : void 0;
1556
+ const resolvedGameOutput = options.config.gameOutput !== void 0 ? path$1.resolve(options.config.gameOutput) : void 0;
1317
1557
  let output = "";
1318
- if (!options.config.silent) if (options.config.compact) output = formatCompact(result, {
1558
+ if (!options.config.silent) if (options.config.compact && !options.config.verbose) output = formatCompact(result, {
1559
+ gameOutput: resolvedGameOutput,
1319
1560
  maxFailures: options.config.compactMaxFailures,
1561
+ outputFile: resolvedOutputFile,
1320
1562
  rootDir: options.config.rootDir,
1321
1563
  sourceMapper
1322
1564
  });
1323
1565
  else if (options.config.json) output = formatJson(result);
1324
1566
  else output = formatResult(result, timing, {
1325
1567
  color: options.config.color,
1568
+ gameOutput: resolvedGameOutput,
1569
+ outputFile: resolvedOutputFile,
1326
1570
  rootDir: options.config.rootDir,
1327
1571
  showLuau: options.config.showLuau,
1328
1572
  sourceMapper,
@@ -1337,19 +1581,34 @@ async function execute(options) {
1337
1581
  result
1338
1582
  };
1339
1583
  }
1340
- function buildSourceMapper(config) {
1584
+ function normalizeDirectoryPath(directory) {
1585
+ return path$1.normalize(directory).replaceAll("\\", "/");
1586
+ }
1587
+ function resolveTsconfigDirectories(projectRoot) {
1588
+ const tsconfig = getTsconfig(projectRoot);
1589
+ const outDirectory = tsconfig?.config.compilerOptions?.outDir ?? "out";
1590
+ const rootDirectory = tsconfig?.config.compilerOptions?.rootDir ?? "src";
1591
+ return {
1592
+ outDir: normalizeDirectoryPath(outDirectory),
1593
+ rootDir: normalizeDirectoryPath(rootDirectory)
1594
+ };
1595
+ }
1596
+ function findRojoProject(rootDirectory) {
1597
+ const defaultPath = path$1.join(rootDirectory, "default.project.json");
1598
+ if (fs.existsSync(defaultPath)) return defaultPath;
1599
+ const projectFile = fs.readdirSync(rootDirectory).find((file) => file.endsWith(".project.json"));
1600
+ return projectFile !== void 0 ? path$1.join(rootDirectory, projectFile) : void 0;
1601
+ }
1602
+ function buildSourceMapper(config, tsconfigDirectories) {
1341
1603
  const rojoProjectPath = config.rojoProject ?? findRojoProject(config.rootDir);
1342
- if (rojoProjectPath === void 0) return;
1343
- const tsconfigPath = path$1.join(config.rootDir, "tsconfig.json");
1344
- if (!fs.existsSync(rojoProjectPath) || !fs.existsSync(tsconfigPath)) return;
1604
+ if (rojoProjectPath === void 0 || !fs.existsSync(rojoProjectPath)) return;
1345
1605
  try {
1346
1606
  const rojoResult = rojoProjectSchema(JSON.parse(fs.readFileSync(rojoProjectPath, "utf-8")));
1347
1607
  if (rojoResult instanceof type.errors) return;
1348
- const tsconfigResult = tsconfigSchema(JSON.parse(fs.readFileSync(tsconfigPath, "utf-8")));
1349
1608
  return createSourceMapper({
1350
- outDir: tsconfigResult instanceof type.errors ? "out" : tsconfigResult.compilerOptions?.outDir ?? "out",
1609
+ outDir: tsconfigDirectories.outDir,
1351
1610
  rojoProject: rojoResult,
1352
- rootDir: tsconfigResult instanceof type.errors ? "src" : tsconfigResult.compilerOptions?.rootDir ?? "src"
1611
+ rootDir: tsconfigDirectories.rootDir
1353
1612
  });
1354
1613
  } catch {
1355
1614
  return;
@@ -1360,12 +1619,6 @@ function calculateTestsMs(testResults) {
1360
1619
  for (const file of testResults) for (const test of file.testResults) if (test.duration !== void 0) total += test.duration;
1361
1620
  return total;
1362
1621
  }
1363
- function findRojoProject(rootDirectory) {
1364
- const defaultPath = path$1.join(rootDirectory, "default.project.json");
1365
- if (fs.existsSync(defaultPath)) return defaultPath;
1366
- const projectFile = fs.readdirSync(rootDirectory).find((file) => file.endsWith(".project.json"));
1367
- return projectFile !== void 0 ? path$1.join(rootDirectory, projectFile) : void 0;
1368
- }
1369
1622
  function printLuauTiming(timing) {
1370
1623
  let total = 0;
1371
1624
  for (const [phase, seconds] of Object.entries(timing)) {
@@ -1375,7 +1628,7 @@ function printLuauTiming(timing) {
1375
1628
  }
1376
1629
  process.stderr.write(`[TIMING] total: ${String(total)}ms\n`);
1377
1630
  }
1378
- function writeSnapshots(snapshotWrites, config) {
1631
+ function writeSnapshots(snapshotWrites, config, tsconfigDirectories) {
1379
1632
  const rojoProjectPath = config.rojoProject ?? findRojoProject(config.rootDir);
1380
1633
  if (rojoProjectPath === void 0 || !fs.existsSync(rojoProjectPath)) {
1381
1634
  process.stderr.write("Warning: Cannot write snapshots - no rojo project found\n");
@@ -1387,16 +1640,7 @@ function writeSnapshots(snapshotWrites, config) {
1387
1640
  process.stderr.write("Warning: Cannot write snapshots - invalid rojo project\n");
1388
1641
  return;
1389
1642
  }
1390
- const tsconfigPath = path$1.join(config.rootDir, "tsconfig.json");
1391
- let outDirectory;
1392
- let rootDirectory;
1393
- if (fs.existsSync(tsconfigPath)) {
1394
- const tsconfigResult = tsconfigSchema(JSON.parse(fs.readFileSync(tsconfigPath, "utf-8")));
1395
- if (!(tsconfigResult instanceof type.errors)) {
1396
- outDirectory = tsconfigResult.compilerOptions?.outDir ?? "out";
1397
- rootDirectory = tsconfigResult.compilerOptions?.rootDir ?? "src";
1398
- }
1399
- }
1643
+ const { outDir: outDirectory, rootDir: rootDirectory } = tsconfigDirectories;
1400
1644
  const resolver = createSnapshotPathResolver({
1401
1645
  outDir: outDirectory,
1402
1646
  rojoProject: rojoResult,
@@ -1412,6 +1656,12 @@ function writeSnapshots(snapshotWrites, config) {
1412
1656
  const absolutePath = path$1.resolve(config.rootDir, fsPath);
1413
1657
  fs.mkdirSync(path$1.dirname(absolutePath), { recursive: true });
1414
1658
  fs.writeFileSync(absolutePath, content);
1659
+ if (fsPath.startsWith(`${rootDirectory}/`)) {
1660
+ const outPath = outDirectory + fsPath.slice(rootDirectory.length);
1661
+ const absoluteOutPath = path$1.resolve(config.rootDir, outPath);
1662
+ fs.mkdirSync(path$1.dirname(absoluteOutPath), { recursive: true });
1663
+ fs.writeFileSync(absoluteOutPath, content);
1664
+ }
1415
1665
  written++;
1416
1666
  }
1417
1667
  if (written > 0) process.stderr.write(`Wrote ${String(written)} snapshot file${written === 1 ? "" : "s"}\n`);
@@ -1420,6 +1670,240 @@ function writeSnapshots(snapshotWrites, config) {
1420
1670
  }
1421
1671
  }
1422
1672
 
1673
+ //#endregion
1674
+ //#region src/typecheck/collect.ts
1675
+ const TEST_FUNCTIONS = new Set(["it", "test"]);
1676
+ const SUITE_FUNCTIONS = new Set(["describe", "suite"]);
1677
+ const ALL_FUNCTIONS = new Set([...SUITE_FUNCTIONS, ...TEST_FUNCTIONS]);
1678
+ function collectTestDefinitions(source) {
1679
+ const result = parseSync("test.ts", source);
1680
+ const raw = [];
1681
+ new Visitor({ CallExpression(node) {
1682
+ const definition = extractDefinition(node, source);
1683
+ if (definition) raw.push(definition);
1684
+ } }).visit(result.program);
1685
+ raw.sort((a, b) => a.start - b.start);
1686
+ return buildAncestorChain(raw);
1687
+ }
1688
+ function buildAncestorChain(sorted) {
1689
+ const result = [];
1690
+ const suiteStack = [];
1691
+ for (const definition of sorted) {
1692
+ while (suiteStack.length > 0 && suiteStack.at(-1).end <= definition.start) suiteStack.pop();
1693
+ result.push({
1694
+ ...definition,
1695
+ ancestorNames: suiteStack.map((suite) => suite.name)
1696
+ });
1697
+ if (definition.type === "suite") suiteStack.push({
1698
+ name: definition.name,
1699
+ end: definition.end
1700
+ });
1701
+ }
1702
+ return result;
1703
+ }
1704
+ function isStringLiteral(node) {
1705
+ return node.type === "Literal" && typeof node.value === "string";
1706
+ }
1707
+ function isTemplateLiteral(node) {
1708
+ return node.type === "TemplateLiteral";
1709
+ }
1710
+ function extractStringArgument(node, source) {
1711
+ if (isStringLiteral(node)) return node.value;
1712
+ if (isTemplateLiteral(node)) {
1713
+ if (node.quasis.length === 1 && node.quasis[0] !== void 0) return node.quasis[0].value.raw;
1714
+ return source.slice(node.start + 1, node.end - 1);
1715
+ }
1716
+ return source.slice(node.start, node.end);
1717
+ }
1718
+ function isIdentifier(node) {
1719
+ return node.type === "Identifier";
1720
+ }
1721
+ function isStaticMemberExpression(node) {
1722
+ return node.type === "MemberExpression" && "computed" in node && !node.computed;
1723
+ }
1724
+ function getCalleeName(callee) {
1725
+ if (isIdentifier(callee)) return callee.name;
1726
+ if (isStaticMemberExpression(callee) && isIdentifier(callee.object) && ALL_FUNCTIONS.has(callee.object.name)) return callee.object.name;
1727
+ }
1728
+ function extractDefinition(node, source) {
1729
+ const name = getCalleeName(node.callee);
1730
+ if (name === void 0 || !ALL_FUNCTIONS.has(name)) return;
1731
+ const firstArgument = node.arguments[0];
1732
+ if (firstArgument === void 0 || firstArgument.type === "SpreadElement") return;
1733
+ return {
1734
+ name: extractStringArgument(firstArgument, source),
1735
+ end: node.end,
1736
+ start: node.start,
1737
+ type: TEST_FUNCTIONS.has(name) ? "test" : "suite"
1738
+ };
1739
+ }
1740
+
1741
+ //#endregion
1742
+ //#region src/typecheck/parse.ts
1743
+ const errCodeRegExp = /error TS(?<errCode>\d+)/;
1744
+ function parseTscErrorLine(line) {
1745
+ const parenIndex = line.lastIndexOf("(", line.indexOf("): error TS"));
1746
+ if (parenIndex === -1) return ["", null];
1747
+ const filePath = line.slice(0, parenIndex);
1748
+ const rest = line.slice(parenIndex);
1749
+ const closeParenIndex = rest.indexOf(")");
1750
+ const [lineStr, colStr] = rest.slice(1, closeParenIndex).split(",");
1751
+ if (lineStr === void 0 || lineStr === "" || colStr === void 0 || colStr === "") return [filePath, null];
1752
+ const afterParen = rest.slice(closeParenIndex + 1);
1753
+ const errCodeStr = errCodeRegExp.exec(afterParen)?.groups?.["errCode"];
1754
+ if (errCodeStr === void 0) return [filePath, null];
1755
+ const errCode = Number(errCodeStr);
1756
+ const marker = `error TS${String(errCode)}: `;
1757
+ const markerIndex = afterParen.indexOf(marker);
1758
+ const errMessage = afterParen.slice(markerIndex + marker.length).trim();
1759
+ return [filePath, {
1760
+ column: Number(colStr),
1761
+ errCode,
1762
+ errMsg: errMessage,
1763
+ filePath,
1764
+ line: Number(lineStr)
1765
+ }];
1766
+ }
1767
+ function parseTscOutput(stdout) {
1768
+ const map = /* @__PURE__ */ new Map();
1769
+ const merged = stdout.split(/\r?\n/).reduce((lines, next) => {
1770
+ if (!next) return lines;
1771
+ if (next[0] !== " ") lines.push(next);
1772
+ else if (lines.length > 0) lines[lines.length - 1] += `\n${next}`;
1773
+ return lines;
1774
+ }, []);
1775
+ for (const line of merged) {
1776
+ const [filePath, info] = parseTscErrorLine(line);
1777
+ if (!info) continue;
1778
+ const existing = map.get(filePath);
1779
+ if (existing) existing.push(info);
1780
+ else map.set(filePath, [info]);
1781
+ }
1782
+ return map;
1783
+ }
1784
+
1785
+ //#endregion
1786
+ //#region src/typecheck/runner.ts
1787
+ function createLocationsIndexMap(source) {
1788
+ const map = /* @__PURE__ */ new Map();
1789
+ let index = 0;
1790
+ let line = 1;
1791
+ let column = 1;
1792
+ for (const char of source) {
1793
+ map.set(`${String(line)}:${String(column)}`, index);
1794
+ index++;
1795
+ if (char === "\n") {
1796
+ line++;
1797
+ column = 1;
1798
+ } else column++;
1799
+ }
1800
+ return map;
1801
+ }
1802
+ function mapErrorsToTests(errors, files) {
1803
+ const testResults = [];
1804
+ let numberFailed = 0;
1805
+ let numberPassed = 0;
1806
+ for (const [filePath, fileInfo] of files) {
1807
+ const fileResult = buildFileResult(filePath, fileInfo, errors.get(filePath) ?? []);
1808
+ testResults.push(fileResult);
1809
+ numberFailed += fileResult.numFailingTests;
1810
+ numberPassed += fileResult.numPassingTests;
1811
+ }
1812
+ return {
1813
+ numFailedTests: numberFailed,
1814
+ numPassedTests: numberPassed,
1815
+ numPendingTests: 0,
1816
+ numTotalTests: numberFailed + numberPassed,
1817
+ startTime: Date.now(),
1818
+ success: numberFailed === 0,
1819
+ testResults
1820
+ };
1821
+ }
1822
+ function runTypecheck(options) {
1823
+ const errors = parseTscOutput(spawnTsgo(options));
1824
+ const files = /* @__PURE__ */ new Map();
1825
+ for (const filePath of options.files) {
1826
+ const source = fs.readFileSync(filePath, "utf-8");
1827
+ const definitions = collectTestDefinitions(source);
1828
+ const resolvedPath = path$1.resolve(filePath);
1829
+ files.set(resolvedPath, {
1830
+ definitions,
1831
+ source
1832
+ });
1833
+ }
1834
+ const resolvedErrors = /* @__PURE__ */ new Map();
1835
+ for (const [errorPath, errorList] of errors) {
1836
+ const resolved = path$1.resolve(options.rootDir, errorPath);
1837
+ resolvedErrors.set(resolved, errorList);
1838
+ }
1839
+ return mapErrorsToTests(resolvedErrors, files);
1840
+ }
1841
+ function buildFileResult(filePath, fileInfo, errors) {
1842
+ const indexMap = createLocationsIndexMap(fileInfo.source);
1843
+ const testDefinitions = fileInfo.definitions.filter((definition) => definition.type === "test");
1844
+ const sortedDefinitions = [...testDefinitions].sort((a, b) => b.start - a.start);
1845
+ const errorsByTest = /* @__PURE__ */ new Map();
1846
+ const fileErrors = [];
1847
+ for (const error of errors) {
1848
+ const charIndex = indexMap.get(`${String(error.line)}:${String(error.column)}`);
1849
+ const definition = charIndex !== void 0 ? sortedDefinitions.find((td) => td.start <= charIndex && td.end >= charIndex) : void 0;
1850
+ const message = `TS${String(error.errCode)}: ${error.errMsg}`;
1851
+ if (definition) {
1852
+ const existing = errorsByTest.get(definition.name) ?? [];
1853
+ existing.push(message);
1854
+ errorsByTest.set(definition.name, existing);
1855
+ } else fileErrors.push(message);
1856
+ }
1857
+ const testCases = testDefinitions.map((definition) => {
1858
+ const failures = errorsByTest.get(definition.name) ?? [];
1859
+ return {
1860
+ ancestorTitles: definition.ancestorNames,
1861
+ failureMessages: failures,
1862
+ fullName: [...definition.ancestorNames, definition.name].join(" > "),
1863
+ status: failures.length > 0 ? "failed" : "passed",
1864
+ title: definition.name
1865
+ };
1866
+ });
1867
+ if (fileErrors.length > 0) testCases.unshift({
1868
+ ancestorTitles: [],
1869
+ failureMessages: fileErrors,
1870
+ fullName: "<file-level type error>",
1871
+ status: "failed",
1872
+ title: "<file-level type error>"
1873
+ });
1874
+ const numberFailing = testCases.filter((tc) => tc.status === "failed").length;
1875
+ return {
1876
+ numFailingTests: numberFailing,
1877
+ numPassingTests: testCases.length - numberFailing,
1878
+ numPendingTests: 0,
1879
+ testFilePath: filePath,
1880
+ testResults: testCases
1881
+ };
1882
+ }
1883
+ function spawnTsgo(options) {
1884
+ const args = [
1885
+ "tsgo",
1886
+ "--noEmit",
1887
+ "--pretty",
1888
+ "false"
1889
+ ];
1890
+ if (options.tsconfig !== void 0) args.push("-p", path$1.resolve(options.rootDir, options.tsconfig));
1891
+ try {
1892
+ return execSync(args.join(" "), {
1893
+ cwd: options.rootDir,
1894
+ encoding: "utf-8",
1895
+ stdio: [
1896
+ "ignore",
1897
+ "pipe",
1898
+ "pipe"
1899
+ ]
1900
+ });
1901
+ } catch (err) {
1902
+ const execError = err;
1903
+ return execError.stdout ?? execError.stderr ?? "";
1904
+ }
1905
+ }
1906
+
1423
1907
  //#endregion
1424
1908
  //#region src/utils/game-output.ts
1425
1909
  function formatGameOutputNotice(filePath, entryCount) {
@@ -1442,4 +1926,4 @@ function writeGameOutput(filePath, entries) {
1442
1926
  }
1443
1927
 
1444
1928
  //#endregion
1445
- export { buildJestArgv as _, formatJson as a, extractJsonFromOutput as b, formatResult as c, resolveConfig as d, DEFAULT_CONFIG as f, createOpenCloudBackend as g, OpenCloudBackend as h, execute as i, formatTestSummary as l, createStudioBackend as m, parseGameOutput as n, writeJsonFile as o, StudioBackend as p, writeGameOutput as r, formatFailure as s, formatGameOutputNotice as t, loadConfig as u, generateTestScript as v, parseJestOutput as x, LuauScriptError as y };
1929
+ export { extractJsonFromOutput as C, LuauScriptError as S, createStudioBackend as _, execute as a, buildJestArgv as b, formatFailure as c, loadConfig as d, resolveConfig as f, StudioBackend as g, isValidBackend as h, runTypecheck as i, formatResult as l, VALID_BACKENDS as m, parseGameOutput as n, formatJson as o, DEFAULT_CONFIG as p, writeGameOutput as r, writeJsonFile as s, formatGameOutputNotice as t, formatTestSummary as u, OpenCloudBackend as v, parseJestOutput as w, generateTestScript as x, createOpenCloudBackend as y };