@isentinel/jest-roblox 0.0.8 → 0.1.1

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.
@@ -1,27 +1,30 @@
1
+ import { createRequire } from "node:module";
2
+ import { type } from "arktype";
1
3
  import assert from "node:assert";
2
- import * as fs from "node:fs";
4
+ import * as fs$1 from "node:fs";
3
5
  import { existsSync } from "node:fs";
4
6
  import * as path$1 from "node:path";
5
7
  import path from "node:path";
6
8
  import process from "node:process";
7
9
  import color from "tinyrainbow";
8
10
  import { WebSocketServer } from "ws";
9
- import { type } from "arktype";
11
+ import { createDefineConfig, loadConfig } from "c12";
10
12
  import { homedir, tmpdir } from "node:os";
11
13
  import * as crypto from "node:crypto";
12
14
  import { randomUUID } from "node:crypto";
13
15
  import buffer from "node:buffer";
14
- import { createDefineConfig, loadConfig } from "c12";
16
+ import { defuFn } from "defu";
15
17
  import { TraceMap, originalPositionFor, sourceContentFor } from "@jridgewell/trace-mapping";
16
18
  import { getTsconfig } from "get-tsconfig";
17
- import { execSync } from "node:child_process";
19
+ import { execFileSync } from "node:child_process";
18
20
  import hljs from "highlight.js/lib/core";
19
21
  import typescript from "highlight.js/lib/languages/typescript";
22
+ import { parseJSONC } from "confbox";
20
23
  import { Visitor, parseSync } from "oxc-parser";
21
-
22
24
  //#region src/reporter/parser.ts
23
25
  const TASK_SCRIPT_PREFIX = /^TaskScript:\d+:\s*/;
24
26
  var LuauScriptError = class extends Error {
27
+ gameOutput;
25
28
  constructor(rawMessage) {
26
29
  super(rawMessage.replace(TASK_SCRIPT_PREFIX, ""));
27
30
  }
@@ -175,6 +178,7 @@ function extractSnapshotWrites(parsed) {
175
178
  function stringifyError(err) {
176
179
  if (typeof err === "string") return err;
177
180
  if (typeof err === "object" && err !== null && "message" in err && typeof err.message === "string") return err.message;
181
+ if (typeof err === "object" && err !== null && "kind" in err && err["kind"] === "ExecutionError") return extractExecutionError(err);
178
182
  const serialized = JSON.stringify(err);
179
183
  assert(serialized !== void 0, "JSON-parsed values are always serializable");
180
184
  return serialized;
@@ -189,26 +193,21 @@ function validateJestResult(value) {
189
193
  if (result instanceof type.errors) throw new Error(`Invalid Jest result: ${result.summary}`);
190
194
  return result;
191
195
  }
192
-
193
196
  //#endregion
194
- //#region src/test-runner.bundled.luau
195
- 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}\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\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 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)),\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 -- 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(\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";
196
-
197
- //#endregion
198
- //#region src/test-script.ts
199
- const CLI_KEYS = new Set([
197
+ //#region src/config/schema.ts
198
+ const ROOT_ONLY_KEYS = new Set([
200
199
  "backend",
201
200
  "cache",
202
201
  "collectCoverage",
203
202
  "collectCoverageFrom",
204
- "compact",
205
- "compactMaxFailures",
206
203
  "coverageDirectory",
207
204
  "coveragePathIgnorePatterns",
208
205
  "coverageReporters",
209
206
  "coverageThreshold",
207
+ "formatters",
210
208
  "gameOutput",
211
209
  "jestPath",
210
+ "luauRoots",
212
211
  "placeFile",
213
212
  "pollInterval",
214
213
  "port",
@@ -216,11 +215,208 @@ const CLI_KEYS = new Set([
216
215
  "rootDir",
217
216
  "showLuau",
218
217
  "sourceMap",
219
- "timeout"
218
+ "timeout",
219
+ "typecheck",
220
+ "typecheckOnly",
221
+ "typecheckTsconfig"
220
222
  ]);
223
+ const VALID_BACKENDS = new Set([
224
+ "auto",
225
+ "open-cloud",
226
+ "studio"
227
+ ]);
228
+ function isValidBackend(value) {
229
+ return VALID_BACKENDS.has(value);
230
+ }
231
+ const DEFAULT_CONFIG = {
232
+ backend: "auto",
233
+ cache: true,
234
+ collectCoverage: false,
235
+ color: true,
236
+ coverageDirectory: "coverage",
237
+ coveragePathIgnorePatterns: [
238
+ "**/*.spec.lua",
239
+ "**/*.spec.luau",
240
+ "**/*.test.lua",
241
+ "**/*.test.luau",
242
+ "**/node_modules/**",
243
+ "**/rbxts_include/**"
244
+ ],
245
+ coverageReporters: ["text", "lcov"],
246
+ placeFile: "./game.rbxl",
247
+ pollInterval: 500,
248
+ port: 3001,
249
+ rootDir: process.cwd(),
250
+ showLuau: true,
251
+ silent: false,
252
+ sourceMap: true,
253
+ testMatch: [
254
+ "**/*.spec.ts",
255
+ "**/*.spec.tsx",
256
+ "**/*.test.ts",
257
+ "**/*.test.tsx",
258
+ "**/*.spec-d.ts",
259
+ "**/*.test-d.ts",
260
+ "**/*.spec.lua",
261
+ "**/*.spec.luau",
262
+ "**/*.test.lua",
263
+ "**/*.test.luau"
264
+ ],
265
+ testPathIgnorePatterns: [
266
+ "/node_modules/",
267
+ "/dist/",
268
+ "/out/"
269
+ ],
270
+ timeout: 3e5,
271
+ typecheck: false,
272
+ typecheckOnly: false,
273
+ verbose: false
274
+ };
275
+ const snapshotFormatSchema = type({
276
+ "+": "reject",
277
+ "callToJSON?": "boolean",
278
+ "escapeRegex?": "boolean",
279
+ "escapeString?": "boolean",
280
+ "indent?": "number",
281
+ "maxDepth?": "number",
282
+ "min?": "boolean",
283
+ "printBasicPrototype?": "boolean",
284
+ "printFunctionName?": "boolean"
285
+ });
286
+ const coverageThresholdSchema = type({
287
+ "+": "reject",
288
+ "branches?": "number",
289
+ "functions?": "number",
290
+ "lines?": "number",
291
+ "statements?": "number"
292
+ });
293
+ const displayNameSchema = type({
294
+ "name": "string",
295
+ "+": "reject",
296
+ "color": "string"
297
+ });
298
+ const projectTestConfigSchema = type({
299
+ "+": "reject",
300
+ "automock?": "boolean",
301
+ "clearMocks?": "boolean",
302
+ "displayName": type("string").or(displayNameSchema),
303
+ "exclude?": "string[]",
304
+ "include": "string[]",
305
+ "injectGlobals?": "boolean",
306
+ "mockDataModel?": "boolean",
307
+ "outDir?": "string",
308
+ "resetMocks?": "boolean",
309
+ "resetModules?": "boolean",
310
+ "restoreMocks?": "boolean",
311
+ "root?": "string",
312
+ "setupFiles?": "string[]",
313
+ "setupFilesAfterEnv?": "string[]",
314
+ "slowTestThreshold?": "number",
315
+ "snapshotFormat?": snapshotFormatSchema,
316
+ "snapshotSerializers?": "string[]",
317
+ "testEnvironment?": "string",
318
+ "testEnvironmentOptions?": type("string").or(type("object")),
319
+ "testMatch?": "string[]",
320
+ "testPathIgnorePatterns?": "string[]",
321
+ "testRegex?": type("string").or(type("string[]")),
322
+ "testTimeout?": "number"
323
+ });
324
+ const inlineProjectSchema = type({
325
+ "+": "reject",
326
+ "test": projectTestConfigSchema
327
+ });
328
+ const formatterEntrySchema = type("string").or(type(["string", type("object")]));
329
+ const projectEntrySchema = type("string").or(inlineProjectSchema);
330
+ const configSchema = type({
331
+ "+": "reject",
332
+ "all?": "boolean",
333
+ "automock?": "boolean",
334
+ "backend?": type("'auto'|'open-cloud'|'studio'"),
335
+ "bail?": type("boolean").or(type("number")),
336
+ "cache?": "boolean",
337
+ "changedSince?": "string",
338
+ "ci?": "boolean",
339
+ "clearCache?": "boolean",
340
+ "clearMocks?": "boolean",
341
+ "collectCoverage?": "boolean",
342
+ "collectCoverageFrom?": "string[]",
343
+ "color?": "boolean",
344
+ "colors?": "boolean",
345
+ "config?": "string",
346
+ "coverage?": "boolean",
347
+ "coverageDirectory?": "string",
348
+ "coveragePathIgnorePatterns?": "string[]",
349
+ "coverageReporters?": "string[]",
350
+ "coverageThreshold?": coverageThresholdSchema,
351
+ "debug?": "boolean",
352
+ "env?": "string",
353
+ "expand?": "boolean",
354
+ "formatters?": formatterEntrySchema.array(),
355
+ "gameOutput?": "string",
356
+ "globals?": "string",
357
+ "init?": "boolean",
358
+ "injectGlobals?": "boolean",
359
+ "jestPath?": "string",
360
+ "luauRoots?": "string[]",
361
+ "maxWorkers?": type("number").or(type("string")),
362
+ "noStackTrace?": "boolean",
363
+ "outputFile?": "string",
364
+ "placeFile?": "string",
365
+ "pollInterval?": "number",
366
+ "port?": "number",
367
+ "preset?": "string",
368
+ "projects?": projectEntrySchema.array(),
369
+ "reporters?": "string[]",
370
+ "resetMocks?": "boolean",
371
+ "resetModules?": "boolean",
372
+ "restoreMocks?": "boolean",
373
+ "rojoProject?": "string",
374
+ "rootDir?": "string",
375
+ "roots?": "string[]",
376
+ "runInBand?": "boolean",
377
+ "selectProjects?": "string[]",
378
+ "setupFiles?": "string[]",
379
+ "setupFilesAfterEnv?": "string[]",
380
+ "showConfig?": "boolean",
381
+ "showLuau?": "boolean",
382
+ "silent?": "boolean",
383
+ "snapshotFormat?": snapshotFormatSchema,
384
+ "snapshotSerializers?": "string[]",
385
+ "sourceMap?": "boolean",
386
+ "testEnvironment?": "string",
387
+ "testEnvironmentOptions?": type("string").or(type("object")),
388
+ "testFailureExitCode?": "string",
389
+ "testMatch?": "string[]",
390
+ "testNamePattern?": "string",
391
+ "testPathIgnorePatterns?": "string[]",
392
+ "testPathPattern?": "string",
393
+ "testRegex?": type("string").or(type("string[]")),
394
+ "testTimeout?": "number",
395
+ "timeout?": "number",
396
+ "timers?": "string",
397
+ "typecheck?": "boolean",
398
+ "typecheckOnly?": "boolean",
399
+ "typecheckTsconfig?": "string",
400
+ "updateSnapshot?": "boolean",
401
+ "verbose?": "boolean",
402
+ "version?": "boolean"
403
+ }).as();
404
+ function validateConfig(raw) {
405
+ const result = configSchema(raw);
406
+ if (result instanceof type.errors) throw new Error(`Invalid config: ${result.summary}`);
407
+ return result;
408
+ }
409
+ const defineConfig = createDefineConfig();
410
+ const defineProject = createDefineConfig();
411
+ //#endregion
412
+ //#region src/test-runner.bundled.luau
413
+ 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 __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(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.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 = 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.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\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 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";
414
+ //#endregion
415
+ //#region src/test-script.ts
221
416
  function buildJestArgv(options) {
222
417
  const argv = {};
223
- for (const [key, value] of Object.entries(options.config)) if (!CLI_KEYS.has(key) && value !== void 0) argv[key] = value;
418
+ for (const [key, value] of Object.entries(options.config)) if (!ROOT_ONLY_KEYS.has(key) && value !== void 0) argv[key] = value;
419
+ if (options.config.jestPath !== void 0) argv["jestPath"] = options.config.jestPath;
224
420
  if (process.env["TIMING"] !== void 0) argv["_timing"] = true;
225
421
  if (options.config.collectCoverage) argv["_coverage"] = true;
226
422
  return {
@@ -233,7 +429,6 @@ function generateTestScript(options) {
233
429
  const config = buildJestArgv(options);
234
430
  return test_runner_bundled_default.replace("__CONFIG_JSON__", () => JSON.stringify(config));
235
431
  }
236
-
237
432
  //#endregion
238
433
  //#region src/utils/cache.ts
239
434
  const CACHE_DIR_NAME = "jest-roblox";
@@ -261,7 +456,7 @@ function markUploaded(cache, key, fileHash) {
261
456
  }
262
457
  function readCache(cacheFilePath) {
263
458
  try {
264
- const data = fs.readFileSync(cacheFilePath, "utf-8");
459
+ const data = fs$1.readFileSync(cacheFilePath, "utf-8");
265
460
  return JSON.parse(data);
266
461
  } catch {
267
462
  return {};
@@ -269,16 +464,14 @@ function readCache(cacheFilePath) {
269
464
  }
270
465
  function writeCache(cacheFilePath, cache) {
271
466
  const cacheDirectory = path$1.dirname(cacheFilePath);
272
- fs.mkdirSync(cacheDirectory, { recursive: true });
273
- fs.writeFileSync(cacheFilePath, JSON.stringify(cache, null, 2));
467
+ fs$1.mkdirSync(cacheDirectory, { recursive: true });
468
+ fs$1.writeFileSync(cacheFilePath, JSON.stringify(cache, null, 2));
274
469
  }
275
-
276
470
  //#endregion
277
471
  //#region src/utils/hash.ts
278
472
  function hashBuffer(data) {
279
473
  return crypto.createHash("sha256").update(data).digest("hex");
280
474
  }
281
-
282
475
  //#endregion
283
476
  //#region src/backends/http-client.ts
284
477
  function createFetchClient(defaultHeaders) {
@@ -305,7 +498,6 @@ function createFetchClient(defaultHeaders) {
305
498
  };
306
499
  } };
307
500
  }
308
-
309
501
  //#endregion
310
502
  //#region src/backends/open-cloud.ts
311
503
  const OPEN_CLOUD_BASE_URL = "https://apis.roblox.com";
@@ -325,7 +517,7 @@ var OpenCloudBackend = class {
325
517
  constructor(credentials, options) {
326
518
  this.credentials = credentials;
327
519
  this.http = options?.http ?? createFetchClient({ "x-api-key": credentials.apiKey });
328
- this.readFile = options?.readFile ?? ((filePath) => fs.readFileSync(filePath));
520
+ this.readFile = options?.readFile ?? ((filePath) => fs$1.readFileSync(filePath));
329
521
  this.sleepFn = options?.sleep ?? (async (ms) => {
330
522
  return new Promise((resolve) => {
331
523
  setTimeout(resolve, ms);
@@ -354,7 +546,13 @@ var OpenCloudBackend = class {
354
546
  const taskPath = await this.createExecutionTask(options);
355
547
  const { gameOutput, jestOutput } = await this.pollForCompletion(taskPath, options.config.timeout, options.config.pollInterval);
356
548
  const executionMs = Date.now() - executionStart;
357
- const parsed = parseJestOutput(jestOutput);
549
+ let parsed;
550
+ try {
551
+ parsed = parseJestOutput(jestOutput);
552
+ } catch (err) {
553
+ if (err instanceof LuauScriptError) err.gameOutput = gameOutput;
554
+ throw err;
555
+ }
358
556
  return {
359
557
  coverageData: parsed.coverageData,
360
558
  gameOutput,
@@ -447,7 +645,6 @@ function parseRetryAfter(headers) {
447
645
  if (Number.isNaN(seconds) || seconds <= 0) return RATE_LIMIT_DEFAULT_WAIT_MS;
448
646
  return seconds * 1e3;
449
647
  }
450
-
451
648
  //#endregion
452
649
  //#region src/backends/studio.ts
453
650
  const DEFAULT_STUDIO_TIMEOUT = 3e5;
@@ -484,7 +681,13 @@ var StudioBackend = class {
484
681
  const executionStart = Date.now();
485
682
  const message = await this.waitForResult(wss, requestId, config, existingSocket);
486
683
  const executionMs = Date.now() - executionStart;
487
- const parsed = parseJestOutput(message.jestOutput);
684
+ let parsed;
685
+ try {
686
+ parsed = parseJestOutput(message.jestOutput);
687
+ } catch (err) {
688
+ if (err instanceof LuauScriptError) err.gameOutput = message.gameOutput;
689
+ throw err;
690
+ }
488
691
  return {
489
692
  coverageData: parsed.coverageData,
490
693
  gameOutput: message.gameOutput,
@@ -540,73 +743,8 @@ var StudioBackend = class {
540
743
  function createStudioBackend(options) {
541
744
  return new StudioBackend(options);
542
745
  }
543
-
544
- //#endregion
545
- //#region src/config/schema.ts
546
- const VALID_BACKENDS = new Set([
547
- "auto",
548
- "open-cloud",
549
- "studio"
550
- ]);
551
- function isValidBackend(value) {
552
- return VALID_BACKENDS.has(value);
553
- }
554
- const DEFAULT_CONFIG = {
555
- backend: "auto",
556
- cache: true,
557
- collectCoverage: false,
558
- color: true,
559
- compact: false,
560
- compactMaxFailures: 10,
561
- coverageDirectory: "coverage",
562
- coveragePathIgnorePatterns: [
563
- "**/*.spec.lua",
564
- "**/*.spec.luau",
565
- "**/*.test.lua",
566
- "**/*.test.luau",
567
- "**/node_modules/**",
568
- "**/rbxts_include/**"
569
- ],
570
- coverageReporters: ["text", "lcov"],
571
- json: false,
572
- placeFile: "./game.rbxl",
573
- pollInterval: 500,
574
- port: 3001,
575
- rootDir: process.cwd(),
576
- showLuau: true,
577
- silent: false,
578
- sourceMap: true,
579
- testMatch: [
580
- "**/*.spec.ts",
581
- "**/*.spec.tsx",
582
- "**/*.test.ts",
583
- "**/*.test.tsx",
584
- "**/*.spec-d.ts",
585
- "**/*.test-d.ts",
586
- "**/*.spec.lua",
587
- "**/*.spec.luau",
588
- "**/*.test.lua",
589
- "**/*.test.luau"
590
- ],
591
- testPathIgnorePatterns: [
592
- "/node_modules/",
593
- "/dist/",
594
- "/out/"
595
- ],
596
- timeout: 3e5,
597
- typecheck: false,
598
- typecheckOnly: false,
599
- verbose: false
600
- };
601
- const defineConfig = createDefineConfig();
602
-
603
746
  //#endregion
604
747
  //#region src/config/loader.ts
605
- function validateBackend(config) {
606
- if (config.backend === void 0 || isValidBackend(config.backend)) return;
607
- const valid = [...VALID_BACKENDS].join(", ");
608
- throw new Error(`Invalid backend "${config.backend}" in config file. Must be one of: ${valid}`);
609
- }
610
748
  function applySnapshotFormatDefaults(config, isLuauProject) {
611
749
  if (config.snapshotFormat?.printBasicPrototype !== void 0) return config;
612
750
  return {
@@ -617,19 +755,24 @@ function applySnapshotFormatDefaults(config, isLuauProject) {
617
755
  }
618
756
  };
619
757
  }
620
- /**
621
- * Merges a partial Config with DEFAULT_CONFIG. Useful for external consumers
622
- * who build a Config manually (e.g. from CLI args) without going through
623
- * file-based loadConfig.
624
- */
625
758
  function resolveConfig(config) {
626
- validateBackend(config);
759
+ validateConfig(config);
627
760
  const defined = Object.fromEntries(Object.entries(config).filter(([, value]) => value !== void 0));
628
761
  return Object.assign({}, DEFAULT_CONFIG, defined);
629
762
  }
630
763
  async function loadConfig$1(configPath, cwd = process.cwd()) {
631
764
  let result;
765
+ const extendWarnings = [];
766
+ const originalWarn = console.warn;
632
767
  try {
768
+ console.warn = (...args) => {
769
+ const message = args.join(" ");
770
+ if (typeof message === "string" && message.includes("Cannot extend config")) {
771
+ extendWarnings.push(message);
772
+ return;
773
+ }
774
+ originalWarn.apply(console, args);
775
+ };
633
776
  result = await loadConfig({
634
777
  name: "jest",
635
778
  configFile: configPath,
@@ -637,6 +780,7 @@ async function loadConfig$1(configPath, cwd = process.cwd()) {
637
780
  cwd,
638
781
  dotenv: false,
639
782
  globalRc: false,
783
+ merger,
640
784
  omit$Keys: true,
641
785
  packageJson: false,
642
786
  rcFile: false
@@ -644,11 +788,28 @@ async function loadConfig$1(configPath, cwd = process.cwd()) {
644
788
  } catch (err) {
645
789
  if (configPath !== void 0) throw new Error(`Config file not found: ${configPath}`, { cause: err });
646
790
  throw err;
791
+ } finally {
792
+ console.warn = originalWarn;
793
+ }
794
+ if (extendWarnings.length > 0) {
795
+ const extendsPath = extendWarnings[0]?.match(/`([^`]+)`/)?.[1];
796
+ throw new Error(`Failed to resolve extends: "${extendsPath}". If the file exists, try adding the file extension (e.g. ".ts").`);
647
797
  }
648
- result.config.rootDir ??= cwd;
649
- return resolveConfig(result.config);
798
+ const config = resolveFunctionValues(result.config);
799
+ config.rootDir ??= cwd;
800
+ return resolveConfig(config);
801
+ }
802
+ function merger(...sources) {
803
+ return defuFn(...sources.filter(Boolean));
804
+ }
805
+ function isMergerFunction(value) {
806
+ return typeof value === "function";
807
+ }
808
+ function resolveFunctionValues(config) {
809
+ const resolved = {};
810
+ for (const [key, value] of Object.entries(config)) resolved[key] = isMergerFunction(value) ? value(void 0) : value;
811
+ return resolved;
650
812
  }
651
-
652
813
  //#endregion
653
814
  //#region src/types/rojo.ts
654
815
  const rojoProjectSchema = type({
@@ -656,7 +817,6 @@ const rojoProjectSchema = type({
656
817
  "servePort?": "number.integer",
657
818
  "tree": "object"
658
819
  }).as();
659
-
660
820
  //#endregion
661
821
  //#region src/utils/normalize-windows-path.ts
662
822
  const DRIVE_LETTER_START_REGEX = /^[A-Za-z]:\//;
@@ -664,7 +824,25 @@ function normalizeWindowsPath(input = "") {
664
824
  if (!input) return input;
665
825
  return input.replace(/\\/g, "/").replace(DRIVE_LETTER_START_REGEX, (driveLetterMatch) => driveLetterMatch.toUpperCase());
666
826
  }
667
-
827
+ //#endregion
828
+ //#region src/utils/tsconfig-mapping.ts
829
+ function findMapping(filePath, mappings, key = "outDir") {
830
+ let best;
831
+ let bestLength = -1;
832
+ for (const mapping of mappings) {
833
+ const prefix = mapping[key];
834
+ if ((filePath === prefix || filePath.startsWith(`${prefix}/`)) && prefix.length > bestLength) {
835
+ best = mapping;
836
+ bestLength = prefix.length;
837
+ }
838
+ }
839
+ return best;
840
+ }
841
+ function replacePrefix(filePath, from, to) {
842
+ if (filePath === from) return to;
843
+ if (filePath.startsWith(`${from}/`)) return `${to}${filePath.slice(from.length)}`;
844
+ return filePath;
845
+ }
668
846
  //#endregion
669
847
  //#region src/source-mapper/column-finder.ts
670
848
  /**
@@ -683,29 +861,32 @@ function findExpectationColumn(lineText) {
683
861
  if (lastMatcher === null || matcherName === void 0) return;
684
862
  return expectIndex + (lastMatcher.index + lastMatcher[0].indexOf(matcherName)) + 1;
685
863
  }
686
-
687
864
  //#endregion
688
865
  //#region src/source-mapper/path-resolver.ts
689
866
  function createPathResolver(rojoProject, config) {
690
- const mappings = /* @__PURE__ */ new Map();
867
+ const rojoMappings = /* @__PURE__ */ new Map();
691
868
  function walkTree(tree, prefix) {
692
869
  for (const [key, value] of Object.entries(tree)) {
693
870
  if (key.startsWith("$") || typeof value !== "object") continue;
694
871
  const dataModelPath = prefix ? `${prefix}.${key}` : key;
695
872
  const node = value;
696
- if (typeof node.$path === "string") mappings.set(dataModelPath, node.$path);
873
+ if (typeof node.$path === "string") rojoMappings.set(dataModelPath, node.$path);
697
874
  walkTree(node, dataModelPath);
698
875
  }
699
876
  }
700
877
  walkTree(rojoProject.tree, "");
701
- const outDirectory = config?.outDir;
702
- const rootDirectory = config?.rootDir;
703
- const useTypeScript = outDirectory !== void 0 && rootDirectory !== void 0;
878
+ const tsconfigMappings = config?.mappings ?? [];
879
+ const sortedRojoMappings = [...rojoMappings.entries()].sort(([a], [b]) => b.length - a.length);
704
880
  return { resolve(dataModelPath) {
705
- for (const [prefix, basePath] of mappings) if (dataModelPath.startsWith(prefix)) {
881
+ for (const [prefix, basePath] of sortedRojoMappings) {
882
+ if (dataModelPath !== prefix && !dataModelPath.startsWith(`${prefix}.`)) continue;
706
883
  const result = `${basePath}/${convertToFilePath(dataModelPath.slice(prefix.length + 1))}`;
707
- if (useTypeScript) return `${result.replace(outDirectory, rootDirectory)}.ts`;
708
- return findLuaFile(result);
884
+ const mapping = findMapping(result, tsconfigMappings);
885
+ if (mapping !== void 0) return {
886
+ filePath: `${luauInitToIndex(replacePrefix(result, mapping.outDir, mapping.rootDir).replace(/^\.\//, ""))}.ts`,
887
+ mapping
888
+ };
889
+ return { filePath: findLuaFile(result) };
709
890
  }
710
891
  } };
711
892
  }
@@ -722,6 +903,10 @@ function convertToFilePath(suffix) {
722
903
  }
723
904
  return result.join("/");
724
905
  }
906
+ /** roblox-ts compiles index.ts → init.luau; reverse the rename for TS paths. */
907
+ function luauInitToIndex(filePath) {
908
+ return filePath.replace(/(^|\/)(init)(\.|\/)/, "$1index$3");
909
+ }
725
910
  function findLuaFile(basePath) {
726
911
  const luauPath = `${basePath}.luau`;
727
912
  if (existsSync(luauPath)) return luauPath;
@@ -729,7 +914,6 @@ function findLuaFile(basePath) {
729
914
  if (existsSync(luaPath)) return luaPath;
730
915
  return luauPath;
731
916
  }
732
-
733
917
  //#endregion
734
918
  //#region src/source-mapper/stack-parser.ts
735
919
  const FRAME_REGEX = /\[string "([^"]+)"\]:(\d+)(?::(\d+))?/g;
@@ -749,7 +933,6 @@ function parseStack(input) {
749
933
  message: input.slice(0, firstMatchIndex).trim()
750
934
  };
751
935
  }
752
-
753
936
  //#endregion
754
937
  //#region src/source-mapper/v3-mapper.ts
755
938
  const mapCache = /* @__PURE__ */ new Map();
@@ -772,34 +955,31 @@ function getTraceMap(luauPath) {
772
955
  let traced = mapCache.get(luauPath);
773
956
  if (traced !== void 0) return traced;
774
957
  const mapPath = `${luauPath}.map`;
775
- if (!fs.existsSync(mapPath)) return;
776
- traced = new TraceMap(fs.readFileSync(mapPath, "utf-8"));
958
+ if (!fs$1.existsSync(mapPath)) return;
959
+ traced = new TraceMap(fs$1.readFileSync(mapPath, "utf-8"));
777
960
  mapCache.set(luauPath, traced);
778
961
  return traced;
779
962
  }
780
-
781
963
  //#endregion
782
964
  //#region src/source-mapper/index.ts
783
965
  function createSourceMapper(config) {
784
- const pathResolver = createPathResolver(config.rojoProject, {
785
- outDir: config.outDir,
786
- rootDir: config.rootDir
787
- });
966
+ const pathResolver = createPathResolver(config.rojoProject, { mappings: config.mappings });
788
967
  function mapFrame(frame) {
789
- const resolvedPath = pathResolver.resolve(frame.dataModelPath);
790
- if (resolvedPath === void 0) return;
791
- if (config.outDir === void 0 || config.rootDir === void 0) return {
792
- luauPath: resolvedPath,
968
+ const resolved = pathResolver.resolve(frame.dataModelPath);
969
+ if (resolved === void 0) return;
970
+ if (resolved.mapping === void 0) return {
971
+ luauPath: resolved.filePath,
793
972
  mapped: void 0
794
973
  };
795
- const luauPath = resolvedPath.replace(config.rootDir, config.outDir).replace(/\.ts$/, ".luau");
974
+ const { filePath, mapping } = resolved;
975
+ const luauPath = replacePrefix(filePath, mapping.rootDir, mapping.outDir).replace(/\.ts$/, ".luau");
796
976
  const v3Result = mapFromSourceMap(luauPath, frame.line, frame.column);
797
977
  if (v3Result !== void 0 && v3Result.source !== null && v3Result.line !== null) {
798
978
  const mapDirectory = path$1.dirname(luauPath);
799
979
  const resolvedTsPath = normalizeWindowsPath(path$1.resolve(mapDirectory, v3Result.source));
800
980
  const tsLine = v3Result.line;
801
981
  const embeddedContent = getSourceContent(luauPath, v3Result.source) ?? void 0;
802
- const tsContent = embeddedContent ?? (fs.existsSync(resolvedTsPath) ? fs.readFileSync(resolvedTsPath, "utf-8") : void 0);
982
+ const tsContent = embeddedContent ?? (fs$1.existsSync(resolvedTsPath) ? fs$1.readFileSync(resolvedTsPath, "utf-8") : void 0);
803
983
  return {
804
984
  luauPath,
805
985
  mapped: {
@@ -868,12 +1048,12 @@ function createSourceMapper(config) {
868
1048
  },
869
1049
  resolveTestFilePath(testFilePath) {
870
1050
  const dataModelPath = testFilePath.replace(/^\//, "").replaceAll("/", ".");
871
- return pathResolver.resolve(dataModelPath);
1051
+ return pathResolver.resolve(dataModelPath)?.filePath;
872
1052
  }
873
1053
  };
874
1054
  }
875
1055
  function getSourceSnippet({ column, context = 2, filePath, line, sourceContent }) {
876
- const content = sourceContent ?? (fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf-8") : void 0);
1056
+ const content = sourceContent ?? (fs$1.existsSync(filePath) ? fs$1.readFileSync(filePath, "utf-8") : void 0);
877
1057
  if (content === void 0) return;
878
1058
  const allLines = content.split("\n");
879
1059
  const startLine = Math.max(1, line - context);
@@ -894,13 +1074,11 @@ function getSourceSnippet({ column, context = 2, filePath, line, sourceContent }
894
1074
  lines
895
1075
  };
896
1076
  }
897
-
898
1077
  //#endregion
899
1078
  //#region src/types/jest-result.ts
900
1079
  function hasExecError(file) {
901
1080
  return file.failureMessage !== void 0 && file.failureMessage !== "" && file.testResults.length === 0;
902
1081
  }
903
-
904
1082
  //#endregion
905
1083
  //#region src/utils/banner.ts
906
1084
  const SEPARATOR$1 = "⎯";
@@ -938,7 +1116,6 @@ function formatBanner({ body, level, termWidth, title }) {
938
1116
  function getDefaultWidth() {
939
1117
  return process.stderr.columns || 80;
940
1118
  }
941
-
942
1119
  //#endregion
943
1120
  //#region src/highlighter/luau-grammar.ts
944
1121
  const OPENING_LONG_BRACKET = "\\[=*\\[";
@@ -1003,7 +1180,6 @@ function luauGrammar(hljs) {
1003
1180
  }
1004
1181
  };
1005
1182
  }
1006
-
1007
1183
  //#endregion
1008
1184
  //#region src/utils/colors.ts
1009
1185
  hljs.registerLanguage("luau", luauGrammar);
@@ -1068,7 +1244,6 @@ function highlightLuau(source) {
1068
1244
  function highlightTypeScript(source) {
1069
1245
  return convertHljsToAnsi(hljs.highlight(source, { language: "typescript" }).value).replace(/=>/g, color.yellow("=>"));
1070
1246
  }
1071
-
1072
1247
  //#endregion
1073
1248
  //#region src/formatters/formatter.ts
1074
1249
  const EXEC_ERROR_HINTS = [[/loadstring\(\) is not available/, "loadstring() must be enabled for Jest to run. Add to your project.json:\n\n \"ServerScriptService\": {\n \"$properties\": {\n \"LoadStringEnabled\": true\n }\n }"]];
@@ -1200,7 +1375,7 @@ function formatRunHeader(options, styles) {
1200
1375
  if (options.collectCoverage === true) return `${header}\n${`${st.dim(" Coverage enabled with")} ${st.status.pending("istanbul")}`}\n`;
1201
1376
  return `${header}\n`;
1202
1377
  }
1203
- function formatTestSummary(result, timing, styles) {
1378
+ function formatTestSummary(result, timing, styles, options) {
1204
1379
  const st = styles ?? createStyles(true);
1205
1380
  const lines = [];
1206
1381
  const execErrorFiles = result.testResults.filter(hasExecError).length;
@@ -1222,14 +1397,21 @@ function formatTestSummary(result, timing, styles) {
1222
1397
  if (result.numPendingTests > 0) testParts.push(st.summary.pending(`${result.numPendingTests} skipped`));
1223
1398
  const testTotalLabel = st.dim(`(${result.numTotalTests})`);
1224
1399
  lines.push(`${st.dim(" Tests")} ${testParts.join(" | ")} ${testTotalLabel}`);
1400
+ if (options?.typeErrors !== void 0) {
1401
+ const typeErrorLabel = st.dim("Type Errors");
1402
+ const typeErrorValue = options.typeErrors > 0 ? st.summary.failed(`${options.typeErrors} failed`) : st.dim("no errors");
1403
+ lines.push(`${typeErrorLabel} ${typeErrorValue}`);
1404
+ }
1225
1405
  const startAtStr = new Date(timing.startTime).toLocaleTimeString("en-GB", { hour12: false });
1226
1406
  lines.push(`${st.dim(" Start at")} ${startAtStr}`);
1227
1407
  const environmentMs = timing.executionMs - timing.testsMs;
1228
1408
  const uploadMs = timing.uploadMs ?? 0;
1229
- const cliMs = timing.totalMs - uploadMs - timing.executionMs;
1409
+ const coverageMs = timing.coverageMs ?? 0;
1410
+ const cliMs = Math.max(0, timing.totalMs - uploadMs - timing.executionMs - coverageMs);
1230
1411
  const breakdownParts = [];
1231
1412
  if (timing.uploadMs !== void 0) breakdownParts.push(timing.uploadCached === true ? `upload ${timing.uploadMs}ms (cached)` : `upload ${timing.uploadMs}ms`);
1232
1413
  breakdownParts.push(`environment ${environmentMs}ms`, `tests ${timing.testsMs}ms`, `cli ${cliMs}ms`);
1414
+ if (coverageMs > 0) breakdownParts.push(`coverage ${coverageMs}ms`);
1233
1415
  const breakdown = st.dim(`(${breakdownParts.join(", ")})`);
1234
1416
  lines.push(`${st.dim(" Duration")} ${timing.totalMs}ms ${breakdown}`);
1235
1417
  return lines.join("\n");
@@ -1255,15 +1437,136 @@ function formatResult(result, timing, options) {
1255
1437
  }
1256
1438
  for (const file of execErrors) lines.push(formatExecErrorDetail(file, styles, failureCtx, options.sourceMapper));
1257
1439
  }
1258
- lines.push("", formatTestSummary(result, timing, styles));
1440
+ lines.push("", formatTestSummary(result, timing, styles, { typeErrors: options.typeErrors }));
1259
1441
  if (!result.success) {
1260
1442
  const hints = formatLogHints(options, styles);
1261
1443
  if (hints !== "") lines.push("", hints);
1262
1444
  }
1263
1445
  return lines.join("\n");
1264
1446
  }
1265
- function getTerminalWidth() {
1266
- return "columns" in process.stdout && process.stdout.columns || 80;
1447
+ function formatTypecheckFailures(result, useColor = true) {
1448
+ const styles = createStyles(useColor);
1449
+ const lines = [];
1450
+ for (const file of result.testResults) for (const test of file.testResults) {
1451
+ if (test.status !== "failed") continue;
1452
+ const badge = styles.failBadge(" FAIL ");
1453
+ lines.push(` ${badge} ${styles.status.fail(test.fullName)}`);
1454
+ for (const message of test.failureMessages) lines.push(` ${styles.dim(message)}`);
1455
+ }
1456
+ return lines.join("\n");
1457
+ }
1458
+ function formatTypecheckSummary(result, useColor = true) {
1459
+ const styles = createStyles(useColor);
1460
+ const passed = result.numPassedTests;
1461
+ const failed = result.numFailedTests;
1462
+ const total = result.numTotalTests;
1463
+ const parts = [];
1464
+ if (failed > 0) parts.push(formatTypecheckFailures(result, useColor));
1465
+ const failedLabel = styles.summary.failed(`${String(failed)} failed`);
1466
+ const failedPart = failed > 0 ? `${failedLabel}, ` : "";
1467
+ const passedPart = styles.summary.passed(`${String(passed)} passed`);
1468
+ const label = styles.dim("Type Tests:");
1469
+ parts.push(`\n${label} ${failedPart}${passedPart}, ${String(total)} total\n`);
1470
+ return parts.join("\n");
1471
+ }
1472
+ const PROJECT_BADGE_COLORS = [
1473
+ (text) => color.bgYellow(color.black(text)),
1474
+ (text) => color.bgCyan(color.black(text)),
1475
+ (text) => color.bgGreen(color.black(text)),
1476
+ (text) => color.bgMagenta(color.black(text))
1477
+ ];
1478
+ const NAMED_BADGE_COLORS = {
1479
+ blue: (text) => color.bgBlue(color.white(text)),
1480
+ cyan: (text) => color.bgCyan(color.black(text)),
1481
+ green: (text) => color.bgGreen(color.black(text)),
1482
+ magenta: (text) => color.bgMagenta(color.black(text)),
1483
+ red: (text) => color.bgRed(color.white(text)),
1484
+ white: (text) => color.bgWhite(color.black(text)),
1485
+ yellow: (text) => color.bgYellow(color.black(text))
1486
+ };
1487
+ function formatProjectBadge(displayName, useColor, displayColor) {
1488
+ if (!useColor) return `▶ ${displayName}`;
1489
+ return `▶ ${resolveBadgeColor(displayName, displayColor)(` ${displayName} `)}`;
1490
+ }
1491
+ function formatProjectHeader(options) {
1492
+ const { displayColor, displayName, result, styles: headerStyles, useColor = true } = options;
1493
+ const resolved = headerStyles ?? createStyles(useColor);
1494
+ const stats = computeProjectStats(result);
1495
+ const parts = [];
1496
+ if (stats.passedFiles > 0) parts.push(resolved.summary.passed(`${stats.passedFiles} passed`));
1497
+ if (stats.failedFiles > 0) parts.push(resolved.summary.failed(`${stats.failedFiles} failed`));
1498
+ if (stats.skippedFiles > 0) parts.push(resolved.summary.pending(`${stats.skippedFiles} skipped`));
1499
+ const duration = stats.durationMs > 0 ? ` - ${stats.durationMs}ms` : "";
1500
+ const meta = resolved.dim(`(${stats.totalTests} tests${duration})`);
1501
+ const fileStats = parts.join(" | ");
1502
+ return `${formatProjectBadge(displayName, useColor, displayColor)} ${fileStats} ${meta}`;
1503
+ }
1504
+ function formatProjectSection(section) {
1505
+ const { displayColor, displayName, failureCtx, options, result, styles: sectionStyles } = section;
1506
+ const resolved = sectionStyles ?? createStyles(options.color);
1507
+ const lines = [formatProjectHeader({
1508
+ displayColor,
1509
+ displayName,
1510
+ result,
1511
+ styles: resolved,
1512
+ useColor: options.color
1513
+ })];
1514
+ for (const file of result.testResults) {
1515
+ if (options.failuresOnly === true && file.numFailingTests === 0 && !hasExecError(file)) continue;
1516
+ lines.push(formatFileSummary(file, options, resolved));
1517
+ }
1518
+ const execErrors = result.testResults.filter(hasExecError);
1519
+ if (result.numFailedTests + execErrors.length > 0) {
1520
+ for (const file of result.testResults) {
1521
+ const failures = formatFileFailures(file, options, resolved, failureCtx);
1522
+ if (failures !== "") lines.push(failures);
1523
+ }
1524
+ for (const file of execErrors) lines.push(formatExecErrorDetail(file, resolved, failureCtx, options.sourceMapper));
1525
+ }
1526
+ return lines.join("\n");
1527
+ }
1528
+ function formatMultiProjectResult(projects, timing, options) {
1529
+ const styles = createStyles(options.color);
1530
+ let totalFailures = 0;
1531
+ for (const { result } of projects) {
1532
+ const execErrors = result.testResults.filter(hasExecError).length;
1533
+ totalFailures += result.numFailedTests + execErrors;
1534
+ }
1535
+ const failureCtx = {
1536
+ currentIndex: 1,
1537
+ totalFailures
1538
+ };
1539
+ const sections = [];
1540
+ for (const { displayColor, displayName, result } of projects) sections.push(formatProjectSection({
1541
+ displayColor,
1542
+ displayName,
1543
+ failureCtx,
1544
+ options,
1545
+ result,
1546
+ styles
1547
+ }));
1548
+ const lines = [formatRunHeader(options, styles), sections.join("\n\n")];
1549
+ const mergedResult = mergeJestResults(projects.map((project) => project.result));
1550
+ lines.push("", formatTestSummary(mergedResult, timing, styles, { typeErrors: options.typeErrors }));
1551
+ if (!mergedResult.success) {
1552
+ const hints = formatLogHints(options, styles);
1553
+ if (hints !== "") lines.push("", hints);
1554
+ }
1555
+ return lines.join("\n");
1556
+ }
1557
+ function hashProjectName(name) {
1558
+ let hash = 0;
1559
+ for (let index = 0; index < name.length; index++) hash += name.charCodeAt(index) + index;
1560
+ return hash % PROJECT_BADGE_COLORS.length;
1561
+ }
1562
+ function resolveBadgeColor(displayName, displayColor) {
1563
+ if (displayColor !== void 0) {
1564
+ const named = NAMED_BADGE_COLORS[displayColor];
1565
+ if (named !== void 0) return named;
1566
+ }
1567
+ const hashed = PROJECT_BADGE_COLORS[hashProjectName(displayName)];
1568
+ assert(hashed !== void 0, "hash always returns valid index");
1569
+ return hashed;
1267
1570
  }
1268
1571
  function identity(text) {
1269
1572
  return text;
@@ -1320,171 +1623,51 @@ function createStyles(useColor) {
1320
1623
  }
1321
1624
  };
1322
1625
  }
1323
- function expandTabs(text, tabWidth = 4) {
1324
- let result = "";
1325
- for (const char of text) if (char === " ") {
1326
- const spaces = tabWidth - result.length % tabWidth;
1327
- result += " ".repeat(spaces);
1328
- } else result += char;
1329
- return result;
1626
+ function sumFileDuration(file) {
1627
+ let total = 0;
1628
+ for (const test of file.testResults) if (test.duration !== void 0) total += test.duration;
1629
+ return total;
1330
1630
  }
1331
- function highlightSyntax(filePath, code, useColor) {
1332
- if (!useColor) return code;
1333
- return highlightCode(filePath, code);
1631
+ function computeProjectStats(result) {
1632
+ let durationMs = 0;
1633
+ let failedFiles = 0;
1634
+ let passedFiles = 0;
1635
+ let skippedFiles = 0;
1636
+ for (const file of result.testResults) {
1637
+ if (file.numFailingTests > 0 || hasExecError(file)) failedFiles++;
1638
+ else if (file.numPassingTests === 0 && file.numPendingTests > 0) skippedFiles++;
1639
+ else passedFiles++;
1640
+ durationMs += sumFileDuration(file);
1641
+ }
1642
+ return {
1643
+ durationMs,
1644
+ failedFiles,
1645
+ passedFiles,
1646
+ skippedFiles,
1647
+ totalTests: result.numTotalTests
1648
+ };
1334
1649
  }
1335
- function formatDiffBlock(parsed, styles) {
1336
- if (parsed.snapshotDiff !== void 0) {
1337
- const lines = [""];
1338
- for (const diffLine of parsed.snapshotDiff.split("\n")) if (diffLine.startsWith("- ")) lines.push(styles.diff.expected(diffLine));
1339
- else if (diffLine.startsWith("+ ")) lines.push(styles.diff.received(diffLine));
1340
- else lines.push(styles.dim(diffLine));
1341
- return lines;
1650
+ function formatFileFailures(file, options, styles, failureCtx) {
1651
+ const lines = [];
1652
+ const displayPath = resolveDisplayPath(file.testFilePath, options.sourceMapper);
1653
+ for (const testCase of file.testResults) if (testCase.status === "failed") {
1654
+ const index = failureCtx.currentIndex;
1655
+ failureCtx.currentIndex++;
1656
+ lines.push(formatFailure({
1657
+ failureIndex: index,
1658
+ filePath: displayPath,
1659
+ showLuau: options.showLuau,
1660
+ sourceMapper: options.sourceMapper,
1661
+ styles,
1662
+ test: testCase,
1663
+ totalFailures: failureCtx.totalFailures,
1664
+ useColor: options.color
1665
+ }));
1342
1666
  }
1343
- if (parsed.expected !== void 0 && parsed.received !== void 0) return [
1344
- "",
1345
- styles.diff.expected("- Expected"),
1346
- styles.diff.received("+ Received"),
1347
- "",
1348
- styles.diff.expected(`- ${parsed.expected}`),
1349
- styles.diff.received(`+ ${parsed.received}`)
1350
- ];
1351
- return [];
1667
+ return lines.join("\n");
1352
1668
  }
1353
- function formatErrorLine(parsed, styles, useColor) {
1354
- if (useColor && parsed.message.startsWith("Error:")) return styles.status.fail(color.bold("Error:") + parsed.message.slice(6));
1355
- return styles.status.fail(parsed.message);
1356
- }
1357
- function formatFallbackSnippet(message, styles, useColor) {
1358
- const location = parseSourceLocation(message);
1359
- if (location === void 0) return [];
1360
- const snippet = getSourceSnippet({
1361
- column: location.column,
1362
- context: 2,
1363
- filePath: location.path,
1364
- line: location.line
1365
- });
1366
- if (snippet === void 0) return [];
1367
- return ["", formatSourceSnippet(snippet, location.path, {
1368
- styles,
1369
- useColor
1370
- })];
1371
- }
1372
- function formatMappedLocationSnippets(loc, showLuau, styles, useColor) {
1373
- const snippets = [];
1374
- if (loc.tsPath !== void 0 && loc.tsLine !== void 0) {
1375
- const tsSnippet = getSourceSnippet({
1376
- column: loc.tsColumn,
1377
- context: 2,
1378
- filePath: loc.tsPath,
1379
- line: loc.tsLine,
1380
- sourceContent: loc.sourceContent
1381
- });
1382
- if (tsSnippet !== void 0) {
1383
- const label = showLuau ? "TypeScript" : void 0;
1384
- snippets.push("", formatSourceSnippet(tsSnippet, loc.tsPath, {
1385
- language: label,
1386
- styles,
1387
- useColor
1388
- }));
1389
- }
1390
- if (showLuau) {
1391
- const luauSnippet = getSourceSnippet({
1392
- context: 2,
1393
- filePath: loc.luauPath,
1394
- line: loc.luauLine
1395
- });
1396
- if (luauSnippet !== void 0) snippets.push("", formatSourceSnippet(luauSnippet, loc.luauPath, {
1397
- language: "Luau",
1398
- styles,
1399
- useColor
1400
- }));
1401
- }
1402
- } else {
1403
- const luauSnippet = getSourceSnippet({
1404
- context: 2,
1405
- filePath: loc.luauPath,
1406
- line: loc.luauLine
1407
- });
1408
- if (luauSnippet !== void 0) snippets.push("", formatSourceSnippet(luauSnippet, loc.luauPath, {
1409
- styles,
1410
- useColor
1411
- }));
1412
- }
1413
- return snippets;
1414
- }
1415
- function formatSnapshotCallSnippet(filePath, styles, useColor) {
1416
- if (!fs.existsSync(filePath)) return [];
1417
- const content = fs.readFileSync(filePath, "utf-8");
1418
- const snapshotIndices = content.split("\n").reduce((accumulator, fileLine, index) => {
1419
- if (fileLine.includes("toMatchSnapshot")) accumulator.push(index);
1420
- return accumulator;
1421
- }, []);
1422
- if (snapshotIndices.length !== 1) return [];
1423
- const snippet = getSourceSnippet({
1424
- context: 2,
1425
- filePath,
1426
- line: snapshotIndices[0] + 1,
1427
- sourceContent: content
1428
- });
1429
- if (snippet === void 0) return [];
1430
- return ["", formatSourceSnippet(snippet, filePath, {
1431
- styles,
1432
- useColor
1433
- })];
1434
- }
1435
- function resolveSourceSnippets(options) {
1436
- const { filePath, hasSnapshotDiff, mappedLocations, message, showLuau, sourceMapper, styles, useColor } = options;
1437
- if (mappedLocations.length > 0) return mappedLocations.flatMap((loc) => {
1438
- return formatMappedLocationSnippets(loc, showLuau, styles, useColor);
1439
- });
1440
- const fallback = formatFallbackSnippet(message, styles, useColor);
1441
- if (fallback.length > 0) return fallback;
1442
- if (hasSnapshotDiff && filePath !== void 0) return formatSnapshotCallSnippet(resolveDisplayPath(filePath, sourceMapper), styles, useColor);
1443
- return [];
1444
- }
1445
- function formatFailureMessage(originalMessage, options) {
1446
- const { filePath, showLuau, sourceMapper, styles, useColor } = options;
1447
- let mappedLocations = [];
1448
- let message = originalMessage;
1449
- if (sourceMapper !== void 0) ({locations: mappedLocations, message} = sourceMapper.mapFailureWithLocations(originalMessage));
1450
- const parsed = parseErrorMessage(originalMessage);
1451
- return [
1452
- formatErrorLine(parsed, styles, useColor),
1453
- ...formatDiffBlock(parsed, styles),
1454
- ...resolveSourceSnippets({
1455
- filePath,
1456
- hasSnapshotDiff: parsed.snapshotDiff !== void 0,
1457
- mappedLocations,
1458
- message,
1459
- showLuau,
1460
- sourceMapper,
1461
- styles,
1462
- useColor
1463
- })
1464
- ];
1465
- }
1466
- function formatSnapshotLine(snapshot, styles) {
1467
- if (snapshot === void 0 || snapshot.unmatched === 0) return;
1468
- return `${styles.dim(" Snapshots")} ${styles.summary.failed(`${snapshot.unmatched} failed`)}`;
1469
- }
1470
- function formatFileFailures(file, options, styles, failureCtx) {
1471
- const lines = [];
1472
- const displayPath = resolveDisplayPath(file.testFilePath, options.sourceMapper);
1473
- for (const testCase of file.testResults) if (testCase.status === "failed") {
1474
- const index = failureCtx.currentIndex;
1475
- failureCtx.currentIndex++;
1476
- lines.push(formatFailure({
1477
- failureIndex: index,
1478
- filePath: displayPath,
1479
- showLuau: options.showLuau,
1480
- sourceMapper: options.sourceMapper,
1481
- styles,
1482
- test: testCase,
1483
- totalFailures: failureCtx.totalFailures,
1484
- useColor: options.color
1485
- }));
1486
- }
1487
- return lines.join("\n");
1669
+ function getTerminalWidth() {
1670
+ return "columns" in process.stdout && process.stdout.columns || 80;
1488
1671
  }
1489
1672
  function formatExecErrorDetail(file, styles, failureCtx, sourceMapper) {
1490
1673
  const lines = [];
@@ -1503,12 +1686,6 @@ function formatExecErrorDetail(file, styles, failureCtx, sourceMapper) {
1503
1686
  lines.push("", ` ${separator}`);
1504
1687
  return lines.join("\n");
1505
1688
  }
1506
- function formatLogHints(options, styles) {
1507
- const lines = [];
1508
- if (options.outputFile !== void 0) lines.push(styles.dim(` View ${options.outputFile} for full Jest output`));
1509
- if (options.gameOutput !== void 0) lines.push(styles.dim(` View ${options.gameOutput} for Roblox game logs`));
1510
- return lines.join("\n");
1511
- }
1512
1689
  function formatTestInGroup(testCase, styles) {
1513
1690
  const duration = testCase.duration !== void 0 ? styles.lineNumber(` ${testCase.duration}ms`) : "";
1514
1691
  if (testCase.status === "passed") return `${styles.status.pass(" ✓")}${styles.status.fail(` ${testCase.title}`)}${duration}`;
@@ -1572,11 +1749,6 @@ function formatPass(test, styles) {
1572
1749
  const duration = test.duration !== void 0 ? styles.dim(` ${test.duration}ms`) : "";
1573
1750
  return styles.status.pass(` ✓ ${test.fullName}`) + duration;
1574
1751
  }
1575
- function sumFileDuration(file) {
1576
- let total = 0;
1577
- for (const test of file.testResults) if (test.duration !== void 0) total += test.duration;
1578
- return total;
1579
- }
1580
1752
  function formatPassedFileSummary(file, ctx) {
1581
1753
  const lines = [];
1582
1754
  const fileMs = sumFileDuration(file);
@@ -1603,51 +1775,323 @@ function formatFileSummary(file, options, styles) {
1603
1775
  verbose: options.verbose
1604
1776
  }).join("\n");
1605
1777
  }
1606
-
1778
+ function formatLogHints(options, styles) {
1779
+ const lines = [];
1780
+ if (options.outputFile !== void 0) lines.push(styles.dim(` View ${options.outputFile} for full Jest output`));
1781
+ if (options.gameOutput !== void 0) lines.push(styles.dim(` View ${options.gameOutput} for Roblox game logs`));
1782
+ return lines.join("\n");
1783
+ }
1784
+ function mergeJestResults(results) {
1785
+ let numberFailedTests = 0;
1786
+ let numberPassedTests = 0;
1787
+ let numberPendingTests = 0;
1788
+ let numberTodoTests = 0;
1789
+ let numberTotalTests = 0;
1790
+ let startTime = Number.POSITIVE_INFINITY;
1791
+ let success = true;
1792
+ let snapshotAdded = 0;
1793
+ let snapshotMatched = 0;
1794
+ let snapshotTotal = 0;
1795
+ let snapshotUnmatched = 0;
1796
+ let snapshotUpdated = 0;
1797
+ let hasSnapshot = false;
1798
+ const testResults = [];
1799
+ for (const result of results) {
1800
+ numberFailedTests += result.numFailedTests;
1801
+ numberPassedTests += result.numPassedTests;
1802
+ numberPendingTests += result.numPendingTests;
1803
+ numberTodoTests += result.numTodoTests ?? 0;
1804
+ numberTotalTests += result.numTotalTests;
1805
+ startTime = Math.min(startTime, result.startTime);
1806
+ success &&= result.success;
1807
+ testResults.push(...result.testResults);
1808
+ if (result.snapshot !== void 0) {
1809
+ hasSnapshot = true;
1810
+ snapshotAdded += result.snapshot.added;
1811
+ snapshotMatched += result.snapshot.matched;
1812
+ snapshotTotal += result.snapshot.total;
1813
+ snapshotUnmatched += result.snapshot.unmatched;
1814
+ snapshotUpdated += result.snapshot.updated;
1815
+ }
1816
+ }
1817
+ return {
1818
+ numFailedTests: numberFailedTests,
1819
+ numPassedTests: numberPassedTests,
1820
+ numPendingTests: numberPendingTests,
1821
+ numTodoTests: numberTodoTests > 0 ? numberTodoTests : void 0,
1822
+ numTotalTests: numberTotalTests,
1823
+ snapshot: hasSnapshot ? {
1824
+ added: snapshotAdded,
1825
+ matched: snapshotMatched,
1826
+ total: snapshotTotal,
1827
+ unmatched: snapshotUnmatched,
1828
+ updated: snapshotUpdated
1829
+ } : void 0,
1830
+ startTime,
1831
+ success,
1832
+ testResults
1833
+ };
1834
+ }
1835
+ function expandTabs(text, tabWidth = 4) {
1836
+ let result = "";
1837
+ for (const char of text) if (char === " ") {
1838
+ const spaces = tabWidth - result.length % tabWidth;
1839
+ result += " ".repeat(spaces);
1840
+ } else result += char;
1841
+ return result;
1842
+ }
1843
+ function highlightSyntax(filePath, code, useColor) {
1844
+ if (!useColor) return code;
1845
+ return highlightCode(filePath, code);
1846
+ }
1847
+ function formatDiffBlock(parsed, styles) {
1848
+ if (parsed.snapshotDiff !== void 0) {
1849
+ const lines = [""];
1850
+ for (const diffLine of parsed.snapshotDiff.split("\n")) if (diffLine.startsWith("- ")) lines.push(styles.diff.expected(diffLine));
1851
+ else if (diffLine.startsWith("+ ")) lines.push(styles.diff.received(diffLine));
1852
+ else lines.push(styles.dim(diffLine));
1853
+ return lines;
1854
+ }
1855
+ if (parsed.expected !== void 0 && parsed.received !== void 0) return [
1856
+ "",
1857
+ styles.diff.expected("- Expected"),
1858
+ styles.diff.received("+ Received"),
1859
+ "",
1860
+ styles.diff.expected(`- ${parsed.expected}`),
1861
+ styles.diff.received(`+ ${parsed.received}`)
1862
+ ];
1863
+ return [];
1864
+ }
1865
+ function formatErrorLine(parsed, styles, useColor) {
1866
+ if (useColor && parsed.message.startsWith("Error:")) return styles.status.fail(color.bold("Error:") + parsed.message.slice(6));
1867
+ return styles.status.fail(parsed.message);
1868
+ }
1869
+ function formatFallbackSnippet(message, styles, useColor) {
1870
+ const location = parseSourceLocation(message);
1871
+ if (location === void 0) return [];
1872
+ const snippet = getSourceSnippet({
1873
+ column: location.column,
1874
+ context: 2,
1875
+ filePath: location.path,
1876
+ line: location.line
1877
+ });
1878
+ if (snippet === void 0) return [];
1879
+ return ["", formatSourceSnippet(snippet, location.path, {
1880
+ styles,
1881
+ useColor
1882
+ })];
1883
+ }
1884
+ function formatMappedLocationSnippets(loc, showLuau, styles, useColor) {
1885
+ const snippets = [];
1886
+ if (loc.tsPath !== void 0 && loc.tsLine !== void 0) {
1887
+ const tsSnippet = getSourceSnippet({
1888
+ column: loc.tsColumn,
1889
+ context: 2,
1890
+ filePath: loc.tsPath,
1891
+ line: loc.tsLine,
1892
+ sourceContent: loc.sourceContent
1893
+ });
1894
+ if (tsSnippet !== void 0) {
1895
+ const label = showLuau ? "TypeScript" : void 0;
1896
+ snippets.push("", formatSourceSnippet(tsSnippet, loc.tsPath, {
1897
+ language: label,
1898
+ styles,
1899
+ useColor
1900
+ }));
1901
+ }
1902
+ if (showLuau) {
1903
+ const luauSnippet = getSourceSnippet({
1904
+ context: 2,
1905
+ filePath: loc.luauPath,
1906
+ line: loc.luauLine
1907
+ });
1908
+ if (luauSnippet !== void 0) snippets.push("", formatSourceSnippet(luauSnippet, loc.luauPath, {
1909
+ language: "Luau",
1910
+ styles,
1911
+ useColor
1912
+ }));
1913
+ }
1914
+ } else {
1915
+ const luauSnippet = getSourceSnippet({
1916
+ context: 2,
1917
+ filePath: loc.luauPath,
1918
+ line: loc.luauLine
1919
+ });
1920
+ if (luauSnippet !== void 0) snippets.push("", formatSourceSnippet(luauSnippet, loc.luauPath, {
1921
+ styles,
1922
+ useColor
1923
+ }));
1924
+ }
1925
+ return snippets;
1926
+ }
1927
+ function formatSnapshotCallSnippet(filePath, styles, useColor) {
1928
+ if (!fs$1.existsSync(filePath)) return [];
1929
+ const content = fs$1.readFileSync(filePath, "utf-8");
1930
+ const snapshotIndices = content.split("\n").reduce((accumulator, fileLine, index) => {
1931
+ if (fileLine.includes("toMatchSnapshot")) accumulator.push(index);
1932
+ return accumulator;
1933
+ }, []);
1934
+ if (snapshotIndices.length !== 1) return [];
1935
+ const snippet = getSourceSnippet({
1936
+ context: 2,
1937
+ filePath,
1938
+ line: snapshotIndices[0] + 1,
1939
+ sourceContent: content
1940
+ });
1941
+ if (snippet === void 0) return [];
1942
+ return ["", formatSourceSnippet(snippet, filePath, {
1943
+ styles,
1944
+ useColor
1945
+ })];
1946
+ }
1947
+ function resolveSourceSnippets(options) {
1948
+ const { filePath, hasSnapshotDiff, mappedLocations, message, showLuau, sourceMapper, styles, useColor } = options;
1949
+ if (mappedLocations.length > 0) return mappedLocations.flatMap((loc) => {
1950
+ return formatMappedLocationSnippets(loc, showLuau, styles, useColor);
1951
+ });
1952
+ const fallback = formatFallbackSnippet(message, styles, useColor);
1953
+ if (fallback.length > 0) return fallback;
1954
+ if (hasSnapshotDiff && filePath !== void 0) return formatSnapshotCallSnippet(resolveDisplayPath(filePath, sourceMapper), styles, useColor);
1955
+ return [];
1956
+ }
1957
+ function formatFailureMessage(originalMessage, options) {
1958
+ const { filePath, showLuau, sourceMapper, styles, useColor } = options;
1959
+ let mappedLocations = [];
1960
+ let message = originalMessage;
1961
+ if (sourceMapper !== void 0) ({locations: mappedLocations, message} = sourceMapper.mapFailureWithLocations(originalMessage));
1962
+ const parsed = parseErrorMessage(originalMessage);
1963
+ return [
1964
+ formatErrorLine(parsed, styles, useColor),
1965
+ ...formatDiffBlock(parsed, styles),
1966
+ ...resolveSourceSnippets({
1967
+ filePath,
1968
+ hasSnapshotDiff: parsed.snapshotDiff !== void 0,
1969
+ mappedLocations,
1970
+ message,
1971
+ showLuau,
1972
+ sourceMapper,
1973
+ styles,
1974
+ useColor
1975
+ })
1976
+ ];
1977
+ }
1978
+ function formatSnapshotLine(snapshot, styles) {
1979
+ if (snapshot === void 0 || snapshot.unmatched === 0) return;
1980
+ return `${styles.dim(" Snapshots")} ${styles.summary.failed(`${snapshot.unmatched} failed`)}`;
1981
+ }
1607
1982
  //#endregion
1608
1983
  //#region src/formatters/compact.ts
1609
- function formatCompactSummary(result) {
1610
- const parts = [];
1611
- const execErrorCount = result.testResults.filter(hasExecError).length;
1612
- const failCount = result.numFailedTests + execErrorCount;
1613
- if (result.numPassedTests > 0) parts.push(`PASS ${result.numPassedTests}`);
1614
- if (failCount > 0) parts.push(`FAIL ${failCount}`);
1615
- if (result.numPendingTests > 0) parts.push(`SKIP ${result.numPendingTests}`);
1616
- return parts.join(" | ");
1617
- }
1618
1984
  function formatCompact(result, options) {
1619
- const lines = [formatCompactSummary(result)];
1985
+ const lines = [];
1620
1986
  const execErrors = result.testResults.filter(hasExecError);
1621
1987
  if (result.numFailedTests > 0 || execErrors.length > 0) {
1622
- lines.push("");
1988
+ lines.push(...formatFileHeaders(result, options), "");
1989
+ const totalFailures = result.numFailedTests + execErrors.length;
1990
+ lines.push(`${"⎯".repeat(3)} Failed Tests ${totalFailures} ${"⎯".repeat(3)}`, "");
1623
1991
  if (result.numFailedTests > 0) {
1624
- const failureLines = formatFailures(result, options);
1992
+ const failureLines = formatFailures(result, result.numFailedTests, options);
1625
1993
  lines.push(...failureLines);
1626
1994
  }
1627
- for (const file of execErrors) {
1628
- const relativePath = makeRelative$1(resolveDisplayPath(file.testFilePath, options.sourceMapper), options.rootDir);
1629
- assert(file.failureMessage !== void 0, "exec error files have failureMessage");
1630
- const errorMessage = cleanExecErrorMessage(file.failureMessage);
1631
- lines.push(`[FAIL] ${relativePath} - suite failed to run`, errorMessage);
1632
- const hint = getExecErrorHint(errorMessage);
1633
- if (hint !== void 0) lines.push(`Hint: ${hint}`);
1634
- lines.push("");
1635
- }
1995
+ for (const file of execErrors) lines.push(...formatExecError(file, options));
1636
1996
  const hints = formatCompactLogHints(options);
1637
1997
  if (hints !== "") lines.push(hints);
1638
1998
  }
1999
+ lines.push(...formatSummarySection(result, options));
1639
2000
  return lines.join("\n");
1640
2001
  }
1641
- function formatCompactLogHints(options) {
2002
+ function formatCompactMultiProject(projects, options) {
1642
2003
  const lines = [];
1643
- if (options.outputFile !== void 0) lines.push(`View ${options.outputFile} for full Jest output`);
1644
- if (options.gameOutput !== void 0) lines.push(`View ${options.gameOutput} for Roblox game logs`);
2004
+ for (const { displayName, result } of projects) lines.push(...formatCompactProjectHeader(displayName, result, options));
2005
+ const stats = collectMultiProjectStats(projects);
2006
+ if (stats.totalFailed + stats.allExecErrors.length > 0) lines.push(...formatMultiProjectFailures(projects, stats, options));
2007
+ lines.push(...formatMultiProjectSummary(stats, options));
1645
2008
  return lines.join("\n");
1646
2009
  }
2010
+ function formatTypeErrorLabel(count) {
2011
+ if (count === 0) return "no errors";
2012
+ return `${count} error${count === 1 ? "" : "s"}`;
2013
+ }
2014
+ function formatSummarySection(result, options) {
2015
+ const lines = [];
2016
+ const failedFiles = result.testResults.filter((file) => file.numFailingTests > 0 || hasExecError(file)).length;
2017
+ const passedFiles = result.testResults.filter((file) => file.numFailingTests === 0 && !hasExecError(file)).length;
2018
+ const totalFiles = failedFiles + passedFiles;
2019
+ const fileParts = [];
2020
+ if (failedFiles > 0) fileParts.push(`${failedFiles} failed`);
2021
+ if (passedFiles > 0) fileParts.push(`${passedFiles} passed`);
2022
+ lines.push(` Test Files ${fileParts.join(" | ")} (${totalFiles})`);
2023
+ const testParts = [];
2024
+ if (result.numFailedTests > 0) testParts.push(`${result.numFailedTests} failed`);
2025
+ if (result.numPassedTests > 0) testParts.push(`${result.numPassedTests} passed`);
2026
+ if (result.numPendingTests > 0) testParts.push(`${result.numPendingTests} skipped`);
2027
+ const totalTests = result.numTotalTests;
2028
+ lines.push(` Tests ${testParts.join(" | ")} (${totalTests})`);
2029
+ if (options.typeErrorCount !== void 0) {
2030
+ const typeLabel = formatTypeErrorLabel(options.typeErrorCount);
2031
+ lines.push(`Type Errors ${typeLabel}`);
2032
+ }
2033
+ return lines;
2034
+ }
1647
2035
  function makeRelative$1(filePath, rootDirectory) {
1648
- if (filePath.startsWith(rootDirectory)) return path.relative(rootDirectory, filePath);
2036
+ const normalizedPath = filePath.replaceAll("\\", "/");
2037
+ const normalizedRoot = rootDirectory.replaceAll("\\", "/");
2038
+ if (normalizedPath.startsWith(normalizedRoot)) return path.relative(normalizedRoot, normalizedPath).replaceAll("\\", "/");
1649
2039
  return filePath;
1650
2040
  }
2041
+ function formatFileHeaderExecError(file, options) {
2042
+ return [` ❯ ${makeRelative$1(resolveDisplayPath(file.testFilePath, options.sourceMapper), options.rootDir)} (suite failed to run)`];
2043
+ }
2044
+ function formatFileHeaderFailures(file, options) {
2045
+ const lines = [];
2046
+ const relativePath = makeRelative$1(resolveDisplayPath(file.testFilePath, options.sourceMapper), options.rootDir);
2047
+ const totalTests = file.numFailingTests + file.numPassingTests + file.numPendingTests;
2048
+ const testWord = totalTests === 1 ? "test" : "tests";
2049
+ lines.push(` ❯ ${relativePath} (${totalTests} ${testWord} | ${file.numFailingTests} failed)`);
2050
+ for (const test of file.testResults) if (test.status === "failed") {
2051
+ const duration = test.duration !== void 0 ? ` ${String(test.duration)}ms` : "";
2052
+ lines.push(` × ${test.title}${duration}`);
2053
+ }
2054
+ return lines;
2055
+ }
2056
+ function formatFileHeaders(result, options) {
2057
+ const lines = [];
2058
+ for (const file of result.testResults) {
2059
+ if (hasExecError(file)) {
2060
+ lines.push(...formatFileHeaderExecError(file, options));
2061
+ continue;
2062
+ }
2063
+ if (file.numFailingTests === 0) continue;
2064
+ lines.push(...formatFileHeaderFailures(file, options));
2065
+ }
2066
+ return lines;
2067
+ }
2068
+ function formatExecError(file, options) {
2069
+ const lines = [];
2070
+ const relativePath = makeRelative$1(resolveDisplayPath(file.testFilePath, options.sourceMapper), options.rootDir);
2071
+ assert(file.failureMessage !== void 0, "exec error files have failureMessage");
2072
+ const errorMessage = cleanExecErrorMessage(file.failureMessage);
2073
+ lines.push(` FAIL ${relativePath}`, errorMessage);
2074
+ const hint = getExecErrorHint(errorMessage);
2075
+ if (hint !== void 0) lines.push(`Hint: ${hint}`);
2076
+ lines.push("");
2077
+ return lines;
2078
+ }
2079
+ function formatSize(bytes) {
2080
+ if (bytes < 1024) return `${bytes}b`;
2081
+ return `${Math.round(bytes / 1024)}kb`;
2082
+ }
2083
+ function formatCompactLogHints(options) {
2084
+ const lines = [];
2085
+ if (options.outputFile !== void 0) {
2086
+ const size = options.outputFileSize !== void 0 ? ` (${formatSize(options.outputFileSize)})` : "";
2087
+ lines.push(`View ${options.outputFile} for full Jest output${size}`);
2088
+ }
2089
+ if (options.gameOutput !== void 0) {
2090
+ const size = options.gameOutputSize !== void 0 ? ` (${formatSize(options.gameOutputSize)})` : "";
2091
+ lines.push(`View ${options.gameOutput} for Roblox game logs${size}`);
2092
+ }
2093
+ return lines.join("\n");
2094
+ }
1651
2095
  function collectFailedTests(result, sourceMapper) {
1652
2096
  const failures = [];
1653
2097
  for (const file of result.testResults) {
@@ -1659,6 +2103,11 @@ function collectFailedTests(result, sourceMapper) {
1659
2103
  }
1660
2104
  return failures;
1661
2105
  }
2106
+ function getSnippetLevel(totalFailures) {
2107
+ if (totalFailures <= 2) return "both";
2108
+ if (totalFailures <= 5) return "ts-only";
2109
+ return "none";
2110
+ }
1662
2111
  function findFailureLocation(mappedLocations, message) {
1663
2112
  if (mappedLocations.length > 0) {
1664
2113
  const loc = mappedLocations[0];
@@ -1674,37 +2123,74 @@ function findFailureLocation(mappedLocations, message) {
1674
2123
  }
1675
2124
  return parseSourceLocation(message);
1676
2125
  }
1677
- function getFailureSnippet(mappedLocations, location) {
1678
- let snippet;
1679
- if (mappedLocations.length > 0) {
1680
- const loc = mappedLocations[0];
1681
- assert(loc !== void 0, "array with length > 0 has element 0");
1682
- if (loc.tsPath !== void 0 && loc.tsLine !== void 0) snippet = getSourceSnippet({
1683
- column: loc.tsColumn,
1684
- context: 1,
1685
- filePath: loc.tsPath,
1686
- line: loc.tsLine,
1687
- sourceContent: loc.sourceContent
1688
- });
1689
- else snippet = getSourceSnippet({
2126
+ function formatSnippetBlock(snippetResult) {
2127
+ if (snippetResult === void 0) return;
2128
+ const lines = [];
2129
+ for (const line of snippetResult.lines) {
2130
+ const prefix = line.num === snippetResult.failureLine ? ">" : " ";
2131
+ lines.push(`${prefix} ${line.num}| ${line.content}`);
2132
+ }
2133
+ return lines.join("\n");
2134
+ }
2135
+ function getTsSnippets(loc, snippetLevel, rootDirectory) {
2136
+ assert(loc.tsPath !== void 0 && loc.tsLine !== void 0, "caller checked ts fields");
2137
+ const result = [];
2138
+ const tsSnippet = formatSnippetBlock(getSourceSnippet({
2139
+ column: loc.tsColumn,
2140
+ context: 1,
2141
+ filePath: loc.tsPath,
2142
+ line: loc.tsLine,
2143
+ sourceContent: loc.sourceContent
2144
+ }));
2145
+ if (tsSnippet !== void 0) {
2146
+ const relativeTsPath = makeRelative$1(loc.tsPath, rootDirectory);
2147
+ const label = snippetLevel === "both" ? `TS ${relativeTsPath}:${loc.tsLine}\n` : "";
2148
+ result.push(`${label}${tsSnippet}`);
2149
+ }
2150
+ if (snippetLevel === "both") {
2151
+ const luauSnippet = formatSnippetBlock(getSourceSnippet({
1690
2152
  context: 1,
1691
2153
  filePath: loc.luauPath,
1692
2154
  line: loc.luauLine
1693
- });
1694
- } else if (location !== void 0) snippet = getSourceSnippet({
2155
+ }));
2156
+ if (luauSnippet !== void 0) {
2157
+ const relativeLuauPath = makeRelative$1(loc.luauPath, rootDirectory);
2158
+ result.push(`Luau ${relativeLuauPath}:${loc.luauLine}\n${luauSnippet}`);
2159
+ }
2160
+ }
2161
+ return result;
2162
+ }
2163
+ function getLuauOnlySnippet(loc) {
2164
+ const snippet = formatSnippetBlock(getSourceSnippet({
2165
+ context: 1,
2166
+ filePath: loc.luauPath,
2167
+ line: loc.luauLine
2168
+ }));
2169
+ return snippet !== void 0 ? [snippet] : [];
2170
+ }
2171
+ function getMappedSnippets(loc, snippetLevel, rootDirectory) {
2172
+ if (loc.tsPath !== void 0 && loc.tsLine !== void 0) return getTsSnippets(loc, snippetLevel, rootDirectory);
2173
+ return getLuauOnlySnippet(loc);
2174
+ }
2175
+ function getFallbackSnippet(location) {
2176
+ const snippet = formatSnippetBlock(getSourceSnippet({
1695
2177
  context: 1,
1696
2178
  filePath: location.path,
1697
2179
  line: location.line
1698
- });
1699
- if (snippet === void 0) return;
1700
- const snippetLines = [];
1701
- for (const line of snippet.lines) {
1702
- const prefix = line.num === snippet.failureLine ? ">" : " ";
1703
- snippetLines.push(`${prefix} ${line.num}| ${line.content}`);
2180
+ }));
2181
+ return snippet !== void 0 ? [snippet] : [];
2182
+ }
2183
+ function getFailureSnippets(mappedLocations, location, snippetLevel, rootDirectory) {
2184
+ if (snippetLevel === "none") return [];
2185
+ if (mappedLocations.length > 0) {
2186
+ const loc = mappedLocations[0];
2187
+ assert(loc !== void 0, "array with length > 0 has element 0");
2188
+ return getMappedSnippets(loc, snippetLevel, rootDirectory);
1704
2189
  }
1705
- return snippetLines.join("\n");
2190
+ if (location !== void 0) return getFallbackSnippet(location);
2191
+ return [];
1706
2192
  }
1707
- function formatCompactFailure(test, filePath, options) {
2193
+ function formatCompactFailure(test, filePath, options, snippetLevel) {
1708
2194
  const lines = [];
1709
2195
  for (const originalMessage of test.failureMessages) {
1710
2196
  let mappedLocations = [];
@@ -1714,28 +2200,97 @@ function formatCompactFailure(test, filePath, options) {
1714
2200
  const location = findFailureLocation(mappedLocations, message);
1715
2201
  const relativePath = makeRelative$1(location?.path ?? filePath, options.rootDir);
1716
2202
  const lineInfo = location?.line !== void 0 ? `:${location.line}` : "";
1717
- lines.push(`[FAIL] ${relativePath}${lineInfo} - ${test.title}`);
2203
+ const ancestors = test.ancestorTitles.length > 0 ? ` > ${test.ancestorTitles.join(" > ")}` : "";
2204
+ lines.push(` FAIL ${relativePath}${lineInfo}${ancestors} > ${test.title}`);
1718
2205
  if (parsed.snapshotDiff !== void 0) lines.push(parsed.snapshotDiff);
1719
- else if (parsed.expected !== void 0 && parsed.received !== void 0) lines.push(`expect: ${parsed.expected}`, `actual: ${parsed.received}`);
1720
- const snippet = getFailureSnippet(mappedLocations, location);
1721
- if (snippet !== void 0) lines.push(snippet);
2206
+ else if (parsed.expected !== void 0 && parsed.received !== void 0) lines.push(`Expected: ${parsed.expected}`, `Received: ${parsed.received}`);
2207
+ const snippets = getFailureSnippets(mappedLocations, location, snippetLevel, options.rootDir);
2208
+ for (const snippet of snippets) lines.push(snippet);
1722
2209
  lines.push("");
1723
2210
  }
1724
2211
  return lines.join("\n");
1725
2212
  }
1726
- function formatFailures(result, options) {
2213
+ function formatFailures(result, totalFailures, options) {
1727
2214
  const lines = [];
1728
2215
  const failures = collectFailedTests(result, options.sourceMapper);
2216
+ const snippetLevel = getSnippetLevel(totalFailures);
1729
2217
  for (const [index, { filePath, test }] of failures.entries()) {
1730
2218
  if (index >= options.maxFailures) {
1731
- lines.push(`\n... ${result.numFailedTests - index} more failures omitted`);
2219
+ lines.push(`... ${result.numFailedTests - index} more failures omitted`, "");
1732
2220
  break;
1733
2221
  }
1734
- lines.push(formatCompactFailure(test, filePath, options));
2222
+ lines.push(formatCompactFailure(test, filePath, options, snippetLevel));
1735
2223
  }
1736
2224
  return lines;
1737
2225
  }
1738
-
2226
+ function formatCompactProjectHeader(displayName, result, options) {
2227
+ const execErrors = result.testResults.filter(hasExecError);
2228
+ const hasFailures = result.numFailedTests > 0 || execErrors.length > 0;
2229
+ const failedFiles = result.testResults.filter((file) => file.numFailingTests > 0 || hasExecError(file)).length;
2230
+ const skippedFiles = result.testResults.filter((file) => file.numFailingTests === 0 && file.numPassingTests === 0 && !hasExecError(file)).length;
2231
+ const passedFiles = result.testResults.length - failedFiles - skippedFiles;
2232
+ const fileParts = [];
2233
+ if (passedFiles > 0) fileParts.push(`${passedFiles} passed`);
2234
+ if (failedFiles > 0) fileParts.push(`${failedFiles} failed`);
2235
+ if (skippedFiles > 0) fileParts.push(`${skippedFiles} skipped`);
2236
+ const lines = [`▶ ${displayName} ${fileParts.join(" | ")} (${result.numTotalTests} tests)`];
2237
+ if (hasFailures) lines.push(...formatFileHeaders(result, options));
2238
+ return lines;
2239
+ }
2240
+ function collectMultiProjectStats(projects) {
2241
+ const stats = {
2242
+ allExecErrors: [],
2243
+ totalFailed: 0,
2244
+ totalFailedFiles: 0,
2245
+ totalPassed: 0,
2246
+ totalPassedFiles: 0,
2247
+ totalPending: 0,
2248
+ totalSkippedFiles: 0,
2249
+ totalTests: 0
2250
+ };
2251
+ for (const { result } of projects) {
2252
+ const failedFiles = result.testResults.filter((file) => file.numFailingTests > 0 || hasExecError(file)).length;
2253
+ const skippedFiles = result.testResults.filter((file) => file.numFailingTests === 0 && file.numPassingTests === 0 && !hasExecError(file)).length;
2254
+ stats.totalFailed += result.numFailedTests;
2255
+ stats.totalPassed += result.numPassedTests;
2256
+ stats.totalPending += result.numPendingTests;
2257
+ stats.totalTests += result.numTotalTests;
2258
+ stats.totalFailedFiles += failedFiles;
2259
+ stats.totalSkippedFiles += skippedFiles;
2260
+ stats.totalPassedFiles += result.testResults.length - failedFiles - skippedFiles;
2261
+ stats.allExecErrors.push(...result.testResults.filter(hasExecError));
2262
+ }
2263
+ return stats;
2264
+ }
2265
+ function formatMultiProjectFailures(projects, stats, options) {
2266
+ const totalFailures = stats.totalFailed + stats.allExecErrors.length;
2267
+ const lines = [
2268
+ "",
2269
+ `${"⎯".repeat(3)} Failed Tests ${totalFailures} ${"⎯".repeat(3)}`,
2270
+ ""
2271
+ ];
2272
+ for (const { result } of projects) if (result.numFailedTests > 0) lines.push(...formatFailures(result, totalFailures, options));
2273
+ for (const file of stats.allExecErrors) lines.push(...formatExecError(file, options));
2274
+ const hints = formatCompactLogHints(options);
2275
+ if (hints !== "") lines.push(hints);
2276
+ return lines;
2277
+ }
2278
+ function formatMultiProjectSummary(stats, options) {
2279
+ const lines = [];
2280
+ const fileParts = [];
2281
+ if (stats.totalFailedFiles > 0) fileParts.push(`${stats.totalFailedFiles} failed`);
2282
+ if (stats.totalPassedFiles > 0) fileParts.push(`${stats.totalPassedFiles} passed`);
2283
+ if (stats.totalSkippedFiles > 0) fileParts.push(`${stats.totalSkippedFiles} skipped`);
2284
+ const totalFiles = stats.totalFailedFiles + stats.totalPassedFiles + stats.totalSkippedFiles;
2285
+ lines.push(` Test Files ${fileParts.join(" | ")} (${totalFiles})`);
2286
+ const testParts = [];
2287
+ if (stats.totalFailed > 0) testParts.push(`${stats.totalFailed} failed`);
2288
+ if (stats.totalPassed > 0) testParts.push(`${stats.totalPassed} passed`);
2289
+ if (stats.totalPending > 0) testParts.push(`${stats.totalPending} skipped`);
2290
+ lines.push(` Tests ${testParts.join(" | ")} (${stats.totalTests})`);
2291
+ if (options.typeErrorCount !== void 0) lines.push(`Type Errors ${formatTypeErrorLabel(options.typeErrorCount)}`);
2292
+ return lines;
2293
+ }
1739
2294
  //#endregion
1740
2295
  //#region src/formatters/json.ts
1741
2296
  function formatJson(result) {
@@ -1744,21 +2299,37 @@ function formatJson(result) {
1744
2299
  async function writeJsonFile(result, filePath) {
1745
2300
  const absolutePath = path$1.resolve(filePath);
1746
2301
  const directoryPath = path$1.dirname(absolutePath);
1747
- if (!fs.existsSync(directoryPath)) fs.mkdirSync(directoryPath, { recursive: true });
1748
- fs.writeFileSync(absolutePath, formatJson(result), "utf8");
2302
+ if (!fs$1.existsSync(directoryPath)) fs$1.mkdirSync(directoryPath, { recursive: true });
2303
+ fs$1.writeFileSync(absolutePath, formatJson(result), "utf8");
2304
+ }
2305
+ //#endregion
2306
+ //#region src/formatters/utils.ts
2307
+ /**
2308
+ * Find the options object for a named formatter in a resolved formatter list.
2309
+ * Returns `{}` if the formatter is present without options, or `undefined` if absent.
2310
+ */
2311
+ function findFormatterOptions(formatters, name) {
2312
+ for (const entry of formatters) {
2313
+ if (entry === name) return {};
2314
+ if (Array.isArray(entry) && entry[0] === name) return entry[1];
2315
+ }
1749
2316
  }
1750
-
1751
2317
  //#endregion
1752
2318
  //#region src/snapshot/path-resolver.ts
1753
2319
  function createSnapshotPathResolver(config) {
1754
- const mappings = buildMappings(config.rojoProject.tree, "");
2320
+ const rojoMappings = buildMappings(config.rojoProject.tree, "");
2321
+ const tsconfigMappings = config.mappings ?? [];
1755
2322
  return { resolve(virtualPath) {
1756
2323
  const normalized = virtualPath.replaceAll("\\", "/");
1757
- for (const [prefix, basePath] of mappings) {
2324
+ for (const [prefix, basePath] of rojoMappings) {
1758
2325
  if (!normalized.startsWith(`${prefix}/`) && normalized !== prefix) continue;
1759
- let result = `${basePath}/${normalized.slice(prefix.length + 1)}`;
1760
- if (config.outDir !== void 0 && config.rootDir !== void 0 && result.startsWith(`${config.outDir}/`)) result = config.rootDir + result.slice(config.outDir.length);
1761
- return result;
2326
+ const result = `${basePath}/${normalized.slice(prefix.length + 1)}`;
2327
+ const mapping = findMapping(result, tsconfigMappings);
2328
+ if (mapping !== void 0) return {
2329
+ filePath: replacePrefix(result, mapping.outDir, mapping.rootDir).replace(/^\.\//, ""),
2330
+ mapping
2331
+ };
2332
+ return { filePath: result };
1762
2333
  }
1763
2334
  } };
1764
2335
  }
@@ -1774,41 +2345,76 @@ function buildMappings(tree, prefix) {
1774
2345
  mappings.sort((a, b) => b[0].length - a[0].length);
1775
2346
  return mappings;
1776
2347
  }
1777
-
1778
2348
  //#endregion
1779
2349
  //#region src/executor.ts
1780
- function isLuauProject(testFiles, tsconfigDirectories) {
1781
- if (tsconfigDirectories.outDir !== void 0) return false;
2350
+ function isLuauProject(testFiles, tsconfigMappings) {
2351
+ if (tsconfigMappings.length > 0) return false;
1782
2352
  if (testFiles.some((file) => /\.tsx?$/.test(file))) return false;
1783
2353
  return true;
1784
2354
  }
1785
- function resolveTsconfigDirectories(projectRoot) {
1786
- const tsconfig = getTsconfig(projectRoot);
1787
- const tsconfigDirectory = tsconfig !== null ? path$1.dirname(path$1.resolve(tsconfig.path)) : void 0;
2355
+ function resolveAllTsconfigMappings(projectRoot) {
1788
2356
  const resolvedRoot = path$1.resolve(projectRoot);
1789
- if (!(tsconfigDirectory?.startsWith(resolvedRoot) === true) || tsconfig?.config.compilerOptions === void 0) return {
1790
- outDir: void 0,
1791
- rootDir: void 0
1792
- };
1793
- const outDirectory = tsconfig.config.compilerOptions.outDir ?? "out";
1794
- const rootDirectory = tsconfig.config.compilerOptions.rootDir ?? "src";
1795
- return {
1796
- outDir: normalizeDirectoryPath(outDirectory),
1797
- rootDir: normalizeDirectoryPath(rootDirectory)
1798
- };
2357
+ let files;
2358
+ try {
2359
+ files = fs$1.readdirSync(resolvedRoot).filter((file) => /^tsconfig.*\.json$/i.test(file));
2360
+ } catch {
2361
+ return [];
2362
+ }
2363
+ const seen = /* @__PURE__ */ new Set();
2364
+ const mappings = [];
2365
+ for (const file of files) {
2366
+ const compilerOptions = getTsconfig(resolvedRoot, file)?.config.compilerOptions;
2367
+ if (compilerOptions?.outDir === void 0) continue;
2368
+ const parsed = parseTsconfigMappings(compilerOptions);
2369
+ for (const entry of parsed) {
2370
+ const key = `${entry.outDir}:${entry.rootDir}`;
2371
+ if (!seen.has(key)) {
2372
+ seen.add(key);
2373
+ mappings.push(entry);
2374
+ }
2375
+ }
2376
+ }
2377
+ mappings.sort((a, b) => b.outDir.length - a.outDir.length);
2378
+ return mappings;
2379
+ }
2380
+ function formatExecuteOutput(options) {
2381
+ const { config, result, sourceMapper, timing, version } = options;
2382
+ if (config.silent) return "";
2383
+ const resolvedOutputFile = config.outputFile !== void 0 ? path$1.resolve(config.outputFile) : void 0;
2384
+ const resolvedGameOutput = config.gameOutput !== void 0 ? path$1.resolve(config.gameOutput) : void 0;
2385
+ const agentOptions = findFormatterOptions(config.formatters ?? [], "agent");
2386
+ if (agentOptions !== void 0 && !config.verbose) return formatCompact(result, {
2387
+ gameOutput: resolvedGameOutput,
2388
+ maxFailures: agentOptions.maxFailures ?? 10,
2389
+ outputFile: resolvedOutputFile,
2390
+ rootDir: config.rootDir,
2391
+ sourceMapper
2392
+ });
2393
+ if (findFormatterOptions(config.formatters ?? [], "json") !== void 0) return formatJson(result);
2394
+ return formatResult(result, timing, {
2395
+ collectCoverage: config.collectCoverage,
2396
+ color: config.color,
2397
+ gameOutput: resolvedGameOutput,
2398
+ outputFile: resolvedOutputFile,
2399
+ rootDir: config.rootDir,
2400
+ showLuau: config.showLuau,
2401
+ sourceMapper,
2402
+ verbose: config.verbose,
2403
+ version
2404
+ });
1799
2405
  }
1800
2406
  async function execute(options) {
1801
2407
  const startTime = Date.now();
1802
- const tsconfigDirectories = resolveTsconfigDirectories(options.config.rootDir);
1803
- const luauProject = isLuauProject(options.testFiles, tsconfigDirectories);
2408
+ const tsconfigMappings = resolveAllTsconfigMappings(options.config.rootDir);
2409
+ const luauProject = isLuauProject(options.testFiles, tsconfigMappings);
1804
2410
  const config = applySnapshotFormatDefaults(options.config, luauProject);
1805
2411
  const { coverageData, gameOutput, luauTiming, result, snapshotWrites, timing: backendTiming } = await options.backend.runTests({
1806
2412
  config,
1807
2413
  testFiles: options.testFiles
1808
2414
  });
1809
- if (snapshotWrites !== void 0) writeSnapshots(snapshotWrites, config, tsconfigDirectories);
2415
+ if (snapshotWrites !== void 0) writeSnapshots(snapshotWrites, config, tsconfigMappings);
1810
2416
  const testsMs = calculateTestsMs(result.testResults);
1811
- const sourceMapper = config.sourceMap ? buildSourceMapper(config, tsconfigDirectories) : void 0;
2417
+ const sourceMapper = config.sourceMap ? buildSourceMapper(config, tsconfigMappings) : void 0;
1812
2418
  resolveTestFilePaths(result, sourceMapper);
1813
2419
  const totalMs = Date.now() - startTime;
1814
2420
  const timing = {
@@ -1819,29 +2425,13 @@ async function execute(options) {
1819
2425
  uploadCached: backendTiming.uploadCached,
1820
2426
  uploadMs: backendTiming.uploadMs
1821
2427
  };
1822
- const resolvedOutputFile = config.outputFile !== void 0 ? path$1.resolve(config.outputFile) : void 0;
1823
- const resolvedGameOutput = config.gameOutput !== void 0 ? path$1.resolve(config.gameOutput) : void 0;
1824
- let output = "";
1825
- if (!config.silent) if (config.compact && !config.verbose) output = formatCompact(result, {
1826
- gameOutput: resolvedGameOutput,
1827
- maxFailures: config.compactMaxFailures,
1828
- outputFile: resolvedOutputFile,
1829
- rootDir: config.rootDir,
1830
- sourceMapper
1831
- });
1832
- else if (config.json) output = formatJson(result);
1833
- else output = formatResult(result, timing, {
1834
- collectCoverage: config.collectCoverage,
1835
- color: config.color,
1836
- failuresOnly: config.compact && config.verbose,
1837
- gameOutput: resolvedGameOutput,
1838
- outputFile: resolvedOutputFile,
1839
- rootDir: config.rootDir,
1840
- showLuau: config.showLuau,
2428
+ const output = options.deferFormatting !== true ? formatExecuteOutput({
2429
+ config,
2430
+ result,
1841
2431
  sourceMapper,
1842
- verbose: config.verbose && !config.compact,
2432
+ timing,
1843
2433
  version: options.version
1844
- });
2434
+ }) : "";
1845
2435
  if (luauTiming !== void 0) printLuauTiming(luauTiming);
1846
2436
  return {
1847
2437
  coverageData,
@@ -1849,28 +2439,46 @@ async function execute(options) {
1849
2439
  gameOutput,
1850
2440
  output,
1851
2441
  result,
1852
- sourceMapper
2442
+ sourceMapper,
2443
+ timing
1853
2444
  };
1854
2445
  }
1855
2446
  function normalizeDirectoryPath(directory) {
1856
2447
  return path$1.normalize(directory).replaceAll("\\", "/");
1857
2448
  }
2449
+ function parseTsconfigMappings(options) {
2450
+ const outDirectory = normalizeDirectoryPath(options.outDir ?? "out");
2451
+ if (options.rootDirs !== void 0 && options.rootDirs.length > 0) return [{
2452
+ outDir: outDirectory,
2453
+ rootDir: options.rootDirs.map((directory) => normalizeDirectoryPath(directory)).reduce((ancestor, directory) => {
2454
+ const parts = ancestor.split("/");
2455
+ const directoryParts = directory.split("/");
2456
+ let common = 0;
2457
+ while (common < parts.length && common < directoryParts.length && parts[common] === directoryParts[common]) common++;
2458
+ return parts.slice(0, common).join("/");
2459
+ }) || "."
2460
+ }];
2461
+ if (options.rootDir === null) return [];
2462
+ return [{
2463
+ outDir: outDirectory,
2464
+ rootDir: normalizeDirectoryPath(options.rootDir ?? "src")
2465
+ }];
2466
+ }
1858
2467
  function findRojoProject(rootDirectory) {
1859
2468
  const defaultPath = path$1.join(rootDirectory, "default.project.json");
1860
- if (fs.existsSync(defaultPath)) return defaultPath;
1861
- const projectFile = fs.readdirSync(rootDirectory).find((file) => file.endsWith(".project.json"));
2469
+ if (fs$1.existsSync(defaultPath)) return defaultPath;
2470
+ const projectFile = fs$1.readdirSync(rootDirectory).find((file) => file.endsWith(".project.json"));
1862
2471
  return projectFile !== void 0 ? path$1.join(rootDirectory, projectFile) : void 0;
1863
2472
  }
1864
- function buildSourceMapper(config, tsconfigDirectories) {
2473
+ function buildSourceMapper(config, tsconfigMappings) {
1865
2474
  const rojoProjectPath = config.rojoProject ?? findRojoProject(config.rootDir);
1866
- if (rojoProjectPath === void 0 || !fs.existsSync(rojoProjectPath)) return;
2475
+ if (rojoProjectPath === void 0 || !fs$1.existsSync(rojoProjectPath)) return;
1867
2476
  try {
1868
- const rojoResult = rojoProjectSchema(JSON.parse(fs.readFileSync(rojoProjectPath, "utf-8")));
2477
+ const rojoResult = rojoProjectSchema(JSON.parse(fs$1.readFileSync(rojoProjectPath, "utf-8")));
1869
2478
  if (rojoResult instanceof type.errors) return;
1870
2479
  return createSourceMapper({
1871
- outDir: tsconfigDirectories.outDir,
1872
- rojoProject: rojoResult,
1873
- rootDir: tsconfigDirectories.rootDir
2480
+ mappings: tsconfigMappings,
2481
+ rojoProject: rojoResult
1874
2482
  });
1875
2483
  } catch {
1876
2484
  return;
@@ -1924,7 +2532,7 @@ const coverageManifestSchema = type({
1924
2532
  function loadCoverageManifest(rootDirectory) {
1925
2533
  const manifestPath = path$1.join(rootDirectory, ".jest-roblox-coverage", "manifest.json");
1926
2534
  try {
1927
- const raw = fs.readFileSync(manifestPath, "utf-8");
2535
+ const raw = fs$1.readFileSync(manifestPath, "utf-8");
1928
2536
  const parsed = coverageManifestSchema(JSON.parse(raw));
1929
2537
  if (parsed instanceof type.errors) {
1930
2538
  process.stderr.write(`Warning: Coverage manifest is invalid (re-run \`jest-roblox instrument\`): ${parsed.summary}\n`);
@@ -1936,39 +2544,38 @@ function loadCoverageManifest(rootDirectory) {
1936
2544
  return;
1937
2545
  }
1938
2546
  }
1939
- function writeSnapshots(snapshotWrites, config, tsconfigDirectories) {
2547
+ function writeSnapshots(snapshotWrites, config, tsconfigMappings) {
1940
2548
  const rojoProjectPath = config.rojoProject ?? findRojoProject(config.rootDir);
1941
- if (rojoProjectPath === void 0 || !fs.existsSync(rojoProjectPath)) {
2549
+ if (rojoProjectPath === void 0 || !fs$1.existsSync(rojoProjectPath)) {
1942
2550
  process.stderr.write("Warning: Cannot write snapshots - no rojo project found\n");
1943
2551
  return;
1944
2552
  }
1945
2553
  try {
1946
- const rojoResult = rojoProjectSchema(JSON.parse(fs.readFileSync(rojoProjectPath, "utf-8")));
2554
+ const rojoResult = rojoProjectSchema(JSON.parse(fs$1.readFileSync(rojoProjectPath, "utf-8")));
1947
2555
  if (rojoResult instanceof type.errors) {
1948
2556
  process.stderr.write("Warning: Cannot write snapshots - invalid rojo project\n");
1949
2557
  return;
1950
2558
  }
1951
- const { outDir: outDirectory, rootDir: rootDirectory } = tsconfigDirectories;
1952
2559
  const resolver = createSnapshotPathResolver({
1953
- outDir: outDirectory,
1954
- rojoProject: rojoResult,
1955
- rootDir: rootDirectory
2560
+ mappings: tsconfigMappings,
2561
+ rojoProject: rojoResult
1956
2562
  });
1957
2563
  let written = 0;
1958
2564
  for (const [virtualPath, content] of Object.entries(snapshotWrites)) {
1959
- const fsPath = resolver.resolve(virtualPath);
1960
- if (fsPath === void 0) {
2565
+ const resolved = resolver.resolve(virtualPath);
2566
+ if (resolved === void 0) {
1961
2567
  process.stderr.write(`Warning: Cannot resolve snapshot path: ${virtualPath}\n`);
1962
2568
  continue;
1963
2569
  }
1964
- const absolutePath = path$1.resolve(config.rootDir, fsPath);
1965
- fs.mkdirSync(path$1.dirname(absolutePath), { recursive: true });
1966
- fs.writeFileSync(absolutePath, content);
1967
- if (rootDirectory !== void 0 && outDirectory !== void 0 && fsPath.startsWith(`${rootDirectory}/`)) {
1968
- const outPath = outDirectory + fsPath.slice(rootDirectory.length);
2570
+ const absolutePath = path$1.resolve(config.rootDir, resolved.filePath);
2571
+ fs$1.mkdirSync(path$1.dirname(absolutePath), { recursive: true });
2572
+ fs$1.writeFileSync(absolutePath, content);
2573
+ const { filePath, mapping } = resolved;
2574
+ if (mapping !== void 0) {
2575
+ const outPath = mapping.outDir + filePath.slice(mapping.rootDir.length);
1969
2576
  const absoluteOutPath = path$1.resolve(config.rootDir, outPath);
1970
- fs.mkdirSync(path$1.dirname(absoluteOutPath), { recursive: true });
1971
- fs.writeFileSync(absoluteOutPath, content);
2577
+ fs$1.mkdirSync(path$1.dirname(absoluteOutPath), { recursive: true });
2578
+ fs$1.writeFileSync(absoluteOutPath, content);
1972
2579
  }
1973
2580
  written++;
1974
2581
  }
@@ -1982,7 +2589,6 @@ function writeSnapshots(snapshotWrites, config, tsconfigDirectories) {
1982
2589
  else process.stderr.write(`Warning: Failed to write snapshot files: ${String(err)}\n`);
1983
2590
  }
1984
2591
  }
1985
-
1986
2592
  //#endregion
1987
2593
  //#region src/formatters/github-actions.ts
1988
2594
  const SEPARATOR = " · ";
@@ -2130,7 +2736,6 @@ function createFileLink(options) {
2130
2736
  if (serverUrl === void 0 || repository === void 0 || sha === void 0) return (_filePath) => {};
2131
2737
  return (filePath) => `${serverUrl}/${repository}/blob/${sha}/${filePath}`;
2132
2738
  }
2133
-
2134
2739
  //#endregion
2135
2740
  //#region src/typecheck/collect.ts
2136
2741
  const TEST_FUNCTIONS = new Set(["it", "test"]);
@@ -2198,7 +2803,6 @@ function extractDefinition(node, source) {
2198
2803
  type: TEST_FUNCTIONS.has(name) ? "test" : "suite"
2199
2804
  };
2200
2805
  }
2201
-
2202
2806
  //#endregion
2203
2807
  //#region src/typecheck/parse.ts
2204
2808
  const errorCodeRegExp = /error TS(?<errorCode>\d+)/;
@@ -2242,7 +2846,6 @@ function parseTscOutput(stdout) {
2242
2846
  }
2243
2847
  return map;
2244
2848
  }
2245
-
2246
2849
  //#endregion
2247
2850
  //#region src/typecheck/runner.ts
2248
2851
  function createLocationsIndexMap(source) {
@@ -2260,7 +2863,7 @@ function createLocationsIndexMap(source) {
2260
2863
  }
2261
2864
  return map;
2262
2865
  }
2263
- function mapErrorsToTests(errors, files) {
2866
+ function mapErrorsToTests(errors, files, startTime) {
2264
2867
  const testResults = [];
2265
2868
  let numberFailed = 0;
2266
2869
  let numberPassed = 0;
@@ -2275,19 +2878,33 @@ function mapErrorsToTests(errors, files) {
2275
2878
  numPassedTests: numberPassed,
2276
2879
  numPendingTests: 0,
2277
2880
  numTotalTests: numberFailed + numberPassed,
2278
- startTime: Date.now(),
2881
+ startTime,
2279
2882
  success: numberFailed === 0,
2280
2883
  testResults
2281
2884
  };
2282
2885
  }
2886
+ function isCompositeProject(rootDirectory, tsconfig) {
2887
+ const tsconfigPath = tsconfig !== void 0 ? path$1.resolve(rootDirectory, tsconfig) : path$1.join(rootDirectory, "tsconfig.json");
2888
+ try {
2889
+ return parseJSONC(fs$1.readFileSync(tsconfigPath, "utf-8"))["compilerOptions"]?.["composite"] === true;
2890
+ } catch (err) {
2891
+ if (tsconfig !== void 0) {
2892
+ const message = err instanceof Error ? err.message : String(err);
2893
+ process.stderr.write(`Warning: could not read tsconfig "${tsconfigPath}": ${message}\n`);
2894
+ }
2895
+ return false;
2896
+ }
2897
+ }
2283
2898
  function runTypecheck(options) {
2899
+ const startTime = Date.now();
2284
2900
  const errors = parseTscOutput(spawnTsgo(options));
2285
2901
  const files = /* @__PURE__ */ new Map();
2286
2902
  for (const filePath of options.files) {
2287
- const source = fs.readFileSync(filePath, "utf-8");
2903
+ const source = fs$1.readFileSync(filePath, "utf-8");
2288
2904
  const definitions = collectTestDefinitions(source);
2289
2905
  const resolvedPath = path$1.resolve(filePath);
2290
- files.set(resolvedPath, {
2906
+ const key = normalizeWindowsPath(path$1.relative(options.rootDir, resolvedPath));
2907
+ files.set(key, {
2291
2908
  definitions,
2292
2909
  source
2293
2910
  });
@@ -2295,9 +2912,10 @@ function runTypecheck(options) {
2295
2912
  const resolvedErrors = /* @__PURE__ */ new Map();
2296
2913
  for (const [errorPath, errorList] of errors) {
2297
2914
  const resolved = path$1.resolve(options.rootDir, errorPath);
2298
- resolvedErrors.set(resolved, errorList);
2915
+ const key = normalizeWindowsPath(path$1.relative(options.rootDir, resolved));
2916
+ resolvedErrors.set(key, errorList);
2299
2917
  }
2300
- return mapErrorsToTests(resolvedErrors, files);
2918
+ return mapErrorsToTests(resolvedErrors, files, startTime);
2301
2919
  }
2302
2920
  function buildFileResult(filePath, fileInfo, errors) {
2303
2921
  const indexMap = createLocationsIndexMap(fileInfo.source);
@@ -2341,16 +2959,27 @@ function buildFileResult(filePath, fileInfo, errors) {
2341
2959
  testResults: testCases
2342
2960
  };
2343
2961
  }
2962
+ function isExecSyncError(err) {
2963
+ return typeof err === "object" && err !== null && ("stdout" in err || "stderr" in err);
2964
+ }
2965
+ function resolveTsgoScript() {
2966
+ const packageJsonPath = createRequire(import.meta.url).resolve("@typescript/native-preview/package.json");
2967
+ return path$1.join(path$1.dirname(packageJsonPath), "bin", "tsgo.js");
2968
+ }
2344
2969
  function spawnTsgo(options) {
2345
- const args = [
2346
- "tsgo",
2347
- "--noEmit",
2348
- "--pretty",
2349
- "false"
2350
- ];
2351
- if (options.tsconfig !== void 0) args.push("-p", path$1.resolve(options.rootDir, options.tsconfig));
2970
+ const composite = isCompositeProject(options.rootDir, options.tsconfig);
2971
+ const args = [];
2972
+ if (composite) args.push("--build", "--emitDeclarationOnly");
2973
+ else args.push("--noEmit");
2974
+ args.push("--pretty", "false");
2975
+ if (options.tsconfig !== void 0) {
2976
+ const resolvedTsconfig = path$1.resolve(options.rootDir, options.tsconfig);
2977
+ if (composite) args.push(resolvedTsconfig);
2978
+ else args.push("-p", resolvedTsconfig);
2979
+ }
2980
+ const tsgoScript = resolveTsgoScript();
2352
2981
  try {
2353
- return execSync(args.join(" "), {
2982
+ return execFileSync(process.execPath, [tsgoScript, ...args], {
2354
2983
  cwd: options.rootDir,
2355
2984
  encoding: "utf-8",
2356
2985
  stdio: [
@@ -2360,11 +2989,10 @@ function spawnTsgo(options) {
2360
2989
  ]
2361
2990
  });
2362
2991
  } catch (err) {
2363
- const execError = err;
2364
- return execError.stdout ?? execError.stderr ?? "";
2992
+ if (!isExecSyncError(err)) throw err;
2993
+ return err.stdout ?? err.stderr ?? "";
2365
2994
  }
2366
2995
  }
2367
-
2368
2996
  //#endregion
2369
2997
  //#region src/utils/game-output.ts
2370
2998
  function formatGameOutputNotice(filePath, entryCount) {
@@ -2384,9 +3012,8 @@ function parseGameOutput(raw) {
2384
3012
  function writeGameOutput(filePath, entries) {
2385
3013
  const absolutePath = path$1.resolve(filePath);
2386
3014
  const directoryPath = path$1.dirname(absolutePath);
2387
- if (!fs.existsSync(directoryPath)) fs.mkdirSync(directoryPath, { recursive: true });
2388
- fs.writeFileSync(absolutePath, JSON.stringify(entries, null, 2));
3015
+ if (!fs$1.existsSync(directoryPath)) fs$1.mkdirSync(directoryPath, { recursive: true });
3016
+ fs$1.writeFileSync(absolutePath, JSON.stringify(entries, null, 2));
2389
3017
  }
2390
-
2391
3018
  //#endregion
2392
- export { parseJestOutput as A, createStudioBackend as C, generateTestScript as D, buildJestArgv as E, LuauScriptError as O, StudioBackend as S, createOpenCloudBackend as T, resolveConfig as _, formatAnnotations as a, defineConfig as b, execute as c, writeJsonFile as d, formatFailure as f, loadConfig$1 as g, rojoProjectSchema as h, runTypecheck as i, extractJsonFromOutput as k, loadCoverageManifest as l, formatTestSummary as m, parseGameOutput as n, formatJobSummary as o, formatResult as p, writeGameOutput as r, resolveGitHubActionsOptions as s, formatGameOutputNotice as t, formatJson as u, DEFAULT_CONFIG as v, OpenCloudBackend as w, isValidBackend as x, VALID_BACKENDS as y };
3019
+ export { generateTestScript as A, resolveConfig as C, createOpenCloudBackend as D, OpenCloudBackend as E, defineProject as F, isValidBackend as I, LuauScriptError as L, ROOT_ONLY_KEYS as M, VALID_BACKENDS as N, hashBuffer as O, defineConfig as P, extractJsonFromOutput as R, loadConfig$1 as S, createStudioBackend as T, formatResult as _, formatAnnotations as a, formatBanner as b, execute as c, findFormatterOptions as d, formatJson as f, formatMultiProjectResult as g, formatFailure as h, runTypecheck as i, DEFAULT_CONFIG as j, buildJestArgv as k, formatExecuteOutput as l, formatCompactMultiProject as m, parseGameOutput as n, formatJobSummary as o, writeJsonFile as p, writeGameOutput as r, resolveGitHubActionsOptions as s, formatGameOutputNotice as t, loadCoverageManifest as u, formatTestSummary as v, StudioBackend as w, rojoProjectSchema as x, formatTypecheckSummary as y, parseJestOutput as z };