@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.
@@ -686,7 +686,7 @@ function C$4({ force: e } = {}) {
686
686
  var y$3 = C$4();
687
687
  //#endregion
688
688
  //#region package.json
689
- var version = "0.3.0";
689
+ var version = "0.3.2";
690
690
  //#endregion
691
691
  //#region src/config/errors.ts
692
692
  var ConfigError = class extends Error {
@@ -5332,6 +5332,11 @@ async function findWorkspaceDir(id = process.cwd(), options = {}) {
5332
5332
  throw new Error(`Cannot detect workspace root from ${id}`);
5333
5333
  }
5334
5334
  //#endregion
5335
+ //#region \0sea-stub:giget
5336
+ var require__sea_stub_giget = /* @__PURE__ */ __commonJSMin(((exports, module) => {
5337
+ module.exports = {};
5338
+ }));
5339
+ //#endregion
5335
5340
  //#region node_modules/.pnpm/c12@4.0.0-beta.5_jiti@2.7.0_magicast@0.5.3/node_modules/c12/dist/_chunks/libs/ohash.mjs
5336
5341
  var ohash_exports = /* @__PURE__ */ __exportAll$1({
5337
5342
  n: () => dist_exports,
@@ -11261,7 +11266,7 @@ async function resolveConfig$1(source, options, sourceOptions = {}) {
11261
11266
  const customProviderKeys = Object.keys(sourceOptions.giget?.providers || {}).map((key) => `${key}:`);
11262
11267
  const gigetPrefixes = customProviderKeys.length > 0 ? [...new Set([...customProviderKeys, ...GIGET_PREFIXES])] : GIGET_PREFIXES;
11263
11268
  if (options.giget !== false && gigetPrefixes.some((prefix) => source.startsWith(prefix))) {
11264
- const { downloadTemplate } = await import("giget").catch((error) => {
11269
+ const { downloadTemplate } = await Promise.resolve().then(() => /* @__PURE__ */ __toESM(require__sea_stub_giget(), 1)).catch((error) => {
11265
11270
  throw new Error(`Extending config from \`${source}\` requires \`giget\` peer dependency to be installed.\n\nInstall it with: \`npx nypm i giget\``, { cause: error });
11266
11271
  });
11267
11272
  const { digest } = await Promise.resolve().then(() => (init_ohash(), ohash_exports)).then((n) => n.n);
@@ -20145,12 +20150,35 @@ function mapBranchArmLocations(traceMap, locations, sourceMapDirectory) {
20145
20150
  tsPath
20146
20151
  };
20147
20152
  }
20153
+ /**
20154
+ * Detects a phantom branch arm produced by a source-less synthetic statement
20155
+ * `if` (e.g. a roblox-ts Array polyfill like `.filter`/`.includes`). The
20156
+ * synthetic `if` has no source map entry, so trace-mapping's greatest-lower-
20157
+ * bound bias snaps both arms onto the nearest preceding segment — the then-
20158
+ * arm's own start — yielding a zero-width arm that coincides with another
20159
+ * arm's start and can never be covered.
20160
+ *
20161
+ * A genuine statement `if` is safe: roblox-ts always renders it multi-line, so
20162
+ * the then-body (generated line `if+1`) and the implicit-else arm (generated
20163
+ * line `if`) carry distinct source-map segments and never collapse. This is
20164
+ * gated to `type === "if"` by the caller: a single-line `expr-if` (ternary)
20165
+ * legitimately collapses to one column-0 segment and must NOT be dropped.
20166
+ */
20167
+ function hasCollapsedPhantomArm(locations) {
20168
+ return locations.some((arm, index) => {
20169
+ if (!(arm.start.line === arm.end.line && arm.start.column === arm.end.column)) return false;
20170
+ return locations.some((other, otherIndex) => {
20171
+ return otherIndex !== index && other.start.line === arm.start.line && other.start.column === arm.start.column;
20172
+ });
20173
+ });
20174
+ }
20148
20175
  function mapFileBranches(resources, fileCoverage, pendingBranches) {
20149
20176
  if (resources.coverageMap.branchMap === void 0) return;
20150
20177
  for (const [branchId, entry] of Object.entries(resources.coverageMap.branchMap)) {
20151
20178
  const armHitCounts = fileCoverage.b?.[branchId] ?? [];
20152
20179
  const result = mapBranchArmLocations(resources.traceMap, entry.locations, resources.sourceMapDirectory);
20153
20180
  if (result === void 0) continue;
20181
+ if (entry.type === "if" && hasCollapsedPhantomArm(result.locations)) continue;
20154
20182
  let fileBranches = pendingBranches.get(result.tsPath);
20155
20183
  if (fileBranches === void 0) {
20156
20184
  fileBranches = [];
@@ -24412,6 +24440,7 @@ function convertToLuau(filePath) {
24412
24440
  const RbxPathParent = Symbol("Parent");
24413
24441
  var RojoResolver = class RojoResolver {
24414
24442
  rbxPath = new Array();
24443
+ realpathCache = /* @__PURE__ */ new Map();
24415
24444
  walkedConfigFilesInternal = /* @__PURE__ */ new Set();
24416
24445
  walkedDirectoriesInternal = /* @__PURE__ */ new Set();
24417
24446
  filePathToRbxPathMap = /* @__PURE__ */ new Map();
@@ -24422,12 +24451,12 @@ var RojoResolver = class RojoResolver {
24422
24451
  static findRojoConfigFilePath(projectPath) {
24423
24452
  const warnings = new Array();
24424
24453
  const defaultPath = node_path.default.join(projectPath, ROJO_DEFAULT_NAME);
24425
- if ((0, node_fs.existsSync)(defaultPath)) return {
24454
+ if (node_fs.existsSync(defaultPath)) return {
24426
24455
  path: defaultPath,
24427
24456
  warnings
24428
24457
  };
24429
24458
  const candidates = new Array();
24430
- for (const fileName of (0, node_fs.readdirSync)(projectPath)) if (fileName !== ROJO_DEFAULT_NAME && (fileName === ROJO_OLD_NAME || ROJO_FILE_REGEX.test(fileName))) candidates.push(node_path.default.join(projectPath, fileName));
24459
+ for (const fileName of node_fs.readdirSync(projectPath)) if (fileName !== ROJO_DEFAULT_NAME && (fileName === ROJO_OLD_NAME || ROJO_FILE_REGEX.test(fileName))) candidates.push(node_path.default.join(projectPath, fileName));
24431
24460
  if (candidates.length > 1) warnings.push(`Multiple *.project.json files found, using ${candidates[0]}`);
24432
24461
  return {
24433
24462
  path: candidates[0],
@@ -24563,34 +24592,42 @@ var RojoResolver = class RojoResolver {
24563
24592
  get walkedDirectories() {
24564
24593
  return this.walkedDirectoriesInternal;
24565
24594
  }
24595
+ cachedRealpath(targetPath) {
24596
+ let resolved = this.realpathCache.get(targetPath);
24597
+ if (resolved === void 0) {
24598
+ resolved = node_fs.realpathSync(targetPath);
24599
+ this.realpathCache.set(targetPath, resolved);
24600
+ }
24601
+ return resolved;
24602
+ }
24566
24603
  getContainer(from, rbxPath) {
24567
24604
  if (this.isGame && rbxPath) {
24568
24605
  for (const container of from) if (arrayStartsWith(rbxPath, container)) return container;
24569
24606
  }
24570
24607
  }
24571
24608
  parseConfig(rojoConfigFilePath, doNotPush = false) {
24572
- if (!(0, node_fs.existsSync)(rojoConfigFilePath)) {
24609
+ if (!node_fs.existsSync(rojoConfigFilePath)) {
24573
24610
  this.warn(`RojoResolver: Path does not exist "${rojoConfigFilePath}"`);
24574
24611
  return;
24575
24612
  }
24576
- const realPath = (0, node_fs.realpathSync)(rojoConfigFilePath);
24613
+ const realPath = this.cachedRealpath(rojoConfigFilePath);
24577
24614
  this.walkedConfigFilesInternal.add(realPath);
24578
24615
  let configJson;
24579
24616
  try {
24580
- configJson = JSON.parse((0, node_fs.readFileSync)(realPath, "utf8"));
24617
+ configJson = JSON.parse(node_fs.readFileSync(realPath, "utf8"));
24581
24618
  } catch {}
24582
24619
  if (isValidRojoConfig(configJson)) this.parseTree(node_path.default.dirname(rojoConfigFilePath), configJson.name, configJson.tree, doNotPush);
24583
24620
  else this.warn("RojoResolver: Invalid configuration!");
24584
24621
  }
24585
24622
  parsePath(itemPath) {
24586
24623
  const luauPath = convertToLuau(itemPath);
24587
- const realPath = (0, node_fs.existsSync)(luauPath) ? (0, node_fs.realpathSync)(luauPath) : luauPath;
24624
+ const realPath = node_fs.existsSync(luauPath) ? this.cachedRealpath(luauPath) : luauPath;
24588
24625
  const extension = node_path.default.extname(luauPath);
24589
24626
  if (ROJO_MODULE_EXTS.has(extension)) this.filePathToRbxPathMap.set(luauPath, [...this.rbxPath]);
24590
24627
  else {
24591
- const isDirectory = (0, node_fs.existsSync)(realPath) && (0, node_fs.statSync)(realPath).isDirectory();
24628
+ const isDirectory = node_fs.existsSync(realPath) && node_fs.statSync(realPath).isDirectory();
24592
24629
  if (isDirectory) this.walkedDirectoriesInternal.add(realPath);
24593
- if (isDirectory && (0, node_fs.readdirSync)(realPath).includes(ROJO_DEFAULT_NAME)) this.parseConfig(node_path.default.join(luauPath, ROJO_DEFAULT_NAME), true);
24630
+ if (isDirectory && node_fs.readdirSync(realPath).includes(ROJO_DEFAULT_NAME)) this.parseConfig(node_path.default.join(luauPath, ROJO_DEFAULT_NAME), true);
24594
24631
  else {
24595
24632
  this.partitions.unshift({
24596
24633
  fsPath: luauPath,
@@ -24607,26 +24644,40 @@ var RojoResolver = class RojoResolver {
24607
24644
  for (const childName of Object.keys(tree).filter((value) => !value.startsWith("$"))) this.parseTree(basePath, childName, tree[childName]);
24608
24645
  if (!doNotPush) this.rbxPath.pop();
24609
24646
  }
24610
- searchChildren(directory, children) {
24611
- for (const child of children) {
24612
- const childPath = node_path.default.join(directory, child);
24613
- if ((0, node_fs.statSync)((0, node_fs.realpathSync)(childPath)).isFile() && child !== ROJO_DEFAULT_NAME && ROJO_FILE_REGEX.test(child)) this.parseConfig(childPath);
24614
- }
24615
- for (const child of children) {
24616
- const childPath = node_path.default.join(directory, child);
24617
- if ((0, node_fs.statSync)((0, node_fs.realpathSync)(childPath)).isDirectory()) this.searchDirectory(childPath, child);
24647
+ searchChildren(directory, directoryEntries) {
24648
+ const projectFiles = new Array();
24649
+ const subDirectories = new Array();
24650
+ for (const entry of directoryEntries) {
24651
+ const childPath = node_path.default.join(directory, entry.name);
24652
+ let isFile = entry.isFile();
24653
+ let isDirectory = entry.isDirectory();
24654
+ if (!isFile && !isDirectory) try {
24655
+ const stat = node_fs.statSync(this.cachedRealpath(childPath));
24656
+ isFile = stat.isFile();
24657
+ isDirectory = stat.isDirectory();
24658
+ } catch (err) {
24659
+ this.warn(`RojoResolver: Failed to resolve "${childPath}" (${err.message})`);
24660
+ continue;
24661
+ }
24662
+ if (isFile && ROJO_FILE_REGEX.test(entry.name)) projectFiles.push(childPath);
24663
+ else if (isDirectory) subDirectories.push({
24664
+ name: entry.name,
24665
+ path: childPath
24666
+ });
24618
24667
  }
24668
+ for (const childPath of projectFiles) this.parseConfig(childPath);
24669
+ for (const { name, path: childPath } of subDirectories) this.searchDirectory(childPath, name);
24619
24670
  }
24620
24671
  searchDirectory(directory, item) {
24621
- const realPath = (0, node_fs.realpathSync)(directory);
24672
+ const realPath = this.cachedRealpath(directory);
24622
24673
  this.walkedDirectoriesInternal.add(realPath);
24623
- const children = (0, node_fs.readdirSync)(realPath);
24624
- if (children.includes(ROJO_DEFAULT_NAME)) {
24674
+ const directoryEntries = node_fs.readdirSync(directory, { withFileTypes: true });
24675
+ if (directoryEntries.some((entry) => entry.name === ROJO_DEFAULT_NAME)) {
24625
24676
  this.parseConfig(node_path.default.join(directory, ROJO_DEFAULT_NAME));
24626
24677
  return;
24627
24678
  }
24628
24679
  if (item !== void 0) this.rbxPath.push(item);
24629
- this.searchChildren(directory, children);
24680
+ this.searchChildren(directory, directoryEntries);
24630
24681
  if (item !== void 0) this.rbxPath.pop();
24631
24682
  }
24632
24683
  warn(str) {
@@ -25891,6 +25942,7 @@ function findMapping(filePath, mappings, key = "outDir") {
25891
25942
  }
25892
25943
  function replacePrefix(filePath, from, to) {
25893
25944
  if (filePath === from) return to;
25945
+ if (from === ".") return `${to}/${filePath.startsWith("./") ? filePath.slice(2) : filePath}`;
25894
25946
  if (filePath.startsWith(`${from}/`)) return `${to}${filePath.slice(from.length)}`;
25895
25947
  return filePath;
25896
25948
  }
@@ -26730,7 +26782,7 @@ var core_default = (/* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exp
26730
26782
  }
26731
26783
  });
26732
26784
  };
26733
- var MODES = /* @__PURE__ */ Object.freeze({
26785
+ var MODES = /*#__PURE__*/ Object.freeze({
26734
26786
  __proto__: null,
26735
26787
  APOS_STRING_MODE,
26736
26788
  BACKSLASH_ESCAPE,
@@ -29027,7 +29079,7 @@ function formatTestSummary(result, timing, styles, options) {
29027
29079
  }
29028
29080
  function formatResult(result, timing, options) {
29029
29081
  const styles = createStyles(options.color, options.slowTestThreshold);
29030
- const lines = [formatRunHeader(options, styles)];
29082
+ const lines = [""];
29031
29083
  for (const file of result.testResults) {
29032
29084
  if (options.failuresOnly === true && file.numFailingTests === 0 && !hasExecError(file)) continue;
29033
29085
  lines.push(formatFileSummary(file, options, styles));
@@ -29201,7 +29253,7 @@ function formatMultiProjectResult(projects, timing, options) {
29201
29253
  result,
29202
29254
  styles
29203
29255
  }));
29204
- const lines = [formatRunHeader(options, styles), sections.join("\n\n")];
29256
+ const lines = ["", sections.join("\n\n")];
29205
29257
  const mergedResult = mergeJestResults(projects.map((project) => project.result));
29206
29258
  lines.push("", formatTestSummary(mergedResult, timing, styles, {
29207
29259
  snapshotWriteFailures: options.snapshotWriteFailures,
@@ -30008,6 +30060,15 @@ function hasFormatter(formatters, name) {
30008
30060
  function usesAgentFormatter(formatters, verbose = false) {
30009
30061
  return hasFormatter(formatters, "agent") && !verbose;
30010
30062
  }
30063
+ /**
30064
+ * Whether human-facing progress output (the run header, the "Running X of Y"
30065
+ * notice, the workspace streaming lines) should be written: not silent and not
30066
+ * a machine-readable formatter (json, or non-verbose agent). The single source
30067
+ * of truth so these sinks can't drift apart.
30068
+ */
30069
+ function isDefaultHumanFormatter(options) {
30070
+ return options.silent !== true && !usesAgentFormatter(options.formatters, options.verbose) && !hasFormatter(options.formatters, "json");
30071
+ }
30011
30072
  //#endregion
30012
30073
  //#region src/snapshot/path-resolver.ts
30013
30074
  function createSnapshotPathResolver(config) {
@@ -30020,7 +30081,7 @@ function createSnapshotPathResolver(config) {
30020
30081
  const result = `${basePath}/${normalized.slice(prefix.length + 1)}`;
30021
30082
  const mapping = findMapping(result, tsconfigMappings);
30022
30083
  if (mapping !== void 0) return {
30023
- filePath: replacePrefix(result, mapping.outDir, mapping.rootDir).replace(/^\.\//, ""),
30084
+ filePath: replacePrefix(result, mapping.outDir, mapping.rootDir),
30024
30085
  mapping
30025
30086
  };
30026
30087
  return { filePath: result };
@@ -30040,6 +30101,96 @@ function buildMappings(tree, prefix) {
30040
30101
  return mappings;
30041
30102
  }
30042
30103
  //#endregion
30104
+ //#region src/timing/orchestration-collector.ts
30105
+ /**
30106
+ * A buffered span-tree profiler for a single, sequential host run. Nesting is
30107
+ * tracked with one shared stack, so spans must open and close in LIFO order:
30108
+ * profile a phase, and any spans it opens nest under it. It is NOT safe to run
30109
+ * two `profile` / `profileAsync` calls concurrently on the same collector (e.g.
30110
+ * `Promise.all`) — interleaved opens/closes would corrupt the stack. Create one
30111
+ * collector per run; `flushTimingReport` empties it so a second flush is a
30112
+ * no-op.
30113
+ */
30114
+ function createTimingCollector(options = {}) {
30115
+ const clock = options.clock ?? { now: () => node_perf_hooks.performance.now() };
30116
+ const sink = options.sink ?? ((line) => void node_process.default.stderr.write(`${line}\n`));
30117
+ const enabled = options.enabled ?? node_process.default.env["TIMING"] !== void 0;
30118
+ const roots = /* @__PURE__ */ new Map();
30119
+ const stack = [];
30120
+ function open(name) {
30121
+ const top = stack.at(-1);
30122
+ const node = childOf(top === void 0 ? roots : top.children, name);
30123
+ stack.push(node);
30124
+ const start = clock.now();
30125
+ return () => {
30126
+ node.elapsedMs += clock.now() - start;
30127
+ stack.pop();
30128
+ };
30129
+ }
30130
+ function profile(name, func) {
30131
+ if (!enabled) return func();
30132
+ const close = open(name);
30133
+ try {
30134
+ return func();
30135
+ } finally {
30136
+ close();
30137
+ }
30138
+ }
30139
+ async function profileAsync(name, func) {
30140
+ if (!enabled) return func();
30141
+ const close = open(name);
30142
+ try {
30143
+ return await func();
30144
+ } finally {
30145
+ close();
30146
+ }
30147
+ }
30148
+ function record(name, elapsedMs) {
30149
+ if (!enabled) return;
30150
+ const top = stack.at(-1);
30151
+ const node = childOf(top === void 0 ? roots : top.children, name);
30152
+ node.elapsedMs += elapsedMs;
30153
+ }
30154
+ function emit(node, depth) {
30155
+ sink(`[TIMING] ${" ".repeat(depth)}${node.name}: ${String(Math.round(node.elapsedMs))}ms`);
30156
+ for (const child of node.children.values()) emit(child, depth + 1);
30157
+ }
30158
+ function flushTimingReport() {
30159
+ if (!enabled || roots.size === 0) return;
30160
+ let total = 0;
30161
+ for (const node of roots.values()) {
30162
+ emit(node, 0);
30163
+ total += Math.round(node.elapsedMs);
30164
+ }
30165
+ sink(`[TIMING] TOTAL (host): ${String(total)}ms`);
30166
+ roots.clear();
30167
+ }
30168
+ return {
30169
+ flushTimingReport,
30170
+ profile,
30171
+ profileAsync,
30172
+ record
30173
+ };
30174
+ }
30175
+ /**
30176
+ * Shared disabled collector for callers that thread a profiler through their
30177
+ * signatures but are invoked outside a profiled workspace run (single-mode
30178
+ * coverage, the `instrument` subcommand, tests). Every method is a no-op.
30179
+ */
30180
+ const NOOP_TIMING_COLLECTOR = createTimingCollector({ enabled: false });
30181
+ function childOf(parent, name) {
30182
+ let node = parent.get(name);
30183
+ if (node === void 0) {
30184
+ node = {
30185
+ name,
30186
+ children: /* @__PURE__ */ new Map(),
30187
+ elapsedMs: 0
30188
+ };
30189
+ parent.set(name, node);
30190
+ }
30191
+ return node;
30192
+ }
30193
+ //#endregion
30043
30194
  //#region src/types/rojo.ts
30044
30195
  const rojoProjectSchema = type({
30045
30196
  "name": "string",
@@ -30189,38 +30340,48 @@ function formatExecuteOutput(options) {
30189
30340
  * config.
30190
30341
  */
30191
30342
  async function runProjects(options) {
30192
- const jobs = options.projects.map((project) => buildProjectJob(project));
30193
- const { rawResults, timing: backendTiming } = await options.backend.runTests({
30194
- jobs,
30195
- parallel: options.parallel,
30196
- scriptOverride: options.scriptOverride,
30197
- streaming: options.streaming,
30198
- workStealing: options.workStealing
30343
+ const timing = options.timing ?? NOOP_TIMING_COLLECTOR;
30344
+ const jobs = timing.profile("buildJobs", () => {
30345
+ return options.projects.map((project) => buildProjectJob(project, timing));
30346
+ });
30347
+ const { rawResults, timing: backendTiming } = await timing.profileAsync("backend.runTests", async () => {
30348
+ const result = await options.backend.runTests({
30349
+ jobs,
30350
+ parallel: options.parallel,
30351
+ scriptOverride: options.scriptOverride,
30352
+ streaming: options.streaming,
30353
+ workStealing: options.workStealing
30354
+ });
30355
+ recordBackendTimingSpans(timing, result.timing);
30356
+ return result;
30199
30357
  });
30200
30358
  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`);
30201
30359
  return {
30202
30360
  backendTiming,
30203
- results: rawResults.map((raw, index) => {
30204
- const job = jobs[index];
30205
- try {
30206
- return processProjectResult(buildProjectResult(raw.entry, job, raw.fallbackGameOutput), {
30207
- backendTiming,
30208
- config: job.config,
30209
- deferFormatting: options.deferFormatting,
30210
- startTime: options.startTime,
30211
- version: options.version
30212
- });
30213
- } catch (err) {
30214
- if (!(err instanceof LuauScriptError)) throw err;
30215
- return buildExecutionErrorResult({
30216
- backendTiming,
30217
- config: job.config,
30218
- deferFormatting: options.deferFormatting,
30219
- error: err,
30220
- startTime: options.startTime,
30221
- version: options.version
30222
- });
30223
- }
30361
+ results: timing.profile("processResults", () => {
30362
+ return rawResults.map((raw, index) => {
30363
+ const job = jobs[index];
30364
+ try {
30365
+ return processProjectResult(buildProjectResult(raw.entry, job, raw.fallbackGameOutput), {
30366
+ backendTiming,
30367
+ config: job.config,
30368
+ deferFormatting: options.deferFormatting,
30369
+ startTime: options.startTime,
30370
+ timing,
30371
+ version: options.version
30372
+ });
30373
+ } catch (err) {
30374
+ if (!(err instanceof LuauScriptError)) throw err;
30375
+ return buildExecutionErrorResult({
30376
+ backendTiming,
30377
+ config: job.config,
30378
+ deferFormatting: options.deferFormatting,
30379
+ error: err,
30380
+ startTime: options.startTime,
30381
+ version: options.version
30382
+ });
30383
+ }
30384
+ });
30224
30385
  })
30225
30386
  };
30226
30387
  }
@@ -30261,6 +30422,10 @@ function parseTsconfigMappings(options) {
30261
30422
  rootDir: normalizeDirectoryPath(options.rootDir ?? "src")
30262
30423
  }];
30263
30424
  }
30425
+ function recordBackendTimingSpans(timing, backendTiming) {
30426
+ if (backendTiming.uploadMs !== void 0) timing.record("uploadMs", backendTiming.uploadMs);
30427
+ timing.record("executionMs", backendTiming.executionMs);
30428
+ }
30264
30429
  const EXIT_CODE_MESSAGE$1 = /^Exited with code: \d+$/;
30265
30430
  /**
30266
30431
  * Compose the human-readable failure message for an exec-error file
@@ -30461,7 +30626,7 @@ function writeSnapshots(snapshotWrites, config, tsconfigMappings) {
30461
30626
  node_fs.writeFileSync(absolutePath, content);
30462
30627
  const { filePath, mapping } = resolved;
30463
30628
  if (mapping !== void 0) {
30464
- const outPath = mapping.outDir + filePath.slice(mapping.rootDir.length);
30629
+ const outPath = replacePrefix(filePath, mapping.rootDir, mapping.outDir);
30465
30630
  const absoluteOutPath = node_path.resolve(config.rootDir, outPath);
30466
30631
  node_fs.mkdirSync(node_path.dirname(absoluteOutPath), { recursive: true });
30467
30632
  node_fs.writeFileSync(absoluteOutPath, content);
@@ -30490,19 +30655,23 @@ function writeSnapshots(snapshotWrites, config, tsconfigMappings) {
30490
30655
  * formatter output. Called once per job.
30491
30656
  */
30492
30657
  function processProjectResult(entry, options) {
30493
- const { backendTiming, config, deferFormatting, startTime, version } = options;
30658
+ const { backendTiming, config, deferFormatting, startTime, timing, version } = options;
30494
30659
  const { coverageData, gameOutput, luauTiming, result, setupMs, snapshotWrites } = entry;
30495
- const tsconfigMappings = resolveAllTsconfigMappings(config.rootDir);
30496
- const writeCounts = snapshotWrites !== void 0 ? writeSnapshots(snapshotWrites, config, tsconfigMappings) : {
30660
+ const tsconfigMappings = timing.profile("resolveTsconfigMappings", () => {
30661
+ return resolveAllTsconfigMappings(config.rootDir);
30662
+ });
30663
+ const writeCounts = snapshotWrites !== void 0 ? timing.profile("writeSnapshots", () => {
30664
+ return writeSnapshots(snapshotWrites, config, tsconfigMappings);
30665
+ }) : {
30497
30666
  attempted: 0,
30498
30667
  failed: 0,
30499
30668
  written: 0
30500
30669
  };
30501
30670
  const testsMs = calculateTestsMs(result.testResults);
30502
- const sourceMapper = config.sourceMap ? buildSourceMapper(config, tsconfigMappings) : void 0;
30671
+ const sourceMapper = config.sourceMap ? timing.profile("buildSourceMapper", () => buildSourceMapper(config, tsconfigMappings)) : void 0;
30503
30672
  resolveTestFilePaths(result, sourceMapper);
30504
30673
  const totalMs = Date.now() - startTime;
30505
- const timing = {
30674
+ const resultTiming = {
30506
30675
  executionMs: backendTiming.executionMs,
30507
30676
  setupMs,
30508
30677
  startTime,
@@ -30515,7 +30684,7 @@ function processProjectResult(entry, options) {
30515
30684
  result,
30516
30685
  snapshotWriteFailures: writeCounts.failed,
30517
30686
  sourceMapper,
30518
- timing,
30687
+ timing: resultTiming,
30519
30688
  version
30520
30689
  }) : "";
30521
30690
  if (luauTiming !== void 0) printLuauTiming(luauTiming);
@@ -30527,7 +30696,7 @@ function processProjectResult(entry, options) {
30527
30696
  result,
30528
30697
  snapshotWriteFailures: writeCounts.failed > 0 ? writeCounts.failed : void 0,
30529
30698
  sourceMapper,
30530
- timing
30699
+ timing: resultTiming
30531
30700
  };
30532
30701
  }
30533
30702
  /**
@@ -30535,8 +30704,10 @@ function processProjectResult(entry, options) {
30535
30704
  * carries its own config so the Luau runner never re-resolves or shares format
30536
30705
  * state across projects (fixes the spike's snapshot-diff regression — C1).
30537
30706
  */
30538
- function buildProjectJob(parameters) {
30539
- const tsconfigMappings = resolveAllTsconfigMappings(parameters.config.rootDir);
30707
+ function buildProjectJob(parameters, timing) {
30708
+ const tsconfigMappings = timing.profile("resolveTsconfigMappings", () => {
30709
+ return resolveAllTsconfigMappings(parameters.config.rootDir);
30710
+ });
30540
30711
  const luauProject = isLuauProject(parameters.testFiles, tsconfigMappings);
30541
30712
  return {
30542
30713
  config: applySnapshotFormatDefaults(parameters.config, luauProject),
@@ -37331,7 +37502,10 @@ var OcaleRunner = class {
37331
37502
  script,
37332
37503
  timeoutSeconds,
37333
37504
  universeId: this.credentials.universeId
37334
- }, { timeoutMs: timeout });
37505
+ }, {
37506
+ retryableTransportCodes: TRANSIENT_TRANSPORT_CODES,
37507
+ timeoutMs: timeout
37508
+ });
37335
37509
  if (!result.success) {
37336
37510
  if (result.err instanceof PollTimeoutError) throw new Error("Execution timed out", { cause: result.err });
37337
37511
  throw new Error(result.err.message, { cause: result.err });
@@ -37353,7 +37527,7 @@ var OcaleRunner = class {
37353
37527
  placeId: this.credentials.placeId,
37354
37528
  universeId: this.credentials.universeId
37355
37529
  };
37356
- const result = await this.places.save(parameters);
37530
+ const result = await this.places.save(parameters, { retryableTransportCodes: TRANSIENT_TRANSPORT_CODES });
37357
37531
  if (!result.success) throw new Error(`Failed to upload place: ${result.err.message}`, { cause: result.err });
37358
37532
  return {
37359
37533
  uploadMs: Date.now() - uploadStart,
@@ -37471,6 +37645,35 @@ function generateTestScript(options) {
37471
37645
  return test_runner_bundled_default.replace("__CONFIG_JSON__", () => JSON.stringify({ configs }));
37472
37646
  }
37473
37647
  //#endregion
37648
+ //#region src/utils/error-chain.ts
37649
+ const MAX_DEPTH = 5;
37650
+ function walkErrorChain(err) {
37651
+ const entries = [];
37652
+ let current = err;
37653
+ while (current instanceof Error && entries.length < MAX_DEPTH) {
37654
+ entries.push({
37655
+ name: current.constructor.name,
37656
+ code: readStringProperty(current, "code"),
37657
+ errno: readStringProperty(current, "errno"),
37658
+ message: current.message,
37659
+ requiredScopes: current instanceof PermissionError ? current.requiredScopes : void 0,
37660
+ syscall: readStringProperty(current, "syscall")
37661
+ });
37662
+ current = current.cause;
37663
+ }
37664
+ return entries;
37665
+ }
37666
+ function formatMissingScopes(scopes) {
37667
+ if (scopes.length === 0) return "API key has insufficient scopes. Add via Creator Dashboard.";
37668
+ const joined = scopes.join(", ");
37669
+ return `API key missing scope${scopes.length === 1 ? "" : "s"} ${joined}. Add via Creator Dashboard.`;
37670
+ }
37671
+ function readStringProperty(err, key) {
37672
+ const value = Reflect.get(err, key);
37673
+ if (value === void 0 || value === null) return;
37674
+ return String(value);
37675
+ }
37676
+ //#endregion
37474
37677
  //#region src/backends/open-cloud.ts
37475
37678
  const PARALLEL_AUTO_CAP = 3;
37476
37679
  const BASE_URL_ENV = "JEST_ROBLOX_OPEN_CLOUD_BASE_URL";
@@ -37606,10 +37809,7 @@ function createOpenCloudBackend(credentials) {
37606
37809
  }
37607
37810
  function describeError(err) {
37608
37811
  const cause = err instanceof Error ? err.cause : void 0;
37609
- if (cause instanceof PermissionError) {
37610
- const scopes = cause.requiredScopes.join(", ");
37611
- return `API key missing scope${cause.requiredScopes.length === 1 ? "" : "s"} ${scopes}. Add via Creator Dashboard.`;
37612
- }
37812
+ if (cause instanceof PermissionError) return formatMissingScopes(cause.requiredScopes);
37613
37813
  return err instanceof Error ? err.message : String(err);
37614
37814
  }
37615
37815
  function warnStreamingDisabled(err, state) {
@@ -38382,6 +38582,27 @@ function narrowConfigByFiles(config, files) {
38382
38582
  testPathPattern: `(${branches.join("|")})`
38383
38583
  };
38384
38584
  }
38585
+ /**
38586
+ * Forward an Instance-namespace `testPathPattern` to the Luau runner.
38587
+ *
38588
+ * Node-side discovery is the source of truth: the FS-namespace filter
38589
+ * (positional args or `--testPathPattern`) has already resolved to a concrete
38590
+ * file set against real paths. Drop the raw FS-shaped pattern and re-narrow by
38591
+ * the discovered files so Jest-on-Roblox matches the same files — its paths are
38592
+ * Roblox Instance names (e.g. `ServerScriptService/...`) with no `src/` prefix,
38593
+ * so a raw FS pattern like `src/server/foo.spec` matches zero files there.
38594
+ *
38595
+ * `filterActive` gates the rewrite: a bare run (no positionals, no
38596
+ * `testPathPattern`) leaves the config untouched so the Luau side runs every
38597
+ * `testMatch` file rather than a giant basename alternation.
38598
+ */
38599
+ function narrowForLuauRun(config, runtimeFiles, filterActive) {
38600
+ if (!filterActive) return config;
38601
+ return narrowConfigByFiles({
38602
+ ...config,
38603
+ testPathPattern: void 0
38604
+ }, runtimeFiles);
38605
+ }
38385
38606
  function toBasenamePattern(file) {
38386
38607
  const posix = file.replaceAll("\\", "/");
38387
38608
  const lastSlash = posix.lastIndexOf("/");
@@ -38915,89 +39136,6 @@ function buildWithRojo(projectPath, outputPath) {
38915
39136
  }
38916
39137
  }
38917
39138
  //#endregion
38918
- //#region src/timing/orchestration-collector.ts
38919
- /**
38920
- * A buffered span-tree profiler for a single, sequential host run. Nesting is
38921
- * tracked with one shared stack, so spans must open and close in LIFO order:
38922
- * profile a phase, and any spans it opens nest under it. It is NOT safe to run
38923
- * two `profile` / `profileAsync` calls concurrently on the same collector (e.g.
38924
- * `Promise.all`) — interleaved opens/closes would corrupt the stack. Create one
38925
- * collector per run; `flushTimingReport` empties it so a second flush is a
38926
- * no-op.
38927
- */
38928
- function createTimingCollector(options = {}) {
38929
- const clock = options.clock ?? { now: () => node_perf_hooks.performance.now() };
38930
- const sink = options.sink ?? ((line) => void node_process.default.stderr.write(`${line}\n`));
38931
- const enabled = options.enabled ?? node_process.default.env["TIMING"] !== void 0;
38932
- const roots = /* @__PURE__ */ new Map();
38933
- const stack = [];
38934
- function open(name) {
38935
- const top = stack.at(-1);
38936
- const node = childOf(top === void 0 ? roots : top.children, name);
38937
- stack.push(node);
38938
- const start = clock.now();
38939
- return () => {
38940
- node.elapsedMs += clock.now() - start;
38941
- stack.pop();
38942
- };
38943
- }
38944
- function profile(name, func) {
38945
- if (!enabled) return func();
38946
- const close = open(name);
38947
- try {
38948
- return func();
38949
- } finally {
38950
- close();
38951
- }
38952
- }
38953
- async function profileAsync(name, func) {
38954
- if (!enabled) return func();
38955
- const close = open(name);
38956
- try {
38957
- return await func();
38958
- } finally {
38959
- close();
38960
- }
38961
- }
38962
- function emit(node, depth) {
38963
- sink(`[TIMING] ${" ".repeat(depth)}${node.name}: ${String(Math.round(node.elapsedMs))}ms`);
38964
- for (const child of node.children.values()) emit(child, depth + 1);
38965
- }
38966
- function flushTimingReport() {
38967
- if (!enabled || roots.size === 0) return;
38968
- let total = 0;
38969
- for (const node of roots.values()) {
38970
- emit(node, 0);
38971
- total += Math.round(node.elapsedMs);
38972
- }
38973
- sink(`[TIMING] TOTAL (host): ${String(total)}ms`);
38974
- roots.clear();
38975
- }
38976
- return {
38977
- flushTimingReport,
38978
- profile,
38979
- profileAsync
38980
- };
38981
- }
38982
- /**
38983
- * Shared disabled collector for callers that thread a profiler through their
38984
- * signatures but are invoked outside a profiled workspace run (single-mode
38985
- * coverage, the `instrument` subcommand, tests). Every method is a no-op.
38986
- */
38987
- const NOOP_TIMING_COLLECTOR = createTimingCollector({ enabled: false });
38988
- function childOf(parent, name) {
38989
- let node = parent.get(name);
38990
- if (node === void 0) {
38991
- node = {
38992
- name,
38993
- children: /* @__PURE__ */ new Map(),
38994
- elapsedMs: 0
38995
- };
38996
- parent.set(name, node);
38997
- }
38998
- return node;
38999
- }
39000
- //#endregion
39001
39139
  //#region src/utils/hash.ts
39002
39140
  function hashBuffer(data) {
39003
39141
  return (0, node_crypto.createHash)("sha256").update(data).digest("hex");
@@ -40472,15 +40610,47 @@ function classifyTestFiles(files, config) {
40472
40610
  typeTestFiles
40473
40611
  };
40474
40612
  }
40613
+ function resolveAllSetupFilePaths(configs) {
40614
+ const resolvers = /* @__PURE__ */ new Map();
40615
+ for (const config of configs) {
40616
+ if (config.setupFiles === void 0 && config.setupFilesAfterEnv === void 0) continue;
40617
+ const rojoConfigPath = node_path.resolve(config.rootDir, config.rojoProject ?? DEFAULT_ROJO_PROJECT$1);
40618
+ const key = JSON.stringify([config.rootDir, rojoConfigPath]);
40619
+ let resolve = resolvers.get(key);
40620
+ if (resolve === void 0) {
40621
+ resolve = createSetupResolver({
40622
+ configDirectory: config.rootDir,
40623
+ rojoConfigPath
40624
+ });
40625
+ resolvers.set(key, resolve);
40626
+ }
40627
+ if (config.setupFiles !== void 0) config.setupFiles = config.setupFiles.map(resolve);
40628
+ if (config.setupFilesAfterEnv !== void 0) config.setupFilesAfterEnv = config.setupFilesAfterEnv.map(resolve);
40629
+ }
40630
+ }
40475
40631
  function resolveSetupFilePaths(config) {
40476
- if (config.setupFiles === void 0 && config.setupFilesAfterEnv === void 0) return;
40477
- const rojoConfigPath = node_path.resolve(config.rootDir, config.rojoProject ?? DEFAULT_ROJO_PROJECT$1);
40478
- const resolve = createSetupResolver({
40479
- configDirectory: config.rootDir,
40480
- rojoConfigPath
40481
- });
40482
- if (config.setupFiles !== void 0) config.setupFiles = config.setupFiles.map(resolve);
40483
- if (config.setupFilesAfterEnv !== void 0) config.setupFilesAfterEnv = config.setupFilesAfterEnv.map(resolve);
40632
+ resolveAllSetupFilePaths([config]);
40633
+ }
40634
+ //#endregion
40635
+ //#region src/run/run-header.ts
40636
+ /**
40637
+ * Print the ` RUN vX.Y <rootDir>` header to stdout at the moment a run begins
40638
+ * (right before the backend uploads), so the CLI doesn't look stalled while it
40639
+ * waits for remote results. The end-of-run formatters no longer emit it.
40640
+ *
40641
+ * Self-gates to the default human formatter: nothing is written under
40642
+ * `--silent`, `--formatters json`, or `--formatters agent` (without
40643
+ * `--verbose`), which produce machine-readable output that must stay clean.
40644
+ */
40645
+ function emitRunHeader(input) {
40646
+ if (!isDefaultHumanFormatter(input)) return;
40647
+ node_process.default.stdout.write(formatRunHeader({
40648
+ collectCoverage: input.collectCoverage,
40649
+ color: input.color,
40650
+ rootDir: input.rootDir,
40651
+ verbose: input.verbose ?? false,
40652
+ version: input.version
40653
+ }));
40484
40654
  }
40485
40655
  //#endregion
40486
40656
  //#region src/run/multi.ts
@@ -40488,30 +40658,61 @@ const DEFAULT_ROJO_PROJECT = "default.project.json";
40488
40658
  const VERSION$3 = version;
40489
40659
  async function runMultiProject(options) {
40490
40660
  const { cli, config: rootConfig, rawProjects } = options;
40491
- const allProjects = await resolveAllProjects(rawProjects, rootConfig, loadRojoTree(rootConfig), rootConfig.rootDir);
40492
- for (const project of allProjects) resolveSetupFilePaths(project.config);
40493
- const { filesByProject, projects } = selectProjects(allProjects, cli.project, cli.files, rootConfig.rootDir);
40661
+ const timing = options.timing ?? NOOP_TIMING_COLLECTOR;
40662
+ const rojoTree = timing.profile("loadRojoTree", () => loadRojoTree(rootConfig));
40663
+ const allProjects = await timing.profileAsync("resolveAllProjects", async () => {
40664
+ return resolveAllProjects(rawProjects, rootConfig, rojoTree, rootConfig.rootDir);
40665
+ });
40666
+ timing.profile("resolveSetupFilePaths", () => {
40667
+ resolveAllSetupFilePaths(allProjects.map((project) => project.config));
40668
+ });
40669
+ const { filesByProject, projects } = timing.profile("selectProjects", () => {
40670
+ return selectProjects(allProjects, cli.project, cli.files, rootConfig.rootDir);
40671
+ });
40494
40672
  const cacheRoot = node_path.resolve(rootConfig.rootDir, ".jest-roblox", "cache");
40495
- const cleaned = cleanLeftoverStubs(projects, rootConfig.rootDir);
40673
+ const cleaned = timing.profile("cleanLeftoverStubs", () => {
40674
+ return cleanLeftoverStubs(projects, rootConfig.rootDir);
40675
+ });
40496
40676
  if (cleaned.length > 0) node_process.default.stderr.write(`jest-roblox: cleaned ${String(cleaned.length)} leftover stub(s):\n${cleaned.map((stubPath) => ` ${stubPath}\n`).join("")}`);
40497
- generateProjectStubs(projects, rootConfig.rootDir, cacheRoot);
40498
- const { effectiveConfig, preCoverageMs } = prepareMultiProjectCoverage(rootConfig, projects, cacheRoot);
40499
- const backend = await resolveBackend(cli, effectiveConfig);
40677
+ timing.profile("generateProjectStubs", () => {
40678
+ generateProjectStubs(projects, rootConfig.rootDir, cacheRoot);
40679
+ });
40680
+ const { effectiveConfig, preCoverageMs } = timing.profile("prepareCoverage", () => {
40681
+ return prepareMultiProjectCoverage(rootConfig, projects, cacheRoot);
40682
+ });
40683
+ const backend = await timing.profileAsync("resolveBackend", async () => {
40684
+ return resolveBackend(cli, effectiveConfig);
40685
+ });
40500
40686
  const parallel = effectiveParallelForBackend(effectiveConfig.parallel, backend);
40501
- if (!rootConfig.collectCoverage && backend.kind === "open-cloud") buildOpenCloudPlace(rootConfig, projects, cacheRoot);
40502
- const { allTypeTestFiles, pendingJobs } = collectPendingJobs({
40503
- cliFiles: cli.files,
40504
- effectivePlaceFile: effectiveConfig.placeFile,
40505
- filesByProject,
40506
- projects,
40507
- rootConfig
40687
+ if (!rootConfig.collectCoverage && backend.kind === "open-cloud") timing.profile("buildOpenCloudPlace", () => {
40688
+ buildOpenCloudPlace(rootConfig, projects, cacheRoot);
40508
40689
  });
40509
- const projectResults = await runJobs(backend, pendingJobs, parallel);
40510
- const uniqueTypeTestFiles = [...new Set(allTypeTestFiles)];
40511
- const typecheckResult = uniqueTypeTestFiles.length > 0 ? runTypecheck({
40512
- files: uniqueTypeTestFiles,
40690
+ const { allTypeTestFiles, pendingJobs } = timing.profile("collectPendingJobs", () => {
40691
+ return collectPendingJobs({
40692
+ cliFiles: cli.files,
40693
+ effectivePlaceFile: effectiveConfig.placeFile,
40694
+ filesByProject,
40695
+ projects,
40696
+ rootConfig
40697
+ });
40698
+ });
40699
+ if (pendingJobs.length > 0) emitRunHeader({
40700
+ collectCoverage: rootConfig.collectCoverage,
40701
+ color: rootConfig.color,
40702
+ formatters: rootConfig.formatters,
40513
40703
  rootDir: rootConfig.rootDir,
40514
- tsconfig: rootConfig.typecheckTsconfig
40704
+ silent: rootConfig.silent,
40705
+ verbose: rootConfig.verbose,
40706
+ version: VERSION$3
40707
+ });
40708
+ const projectResults = await runJobs(backend, pendingJobs, parallel, timing);
40709
+ const uniqueTypeTestFiles = [...new Set(allTypeTestFiles)];
40710
+ const typecheckResult = uniqueTypeTestFiles.length > 0 ? timing.profile("runTypecheck", () => {
40711
+ return runTypecheck({
40712
+ files: uniqueTypeTestFiles,
40713
+ rootDir: rootConfig.rootDir,
40714
+ tsconfig: rootConfig.typecheckTsconfig
40715
+ });
40515
40716
  }) : void 0;
40516
40717
  if (projectResults.length === 0 && typecheckResult === void 0) {
40517
40718
  if (rootConfig.passWithNoTests) return {
@@ -40575,10 +40776,11 @@ function collectPendingJobs(arguments_) {
40575
40776
  testMatch: project.include
40576
40777
  };
40577
40778
  const { runtimeFiles, typeTestFiles } = classifyTestFiles(discoverTestFiles(discoveryConfig, projectCliFiles).files, rootConfig);
40578
- const projConfig = narrowConfigByFiles({
40779
+ const filterActive = (projectCliFiles?.length ?? 0) > 0 || discoveryConfig.testPathPattern !== void 0;
40780
+ const projConfig = narrowForLuauRun({
40579
40781
  ...discoveryConfig,
40580
40782
  testMatch: project.testMatch
40581
- }, projectCliFiles ?? []);
40783
+ }, runtimeFiles, filterActive);
40582
40784
  allTypeTestFiles.push(...typeTestFiles);
40583
40785
  if (runtimeFiles.length === 0) continue;
40584
40786
  const runtimeInjectionPaths = [];
@@ -40599,28 +40801,31 @@ function collectPendingJobs(arguments_) {
40599
40801
  pendingJobs
40600
40802
  };
40601
40803
  }
40602
- async function runJobs(backend, pendingJobs, parallel) {
40804
+ async function runJobs(backend, pendingJobs, parallel, timing) {
40603
40805
  if (pendingJobs.length === 0) {
40604
40806
  await backend.close?.();
40605
40807
  return [];
40606
40808
  }
40607
40809
  let runResult;
40608
40810
  try {
40609
- runResult = await runProjects({
40610
- backend,
40611
- deferFormatting: true,
40612
- parallel,
40613
- projects: pendingJobs.map((pending) => {
40614
- return {
40615
- config: pending.config,
40616
- displayColor: pending.displayColor,
40617
- displayName: pending.displayName,
40618
- runtimeInjectionPaths: pending.runtimeInjectionPaths,
40619
- testFiles: pending.runtimeFiles
40620
- };
40621
- }),
40622
- startTime: Date.now(),
40623
- version: VERSION$3
40811
+ runResult = await timing.profileAsync("runProjects", async () => {
40812
+ return runProjects({
40813
+ backend,
40814
+ deferFormatting: true,
40815
+ parallel,
40816
+ projects: pendingJobs.map((pending) => {
40817
+ return {
40818
+ config: pending.config,
40819
+ displayColor: pending.displayColor,
40820
+ displayName: pending.displayName,
40821
+ runtimeInjectionPaths: pending.runtimeInjectionPaths,
40822
+ testFiles: pending.runtimeFiles
40823
+ };
40824
+ }),
40825
+ startTime: Date.now(),
40826
+ timing,
40827
+ version: VERSION$3
40828
+ });
40624
40829
  });
40625
40830
  } finally {
40626
40831
  await backend.close?.();
@@ -40697,11 +40902,16 @@ function selectProjects(allProjects, projectNames, cliFiles, rootDirectory) {
40697
40902
  const VERSION$2 = version;
40698
40903
  async function runSingleProject(options) {
40699
40904
  const { cli } = options;
40700
- const config = narrowConfigByFiles(options.config, cli.files ?? []);
40701
- resolveSetupFilePaths(config);
40702
- const discovery = discoverTestFiles(config, cli.files);
40905
+ const timing = options.timing ?? NOOP_TIMING_COLLECTOR;
40906
+ const baseConfig = { ...options.config };
40907
+ timing.profile("resolveSetupFilePaths", () => {
40908
+ resolveSetupFilePaths(baseConfig);
40909
+ });
40910
+ const discovery = timing.profile("discoverTestFiles", () => {
40911
+ return discoverTestFiles(baseConfig, cli.files);
40912
+ });
40703
40913
  if (discovery.files.length === 0) {
40704
- if (config.passWithNoTests) return {
40914
+ if (baseConfig.passWithNoTests) return {
40705
40915
  mode: "single",
40706
40916
  preCoverageMs: 0
40707
40917
  };
@@ -40712,7 +40922,13 @@ async function runSingleProject(options) {
40712
40922
  validationExitCode: 2
40713
40923
  };
40714
40924
  }
40715
- const { runtimeFiles, typeTestFiles } = classifyTestFiles(discovery.files, config);
40925
+ const { runtimeFiles, typeTestFiles } = timing.profile("classifyTestFiles", () => {
40926
+ return classifyTestFiles(discovery.files, baseConfig);
40927
+ });
40928
+ const filterActive = (cli.files?.length ?? 0) > 0 || baseConfig.testPathPattern !== void 0;
40929
+ const config = timing.profile("narrowForLuauRun", () => {
40930
+ return narrowForLuauRun(baseConfig, runtimeFiles, filterActive);
40931
+ });
40716
40932
  if (typeTestFiles.length === 0 && runtimeFiles.length === 0) {
40717
40933
  if (config.passWithNoTests) return {
40718
40934
  mode: "single",
@@ -40729,19 +40945,27 @@ async function runSingleProject(options) {
40729
40945
  let effectiveConfig = config;
40730
40946
  if (config.collectCoverage && !config.typecheckOnly && runtimeFiles.length > 0) {
40731
40947
  const preCoverageStart = Date.now();
40732
- const { placeFile } = prepareCoverage(config);
40948
+ const { placeFile } = timing.profile("prepareCoverage", () => prepareCoverage(config));
40733
40949
  preCoverageMs = Date.now() - preCoverageStart;
40734
40950
  effectiveConfig = {
40735
40951
  ...config,
40736
40952
  placeFile
40737
40953
  };
40738
40954
  }
40739
- const typecheckResult = typeTestFiles.length > 0 ? runTypecheck({
40740
- files: typeTestFiles,
40741
- rootDir: effectiveConfig.rootDir,
40742
- tsconfig: effectiveConfig.typecheckTsconfig
40955
+ const typecheckResult = typeTestFiles.length > 0 ? timing.profile("runTypecheck", () => {
40956
+ return runTypecheck({
40957
+ files: typeTestFiles,
40958
+ rootDir: effectiveConfig.rootDir,
40959
+ tsconfig: effectiveConfig.typecheckTsconfig
40960
+ });
40961
+ }) : void 0;
40962
+ const runtimeResult = runtimeFiles.length > 0 ? await executeRuntimeTests({
40963
+ cli,
40964
+ config: effectiveConfig,
40965
+ testFiles: runtimeFiles,
40966
+ timing,
40967
+ totalFiles: discovery.totalFiles
40743
40968
  }) : void 0;
40744
- const runtimeResult = runtimeFiles.length > 0 ? await executeRuntimeTests(options, effectiveConfig, runtimeFiles, discovery.totalFiles) : void 0;
40745
40969
  return {
40746
40970
  mode: "single",
40747
40971
  preCoverageMs,
@@ -40749,19 +40973,35 @@ async function runSingleProject(options) {
40749
40973
  typecheckResult
40750
40974
  };
40751
40975
  }
40752
- async function executeRuntimeTests(options, config, testFiles, totalFiles) {
40753
- if (!config.silent && !usesAgentFormatter(config.formatters, config.verbose) && !hasFormatter(config.formatters, "json") && testFiles.length !== totalFiles) node_process.default.stderr.write(`Running ${String(testFiles.length)} of ${String(totalFiles)} test files\n`);
40754
- const backend = await resolveBackend(options.cli, config);
40976
+ async function executeRuntimeTests(options) {
40977
+ const { cli, config, testFiles, timing, totalFiles } = options;
40978
+ const useDefaultFormatter = isDefaultHumanFormatter(config);
40979
+ emitRunHeader({
40980
+ collectCoverage: config.collectCoverage,
40981
+ color: config.color,
40982
+ formatters: config.formatters,
40983
+ rootDir: config.rootDir,
40984
+ silent: config.silent,
40985
+ verbose: config.verbose,
40986
+ version: VERSION$2
40987
+ });
40988
+ if (useDefaultFormatter && testFiles.length !== totalFiles) node_process.default.stderr.write(`Running ${String(testFiles.length)} of ${String(totalFiles)} test files\n`);
40989
+ const backend = await timing.profileAsync("resolveBackend", async () => {
40990
+ return resolveBackend(cli, config);
40991
+ });
40755
40992
  try {
40756
- const { results } = await runProjects({
40757
- backend,
40758
- deferFormatting: true,
40759
- projects: [{
40760
- config,
40761
- testFiles
40762
- }],
40763
- startTime: Date.now(),
40764
- version: VERSION$2
40993
+ const { results } = await timing.profileAsync("runProjects", async () => {
40994
+ return runProjects({
40995
+ backend,
40996
+ deferFormatting: true,
40997
+ projects: [{
40998
+ config,
40999
+ testFiles
41000
+ }],
41001
+ startTime: Date.now(),
41002
+ timing,
41003
+ version: VERSION$2
41004
+ });
40765
41005
  });
40766
41006
  return results[0];
40767
41007
  } finally {
@@ -41497,12 +41737,7 @@ const SYNTHESIZED_PLACE_FILE = "synthesized.rbxl";
41497
41737
  const WORKSPACE_CACHE_DIRECTORY = node_path.join(".jest-roblox", "workspace");
41498
41738
  const ROJO_PROJECT_DEFAULT = "test.project.json";
41499
41739
  async function runWorkspace(options) {
41500
- const timing = createTimingCollector();
41501
- try {
41502
- return await runWorkspaceProfiled(options, timing);
41503
- } finally {
41504
- timing.flushTimingReport();
41505
- }
41740
+ return runWorkspaceProfiled(options, options.timing ?? NOOP_TIMING_COLLECTOR);
41506
41741
  }
41507
41742
  function buildCoverageMap(entries) {
41508
41743
  const map = /* @__PURE__ */ new Map();
@@ -41616,6 +41851,7 @@ async function runWorkspaceProfiled(options, timing) {
41616
41851
  };
41617
41852
  }),
41618
41853
  startTime,
41854
+ timing,
41619
41855
  version,
41620
41856
  ...dispatchSpec
41621
41857
  });
@@ -41682,13 +41918,33 @@ async function prepareWorkspaceDispatch(input) {
41682
41918
  }
41683
41919
  return { scriptOverride: generateMaterializerScript(inputs) };
41684
41920
  }
41921
+ /**
41922
+ * Resolve a `--testPathPattern` against this package's files Node-side, then
41923
+ * forward an Instance-namespace basename pattern (see {@link narrowForLuauRun}).
41924
+ *
41925
+ * A pattern that matches no file in this package simply targets a different
41926
+ * package: keep the (zero-matching) raw pattern so Jest-on-Roblox runs nothing,
41927
+ * and set `passWithNoTests` so it doesn't `exit(1)`. The raw pattern is
41928
+ * load-bearing here — clearing it would drop the filter entirely and make the
41929
+ * Luau side fall back to `testMatch`, running the whole package.
41930
+ */
41931
+ function narrowPackageTestPathPattern(packageConfig) {
41932
+ if (packageConfig.testPathPattern === void 0) return packageConfig;
41933
+ const { files } = discoverTestFiles(packageConfig);
41934
+ const { runtimeFiles } = classifyTestFiles(files, packageConfig);
41935
+ if (runtimeFiles.length === 0) return {
41936
+ ...packageConfig,
41937
+ passWithNoTests: true
41938
+ };
41939
+ return narrowForLuauRun(packageConfig, runtimeFiles, true);
41940
+ }
41685
41941
  async function loadPackages(input) {
41686
41942
  const { cli, packageInfos, timing } = input;
41687
41943
  const loaded = [];
41688
41944
  for (const info of packageInfos) {
41689
- const packageConfig = mergeCliWithConfig(cli, await timing.profileAsync(`load-config:${info.name}`, async () => {
41945
+ const packageConfig = narrowPackageTestPathPattern(mergeCliWithConfig(cli, await timing.profileAsync(`load-config:${info.name}`, async () => {
41690
41946
  return loadConfig(void 0, info.packageDirectory);
41691
- }));
41947
+ })));
41692
41948
  const rojoProject = packageConfig.rojoProject ?? ROJO_PROJECT_DEFAULT;
41693
41949
  const hasExplicitIgnore = packageConfig.coveragePathIgnorePatterns !== DEFAULT_CONFIG.coveragePathIgnorePatterns;
41694
41950
  const hasExplicitCoverageCache = packageConfig.coverageCache !== DEFAULT_CONFIG.coverageCache;
@@ -42215,7 +42471,7 @@ const EMPTY_RESULT = {
42215
42471
  preCoverageMs: 0,
42216
42472
  projectResults: []
42217
42473
  };
42218
- async function runWorkspaceMode(cli, workspace) {
42474
+ async function runWorkspaceMode(cli, workspace, timing) {
42219
42475
  const basicValidation = validateBasicWorkspaceFlags(cli);
42220
42476
  if (!basicValidation.ok) return {
42221
42477
  ...EMPTY_RESULT,
@@ -42281,6 +42537,14 @@ async function runWorkspaceMode(cli, workspace) {
42281
42537
  }
42282
42538
  let runtimeResults;
42283
42539
  try {
42540
+ emitRunHeader({
42541
+ color: runOptions.color,
42542
+ formatters: runOptions.formatters,
42543
+ rootDir: workspaceRoot,
42544
+ silent: runOptions.silent,
42545
+ verbose: cli.verbose,
42546
+ version: VERSION$1
42547
+ });
42284
42548
  const onStreamingResult = resolveStreamingProgressSink(runOptions, cli);
42285
42549
  runtimeResults = await runWorkspace({
42286
42550
  backend,
@@ -42288,6 +42552,7 @@ async function runWorkspaceMode(cli, workspace) {
42288
42552
  ...onStreamingResult !== void 0 ? { onStreamingResult } : {},
42289
42553
  packageInfos,
42290
42554
  runOptions,
42555
+ timing,
42291
42556
  version: VERSION$1,
42292
42557
  workspaceRoot,
42293
42558
  workStealingCredentials
@@ -42377,8 +42642,11 @@ function composeWorkspaceDisplayName(package_, project) {
42377
42642
  * either break the structured output or be silenced anyway.
42378
42643
  */
42379
42644
  function resolveStreamingProgressSink(runOptions, cli) {
42380
- if (runOptions.silent) return;
42381
- if (hasFormatter(runOptions.formatters, "json") || usesAgentFormatter(runOptions.formatters, cli.verbose)) return;
42645
+ if (!isDefaultHumanFormatter({
42646
+ formatters: runOptions.formatters,
42647
+ silent: runOptions.silent,
42648
+ verbose: cli.verbose
42649
+ })) return;
42382
42650
  return (entry) => {
42383
42651
  const line = formatStreamingProgressLine(entry, { color: runOptions.color });
42384
42652
  node_process.default.stdout.write(`${line}\n`);
@@ -42390,41 +42658,25 @@ function isWorkspaceInvocation(cli) {
42390
42658
  return cli.workspace === true || cli.packages !== void 0 || cli.affectedSince !== void 0;
42391
42659
  }
42392
42660
  async function runJestRoblox(cli, config) {
42393
- if (isWorkspaceInvocation(cli)) return runWorkspaceMode(cli, config.workspace);
42394
- const merged = mergeCliWithConfig(cli, config);
42395
- const rawProjects = merged.projects;
42396
- if (rawProjects !== void 0 && rawProjects.length > 0) return runMultiProject({
42397
- cli,
42398
- config: merged,
42399
- rawProjects
42400
- });
42401
- return runSingleProject({
42402
- cli,
42403
- config: merged
42404
- });
42405
- }
42406
- //#endregion
42407
- //#region src/utils/error-chain.ts
42408
- const MAX_DEPTH = 5;
42409
- function walkErrorChain(err) {
42410
- const entries = [];
42411
- let current = err;
42412
- while (current instanceof Error && entries.length < MAX_DEPTH) {
42413
- entries.push({
42414
- name: current.constructor.name,
42415
- code: readStringProperty(current, "code"),
42416
- errno: readStringProperty(current, "errno"),
42417
- message: current.message,
42418
- syscall: readStringProperty(current, "syscall")
42661
+ const timing = createTimingCollector();
42662
+ try {
42663
+ if (isWorkspaceInvocation(cli)) return await runWorkspaceMode(cli, config.workspace, timing);
42664
+ const merged = mergeCliWithConfig(cli, config);
42665
+ const rawProjects = merged.projects;
42666
+ if (rawProjects !== void 0 && rawProjects.length > 0) return await runMultiProject({
42667
+ cli,
42668
+ config: merged,
42669
+ rawProjects,
42670
+ timing
42419
42671
  });
42420
- current = current.cause;
42672
+ return await runSingleProject({
42673
+ cli,
42674
+ config: merged,
42675
+ timing
42676
+ });
42677
+ } finally {
42678
+ timing.flushTimingReport();
42421
42679
  }
42422
- return entries;
42423
- }
42424
- function readStringProperty(err, key) {
42425
- const value = Reflect.get(err, key);
42426
- if (value === void 0 || value === null) return;
42427
- return String(value);
42428
42680
  }
42429
42681
  //#endregion
42430
42682
  //#region src/cli.ts
@@ -42448,6 +42700,7 @@ Options:
42448
42700
  --no-color Disable colored output
42449
42701
  -u, --updateSnapshot Update snapshot files
42450
42702
  --coverage Enable coverage collection
42703
+ --no-coverage Disable coverage for this run (overrides config)
42451
42704
  --collectCoverageFrom <glob> Globs for files to include in coverage (repeatable)
42452
42705
  --coverageDirectory <path> Directory for coverage output (default: coverage)
42453
42706
  --coverageReporters <r...> Coverage reporters (default: text, lcov)
@@ -42491,6 +42744,7 @@ Examples:
42491
42744
  jest-roblox -t "should spawn" Run tests matching pattern
42492
42745
  jest-roblox --formatters json Output JSON to file
42493
42746
  jest-roblox --coverage Run tests with coverage instrumentation
42747
+ jest-roblox --no-coverage Skip coverage instrumentation for this run
42494
42748
  `;
42495
42749
  function parseArgs(args) {
42496
42750
  const { positionals, values } = (0, node_util.parseArgs)({
@@ -42523,6 +42777,7 @@ function parseArgs(args) {
42523
42777
  type: "boolean"
42524
42778
  },
42525
42779
  "no-color": { type: "boolean" },
42780
+ "no-coverage": { type: "boolean" },
42526
42781
  "no-coverage-cache": { type: "boolean" },
42527
42782
  "no-show-luau": { type: "boolean" },
42528
42783
  "outputFile": { type: "string" },
@@ -42577,7 +42832,7 @@ function parseArgs(args) {
42577
42832
  affectedSince: values["affected-since"],
42578
42833
  apiKey: values.apiKey,
42579
42834
  backend: validateBackend(values.backend),
42580
- collectCoverage: values.coverage,
42835
+ collectCoverage: values["no-coverage"] === true ? false : values.coverage,
42581
42836
  collectCoverageFrom: values.collectCoverageFrom,
42582
42837
  color: values["no-color"] === true ? false : values.color,
42583
42838
  config: values.config,
@@ -42689,6 +42944,7 @@ function formatBackendErrorBanner(err) {
42689
42944
  const extras = formatChainExtras(entry);
42690
42945
  const label = y$3.dim(`[${index.toString()}]`);
42691
42946
  body.push(` ${label} ${entry.name}: ${entry.message}${extras}`);
42947
+ if (entry.requiredScopes !== void 0) body.push(` ${y$3.yellow(formatMissingScopes(entry.requiredScopes))}`);
42692
42948
  }
42693
42949
  return formatBanner({
42694
42950
  body,