@isentinel/jest-roblox 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@ 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.1";
37
37
  //#endregion
38
38
  //#region src/config/errors.ts
39
39
  var ConfigError = class extends Error {
@@ -1449,6 +1449,7 @@ function convertToLuau(filePath) {
1449
1449
  const RbxPathParent = Symbol("Parent");
1450
1450
  var RojoResolver = class RojoResolver {
1451
1451
  rbxPath = new Array();
1452
+ realpathCache = /* @__PURE__ */ new Map();
1452
1453
  walkedConfigFilesInternal = /* @__PURE__ */ new Set();
1453
1454
  walkedDirectoriesInternal = /* @__PURE__ */ new Set();
1454
1455
  filePathToRbxPathMap = /* @__PURE__ */ new Map();
@@ -1459,12 +1460,12 @@ var RojoResolver = class RojoResolver {
1459
1460
  static findRojoConfigFilePath(projectPath) {
1460
1461
  const warnings = new Array();
1461
1462
  const defaultPath = path.join(projectPath, ROJO_DEFAULT_NAME);
1462
- if (existsSync(defaultPath)) return {
1463
+ if (fs$1.existsSync(defaultPath)) return {
1463
1464
  path: defaultPath,
1464
1465
  warnings
1465
1466
  };
1466
1467
  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));
1468
+ 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
1469
  if (candidates.length > 1) warnings.push(`Multiple *.project.json files found, using ${candidates[0]}`);
1469
1470
  return {
1470
1471
  path: candidates[0],
@@ -1600,34 +1601,42 @@ var RojoResolver = class RojoResolver {
1600
1601
  get walkedDirectories() {
1601
1602
  return this.walkedDirectoriesInternal;
1602
1603
  }
1604
+ cachedRealpath(targetPath) {
1605
+ let resolved = this.realpathCache.get(targetPath);
1606
+ if (resolved === void 0) {
1607
+ resolved = fs$1.realpathSync(targetPath);
1608
+ this.realpathCache.set(targetPath, resolved);
1609
+ }
1610
+ return resolved;
1611
+ }
1603
1612
  getContainer(from, rbxPath) {
1604
1613
  if (this.isGame && rbxPath) {
1605
1614
  for (const container of from) if (arrayStartsWith(rbxPath, container)) return container;
1606
1615
  }
1607
1616
  }
1608
1617
  parseConfig(rojoConfigFilePath, doNotPush = false) {
1609
- if (!existsSync(rojoConfigFilePath)) {
1618
+ if (!fs$1.existsSync(rojoConfigFilePath)) {
1610
1619
  this.warn(`RojoResolver: Path does not exist "${rojoConfigFilePath}"`);
1611
1620
  return;
1612
1621
  }
1613
- const realPath = realpathSync(rojoConfigFilePath);
1622
+ const realPath = this.cachedRealpath(rojoConfigFilePath);
1614
1623
  this.walkedConfigFilesInternal.add(realPath);
1615
1624
  let configJson;
1616
1625
  try {
1617
- configJson = JSON.parse(readFileSync(realPath, "utf8"));
1626
+ configJson = JSON.parse(fs$1.readFileSync(realPath, "utf8"));
1618
1627
  } catch {}
1619
1628
  if (isValidRojoConfig(configJson)) this.parseTree(path.dirname(rojoConfigFilePath), configJson.name, configJson.tree, doNotPush);
1620
1629
  else this.warn("RojoResolver: Invalid configuration!");
1621
1630
  }
1622
1631
  parsePath(itemPath) {
1623
1632
  const luauPath = convertToLuau(itemPath);
1624
- const realPath = existsSync(luauPath) ? realpathSync(luauPath) : luauPath;
1633
+ const realPath = fs$1.existsSync(luauPath) ? this.cachedRealpath(luauPath) : luauPath;
1625
1634
  const extension = path.extname(luauPath);
1626
1635
  if (ROJO_MODULE_EXTS.has(extension)) this.filePathToRbxPathMap.set(luauPath, [...this.rbxPath]);
1627
1636
  else {
1628
- const isDirectory = existsSync(realPath) && statSync(realPath).isDirectory();
1637
+ const isDirectory = fs$1.existsSync(realPath) && fs$1.statSync(realPath).isDirectory();
1629
1638
  if (isDirectory) this.walkedDirectoriesInternal.add(realPath);
1630
- if (isDirectory && readdirSync(realPath).includes(ROJO_DEFAULT_NAME)) this.parseConfig(path.join(luauPath, ROJO_DEFAULT_NAME), true);
1639
+ if (isDirectory && fs$1.readdirSync(realPath).includes(ROJO_DEFAULT_NAME)) this.parseConfig(path.join(luauPath, ROJO_DEFAULT_NAME), true);
1631
1640
  else {
1632
1641
  this.partitions.unshift({
1633
1642
  fsPath: luauPath,
@@ -1644,26 +1653,40 @@ var RojoResolver = class RojoResolver {
1644
1653
  for (const childName of Object.keys(tree).filter((value) => !value.startsWith("$"))) this.parseTree(basePath, childName, tree[childName]);
1645
1654
  if (!doNotPush) this.rbxPath.pop();
1646
1655
  }
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);
1656
+ searchChildren(directory, directoryEntries) {
1657
+ const projectFiles = new Array();
1658
+ const subDirectories = new Array();
1659
+ for (const entry of directoryEntries) {
1660
+ const childPath = path.join(directory, entry.name);
1661
+ let isFile = entry.isFile();
1662
+ let isDirectory = entry.isDirectory();
1663
+ if (!isFile && !isDirectory) try {
1664
+ const stat = fs$1.statSync(this.cachedRealpath(childPath));
1665
+ isFile = stat.isFile();
1666
+ isDirectory = stat.isDirectory();
1667
+ } catch (err) {
1668
+ this.warn(`RojoResolver: Failed to resolve "${childPath}" (${err.message})`);
1669
+ continue;
1670
+ }
1671
+ if (isFile && ROJO_FILE_REGEX.test(entry.name)) projectFiles.push(childPath);
1672
+ else if (isDirectory) subDirectories.push({
1673
+ name: entry.name,
1674
+ path: childPath
1675
+ });
1655
1676
  }
1677
+ for (const childPath of projectFiles) this.parseConfig(childPath);
1678
+ for (const { name, path: childPath } of subDirectories) this.searchDirectory(childPath, name);
1656
1679
  }
1657
1680
  searchDirectory(directory, item) {
1658
- const realPath = realpathSync(directory);
1681
+ const realPath = this.cachedRealpath(directory);
1659
1682
  this.walkedDirectoriesInternal.add(realPath);
1660
- const children = readdirSync(realPath);
1661
- if (children.includes(ROJO_DEFAULT_NAME)) {
1683
+ const directoryEntries = fs$1.readdirSync(directory, { withFileTypes: true });
1684
+ if (directoryEntries.some((entry) => entry.name === ROJO_DEFAULT_NAME)) {
1662
1685
  this.parseConfig(path.join(directory, ROJO_DEFAULT_NAME));
1663
1686
  return;
1664
1687
  }
1665
1688
  if (item !== void 0) this.rbxPath.push(item);
1666
- this.searchChildren(directory, children);
1689
+ this.searchChildren(directory, directoryEntries);
1667
1690
  if (item !== void 0) this.rbxPath.pop();
1668
1691
  }
1669
1692
  warn(str) {
@@ -3687,7 +3710,7 @@ function createSnapshotPathResolver(config) {
3687
3710
  const result = `${basePath}/${normalized.slice(prefix.length + 1)}`;
3688
3711
  const mapping = findMapping(result, tsconfigMappings);
3689
3712
  if (mapping !== void 0) return {
3690
- filePath: replacePrefix(result, mapping.outDir, mapping.rootDir).replace(/^\.\//, ""),
3713
+ filePath: replacePrefix(result, mapping.outDir, mapping.rootDir),
3691
3714
  mapping
3692
3715
  };
3693
3716
  return { filePath: result };
@@ -3707,6 +3730,96 @@ function buildMappings(tree, prefix) {
3707
3730
  return mappings;
3708
3731
  }
3709
3732
  //#endregion
3733
+ //#region src/timing/orchestration-collector.ts
3734
+ /**
3735
+ * A buffered span-tree profiler for a single, sequential host run. Nesting is
3736
+ * tracked with one shared stack, so spans must open and close in LIFO order:
3737
+ * profile a phase, and any spans it opens nest under it. It is NOT safe to run
3738
+ * two `profile` / `profileAsync` calls concurrently on the same collector (e.g.
3739
+ * `Promise.all`) — interleaved opens/closes would corrupt the stack. Create one
3740
+ * collector per run; `flushTimingReport` empties it so a second flush is a
3741
+ * no-op.
3742
+ */
3743
+ function createTimingCollector(options = {}) {
3744
+ const clock = options.clock ?? { now: () => performance.now() };
3745
+ const sink = options.sink ?? ((line) => void process.stderr.write(`${line}\n`));
3746
+ const enabled = options.enabled ?? process.env["TIMING"] !== void 0;
3747
+ const roots = /* @__PURE__ */ new Map();
3748
+ const stack = [];
3749
+ function open(name) {
3750
+ const top = stack.at(-1);
3751
+ const node = childOf(top === void 0 ? roots : top.children, name);
3752
+ stack.push(node);
3753
+ const start = clock.now();
3754
+ return () => {
3755
+ node.elapsedMs += clock.now() - start;
3756
+ stack.pop();
3757
+ };
3758
+ }
3759
+ function profile(name, func) {
3760
+ if (!enabled) return func();
3761
+ const close = open(name);
3762
+ try {
3763
+ return func();
3764
+ } finally {
3765
+ close();
3766
+ }
3767
+ }
3768
+ async function profileAsync(name, func) {
3769
+ if (!enabled) return func();
3770
+ const close = open(name);
3771
+ try {
3772
+ return await func();
3773
+ } finally {
3774
+ close();
3775
+ }
3776
+ }
3777
+ function record(name, elapsedMs) {
3778
+ if (!enabled) return;
3779
+ const top = stack.at(-1);
3780
+ const node = childOf(top === void 0 ? roots : top.children, name);
3781
+ node.elapsedMs += elapsedMs;
3782
+ }
3783
+ function emit(node, depth) {
3784
+ sink(`[TIMING] ${" ".repeat(depth)}${node.name}: ${String(Math.round(node.elapsedMs))}ms`);
3785
+ for (const child of node.children.values()) emit(child, depth + 1);
3786
+ }
3787
+ function flushTimingReport() {
3788
+ if (!enabled || roots.size === 0) return;
3789
+ let total = 0;
3790
+ for (const node of roots.values()) {
3791
+ emit(node, 0);
3792
+ total += Math.round(node.elapsedMs);
3793
+ }
3794
+ sink(`[TIMING] TOTAL (host): ${String(total)}ms`);
3795
+ roots.clear();
3796
+ }
3797
+ return {
3798
+ flushTimingReport,
3799
+ profile,
3800
+ profileAsync,
3801
+ record
3802
+ };
3803
+ }
3804
+ /**
3805
+ * Shared disabled collector for callers that thread a profiler through their
3806
+ * signatures but are invoked outside a profiled workspace run (single-mode
3807
+ * coverage, the `instrument` subcommand, tests). Every method is a no-op.
3808
+ */
3809
+ const NOOP_TIMING_COLLECTOR = createTimingCollector({ enabled: false });
3810
+ function childOf(parent, name) {
3811
+ let node = parent.get(name);
3812
+ if (node === void 0) {
3813
+ node = {
3814
+ name,
3815
+ children: /* @__PURE__ */ new Map(),
3816
+ elapsedMs: 0
3817
+ };
3818
+ parent.set(name, node);
3819
+ }
3820
+ return node;
3821
+ }
3822
+ //#endregion
3710
3823
  //#region src/types/rojo.ts
3711
3824
  const rojoProjectSchema = type({
3712
3825
  "name": "string",
@@ -3856,38 +3969,48 @@ function formatExecuteOutput(options) {
3856
3969
  * config.
3857
3970
  */
3858
3971
  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
3972
+ const timing = options.timing ?? NOOP_TIMING_COLLECTOR;
3973
+ const jobs = timing.profile("buildJobs", () => {
3974
+ return options.projects.map((project) => buildProjectJob(project, timing));
3975
+ });
3976
+ const { rawResults, timing: backendTiming } = await timing.profileAsync("backend.runTests", async () => {
3977
+ const result = await options.backend.runTests({
3978
+ jobs,
3979
+ parallel: options.parallel,
3980
+ scriptOverride: options.scriptOverride,
3981
+ streaming: options.streaming,
3982
+ workStealing: options.workStealing
3983
+ });
3984
+ recordBackendTimingSpans(timing, result.timing);
3985
+ return result;
3866
3986
  });
3867
3987
  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
3988
  return {
3869
3989
  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
- }
3990
+ results: timing.profile("processResults", () => {
3991
+ return rawResults.map((raw, index) => {
3992
+ const job = jobs[index];
3993
+ try {
3994
+ return processProjectResult(buildProjectResult(raw.entry, job, raw.fallbackGameOutput), {
3995
+ backendTiming,
3996
+ config: job.config,
3997
+ deferFormatting: options.deferFormatting,
3998
+ startTime: options.startTime,
3999
+ timing,
4000
+ version: options.version
4001
+ });
4002
+ } catch (err) {
4003
+ if (!(err instanceof LuauScriptError)) throw err;
4004
+ return buildExecutionErrorResult({
4005
+ backendTiming,
4006
+ config: job.config,
4007
+ deferFormatting: options.deferFormatting,
4008
+ error: err,
4009
+ startTime: options.startTime,
4010
+ version: options.version
4011
+ });
4012
+ }
4013
+ });
3891
4014
  })
3892
4015
  };
3893
4016
  }
@@ -3928,6 +4051,10 @@ function parseTsconfigMappings(options) {
3928
4051
  rootDir: normalizeDirectoryPath(options.rootDir ?? "src")
3929
4052
  }];
3930
4053
  }
4054
+ function recordBackendTimingSpans(timing, backendTiming) {
4055
+ if (backendTiming.uploadMs !== void 0) timing.record("uploadMs", backendTiming.uploadMs);
4056
+ timing.record("executionMs", backendTiming.executionMs);
4057
+ }
3931
4058
  const EXIT_CODE_MESSAGE = /^Exited with code: \d+$/;
3932
4059
  /**
3933
4060
  * Compose the human-readable failure message for an exec-error file
@@ -4128,7 +4255,7 @@ function writeSnapshots(snapshotWrites, config, tsconfigMappings) {
4128
4255
  fs$1.writeFileSync(absolutePath, content);
4129
4256
  const { filePath, mapping } = resolved;
4130
4257
  if (mapping !== void 0) {
4131
- const outPath = mapping.outDir + filePath.slice(mapping.rootDir.length);
4258
+ const outPath = replacePrefix(filePath, mapping.rootDir, mapping.outDir);
4132
4259
  const absoluteOutPath = path$1.resolve(config.rootDir, outPath);
4133
4260
  fs$1.mkdirSync(path$1.dirname(absoluteOutPath), { recursive: true });
4134
4261
  fs$1.writeFileSync(absoluteOutPath, content);
@@ -4157,19 +4284,23 @@ function writeSnapshots(snapshotWrites, config, tsconfigMappings) {
4157
4284
  * formatter output. Called once per job.
4158
4285
  */
4159
4286
  function processProjectResult(entry, options) {
4160
- const { backendTiming, config, deferFormatting, startTime, version } = options;
4287
+ const { backendTiming, config, deferFormatting, startTime, timing, version } = options;
4161
4288
  const { coverageData, gameOutput, luauTiming, result, setupMs, snapshotWrites } = entry;
4162
- const tsconfigMappings = resolveAllTsconfigMappings(config.rootDir);
4163
- const writeCounts = snapshotWrites !== void 0 ? writeSnapshots(snapshotWrites, config, tsconfigMappings) : {
4289
+ const tsconfigMappings = timing.profile("resolveTsconfigMappings", () => {
4290
+ return resolveAllTsconfigMappings(config.rootDir);
4291
+ });
4292
+ const writeCounts = snapshotWrites !== void 0 ? timing.profile("writeSnapshots", () => {
4293
+ return writeSnapshots(snapshotWrites, config, tsconfigMappings);
4294
+ }) : {
4164
4295
  attempted: 0,
4165
4296
  failed: 0,
4166
4297
  written: 0
4167
4298
  };
4168
4299
  const testsMs = calculateTestsMs(result.testResults);
4169
- const sourceMapper = config.sourceMap ? buildSourceMapper(config, tsconfigMappings) : void 0;
4300
+ const sourceMapper = config.sourceMap ? timing.profile("buildSourceMapper", () => buildSourceMapper(config, tsconfigMappings)) : void 0;
4170
4301
  resolveTestFilePaths(result, sourceMapper);
4171
4302
  const totalMs = Date.now() - startTime;
4172
- const timing = {
4303
+ const resultTiming = {
4173
4304
  executionMs: backendTiming.executionMs,
4174
4305
  setupMs,
4175
4306
  startTime,
@@ -4182,7 +4313,7 @@ function processProjectResult(entry, options) {
4182
4313
  result,
4183
4314
  snapshotWriteFailures: writeCounts.failed,
4184
4315
  sourceMapper,
4185
- timing,
4316
+ timing: resultTiming,
4186
4317
  version
4187
4318
  }) : "";
4188
4319
  if (luauTiming !== void 0) printLuauTiming(luauTiming);
@@ -4194,7 +4325,7 @@ function processProjectResult(entry, options) {
4194
4325
  result,
4195
4326
  snapshotWriteFailures: writeCounts.failed > 0 ? writeCounts.failed : void 0,
4196
4327
  sourceMapper,
4197
- timing
4328
+ timing: resultTiming
4198
4329
  };
4199
4330
  }
4200
4331
  /**
@@ -4202,8 +4333,10 @@ function processProjectResult(entry, options) {
4202
4333
  * carries its own config so the Luau runner never re-resolves or shares format
4203
4334
  * state across projects (fixes the spike's snapshot-diff regression — C1).
4204
4335
  */
4205
- function buildProjectJob(parameters) {
4206
- const tsconfigMappings = resolveAllTsconfigMappings(parameters.config.rootDir);
4336
+ function buildProjectJob(parameters, timing) {
4337
+ const tsconfigMappings = timing.profile("resolveTsconfigMappings", () => {
4338
+ return resolveAllTsconfigMappings(parameters.config.rootDir);
4339
+ });
4207
4340
  const luauProject = isLuauProject(parameters.testFiles, tsconfigMappings);
4208
4341
  return {
4209
4342
  config: applySnapshotFormatDefaults(parameters.config, luauProject),
@@ -6347,89 +6480,6 @@ function buildWithRojo(projectPath, outputPath) {
6347
6480
  }
6348
6481
  }
6349
6482
  //#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
6483
  //#region src/utils/hash.ts
6434
6484
  function hashBuffer(data) {
6435
6485
  return createHash("sha256").update(data).digest("hex");
@@ -7900,15 +7950,26 @@ function classifyTestFiles(files, config) {
7900
7950
  typeTestFiles
7901
7951
  };
7902
7952
  }
7953
+ function resolveAllSetupFilePaths(configs) {
7954
+ const resolvers = /* @__PURE__ */ new Map();
7955
+ for (const config of configs) {
7956
+ if (config.setupFiles === void 0 && config.setupFilesAfterEnv === void 0) continue;
7957
+ const rojoConfigPath = path$1.resolve(config.rootDir, config.rojoProject ?? DEFAULT_ROJO_PROJECT$1);
7958
+ const key = JSON.stringify([config.rootDir, rojoConfigPath]);
7959
+ let resolve = resolvers.get(key);
7960
+ if (resolve === void 0) {
7961
+ resolve = createSetupResolver({
7962
+ configDirectory: config.rootDir,
7963
+ rojoConfigPath
7964
+ });
7965
+ resolvers.set(key, resolve);
7966
+ }
7967
+ if (config.setupFiles !== void 0) config.setupFiles = config.setupFiles.map(resolve);
7968
+ if (config.setupFilesAfterEnv !== void 0) config.setupFilesAfterEnv = config.setupFilesAfterEnv.map(resolve);
7969
+ }
7970
+ }
7903
7971
  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);
7972
+ resolveAllSetupFilePaths([config]);
7912
7973
  }
7913
7974
  //#endregion
7914
7975
  //#region src/run/multi.ts
@@ -7916,30 +7977,52 @@ const DEFAULT_ROJO_PROJECT = "default.project.json";
7916
7977
  const VERSION$2 = version;
7917
7978
  async function runMultiProject(options) {
7918
7979
  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);
7980
+ const timing = options.timing ?? NOOP_TIMING_COLLECTOR;
7981
+ const rojoTree = timing.profile("loadRojoTree", () => loadRojoTree(rootConfig));
7982
+ const allProjects = await timing.profileAsync("resolveAllProjects", async () => {
7983
+ return resolveAllProjects(rawProjects, rootConfig, rojoTree, rootConfig.rootDir);
7984
+ });
7985
+ timing.profile("resolveSetupFilePaths", () => {
7986
+ resolveAllSetupFilePaths(allProjects.map((project) => project.config));
7987
+ });
7988
+ const { filesByProject, projects } = timing.profile("selectProjects", () => {
7989
+ return selectProjects(allProjects, cli.project, cli.files, rootConfig.rootDir);
7990
+ });
7922
7991
  const cacheRoot = path$1.resolve(rootConfig.rootDir, ".jest-roblox", "cache");
7923
- const cleaned = cleanLeftoverStubs(projects, rootConfig.rootDir);
7992
+ const cleaned = timing.profile("cleanLeftoverStubs", () => {
7993
+ return cleanLeftoverStubs(projects, rootConfig.rootDir);
7994
+ });
7924
7995
  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);
7996
+ timing.profile("generateProjectStubs", () => {
7997
+ generateProjectStubs(projects, rootConfig.rootDir, cacheRoot);
7998
+ });
7999
+ const { effectiveConfig, preCoverageMs } = timing.profile("prepareCoverage", () => {
8000
+ return prepareMultiProjectCoverage(rootConfig, projects, cacheRoot);
8001
+ });
8002
+ const backend = await timing.profileAsync("resolveBackend", async () => {
8003
+ return resolveBackend(cli, effectiveConfig);
8004
+ });
7928
8005
  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
8006
+ if (!rootConfig.collectCoverage && backend.kind === "open-cloud") timing.profile("buildOpenCloudPlace", () => {
8007
+ buildOpenCloudPlace(rootConfig, projects, cacheRoot);
8008
+ });
8009
+ const { allTypeTestFiles, pendingJobs } = timing.profile("collectPendingJobs", () => {
8010
+ return collectPendingJobs({
8011
+ cliFiles: cli.files,
8012
+ effectivePlaceFile: effectiveConfig.placeFile,
8013
+ filesByProject,
8014
+ projects,
8015
+ rootConfig
8016
+ });
7936
8017
  });
7937
- const projectResults = await runJobs(backend, pendingJobs, parallel);
8018
+ const projectResults = await runJobs(backend, pendingJobs, parallel, timing);
7938
8019
  const uniqueTypeTestFiles = [...new Set(allTypeTestFiles)];
7939
- const typecheckResult = uniqueTypeTestFiles.length > 0 ? runTypecheck({
7940
- files: uniqueTypeTestFiles,
7941
- rootDir: rootConfig.rootDir,
7942
- tsconfig: rootConfig.typecheckTsconfig
8020
+ const typecheckResult = uniqueTypeTestFiles.length > 0 ? timing.profile("runTypecheck", () => {
8021
+ return runTypecheck({
8022
+ files: uniqueTypeTestFiles,
8023
+ rootDir: rootConfig.rootDir,
8024
+ tsconfig: rootConfig.typecheckTsconfig
8025
+ });
7943
8026
  }) : void 0;
7944
8027
  if (projectResults.length === 0 && typecheckResult === void 0) {
7945
8028
  if (rootConfig.passWithNoTests) return {
@@ -8027,28 +8110,31 @@ function collectPendingJobs(arguments_) {
8027
8110
  pendingJobs
8028
8111
  };
8029
8112
  }
8030
- async function runJobs(backend, pendingJobs, parallel) {
8113
+ async function runJobs(backend, pendingJobs, parallel, timing) {
8031
8114
  if (pendingJobs.length === 0) {
8032
8115
  await backend.close?.();
8033
8116
  return [];
8034
8117
  }
8035
8118
  let runResult;
8036
8119
  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
8120
+ runResult = await timing.profileAsync("runProjects", async () => {
8121
+ return runProjects({
8122
+ backend,
8123
+ deferFormatting: true,
8124
+ parallel,
8125
+ projects: pendingJobs.map((pending) => {
8126
+ return {
8127
+ config: pending.config,
8128
+ displayColor: pending.displayColor,
8129
+ displayName: pending.displayName,
8130
+ runtimeInjectionPaths: pending.runtimeInjectionPaths,
8131
+ testFiles: pending.runtimeFiles
8132
+ };
8133
+ }),
8134
+ startTime: Date.now(),
8135
+ timing,
8136
+ version: VERSION$2
8137
+ });
8052
8138
  });
8053
8139
  } finally {
8054
8140
  await backend.close?.();
@@ -8125,9 +8211,14 @@ function selectProjects(allProjects, projectNames, cliFiles, rootDirectory) {
8125
8211
  const VERSION$1 = version;
8126
8212
  async function runSingleProject(options) {
8127
8213
  const { cli } = options;
8128
- const config = narrowConfigByFiles(options.config, cli.files ?? []);
8129
- resolveSetupFilePaths(config);
8130
- const discovery = discoverTestFiles(config, cli.files);
8214
+ const timing = options.timing ?? NOOP_TIMING_COLLECTOR;
8215
+ const config = timing.profile("narrowConfigByFiles", () => {
8216
+ return narrowConfigByFiles(options.config, cli.files ?? []);
8217
+ });
8218
+ timing.profile("resolveSetupFilePaths", () => {
8219
+ resolveSetupFilePaths(config);
8220
+ });
8221
+ const discovery = timing.profile("discoverTestFiles", () => discoverTestFiles(config, cli.files));
8131
8222
  if (discovery.files.length === 0) {
8132
8223
  if (config.passWithNoTests) return {
8133
8224
  mode: "single",
@@ -8140,7 +8231,9 @@ async function runSingleProject(options) {
8140
8231
  validationExitCode: 2
8141
8232
  };
8142
8233
  }
8143
- const { runtimeFiles, typeTestFiles } = classifyTestFiles(discovery.files, config);
8234
+ const { runtimeFiles, typeTestFiles } = timing.profile("classifyTestFiles", () => {
8235
+ return classifyTestFiles(discovery.files, config);
8236
+ });
8144
8237
  if (typeTestFiles.length === 0 && runtimeFiles.length === 0) {
8145
8238
  if (config.passWithNoTests) return {
8146
8239
  mode: "single",
@@ -8157,19 +8250,27 @@ async function runSingleProject(options) {
8157
8250
  let effectiveConfig = config;
8158
8251
  if (config.collectCoverage && !config.typecheckOnly && runtimeFiles.length > 0) {
8159
8252
  const preCoverageStart = Date.now();
8160
- const { placeFile } = prepareCoverage(config);
8253
+ const { placeFile } = timing.profile("prepareCoverage", () => prepareCoverage(config));
8161
8254
  preCoverageMs = Date.now() - preCoverageStart;
8162
8255
  effectiveConfig = {
8163
8256
  ...config,
8164
8257
  placeFile
8165
8258
  };
8166
8259
  }
8167
- const typecheckResult = typeTestFiles.length > 0 ? runTypecheck({
8168
- files: typeTestFiles,
8169
- rootDir: effectiveConfig.rootDir,
8170
- tsconfig: effectiveConfig.typecheckTsconfig
8260
+ const typecheckResult = typeTestFiles.length > 0 ? timing.profile("runTypecheck", () => {
8261
+ return runTypecheck({
8262
+ files: typeTestFiles,
8263
+ rootDir: effectiveConfig.rootDir,
8264
+ tsconfig: effectiveConfig.typecheckTsconfig
8265
+ });
8266
+ }) : void 0;
8267
+ const runtimeResult = runtimeFiles.length > 0 ? await executeRuntimeTests({
8268
+ cli,
8269
+ config: effectiveConfig,
8270
+ testFiles: runtimeFiles,
8271
+ timing,
8272
+ totalFiles: discovery.totalFiles
8171
8273
  }) : void 0;
8172
- const runtimeResult = runtimeFiles.length > 0 ? await executeRuntimeTests(options, effectiveConfig, runtimeFiles, discovery.totalFiles) : void 0;
8173
8274
  return {
8174
8275
  mode: "single",
8175
8276
  preCoverageMs,
@@ -8177,19 +8278,25 @@ async function runSingleProject(options) {
8177
8278
  typecheckResult
8178
8279
  };
8179
8280
  }
8180
- async function executeRuntimeTests(options, config, testFiles, totalFiles) {
8281
+ async function executeRuntimeTests(options) {
8282
+ const { cli, config, testFiles, timing, totalFiles } = options;
8181
8283
  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);
8284
+ const backend = await timing.profileAsync("resolveBackend", async () => {
8285
+ return resolveBackend(cli, config);
8286
+ });
8183
8287
  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
8288
+ const { results } = await timing.profileAsync("runProjects", async () => {
8289
+ return runProjects({
8290
+ backend,
8291
+ deferFormatting: true,
8292
+ projects: [{
8293
+ config,
8294
+ testFiles
8295
+ }],
8296
+ startTime: Date.now(),
8297
+ timing,
8298
+ version: VERSION$1
8299
+ });
8193
8300
  });
8194
8301
  return results[0];
8195
8302
  } finally {
@@ -8925,12 +9032,7 @@ const SYNTHESIZED_PLACE_FILE = "synthesized.rbxl";
8925
9032
  const WORKSPACE_CACHE_DIRECTORY = path$1.join(".jest-roblox", "workspace");
8926
9033
  const ROJO_PROJECT_DEFAULT = "test.project.json";
8927
9034
  async function runWorkspace(options) {
8928
- const timing = createTimingCollector();
8929
- try {
8930
- return await runWorkspaceProfiled(options, timing);
8931
- } finally {
8932
- timing.flushTimingReport();
8933
- }
9035
+ return runWorkspaceProfiled(options, options.timing ?? NOOP_TIMING_COLLECTOR);
8934
9036
  }
8935
9037
  function buildCoverageMap(entries) {
8936
9038
  const map = /* @__PURE__ */ new Map();
@@ -9044,6 +9146,7 @@ async function runWorkspaceProfiled(options, timing) {
9044
9146
  };
9045
9147
  }),
9046
9148
  startTime,
9149
+ timing,
9047
9150
  version,
9048
9151
  ...dispatchSpec
9049
9152
  });
@@ -9642,7 +9745,7 @@ const EMPTY_RESULT = {
9642
9745
  preCoverageMs: 0,
9643
9746
  projectResults: []
9644
9747
  };
9645
- async function runWorkspaceMode(cli, workspace) {
9748
+ async function runWorkspaceMode(cli, workspace, timing) {
9646
9749
  const basicValidation = validateBasicWorkspaceFlags(cli);
9647
9750
  if (!basicValidation.ok) return {
9648
9751
  ...EMPTY_RESULT,
@@ -9715,6 +9818,7 @@ async function runWorkspaceMode(cli, workspace) {
9715
9818
  ...onStreamingResult !== void 0 ? { onStreamingResult } : {},
9716
9819
  packageInfos,
9717
9820
  runOptions,
9821
+ timing,
9718
9822
  version: VERSION,
9719
9823
  workspaceRoot,
9720
9824
  workStealingCredentials
@@ -9817,18 +9921,25 @@ function isWorkspaceInvocation(cli) {
9817
9921
  return cli.workspace === true || cli.packages !== void 0 || cli.affectedSince !== void 0;
9818
9922
  }
9819
9923
  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
- });
9924
+ const timing = createTimingCollector();
9925
+ try {
9926
+ if (isWorkspaceInvocation(cli)) return await runWorkspaceMode(cli, config.workspace, timing);
9927
+ const merged = mergeCliWithConfig(cli, config);
9928
+ const rawProjects = merged.projects;
9929
+ if (rawProjects !== void 0 && rawProjects.length > 0) return await runMultiProject({
9930
+ cli,
9931
+ config: merged,
9932
+ rawProjects,
9933
+ timing
9934
+ });
9935
+ return await runSingleProject({
9936
+ cli,
9937
+ config: merged,
9938
+ timing
9939
+ });
9940
+ } finally {
9941
+ timing.flushTimingReport();
9942
+ }
9832
9943
  }
9833
9944
  //#endregion
9834
9945
  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 };