@isentinel/jest-roblox 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,12 @@
1
1
  import { createRequire } from "node:module";
2
- import { PermissionError, PollTimeoutError } from "@bedrock-rbx/ocale";
2
+ import { PermissionError, PollTimeoutError, TRANSIENT_TRANSPORT_CODES } from "@bedrock-rbx/ocale";
3
3
  import process from "node:process";
4
4
  import { isDeepStrictEqual } from "node:util";
5
5
  import color from "tinyrainbow";
6
6
  import { createDefineConfig, loadConfig } from "c12";
7
7
  import { defuFn } from "defu";
8
8
  import * as fs$1 from "node:fs";
9
- import fs, { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
9
+ import fs, { existsSync, readFileSync } from "node:fs";
10
10
  import * as path$1 from "node:path";
11
11
  import path, { dirname, join, relative, resolve } from "node:path";
12
12
  import { type } from "arktype";
@@ -20,6 +20,7 @@ import picomatch from "picomatch";
20
20
  import { getTsconfig } from "get-tsconfig";
21
21
  import hljs from "highlight.js/lib/core";
22
22
  import typescript from "highlight.js/lib/languages/typescript";
23
+ import { performance } from "node:perf_hooks";
23
24
  import { LuauExecutionClient } from "@bedrock-rbx/ocale/luau-execution";
24
25
  import { PlacesClient } from "@bedrock-rbx/ocale/places";
25
26
  import { createHash, randomUUID } from "node:crypto";
@@ -29,11 +30,10 @@ import * as cp from "node:child_process";
29
30
  import { execFileSync } from "node:child_process";
30
31
  import * as os from "node:os";
31
32
  import { Buffer } from "node:buffer";
32
- import { performance } from "node:perf_hooks";
33
33
  import { parseJSONC, parseYAML } from "confbox";
34
34
  import { Visitor, parseSync } from "oxc-parser";
35
35
  //#region package.json
36
- var version = "0.3.0";
36
+ var version = "0.3.2";
37
37
  //#endregion
38
38
  //#region src/config/errors.ts
39
39
  var ConfigError = class extends Error {
@@ -933,12 +933,35 @@ function mapBranchArmLocations(traceMap, locations, sourceMapDirectory) {
933
933
  tsPath
934
934
  };
935
935
  }
936
+ /**
937
+ * Detects a phantom branch arm produced by a source-less synthetic statement
938
+ * `if` (e.g. a roblox-ts Array polyfill like `.filter`/`.includes`). The
939
+ * synthetic `if` has no source map entry, so trace-mapping's greatest-lower-
940
+ * bound bias snaps both arms onto the nearest preceding segment — the then-
941
+ * arm's own start — yielding a zero-width arm that coincides with another
942
+ * arm's start and can never be covered.
943
+ *
944
+ * A genuine statement `if` is safe: roblox-ts always renders it multi-line, so
945
+ * the then-body (generated line `if+1`) and the implicit-else arm (generated
946
+ * line `if`) carry distinct source-map segments and never collapse. This is
947
+ * gated to `type === "if"` by the caller: a single-line `expr-if` (ternary)
948
+ * legitimately collapses to one column-0 segment and must NOT be dropped.
949
+ */
950
+ function hasCollapsedPhantomArm(locations) {
951
+ return locations.some((arm, index) => {
952
+ if (!(arm.start.line === arm.end.line && arm.start.column === arm.end.column)) return false;
953
+ return locations.some((other, otherIndex) => {
954
+ return otherIndex !== index && other.start.line === arm.start.line && other.start.column === arm.start.column;
955
+ });
956
+ });
957
+ }
936
958
  function mapFileBranches(resources, fileCoverage, pendingBranches) {
937
959
  if (resources.coverageMap.branchMap === void 0) return;
938
960
  for (const [branchId, entry] of Object.entries(resources.coverageMap.branchMap)) {
939
961
  const armHitCounts = fileCoverage.b?.[branchId] ?? [];
940
962
  const result = mapBranchArmLocations(resources.traceMap, entry.locations, resources.sourceMapDirectory);
941
963
  if (result === void 0) continue;
964
+ if (entry.type === "if" && hasCollapsedPhantomArm(result.locations)) continue;
942
965
  let fileBranches = pendingBranches.get(result.tsPath);
943
966
  if (fileBranches === void 0) {
944
967
  fileBranches = [];
@@ -1449,6 +1472,7 @@ function convertToLuau(filePath) {
1449
1472
  const RbxPathParent = Symbol("Parent");
1450
1473
  var RojoResolver = class RojoResolver {
1451
1474
  rbxPath = new Array();
1475
+ realpathCache = /* @__PURE__ */ new Map();
1452
1476
  walkedConfigFilesInternal = /* @__PURE__ */ new Set();
1453
1477
  walkedDirectoriesInternal = /* @__PURE__ */ new Set();
1454
1478
  filePathToRbxPathMap = /* @__PURE__ */ new Map();
@@ -1459,12 +1483,12 @@ var RojoResolver = class RojoResolver {
1459
1483
  static findRojoConfigFilePath(projectPath) {
1460
1484
  const warnings = new Array();
1461
1485
  const defaultPath = path.join(projectPath, ROJO_DEFAULT_NAME);
1462
- if (existsSync(defaultPath)) return {
1486
+ if (fs$1.existsSync(defaultPath)) return {
1463
1487
  path: defaultPath,
1464
1488
  warnings
1465
1489
  };
1466
1490
  const candidates = new Array();
1467
- for (const fileName of readdirSync(projectPath)) if (fileName !== ROJO_DEFAULT_NAME && (fileName === ROJO_OLD_NAME || ROJO_FILE_REGEX.test(fileName))) candidates.push(path.join(projectPath, fileName));
1491
+ for (const fileName of fs$1.readdirSync(projectPath)) if (fileName !== ROJO_DEFAULT_NAME && (fileName === ROJO_OLD_NAME || ROJO_FILE_REGEX.test(fileName))) candidates.push(path.join(projectPath, fileName));
1468
1492
  if (candidates.length > 1) warnings.push(`Multiple *.project.json files found, using ${candidates[0]}`);
1469
1493
  return {
1470
1494
  path: candidates[0],
@@ -1600,34 +1624,42 @@ var RojoResolver = class RojoResolver {
1600
1624
  get walkedDirectories() {
1601
1625
  return this.walkedDirectoriesInternal;
1602
1626
  }
1627
+ cachedRealpath(targetPath) {
1628
+ let resolved = this.realpathCache.get(targetPath);
1629
+ if (resolved === void 0) {
1630
+ resolved = fs$1.realpathSync(targetPath);
1631
+ this.realpathCache.set(targetPath, resolved);
1632
+ }
1633
+ return resolved;
1634
+ }
1603
1635
  getContainer(from, rbxPath) {
1604
1636
  if (this.isGame && rbxPath) {
1605
1637
  for (const container of from) if (arrayStartsWith(rbxPath, container)) return container;
1606
1638
  }
1607
1639
  }
1608
1640
  parseConfig(rojoConfigFilePath, doNotPush = false) {
1609
- if (!existsSync(rojoConfigFilePath)) {
1641
+ if (!fs$1.existsSync(rojoConfigFilePath)) {
1610
1642
  this.warn(`RojoResolver: Path does not exist "${rojoConfigFilePath}"`);
1611
1643
  return;
1612
1644
  }
1613
- const realPath = realpathSync(rojoConfigFilePath);
1645
+ const realPath = this.cachedRealpath(rojoConfigFilePath);
1614
1646
  this.walkedConfigFilesInternal.add(realPath);
1615
1647
  let configJson;
1616
1648
  try {
1617
- configJson = JSON.parse(readFileSync(realPath, "utf8"));
1649
+ configJson = JSON.parse(fs$1.readFileSync(realPath, "utf8"));
1618
1650
  } catch {}
1619
1651
  if (isValidRojoConfig(configJson)) this.parseTree(path.dirname(rojoConfigFilePath), configJson.name, configJson.tree, doNotPush);
1620
1652
  else this.warn("RojoResolver: Invalid configuration!");
1621
1653
  }
1622
1654
  parsePath(itemPath) {
1623
1655
  const luauPath = convertToLuau(itemPath);
1624
- const realPath = existsSync(luauPath) ? realpathSync(luauPath) : luauPath;
1656
+ const realPath = fs$1.existsSync(luauPath) ? this.cachedRealpath(luauPath) : luauPath;
1625
1657
  const extension = path.extname(luauPath);
1626
1658
  if (ROJO_MODULE_EXTS.has(extension)) this.filePathToRbxPathMap.set(luauPath, [...this.rbxPath]);
1627
1659
  else {
1628
- const isDirectory = existsSync(realPath) && statSync(realPath).isDirectory();
1660
+ const isDirectory = fs$1.existsSync(realPath) && fs$1.statSync(realPath).isDirectory();
1629
1661
  if (isDirectory) this.walkedDirectoriesInternal.add(realPath);
1630
- if (isDirectory && readdirSync(realPath).includes(ROJO_DEFAULT_NAME)) this.parseConfig(path.join(luauPath, ROJO_DEFAULT_NAME), true);
1662
+ if (isDirectory && fs$1.readdirSync(realPath).includes(ROJO_DEFAULT_NAME)) this.parseConfig(path.join(luauPath, ROJO_DEFAULT_NAME), true);
1631
1663
  else {
1632
1664
  this.partitions.unshift({
1633
1665
  fsPath: luauPath,
@@ -1644,26 +1676,40 @@ var RojoResolver = class RojoResolver {
1644
1676
  for (const childName of Object.keys(tree).filter((value) => !value.startsWith("$"))) this.parseTree(basePath, childName, tree[childName]);
1645
1677
  if (!doNotPush) this.rbxPath.pop();
1646
1678
  }
1647
- searchChildren(directory, children) {
1648
- for (const child of children) {
1649
- const childPath = path.join(directory, child);
1650
- if (statSync(realpathSync(childPath)).isFile() && child !== ROJO_DEFAULT_NAME && ROJO_FILE_REGEX.test(child)) this.parseConfig(childPath);
1651
- }
1652
- for (const child of children) {
1653
- const childPath = path.join(directory, child);
1654
- if (statSync(realpathSync(childPath)).isDirectory()) this.searchDirectory(childPath, child);
1679
+ searchChildren(directory, directoryEntries) {
1680
+ const projectFiles = new Array();
1681
+ const subDirectories = new Array();
1682
+ for (const entry of directoryEntries) {
1683
+ const childPath = path.join(directory, entry.name);
1684
+ let isFile = entry.isFile();
1685
+ let isDirectory = entry.isDirectory();
1686
+ if (!isFile && !isDirectory) try {
1687
+ const stat = fs$1.statSync(this.cachedRealpath(childPath));
1688
+ isFile = stat.isFile();
1689
+ isDirectory = stat.isDirectory();
1690
+ } catch (err) {
1691
+ this.warn(`RojoResolver: Failed to resolve "${childPath}" (${err.message})`);
1692
+ continue;
1693
+ }
1694
+ if (isFile && ROJO_FILE_REGEX.test(entry.name)) projectFiles.push(childPath);
1695
+ else if (isDirectory) subDirectories.push({
1696
+ name: entry.name,
1697
+ path: childPath
1698
+ });
1655
1699
  }
1700
+ for (const childPath of projectFiles) this.parseConfig(childPath);
1701
+ for (const { name, path: childPath } of subDirectories) this.searchDirectory(childPath, name);
1656
1702
  }
1657
1703
  searchDirectory(directory, item) {
1658
- const realPath = realpathSync(directory);
1704
+ const realPath = this.cachedRealpath(directory);
1659
1705
  this.walkedDirectoriesInternal.add(realPath);
1660
- const children = readdirSync(realPath);
1661
- if (children.includes(ROJO_DEFAULT_NAME)) {
1706
+ const directoryEntries = fs$1.readdirSync(directory, { withFileTypes: true });
1707
+ if (directoryEntries.some((entry) => entry.name === ROJO_DEFAULT_NAME)) {
1662
1708
  this.parseConfig(path.join(directory, ROJO_DEFAULT_NAME));
1663
1709
  return;
1664
1710
  }
1665
1711
  if (item !== void 0) this.rbxPath.push(item);
1666
- this.searchChildren(directory, children);
1712
+ this.searchChildren(directory, directoryEntries);
1667
1713
  if (item !== void 0) this.rbxPath.pop();
1668
1714
  }
1669
1715
  warn(str) {
@@ -2058,6 +2104,7 @@ function findMapping(filePath, mappings, key = "outDir") {
2058
2104
  }
2059
2105
  function replacePrefix(filePath, from, to) {
2060
2106
  if (filePath === from) return to;
2107
+ if (from === ".") return `${to}/${filePath.startsWith("./") ? filePath.slice(2) : filePath}`;
2061
2108
  if (filePath.startsWith(`${from}/`)) return `${to}${filePath.slice(from.length)}`;
2062
2109
  return filePath;
2063
2110
  }
@@ -2694,7 +2741,7 @@ function formatTestSummary(result, timing, styles, options) {
2694
2741
  }
2695
2742
  function formatResult(result, timing, options) {
2696
2743
  const styles = createStyles(options.color, options.slowTestThreshold);
2697
- const lines = [formatRunHeader(options, styles)];
2744
+ const lines = [""];
2698
2745
  for (const file of result.testResults) {
2699
2746
  if (options.failuresOnly === true && file.numFailingTests === 0 && !hasExecError(file)) continue;
2700
2747
  lines.push(formatFileSummary(file, options, styles));
@@ -2868,7 +2915,7 @@ function formatMultiProjectResult(projects, timing, options) {
2868
2915
  result,
2869
2916
  styles
2870
2917
  }));
2871
- const lines = [formatRunHeader(options, styles), sections.join("\n\n")];
2918
+ const lines = ["", sections.join("\n\n")];
2872
2919
  const mergedResult = mergeJestResults(projects.map((project) => project.result));
2873
2920
  lines.push("", formatTestSummary(mergedResult, timing, styles, {
2874
2921
  snapshotWriteFailures: options.snapshotWriteFailures,
@@ -3675,6 +3722,15 @@ function hasFormatter(formatters, name) {
3675
3722
  function usesAgentFormatter(formatters, verbose = false) {
3676
3723
  return hasFormatter(formatters, "agent") && !verbose;
3677
3724
  }
3725
+ /**
3726
+ * Whether human-facing progress output (the run header, the "Running X of Y"
3727
+ * notice, the workspace streaming lines) should be written: not silent and not
3728
+ * a machine-readable formatter (json, or non-verbose agent). The single source
3729
+ * of truth so these sinks can't drift apart.
3730
+ */
3731
+ function isDefaultHumanFormatter(options) {
3732
+ return options.silent !== true && !usesAgentFormatter(options.formatters, options.verbose) && !hasFormatter(options.formatters, "json");
3733
+ }
3678
3734
  //#endregion
3679
3735
  //#region src/snapshot/path-resolver.ts
3680
3736
  function createSnapshotPathResolver(config) {
@@ -3687,7 +3743,7 @@ function createSnapshotPathResolver(config) {
3687
3743
  const result = `${basePath}/${normalized.slice(prefix.length + 1)}`;
3688
3744
  const mapping = findMapping(result, tsconfigMappings);
3689
3745
  if (mapping !== void 0) return {
3690
- filePath: replacePrefix(result, mapping.outDir, mapping.rootDir).replace(/^\.\//, ""),
3746
+ filePath: replacePrefix(result, mapping.outDir, mapping.rootDir),
3691
3747
  mapping
3692
3748
  };
3693
3749
  return { filePath: result };
@@ -3707,6 +3763,96 @@ function buildMappings(tree, prefix) {
3707
3763
  return mappings;
3708
3764
  }
3709
3765
  //#endregion
3766
+ //#region src/timing/orchestration-collector.ts
3767
+ /**
3768
+ * A buffered span-tree profiler for a single, sequential host run. Nesting is
3769
+ * tracked with one shared stack, so spans must open and close in LIFO order:
3770
+ * profile a phase, and any spans it opens nest under it. It is NOT safe to run
3771
+ * two `profile` / `profileAsync` calls concurrently on the same collector (e.g.
3772
+ * `Promise.all`) — interleaved opens/closes would corrupt the stack. Create one
3773
+ * collector per run; `flushTimingReport` empties it so a second flush is a
3774
+ * no-op.
3775
+ */
3776
+ function createTimingCollector(options = {}) {
3777
+ const clock = options.clock ?? { now: () => performance.now() };
3778
+ const sink = options.sink ?? ((line) => void process.stderr.write(`${line}\n`));
3779
+ const enabled = options.enabled ?? process.env["TIMING"] !== void 0;
3780
+ const roots = /* @__PURE__ */ new Map();
3781
+ const stack = [];
3782
+ function open(name) {
3783
+ const top = stack.at(-1);
3784
+ const node = childOf(top === void 0 ? roots : top.children, name);
3785
+ stack.push(node);
3786
+ const start = clock.now();
3787
+ return () => {
3788
+ node.elapsedMs += clock.now() - start;
3789
+ stack.pop();
3790
+ };
3791
+ }
3792
+ function profile(name, func) {
3793
+ if (!enabled) return func();
3794
+ const close = open(name);
3795
+ try {
3796
+ return func();
3797
+ } finally {
3798
+ close();
3799
+ }
3800
+ }
3801
+ async function profileAsync(name, func) {
3802
+ if (!enabled) return func();
3803
+ const close = open(name);
3804
+ try {
3805
+ return await func();
3806
+ } finally {
3807
+ close();
3808
+ }
3809
+ }
3810
+ function record(name, elapsedMs) {
3811
+ if (!enabled) return;
3812
+ const top = stack.at(-1);
3813
+ const node = childOf(top === void 0 ? roots : top.children, name);
3814
+ node.elapsedMs += elapsedMs;
3815
+ }
3816
+ function emit(node, depth) {
3817
+ sink(`[TIMING] ${" ".repeat(depth)}${node.name}: ${String(Math.round(node.elapsedMs))}ms`);
3818
+ for (const child of node.children.values()) emit(child, depth + 1);
3819
+ }
3820
+ function flushTimingReport() {
3821
+ if (!enabled || roots.size === 0) return;
3822
+ let total = 0;
3823
+ for (const node of roots.values()) {
3824
+ emit(node, 0);
3825
+ total += Math.round(node.elapsedMs);
3826
+ }
3827
+ sink(`[TIMING] TOTAL (host): ${String(total)}ms`);
3828
+ roots.clear();
3829
+ }
3830
+ return {
3831
+ flushTimingReport,
3832
+ profile,
3833
+ profileAsync,
3834
+ record
3835
+ };
3836
+ }
3837
+ /**
3838
+ * Shared disabled collector for callers that thread a profiler through their
3839
+ * signatures but are invoked outside a profiled workspace run (single-mode
3840
+ * coverage, the `instrument` subcommand, tests). Every method is a no-op.
3841
+ */
3842
+ const NOOP_TIMING_COLLECTOR = createTimingCollector({ enabled: false });
3843
+ function childOf(parent, name) {
3844
+ let node = parent.get(name);
3845
+ if (node === void 0) {
3846
+ node = {
3847
+ name,
3848
+ children: /* @__PURE__ */ new Map(),
3849
+ elapsedMs: 0
3850
+ };
3851
+ parent.set(name, node);
3852
+ }
3853
+ return node;
3854
+ }
3855
+ //#endregion
3710
3856
  //#region src/types/rojo.ts
3711
3857
  const rojoProjectSchema = type({
3712
3858
  "name": "string",
@@ -3856,38 +4002,48 @@ function formatExecuteOutput(options) {
3856
4002
  * config.
3857
4003
  */
3858
4004
  async function runProjects(options) {
3859
- const jobs = options.projects.map((project) => buildProjectJob(project));
3860
- const { rawResults, timing: backendTiming } = await options.backend.runTests({
3861
- jobs,
3862
- parallel: options.parallel,
3863
- scriptOverride: options.scriptOverride,
3864
- streaming: options.streaming,
3865
- workStealing: options.workStealing
4005
+ const timing = options.timing ?? NOOP_TIMING_COLLECTOR;
4006
+ const jobs = timing.profile("buildJobs", () => {
4007
+ return options.projects.map((project) => buildProjectJob(project, timing));
4008
+ });
4009
+ const { rawResults, timing: backendTiming } = await timing.profileAsync("backend.runTests", async () => {
4010
+ const result = await options.backend.runTests({
4011
+ jobs,
4012
+ parallel: options.parallel,
4013
+ scriptOverride: options.scriptOverride,
4014
+ streaming: options.streaming,
4015
+ workStealing: options.workStealing
4016
+ });
4017
+ recordBackendTimingSpans(timing, result.timing);
4018
+ return result;
3866
4019
  });
3867
4020
  if (rawResults.length !== jobs.length) throw new Error(`Backend returned ${rawResults.length.toString()} results for ${jobs.length.toString()} jobs — rawResults must be parallel to jobs`);
3868
4021
  return {
3869
4022
  backendTiming,
3870
- results: rawResults.map((raw, index) => {
3871
- const job = jobs[index];
3872
- try {
3873
- return processProjectResult(buildProjectResult(raw.entry, job, raw.fallbackGameOutput), {
3874
- backendTiming,
3875
- config: job.config,
3876
- deferFormatting: options.deferFormatting,
3877
- startTime: options.startTime,
3878
- version: options.version
3879
- });
3880
- } catch (err) {
3881
- if (!(err instanceof LuauScriptError)) throw err;
3882
- return buildExecutionErrorResult({
3883
- backendTiming,
3884
- config: job.config,
3885
- deferFormatting: options.deferFormatting,
3886
- error: err,
3887
- startTime: options.startTime,
3888
- version: options.version
3889
- });
3890
- }
4023
+ results: timing.profile("processResults", () => {
4024
+ return rawResults.map((raw, index) => {
4025
+ const job = jobs[index];
4026
+ try {
4027
+ return processProjectResult(buildProjectResult(raw.entry, job, raw.fallbackGameOutput), {
4028
+ backendTiming,
4029
+ config: job.config,
4030
+ deferFormatting: options.deferFormatting,
4031
+ startTime: options.startTime,
4032
+ timing,
4033
+ version: options.version
4034
+ });
4035
+ } catch (err) {
4036
+ if (!(err instanceof LuauScriptError)) throw err;
4037
+ return buildExecutionErrorResult({
4038
+ backendTiming,
4039
+ config: job.config,
4040
+ deferFormatting: options.deferFormatting,
4041
+ error: err,
4042
+ startTime: options.startTime,
4043
+ version: options.version
4044
+ });
4045
+ }
4046
+ });
3891
4047
  })
3892
4048
  };
3893
4049
  }
@@ -3928,6 +4084,10 @@ function parseTsconfigMappings(options) {
3928
4084
  rootDir: normalizeDirectoryPath(options.rootDir ?? "src")
3929
4085
  }];
3930
4086
  }
4087
+ function recordBackendTimingSpans(timing, backendTiming) {
4088
+ if (backendTiming.uploadMs !== void 0) timing.record("uploadMs", backendTiming.uploadMs);
4089
+ timing.record("executionMs", backendTiming.executionMs);
4090
+ }
3931
4091
  const EXIT_CODE_MESSAGE = /^Exited with code: \d+$/;
3932
4092
  /**
3933
4093
  * Compose the human-readable failure message for an exec-error file
@@ -4128,7 +4288,7 @@ function writeSnapshots(snapshotWrites, config, tsconfigMappings) {
4128
4288
  fs$1.writeFileSync(absolutePath, content);
4129
4289
  const { filePath, mapping } = resolved;
4130
4290
  if (mapping !== void 0) {
4131
- const outPath = mapping.outDir + filePath.slice(mapping.rootDir.length);
4291
+ const outPath = replacePrefix(filePath, mapping.rootDir, mapping.outDir);
4132
4292
  const absoluteOutPath = path$1.resolve(config.rootDir, outPath);
4133
4293
  fs$1.mkdirSync(path$1.dirname(absoluteOutPath), { recursive: true });
4134
4294
  fs$1.writeFileSync(absoluteOutPath, content);
@@ -4157,19 +4317,23 @@ function writeSnapshots(snapshotWrites, config, tsconfigMappings) {
4157
4317
  * formatter output. Called once per job.
4158
4318
  */
4159
4319
  function processProjectResult(entry, options) {
4160
- const { backendTiming, config, deferFormatting, startTime, version } = options;
4320
+ const { backendTiming, config, deferFormatting, startTime, timing, version } = options;
4161
4321
  const { coverageData, gameOutput, luauTiming, result, setupMs, snapshotWrites } = entry;
4162
- const tsconfigMappings = resolveAllTsconfigMappings(config.rootDir);
4163
- const writeCounts = snapshotWrites !== void 0 ? writeSnapshots(snapshotWrites, config, tsconfigMappings) : {
4322
+ const tsconfigMappings = timing.profile("resolveTsconfigMappings", () => {
4323
+ return resolveAllTsconfigMappings(config.rootDir);
4324
+ });
4325
+ const writeCounts = snapshotWrites !== void 0 ? timing.profile("writeSnapshots", () => {
4326
+ return writeSnapshots(snapshotWrites, config, tsconfigMappings);
4327
+ }) : {
4164
4328
  attempted: 0,
4165
4329
  failed: 0,
4166
4330
  written: 0
4167
4331
  };
4168
4332
  const testsMs = calculateTestsMs(result.testResults);
4169
- const sourceMapper = config.sourceMap ? buildSourceMapper(config, tsconfigMappings) : void 0;
4333
+ const sourceMapper = config.sourceMap ? timing.profile("buildSourceMapper", () => buildSourceMapper(config, tsconfigMappings)) : void 0;
4170
4334
  resolveTestFilePaths(result, sourceMapper);
4171
4335
  const totalMs = Date.now() - startTime;
4172
- const timing = {
4336
+ const resultTiming = {
4173
4337
  executionMs: backendTiming.executionMs,
4174
4338
  setupMs,
4175
4339
  startTime,
@@ -4182,7 +4346,7 @@ function processProjectResult(entry, options) {
4182
4346
  result,
4183
4347
  snapshotWriteFailures: writeCounts.failed,
4184
4348
  sourceMapper,
4185
- timing,
4349
+ timing: resultTiming,
4186
4350
  version
4187
4351
  }) : "";
4188
4352
  if (luauTiming !== void 0) printLuauTiming(luauTiming);
@@ -4194,7 +4358,7 @@ function processProjectResult(entry, options) {
4194
4358
  result,
4195
4359
  snapshotWriteFailures: writeCounts.failed > 0 ? writeCounts.failed : void 0,
4196
4360
  sourceMapper,
4197
- timing
4361
+ timing: resultTiming
4198
4362
  };
4199
4363
  }
4200
4364
  /**
@@ -4202,8 +4366,10 @@ function processProjectResult(entry, options) {
4202
4366
  * carries its own config so the Luau runner never re-resolves or shares format
4203
4367
  * state across projects (fixes the spike's snapshot-diff regression — C1).
4204
4368
  */
4205
- function buildProjectJob(parameters) {
4206
- const tsconfigMappings = resolveAllTsconfigMappings(parameters.config.rootDir);
4369
+ function buildProjectJob(parameters, timing) {
4370
+ const tsconfigMappings = timing.profile("resolveTsconfigMappings", () => {
4371
+ return resolveAllTsconfigMappings(parameters.config.rootDir);
4372
+ });
4207
4373
  const luauProject = isLuauProject(parameters.testFiles, tsconfigMappings);
4208
4374
  return {
4209
4375
  config: applySnapshotFormatDefaults(parameters.config, luauProject),
@@ -4763,7 +4929,10 @@ var OcaleRunner = class {
4763
4929
  script,
4764
4930
  timeoutSeconds,
4765
4931
  universeId: this.credentials.universeId
4766
- }, { timeoutMs: timeout });
4932
+ }, {
4933
+ retryableTransportCodes: TRANSIENT_TRANSPORT_CODES,
4934
+ timeoutMs: timeout
4935
+ });
4767
4936
  if (!result.success) {
4768
4937
  if (result.err instanceof PollTimeoutError) throw new Error("Execution timed out", { cause: result.err });
4769
4938
  throw new Error(result.err.message, { cause: result.err });
@@ -4785,7 +4954,7 @@ var OcaleRunner = class {
4785
4954
  placeId: this.credentials.placeId,
4786
4955
  universeId: this.credentials.universeId
4787
4956
  };
4788
- const result = await this.places.save(parameters);
4957
+ const result = await this.places.save(parameters, { retryableTransportCodes: TRANSIENT_TRANSPORT_CODES });
4789
4958
  if (!result.success) throw new Error(`Failed to upload place: ${result.err.message}`, { cause: result.err });
4790
4959
  return {
4791
4960
  uploadMs: Date.now() - uploadStart,
@@ -4903,6 +5072,35 @@ function generateTestScript(options) {
4903
5072
  return test_runner_bundled_default.replace("__CONFIG_JSON__", () => JSON.stringify({ configs }));
4904
5073
  }
4905
5074
  //#endregion
5075
+ //#region src/utils/error-chain.ts
5076
+ const MAX_DEPTH = 5;
5077
+ function walkErrorChain(err) {
5078
+ const entries = [];
5079
+ let current = err;
5080
+ while (current instanceof Error && entries.length < MAX_DEPTH) {
5081
+ entries.push({
5082
+ name: current.constructor.name,
5083
+ code: readStringProperty(current, "code"),
5084
+ errno: readStringProperty(current, "errno"),
5085
+ message: current.message,
5086
+ requiredScopes: current instanceof PermissionError ? current.requiredScopes : void 0,
5087
+ syscall: readStringProperty(current, "syscall")
5088
+ });
5089
+ current = current.cause;
5090
+ }
5091
+ return entries;
5092
+ }
5093
+ function formatMissingScopes(scopes) {
5094
+ if (scopes.length === 0) return "API key has insufficient scopes. Add via Creator Dashboard.";
5095
+ const joined = scopes.join(", ");
5096
+ return `API key missing scope${scopes.length === 1 ? "" : "s"} ${joined}. Add via Creator Dashboard.`;
5097
+ }
5098
+ function readStringProperty(err, key) {
5099
+ const value = Reflect.get(err, key);
5100
+ if (value === void 0 || value === null) return;
5101
+ return String(value);
5102
+ }
5103
+ //#endregion
4906
5104
  //#region src/backends/open-cloud.ts
4907
5105
  const PARALLEL_AUTO_CAP = 3;
4908
5106
  const BASE_URL_ENV = "JEST_ROBLOX_OPEN_CLOUD_BASE_URL";
@@ -5038,10 +5236,7 @@ function createOpenCloudBackend(credentials) {
5038
5236
  }
5039
5237
  function describeError(err) {
5040
5238
  const cause = err instanceof Error ? err.cause : void 0;
5041
- if (cause instanceof PermissionError) {
5042
- const scopes = cause.requiredScopes.join(", ");
5043
- return `API key missing scope${cause.requiredScopes.length === 1 ? "" : "s"} ${scopes}. Add via Creator Dashboard.`;
5044
- }
5239
+ if (cause instanceof PermissionError) return formatMissingScopes(cause.requiredScopes);
5045
5240
  return err instanceof Error ? err.message : String(err);
5046
5241
  }
5047
5242
  function warnStreamingDisabled(err, state) {
@@ -5814,6 +6009,27 @@ function narrowConfigByFiles(config, files) {
5814
6009
  testPathPattern: `(${branches.join("|")})`
5815
6010
  };
5816
6011
  }
6012
+ /**
6013
+ * Forward an Instance-namespace `testPathPattern` to the Luau runner.
6014
+ *
6015
+ * Node-side discovery is the source of truth: the FS-namespace filter
6016
+ * (positional args or `--testPathPattern`) has already resolved to a concrete
6017
+ * file set against real paths. Drop the raw FS-shaped pattern and re-narrow by
6018
+ * the discovered files so Jest-on-Roblox matches the same files — its paths are
6019
+ * Roblox Instance names (e.g. `ServerScriptService/...`) with no `src/` prefix,
6020
+ * so a raw FS pattern like `src/server/foo.spec` matches zero files there.
6021
+ *
6022
+ * `filterActive` gates the rewrite: a bare run (no positionals, no
6023
+ * `testPathPattern`) leaves the config untouched so the Luau side runs every
6024
+ * `testMatch` file rather than a giant basename alternation.
6025
+ */
6026
+ function narrowForLuauRun(config, runtimeFiles, filterActive) {
6027
+ if (!filterActive) return config;
6028
+ return narrowConfigByFiles({
6029
+ ...config,
6030
+ testPathPattern: void 0
6031
+ }, runtimeFiles);
6032
+ }
5817
6033
  function toBasenamePattern(file) {
5818
6034
  const posix = file.replaceAll("\\", "/");
5819
6035
  const lastSlash = posix.lastIndexOf("/");
@@ -6347,89 +6563,6 @@ function buildWithRojo(projectPath, outputPath) {
6347
6563
  }
6348
6564
  }
6349
6565
  //#endregion
6350
- //#region src/timing/orchestration-collector.ts
6351
- /**
6352
- * A buffered span-tree profiler for a single, sequential host run. Nesting is
6353
- * tracked with one shared stack, so spans must open and close in LIFO order:
6354
- * profile a phase, and any spans it opens nest under it. It is NOT safe to run
6355
- * two `profile` / `profileAsync` calls concurrently on the same collector (e.g.
6356
- * `Promise.all`) — interleaved opens/closes would corrupt the stack. Create one
6357
- * collector per run; `flushTimingReport` empties it so a second flush is a
6358
- * no-op.
6359
- */
6360
- function createTimingCollector(options = {}) {
6361
- const clock = options.clock ?? { now: () => performance.now() };
6362
- const sink = options.sink ?? ((line) => void process.stderr.write(`${line}\n`));
6363
- const enabled = options.enabled ?? process.env["TIMING"] !== void 0;
6364
- const roots = /* @__PURE__ */ new Map();
6365
- const stack = [];
6366
- function open(name) {
6367
- const top = stack.at(-1);
6368
- const node = childOf(top === void 0 ? roots : top.children, name);
6369
- stack.push(node);
6370
- const start = clock.now();
6371
- return () => {
6372
- node.elapsedMs += clock.now() - start;
6373
- stack.pop();
6374
- };
6375
- }
6376
- function profile(name, func) {
6377
- if (!enabled) return func();
6378
- const close = open(name);
6379
- try {
6380
- return func();
6381
- } finally {
6382
- close();
6383
- }
6384
- }
6385
- async function profileAsync(name, func) {
6386
- if (!enabled) return func();
6387
- const close = open(name);
6388
- try {
6389
- return await func();
6390
- } finally {
6391
- close();
6392
- }
6393
- }
6394
- function emit(node, depth) {
6395
- sink(`[TIMING] ${" ".repeat(depth)}${node.name}: ${String(Math.round(node.elapsedMs))}ms`);
6396
- for (const child of node.children.values()) emit(child, depth + 1);
6397
- }
6398
- function flushTimingReport() {
6399
- if (!enabled || roots.size === 0) return;
6400
- let total = 0;
6401
- for (const node of roots.values()) {
6402
- emit(node, 0);
6403
- total += Math.round(node.elapsedMs);
6404
- }
6405
- sink(`[TIMING] TOTAL (host): ${String(total)}ms`);
6406
- roots.clear();
6407
- }
6408
- return {
6409
- flushTimingReport,
6410
- profile,
6411
- profileAsync
6412
- };
6413
- }
6414
- /**
6415
- * Shared disabled collector for callers that thread a profiler through their
6416
- * signatures but are invoked outside a profiled workspace run (single-mode
6417
- * coverage, the `instrument` subcommand, tests). Every method is a no-op.
6418
- */
6419
- const NOOP_TIMING_COLLECTOR = createTimingCollector({ enabled: false });
6420
- function childOf(parent, name) {
6421
- let node = parent.get(name);
6422
- if (node === void 0) {
6423
- node = {
6424
- name,
6425
- children: /* @__PURE__ */ new Map(),
6426
- elapsedMs: 0
6427
- };
6428
- parent.set(name, node);
6429
- }
6430
- return node;
6431
- }
6432
- //#endregion
6433
6566
  //#region src/utils/hash.ts
6434
6567
  function hashBuffer(data) {
6435
6568
  return createHash("sha256").update(data).digest("hex");
@@ -7900,15 +8033,47 @@ function classifyTestFiles(files, config) {
7900
8033
  typeTestFiles
7901
8034
  };
7902
8035
  }
8036
+ function resolveAllSetupFilePaths(configs) {
8037
+ const resolvers = /* @__PURE__ */ new Map();
8038
+ for (const config of configs) {
8039
+ if (config.setupFiles === void 0 && config.setupFilesAfterEnv === void 0) continue;
8040
+ const rojoConfigPath = path$1.resolve(config.rootDir, config.rojoProject ?? DEFAULT_ROJO_PROJECT$1);
8041
+ const key = JSON.stringify([config.rootDir, rojoConfigPath]);
8042
+ let resolve = resolvers.get(key);
8043
+ if (resolve === void 0) {
8044
+ resolve = createSetupResolver({
8045
+ configDirectory: config.rootDir,
8046
+ rojoConfigPath
8047
+ });
8048
+ resolvers.set(key, resolve);
8049
+ }
8050
+ if (config.setupFiles !== void 0) config.setupFiles = config.setupFiles.map(resolve);
8051
+ if (config.setupFilesAfterEnv !== void 0) config.setupFilesAfterEnv = config.setupFilesAfterEnv.map(resolve);
8052
+ }
8053
+ }
7903
8054
  function resolveSetupFilePaths(config) {
7904
- if (config.setupFiles === void 0 && config.setupFilesAfterEnv === void 0) return;
7905
- const rojoConfigPath = path$1.resolve(config.rootDir, config.rojoProject ?? DEFAULT_ROJO_PROJECT$1);
7906
- const resolve = createSetupResolver({
7907
- configDirectory: config.rootDir,
7908
- rojoConfigPath
7909
- });
7910
- if (config.setupFiles !== void 0) config.setupFiles = config.setupFiles.map(resolve);
7911
- if (config.setupFilesAfterEnv !== void 0) config.setupFilesAfterEnv = config.setupFilesAfterEnv.map(resolve);
8055
+ resolveAllSetupFilePaths([config]);
8056
+ }
8057
+ //#endregion
8058
+ //#region src/run/run-header.ts
8059
+ /**
8060
+ * Print the ` RUN vX.Y <rootDir>` header to stdout at the moment a run begins
8061
+ * (right before the backend uploads), so the CLI doesn't look stalled while it
8062
+ * waits for remote results. The end-of-run formatters no longer emit it.
8063
+ *
8064
+ * Self-gates to the default human formatter: nothing is written under
8065
+ * `--silent`, `--formatters json`, or `--formatters agent` (without
8066
+ * `--verbose`), which produce machine-readable output that must stay clean.
8067
+ */
8068
+ function emitRunHeader(input) {
8069
+ if (!isDefaultHumanFormatter(input)) return;
8070
+ process.stdout.write(formatRunHeader({
8071
+ collectCoverage: input.collectCoverage,
8072
+ color: input.color,
8073
+ rootDir: input.rootDir,
8074
+ verbose: input.verbose ?? false,
8075
+ version: input.version
8076
+ }));
7912
8077
  }
7913
8078
  //#endregion
7914
8079
  //#region src/run/multi.ts
@@ -7916,30 +8081,61 @@ const DEFAULT_ROJO_PROJECT = "default.project.json";
7916
8081
  const VERSION$2 = version;
7917
8082
  async function runMultiProject(options) {
7918
8083
  const { cli, config: rootConfig, rawProjects } = options;
7919
- const allProjects = await resolveAllProjects(rawProjects, rootConfig, loadRojoTree(rootConfig), rootConfig.rootDir);
7920
- for (const project of allProjects) resolveSetupFilePaths(project.config);
7921
- const { filesByProject, projects } = selectProjects(allProjects, cli.project, cli.files, rootConfig.rootDir);
8084
+ const timing = options.timing ?? NOOP_TIMING_COLLECTOR;
8085
+ const rojoTree = timing.profile("loadRojoTree", () => loadRojoTree(rootConfig));
8086
+ const allProjects = await timing.profileAsync("resolveAllProjects", async () => {
8087
+ return resolveAllProjects(rawProjects, rootConfig, rojoTree, rootConfig.rootDir);
8088
+ });
8089
+ timing.profile("resolveSetupFilePaths", () => {
8090
+ resolveAllSetupFilePaths(allProjects.map((project) => project.config));
8091
+ });
8092
+ const { filesByProject, projects } = timing.profile("selectProjects", () => {
8093
+ return selectProjects(allProjects, cli.project, cli.files, rootConfig.rootDir);
8094
+ });
7922
8095
  const cacheRoot = path$1.resolve(rootConfig.rootDir, ".jest-roblox", "cache");
7923
- const cleaned = cleanLeftoverStubs(projects, rootConfig.rootDir);
8096
+ const cleaned = timing.profile("cleanLeftoverStubs", () => {
8097
+ return cleanLeftoverStubs(projects, rootConfig.rootDir);
8098
+ });
7924
8099
  if (cleaned.length > 0) process.stderr.write(`jest-roblox: cleaned ${String(cleaned.length)} leftover stub(s):\n${cleaned.map((stubPath) => ` ${stubPath}\n`).join("")}`);
7925
- generateProjectStubs(projects, rootConfig.rootDir, cacheRoot);
7926
- const { effectiveConfig, preCoverageMs } = prepareMultiProjectCoverage(rootConfig, projects, cacheRoot);
7927
- const backend = await resolveBackend(cli, effectiveConfig);
8100
+ timing.profile("generateProjectStubs", () => {
8101
+ generateProjectStubs(projects, rootConfig.rootDir, cacheRoot);
8102
+ });
8103
+ const { effectiveConfig, preCoverageMs } = timing.profile("prepareCoverage", () => {
8104
+ return prepareMultiProjectCoverage(rootConfig, projects, cacheRoot);
8105
+ });
8106
+ const backend = await timing.profileAsync("resolveBackend", async () => {
8107
+ return resolveBackend(cli, effectiveConfig);
8108
+ });
7928
8109
  const parallel = effectiveParallelForBackend(effectiveConfig.parallel, backend);
7929
- if (!rootConfig.collectCoverage && backend.kind === "open-cloud") buildOpenCloudPlace(rootConfig, projects, cacheRoot);
7930
- const { allTypeTestFiles, pendingJobs } = collectPendingJobs({
7931
- cliFiles: cli.files,
7932
- effectivePlaceFile: effectiveConfig.placeFile,
7933
- filesByProject,
7934
- projects,
7935
- rootConfig
8110
+ if (!rootConfig.collectCoverage && backend.kind === "open-cloud") timing.profile("buildOpenCloudPlace", () => {
8111
+ buildOpenCloudPlace(rootConfig, projects, cacheRoot);
7936
8112
  });
7937
- const projectResults = await runJobs(backend, pendingJobs, parallel);
7938
- const uniqueTypeTestFiles = [...new Set(allTypeTestFiles)];
7939
- const typecheckResult = uniqueTypeTestFiles.length > 0 ? runTypecheck({
7940
- files: uniqueTypeTestFiles,
8113
+ const { allTypeTestFiles, pendingJobs } = timing.profile("collectPendingJobs", () => {
8114
+ return collectPendingJobs({
8115
+ cliFiles: cli.files,
8116
+ effectivePlaceFile: effectiveConfig.placeFile,
8117
+ filesByProject,
8118
+ projects,
8119
+ rootConfig
8120
+ });
8121
+ });
8122
+ if (pendingJobs.length > 0) emitRunHeader({
8123
+ collectCoverage: rootConfig.collectCoverage,
8124
+ color: rootConfig.color,
8125
+ formatters: rootConfig.formatters,
7941
8126
  rootDir: rootConfig.rootDir,
7942
- tsconfig: rootConfig.typecheckTsconfig
8127
+ silent: rootConfig.silent,
8128
+ verbose: rootConfig.verbose,
8129
+ version: VERSION$2
8130
+ });
8131
+ const projectResults = await runJobs(backend, pendingJobs, parallel, timing);
8132
+ const uniqueTypeTestFiles = [...new Set(allTypeTestFiles)];
8133
+ const typecheckResult = uniqueTypeTestFiles.length > 0 ? timing.profile("runTypecheck", () => {
8134
+ return runTypecheck({
8135
+ files: uniqueTypeTestFiles,
8136
+ rootDir: rootConfig.rootDir,
8137
+ tsconfig: rootConfig.typecheckTsconfig
8138
+ });
7943
8139
  }) : void 0;
7944
8140
  if (projectResults.length === 0 && typecheckResult === void 0) {
7945
8141
  if (rootConfig.passWithNoTests) return {
@@ -8003,10 +8199,11 @@ function collectPendingJobs(arguments_) {
8003
8199
  testMatch: project.include
8004
8200
  };
8005
8201
  const { runtimeFiles, typeTestFiles } = classifyTestFiles(discoverTestFiles(discoveryConfig, projectCliFiles).files, rootConfig);
8006
- const projConfig = narrowConfigByFiles({
8202
+ const filterActive = (projectCliFiles?.length ?? 0) > 0 || discoveryConfig.testPathPattern !== void 0;
8203
+ const projConfig = narrowForLuauRun({
8007
8204
  ...discoveryConfig,
8008
8205
  testMatch: project.testMatch
8009
- }, projectCliFiles ?? []);
8206
+ }, runtimeFiles, filterActive);
8010
8207
  allTypeTestFiles.push(...typeTestFiles);
8011
8208
  if (runtimeFiles.length === 0) continue;
8012
8209
  const runtimeInjectionPaths = [];
@@ -8027,28 +8224,31 @@ function collectPendingJobs(arguments_) {
8027
8224
  pendingJobs
8028
8225
  };
8029
8226
  }
8030
- async function runJobs(backend, pendingJobs, parallel) {
8227
+ async function runJobs(backend, pendingJobs, parallel, timing) {
8031
8228
  if (pendingJobs.length === 0) {
8032
8229
  await backend.close?.();
8033
8230
  return [];
8034
8231
  }
8035
8232
  let runResult;
8036
8233
  try {
8037
- runResult = await runProjects({
8038
- backend,
8039
- deferFormatting: true,
8040
- parallel,
8041
- projects: pendingJobs.map((pending) => {
8042
- return {
8043
- config: pending.config,
8044
- displayColor: pending.displayColor,
8045
- displayName: pending.displayName,
8046
- runtimeInjectionPaths: pending.runtimeInjectionPaths,
8047
- testFiles: pending.runtimeFiles
8048
- };
8049
- }),
8050
- startTime: Date.now(),
8051
- version: VERSION$2
8234
+ runResult = await timing.profileAsync("runProjects", async () => {
8235
+ return runProjects({
8236
+ backend,
8237
+ deferFormatting: true,
8238
+ parallel,
8239
+ projects: pendingJobs.map((pending) => {
8240
+ return {
8241
+ config: pending.config,
8242
+ displayColor: pending.displayColor,
8243
+ displayName: pending.displayName,
8244
+ runtimeInjectionPaths: pending.runtimeInjectionPaths,
8245
+ testFiles: pending.runtimeFiles
8246
+ };
8247
+ }),
8248
+ startTime: Date.now(),
8249
+ timing,
8250
+ version: VERSION$2
8251
+ });
8052
8252
  });
8053
8253
  } finally {
8054
8254
  await backend.close?.();
@@ -8125,11 +8325,16 @@ function selectProjects(allProjects, projectNames, cliFiles, rootDirectory) {
8125
8325
  const VERSION$1 = version;
8126
8326
  async function runSingleProject(options) {
8127
8327
  const { cli } = options;
8128
- const config = narrowConfigByFiles(options.config, cli.files ?? []);
8129
- resolveSetupFilePaths(config);
8130
- const discovery = discoverTestFiles(config, cli.files);
8328
+ const timing = options.timing ?? NOOP_TIMING_COLLECTOR;
8329
+ const baseConfig = { ...options.config };
8330
+ timing.profile("resolveSetupFilePaths", () => {
8331
+ resolveSetupFilePaths(baseConfig);
8332
+ });
8333
+ const discovery = timing.profile("discoverTestFiles", () => {
8334
+ return discoverTestFiles(baseConfig, cli.files);
8335
+ });
8131
8336
  if (discovery.files.length === 0) {
8132
- if (config.passWithNoTests) return {
8337
+ if (baseConfig.passWithNoTests) return {
8133
8338
  mode: "single",
8134
8339
  preCoverageMs: 0
8135
8340
  };
@@ -8140,7 +8345,13 @@ async function runSingleProject(options) {
8140
8345
  validationExitCode: 2
8141
8346
  };
8142
8347
  }
8143
- const { runtimeFiles, typeTestFiles } = classifyTestFiles(discovery.files, config);
8348
+ const { runtimeFiles, typeTestFiles } = timing.profile("classifyTestFiles", () => {
8349
+ return classifyTestFiles(discovery.files, baseConfig);
8350
+ });
8351
+ const filterActive = (cli.files?.length ?? 0) > 0 || baseConfig.testPathPattern !== void 0;
8352
+ const config = timing.profile("narrowForLuauRun", () => {
8353
+ return narrowForLuauRun(baseConfig, runtimeFiles, filterActive);
8354
+ });
8144
8355
  if (typeTestFiles.length === 0 && runtimeFiles.length === 0) {
8145
8356
  if (config.passWithNoTests) return {
8146
8357
  mode: "single",
@@ -8157,19 +8368,27 @@ async function runSingleProject(options) {
8157
8368
  let effectiveConfig = config;
8158
8369
  if (config.collectCoverage && !config.typecheckOnly && runtimeFiles.length > 0) {
8159
8370
  const preCoverageStart = Date.now();
8160
- const { placeFile } = prepareCoverage(config);
8371
+ const { placeFile } = timing.profile("prepareCoverage", () => prepareCoverage(config));
8161
8372
  preCoverageMs = Date.now() - preCoverageStart;
8162
8373
  effectiveConfig = {
8163
8374
  ...config,
8164
8375
  placeFile
8165
8376
  };
8166
8377
  }
8167
- const typecheckResult = typeTestFiles.length > 0 ? runTypecheck({
8168
- files: typeTestFiles,
8169
- rootDir: effectiveConfig.rootDir,
8170
- tsconfig: effectiveConfig.typecheckTsconfig
8378
+ const typecheckResult = typeTestFiles.length > 0 ? timing.profile("runTypecheck", () => {
8379
+ return runTypecheck({
8380
+ files: typeTestFiles,
8381
+ rootDir: effectiveConfig.rootDir,
8382
+ tsconfig: effectiveConfig.typecheckTsconfig
8383
+ });
8384
+ }) : void 0;
8385
+ const runtimeResult = runtimeFiles.length > 0 ? await executeRuntimeTests({
8386
+ cli,
8387
+ config: effectiveConfig,
8388
+ testFiles: runtimeFiles,
8389
+ timing,
8390
+ totalFiles: discovery.totalFiles
8171
8391
  }) : void 0;
8172
- const runtimeResult = runtimeFiles.length > 0 ? await executeRuntimeTests(options, effectiveConfig, runtimeFiles, discovery.totalFiles) : void 0;
8173
8392
  return {
8174
8393
  mode: "single",
8175
8394
  preCoverageMs,
@@ -8177,19 +8396,35 @@ async function runSingleProject(options) {
8177
8396
  typecheckResult
8178
8397
  };
8179
8398
  }
8180
- async function executeRuntimeTests(options, config, testFiles, totalFiles) {
8181
- if (!config.silent && !usesAgentFormatter(config.formatters, config.verbose) && !hasFormatter(config.formatters, "json") && testFiles.length !== totalFiles) process.stderr.write(`Running ${String(testFiles.length)} of ${String(totalFiles)} test files\n`);
8182
- const backend = await resolveBackend(options.cli, config);
8399
+ async function executeRuntimeTests(options) {
8400
+ const { cli, config, testFiles, timing, totalFiles } = options;
8401
+ const useDefaultFormatter = isDefaultHumanFormatter(config);
8402
+ emitRunHeader({
8403
+ collectCoverage: config.collectCoverage,
8404
+ color: config.color,
8405
+ formatters: config.formatters,
8406
+ rootDir: config.rootDir,
8407
+ silent: config.silent,
8408
+ verbose: config.verbose,
8409
+ version: VERSION$1
8410
+ });
8411
+ if (useDefaultFormatter && testFiles.length !== totalFiles) process.stderr.write(`Running ${String(testFiles.length)} of ${String(totalFiles)} test files\n`);
8412
+ const backend = await timing.profileAsync("resolveBackend", async () => {
8413
+ return resolveBackend(cli, config);
8414
+ });
8183
8415
  try {
8184
- const { results } = await runProjects({
8185
- backend,
8186
- deferFormatting: true,
8187
- projects: [{
8188
- config,
8189
- testFiles
8190
- }],
8191
- startTime: Date.now(),
8192
- version: VERSION$1
8416
+ const { results } = await timing.profileAsync("runProjects", async () => {
8417
+ return runProjects({
8418
+ backend,
8419
+ deferFormatting: true,
8420
+ projects: [{
8421
+ config,
8422
+ testFiles
8423
+ }],
8424
+ startTime: Date.now(),
8425
+ timing,
8426
+ version: VERSION$1
8427
+ });
8193
8428
  });
8194
8429
  return results[0];
8195
8430
  } finally {
@@ -8925,12 +9160,7 @@ const SYNTHESIZED_PLACE_FILE = "synthesized.rbxl";
8925
9160
  const WORKSPACE_CACHE_DIRECTORY = path$1.join(".jest-roblox", "workspace");
8926
9161
  const ROJO_PROJECT_DEFAULT = "test.project.json";
8927
9162
  async function runWorkspace(options) {
8928
- const timing = createTimingCollector();
8929
- try {
8930
- return await runWorkspaceProfiled(options, timing);
8931
- } finally {
8932
- timing.flushTimingReport();
8933
- }
9163
+ return runWorkspaceProfiled(options, options.timing ?? NOOP_TIMING_COLLECTOR);
8934
9164
  }
8935
9165
  function buildCoverageMap(entries) {
8936
9166
  const map = /* @__PURE__ */ new Map();
@@ -9044,6 +9274,7 @@ async function runWorkspaceProfiled(options, timing) {
9044
9274
  };
9045
9275
  }),
9046
9276
  startTime,
9277
+ timing,
9047
9278
  version,
9048
9279
  ...dispatchSpec
9049
9280
  });
@@ -9110,13 +9341,33 @@ async function prepareWorkspaceDispatch(input) {
9110
9341
  }
9111
9342
  return { scriptOverride: generateMaterializerScript(inputs) };
9112
9343
  }
9344
+ /**
9345
+ * Resolve a `--testPathPattern` against this package's files Node-side, then
9346
+ * forward an Instance-namespace basename pattern (see {@link narrowForLuauRun}).
9347
+ *
9348
+ * A pattern that matches no file in this package simply targets a different
9349
+ * package: keep the (zero-matching) raw pattern so Jest-on-Roblox runs nothing,
9350
+ * and set `passWithNoTests` so it doesn't `exit(1)`. The raw pattern is
9351
+ * load-bearing here — clearing it would drop the filter entirely and make the
9352
+ * Luau side fall back to `testMatch`, running the whole package.
9353
+ */
9354
+ function narrowPackageTestPathPattern(packageConfig) {
9355
+ if (packageConfig.testPathPattern === void 0) return packageConfig;
9356
+ const { files } = discoverTestFiles(packageConfig);
9357
+ const { runtimeFiles } = classifyTestFiles(files, packageConfig);
9358
+ if (runtimeFiles.length === 0) return {
9359
+ ...packageConfig,
9360
+ passWithNoTests: true
9361
+ };
9362
+ return narrowForLuauRun(packageConfig, runtimeFiles, true);
9363
+ }
9113
9364
  async function loadPackages(input) {
9114
9365
  const { cli, packageInfos, timing } = input;
9115
9366
  const loaded = [];
9116
9367
  for (const info of packageInfos) {
9117
- const packageConfig = mergeCliWithConfig(cli, await timing.profileAsync(`load-config:${info.name}`, async () => {
9368
+ const packageConfig = narrowPackageTestPathPattern(mergeCliWithConfig(cli, await timing.profileAsync(`load-config:${info.name}`, async () => {
9118
9369
  return loadConfig$1(void 0, info.packageDirectory);
9119
- }));
9370
+ })));
9120
9371
  const rojoProject = packageConfig.rojoProject ?? ROJO_PROJECT_DEFAULT;
9121
9372
  const hasExplicitIgnore = packageConfig.coveragePathIgnorePatterns !== DEFAULT_CONFIG.coveragePathIgnorePatterns;
9122
9373
  const hasExplicitCoverageCache = packageConfig.coverageCache !== DEFAULT_CONFIG.coverageCache;
@@ -9642,7 +9893,7 @@ const EMPTY_RESULT = {
9642
9893
  preCoverageMs: 0,
9643
9894
  projectResults: []
9644
9895
  };
9645
- async function runWorkspaceMode(cli, workspace) {
9896
+ async function runWorkspaceMode(cli, workspace, timing) {
9646
9897
  const basicValidation = validateBasicWorkspaceFlags(cli);
9647
9898
  if (!basicValidation.ok) return {
9648
9899
  ...EMPTY_RESULT,
@@ -9708,6 +9959,14 @@ async function runWorkspaceMode(cli, workspace) {
9708
9959
  }
9709
9960
  let runtimeResults;
9710
9961
  try {
9962
+ emitRunHeader({
9963
+ color: runOptions.color,
9964
+ formatters: runOptions.formatters,
9965
+ rootDir: workspaceRoot,
9966
+ silent: runOptions.silent,
9967
+ verbose: cli.verbose,
9968
+ version: VERSION
9969
+ });
9711
9970
  const onStreamingResult = resolveStreamingProgressSink(runOptions, cli);
9712
9971
  runtimeResults = await runWorkspace({
9713
9972
  backend,
@@ -9715,6 +9974,7 @@ async function runWorkspaceMode(cli, workspace) {
9715
9974
  ...onStreamingResult !== void 0 ? { onStreamingResult } : {},
9716
9975
  packageInfos,
9717
9976
  runOptions,
9977
+ timing,
9718
9978
  version: VERSION,
9719
9979
  workspaceRoot,
9720
9980
  workStealingCredentials
@@ -9804,8 +10064,11 @@ function composeWorkspaceDisplayName(package_, project) {
9804
10064
  * either break the structured output or be silenced anyway.
9805
10065
  */
9806
10066
  function resolveStreamingProgressSink(runOptions, cli) {
9807
- if (runOptions.silent) return;
9808
- if (hasFormatter(runOptions.formatters, "json") || usesAgentFormatter(runOptions.formatters, cli.verbose)) return;
10067
+ if (!isDefaultHumanFormatter({
10068
+ formatters: runOptions.formatters,
10069
+ silent: runOptions.silent,
10070
+ verbose: cli.verbose
10071
+ })) return;
9809
10072
  return (entry) => {
9810
10073
  const line = formatStreamingProgressLine(entry, { color: runOptions.color });
9811
10074
  process.stdout.write(`${line}\n`);
@@ -9817,18 +10080,25 @@ function isWorkspaceInvocation(cli) {
9817
10080
  return cli.workspace === true || cli.packages !== void 0 || cli.affectedSince !== void 0;
9818
10081
  }
9819
10082
  async function runJestRoblox(cli, config) {
9820
- if (isWorkspaceInvocation(cli)) return runWorkspaceMode(cli, config.workspace);
9821
- const merged = mergeCliWithConfig(cli, config);
9822
- const rawProjects = merged.projects;
9823
- if (rawProjects !== void 0 && rawProjects.length > 0) return runMultiProject({
9824
- cli,
9825
- config: merged,
9826
- rawProjects
9827
- });
9828
- return runSingleProject({
9829
- cli,
9830
- config: merged
9831
- });
10083
+ const timing = createTimingCollector();
10084
+ try {
10085
+ if (isWorkspaceInvocation(cli)) return await runWorkspaceMode(cli, config.workspace, timing);
10086
+ const merged = mergeCliWithConfig(cli, config);
10087
+ const rawProjects = merged.projects;
10088
+ if (rawProjects !== void 0 && rawProjects.length > 0) return await runMultiProject({
10089
+ cli,
10090
+ config: merged,
10091
+ rawProjects,
10092
+ timing
10093
+ });
10094
+ return await runSingleProject({
10095
+ cli,
10096
+ config: merged,
10097
+ timing
10098
+ });
10099
+ } finally {
10100
+ timing.flushTimingReport();
10101
+ }
9832
10102
  }
9833
10103
  //#endregion
9834
- export { mergeCliWithConfig as A, defineProject as B, formatFailure as C, LuauScriptError as D, formatBanner as E, JEST_ARGV_EXCLUDED_KEYS as F, ConfigError as H, ROOT_CLI_KEYS as I, SHARED_TEST_KEYS as L, resolveConfig as M, DEFAULT_CONFIG as N, extractJsonFromOutput as O, GLOBAL_TEST_KEYS as P, VALID_BACKENDS as R, writeJsonFile$1 as S, formatTestSummary as T, version as U, isValidBackend as V, runProjects as _, visitStatement as a, writeGameOutput as b, OpenCloudBackend as c, generateTestScript as d, outputMultiResult as f, formatExecuteOutput as g, formatJobSummary as h, visitExpression as i, loadConfig$1 as j, parseJestOutput as k, createOpenCloudBackend as l, formatAnnotations as m, runTypecheck as n, StudioBackend as o, outputSingleResult as p, visitBlock as r, createStudioBackend as s, runJestRoblox as t, buildJestArgv as u, formatGameOutputNotice as v, formatResult as w, formatJson as x, parseGameOutput as y, defineConfig as z };
10104
+ export { extractJsonFromOutput as A, VALID_BACKENDS as B, formatJson as C, formatTestSummary as D, formatResult as E, DEFAULT_CONFIG as F, version as G, defineProject as H, GLOBAL_TEST_KEYS as I, JEST_ARGV_EXCLUDED_KEYS as L, mergeCliWithConfig as M, loadConfig$1 as N, formatBanner as O, resolveConfig as P, ROOT_CLI_KEYS as R, writeGameOutput as S, formatFailure as T, isValidBackend as U, defineConfig as V, ConfigError as W, formatJobSummary as _, visitStatement as a, formatGameOutputNotice as b, OpenCloudBackend as c, walkErrorChain as d, buildJestArgv as f, formatAnnotations as g, outputSingleResult as h, visitExpression as i, parseJestOutput as j, LuauScriptError as k, createOpenCloudBackend as l, outputMultiResult as m, runTypecheck as n, StudioBackend as o, generateTestScript as p, visitBlock as r, createStudioBackend as s, runJestRoblox as t, formatMissingScopes as u, formatExecuteOutput as v, writeJsonFile$1 as w, parseGameOutput as x, runProjects as y, SHARED_TEST_KEYS as z };