@isentinel/jest-roblox 0.1.1 → 0.1.3

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.
@@ -7640,7 +7640,7 @@ function f$9() {
7640
7640
  var C$5 = f$9();
7641
7641
  //#endregion
7642
7642
  //#region package.json
7643
- var version = "0.1.1";
7643
+ var version = "0.1.3";
7644
7644
  //#endregion
7645
7645
  //#region node_modules/.pnpm/ws@8.18.0/node_modules/ws/lib/stream.js
7646
7646
  var require_stream = /* @__PURE__ */ __commonJSMin(((exports, module) => {
@@ -11358,7 +11358,7 @@ function normalizeString(path, allowAboveRoot) {
11358
11358
  }
11359
11359
  return res;
11360
11360
  }
11361
- var _DRIVE_LETTER_START_RE, _UNC_REGEX, _IS_ABSOLUTE_RE, _DRIVE_LETTER_RE, _ROOT_FOLDER_RE, _EXTNAME_RE, normalize, join, resolve$2, isAbsolute$1, extname$2, relative, dirname, basename;
11361
+ var _DRIVE_LETTER_START_RE, _UNC_REGEX, _IS_ABSOLUTE_RE, _DRIVE_LETTER_RE, _ROOT_FOLDER_RE, _EXTNAME_RE, normalize, join$2, resolve$2, isAbsolute$1, extname$2, relative$1, dirname$1, basename;
11362
11362
  var init_pathe_M_eThtNZ = __esmMin((() => {
11363
11363
  _DRIVE_LETTER_START_RE = /^[A-Za-z]:\//;
11364
11364
  _UNC_REGEX = /^[/\\]{2}/;
@@ -11385,7 +11385,7 @@ var init_pathe_M_eThtNZ = __esmMin((() => {
11385
11385
  }
11386
11386
  return isPathAbsolute && !isAbsolute$1(path) ? `/${path}` : path;
11387
11387
  };
11388
- join = function(...segments) {
11388
+ join$2 = function(...segments) {
11389
11389
  let path = "";
11390
11390
  for (const seg of segments) {
11391
11391
  if (!seg) continue;
@@ -11420,7 +11420,7 @@ var init_pathe_M_eThtNZ = __esmMin((() => {
11420
11420
  const match = _EXTNAME_RE.exec(normalizeWindowsPath$1(p));
11421
11421
  return match && match[1] || "";
11422
11422
  };
11423
- relative = function(from, to) {
11423
+ relative$1 = function(from, to) {
11424
11424
  const _from = resolve$2(from).replace(_ROOT_FOLDER_RE, "$1").split("/");
11425
11425
  const _to = resolve$2(to).replace(_ROOT_FOLDER_RE, "$1").split("/");
11426
11426
  if (_to[0][1] === ":" && _from[0][1] === ":" && _from[0] !== _to[0]) return _to.join("/");
@@ -11432,7 +11432,7 @@ var init_pathe_M_eThtNZ = __esmMin((() => {
11432
11432
  }
11433
11433
  return [..._from.map(() => ".."), ..._to].join("/");
11434
11434
  };
11435
- dirname = function(p) {
11435
+ dirname$1 = function(p) {
11436
11436
  const segments = normalizeWindowsPath$1(p).replace(/\/$/, "").split("/").slice(0, -1);
11437
11437
  if (segments.length === 1 && _DRIVE_LETTER_RE.test(segments[0])) segments[0] += "/";
11438
11438
  return segments.join("/") || (isAbsolute$1(p) ? "/" : ".");
@@ -13115,11 +13115,11 @@ async function findFile(filename, _options = {}) {
13115
13115
  let root = segments.findIndex((r) => r.match(options.rootPattern));
13116
13116
  if (root === -1) root = 0;
13117
13117
  if (options.reverse) for (let index = root + 1; index <= segments.length; index++) for (const filename2 of filenames) {
13118
- const filePath = join(...segments.slice(0, index), filename2);
13118
+ const filePath = join$2(...segments.slice(0, index), filename2);
13119
13119
  if (await options.test(filePath)) return filePath;
13120
13120
  }
13121
13121
  else for (let index = segments.length; index > root; index--) for (const filename2 of filenames) {
13122
- const filePath = join(...segments.slice(0, index), filename2);
13122
+ const filePath = join$2(...segments.slice(0, index), filename2);
13123
13123
  if (await options.test(filePath)) return filePath;
13124
13124
  }
13125
13125
  throw new Error(`Cannot find matching ${filename} in ${options.startingFrom} or parent directories`);
@@ -13179,10 +13179,10 @@ async function resolvePackageJSON(id = process.cwd(), options = {}) {
13179
13179
  });
13180
13180
  }
13181
13181
  const workspaceTests = {
13182
- workspaceFile: (opts) => findFile(workspaceFiles, opts).then((r) => dirname(r)),
13182
+ workspaceFile: (opts) => findFile(workspaceFiles, opts).then((r) => dirname$1(r)),
13183
13183
  gitConfig: (opts) => findFile(".git/config", opts).then((r) => resolve$2(r, "../..")),
13184
- lockFile: (opts) => findFile(lockFiles, opts).then((r) => dirname(r)),
13185
- packageJson: (opts) => findFile(packageFiles, opts).then((r) => dirname(r))
13184
+ lockFile: (opts) => findFile(lockFiles, opts).then((r) => dirname$1(r)),
13185
+ packageJson: (opts) => findFile(packageFiles, opts).then((r) => dirname$1(r))
13186
13186
  };
13187
13187
  async function findWorkspaceDir(id = process.cwd(), options = {}) {
13188
13188
  const startingFrom = _resolvePath(id, options);
@@ -17263,7 +17263,7 @@ function parsePackageManagerField(packageManager) {
17263
17263
  async function detectPackageManager(cwd, options = {}) {
17264
17264
  const detected = await findup(resolve$2(cwd || "."), async (path) => {
17265
17265
  if (!options.ignorePackageJSON) {
17266
- const packageJSONPath = join(path, "package.json");
17266
+ const packageJSONPath = join$2(path, "package.json");
17267
17267
  if ((0, node_fs.existsSync)(packageJSONPath)) {
17268
17268
  const packageJSON = JSON.parse(await (0, node_fs_promises.readFile)(packageJSONPath, "utf8"));
17269
17269
  if (packageJSON?.packageManager) {
@@ -17284,7 +17284,7 @@ async function detectPackageManager(cwd, options = {}) {
17284
17284
  }
17285
17285
  }
17286
17286
  }
17287
- if ((0, node_fs.existsSync)(join(path, "deno.json"))) return packageManagers.find((pm) => pm.name === "deno");
17287
+ if ((0, node_fs.existsSync)(join$2(path, "deno.json"))) return packageManagers.find((pm) => pm.name === "deno");
17288
17288
  }
17289
17289
  if (!options.ignoreLockFile) {
17290
17290
  for (const packageManager of packageManagers) if ([packageManager.lockFile, packageManager.files].flat().filter(Boolean).some((file) => (0, node_fs.existsSync)(resolve$2(path, file)))) return { ...packageManager };
@@ -35720,7 +35720,7 @@ function currentShell() {
35720
35720
  function startShell(cwd) {
35721
35721
  cwd = resolve$2(cwd);
35722
35722
  const shell = currentShell();
35723
- console.info(`(experimental) Opening shell in ${relative(process.cwd(), cwd)}...`);
35723
+ console.info(`(experimental) Opening shell in ${relative$1(process.cwd(), cwd)}...`);
35724
35724
  (0, node_child_process.spawnSync)(shell, [], {
35725
35725
  cwd,
35726
35726
  shell: true,
@@ -35752,7 +35752,7 @@ async function downloadTemplate(input, options = {}) {
35752
35752
  const tarPath = resolve$2(resolve$2(cacheDirectory(), providerName, template.name), (template.version || template.name) + ".tar.gz");
35753
35753
  if (options.preferOffline && (0, node_fs.existsSync)(tarPath)) options.offline = true;
35754
35754
  if (!options.offline) {
35755
- await (0, node_fs_promises.mkdir)(dirname(tarPath), { recursive: true });
35755
+ await (0, node_fs_promises.mkdir)(dirname$1(tarPath), { recursive: true });
35756
35756
  const s2 = Date.now();
35757
35757
  await download(template.tar, tarPath, { headers: {
35758
35758
  Authorization: options.auth ? `Bearer ${options.auth}` : void 0,
@@ -40894,9 +40894,9 @@ async function resolveConfig$1(source, options, sourceOptions = {}) {
40894
40894
  const cloneName = source.replace(/\W+/g, "_").split("_").splice(0, 3).join("_") + "_" + digest(source).slice(0, 10).replace(/[-_]/g, "");
40895
40895
  let cloneDir;
40896
40896
  const localNodeModules = resolve$2(options.cwd, "node_modules");
40897
- const parentDir = dirname(options.cwd);
40898
- if (basename(parentDir) === ".c12") cloneDir = join(parentDir, cloneName);
40899
- else if ((0, node_fs.existsSync)(localNodeModules)) cloneDir = join(localNodeModules, ".c12", cloneName);
40897
+ const parentDir = dirname$1(options.cwd);
40898
+ if (basename(parentDir) === ".c12") cloneDir = join$2(parentDir, cloneName);
40899
+ else if ((0, node_fs.existsSync)(localNodeModules)) cloneDir = join$2(localNodeModules, ".c12", cloneName);
40900
40900
  else cloneDir = process.env.XDG_CACHE_HOME ? resolve$2(process.env.XDG_CACHE_HOME, "c12", cloneName) : resolve$2((0, node_os.homedir)(), ".cache/c12", cloneName);
40901
40901
  if ((0, node_fs.existsSync)(cloneDir) && !sourceOptions.install) await (0, node_fs_promises.rm)(cloneDir, { recursive: true });
40902
40902
  source = (await downloadTemplate(source, {
@@ -40911,7 +40911,7 @@ async function resolveConfig$1(source, options, sourceOptions = {}) {
40911
40911
  if (NPM_PACKAGE_RE.test(source)) source = tryResolve(source, options) || source;
40912
40912
  const ext = extname$2(source);
40913
40913
  const isDir = _isDirectory(resolve$2(options.cwd, source)) ?? (!ext || ext === basename(source));
40914
- const cwd = resolve$2(options.cwd, isDir ? source : dirname(source));
40914
+ const cwd = resolve$2(options.cwd, isDir ? source : dirname$1(source));
40915
40915
  if (isDir) source = options.configFile;
40916
40916
  const res = {
40917
40917
  config: void 0,
@@ -40935,7 +40935,7 @@ async function resolveConfig$1(source, options, sourceOptions = {}) {
40935
40935
  const { createJiti } = await Promise.resolve().then(() => (init_jiti(), jiti_exports)).catch(() => {
40936
40936
  throw new Error(`Failed to load config file \`${res.configFile}\`: ${error?.message}. Hint install \`jiti\` for compatibility.`, { cause: error });
40937
40937
  });
40938
- const jiti = createJiti(join(options.cwd || ".", options.configFile || "/"), {
40938
+ const jiti = createJiti(join$2(options.cwd || ".", options.configFile || "/"), {
40939
40939
  interopDefault: true,
40940
40940
  moduleCache: false,
40941
40941
  extensions: [...SUPPORTED_EXTENSIONS]
@@ -40963,7 +40963,7 @@ async function resolveConfig$1(source, options, sourceOptions = {}) {
40963
40963
  function tryResolve(id, options) {
40964
40964
  const res = resolveModulePath(id, {
40965
40965
  try: true,
40966
- from: (0, node_url.pathToFileURL)(join(options.cwd || ".", options.configFile || "/")),
40966
+ from: (0, node_url.pathToFileURL)(join$2(options.cwd || ".", options.configFile || "/")),
40967
40967
  suffixes: ["", "/index"],
40968
40968
  extensions: SUPPORTED_EXTENSIONS,
40969
40969
  cache: false
@@ -41031,6 +41031,7 @@ const DEFAULT_CONFIG = {
41031
41031
  "**/rbxts_include/**"
41032
41032
  ],
41033
41033
  coverageReporters: ["text", "lcov"],
41034
+ passWithNoTests: false,
41034
41035
  placeFile: "./game.rbxl",
41035
41036
  pollInterval: 500,
41036
41037
  port: 3001,
@@ -41149,6 +41150,7 @@ const configSchema = type({
41149
41150
  "maxWorkers?": type("number").or(type("string")),
41150
41151
  "noStackTrace?": "boolean",
41151
41152
  "outputFile?": "string",
41153
+ "passWithNoTests?": "boolean",
41152
41154
  "placeFile?": "string",
41153
41155
  "pollInterval?": "number",
41154
41156
  "port?": "number",
@@ -41654,6 +41656,7 @@ async function loadConfig(configPath, cwd = node_process.default.cwd()) {
41654
41656
  cwd,
41655
41657
  dotenv: false,
41656
41658
  globalRc: false,
41659
+ import: isSea() ? seaImport : void 0,
41657
41660
  merger,
41658
41661
  omit$Keys: true,
41659
41662
  packageJson: false,
@@ -41673,6 +41676,16 @@ async function loadConfig(configPath, cwd = node_process.default.cwd()) {
41673
41676
  config.rootDir ??= cwd;
41674
41677
  return resolveConfig(config);
41675
41678
  }
41679
+ function isSea() {
41680
+ return node_process.default.env["JEST_ROBLOX_SEA"] === "true";
41681
+ }
41682
+ async function seaImport(id) {
41683
+ if (id.endsWith(".json")) {
41684
+ const content = (0, node_fs.readFileSync)(id, "utf-8");
41685
+ return JSON.parse(content);
41686
+ }
41687
+ return import(id);
41688
+ }
41676
41689
  function merger(...sources) {
41677
41690
  return defuFn(...sources.filter(Boolean));
41678
41691
  }
@@ -41691,10 +41704,141 @@ function stripTsExtension(pattern) {
41691
41704
  }
41692
41705
  //#endregion
41693
41706
  //#region src/utils/rojo-tree.ts
41707
+ function resolveNestedProjects(tree, rootDirectory) {
41708
+ return resolveTree(tree, rootDirectory, rootDirectory, /* @__PURE__ */ new Set());
41709
+ }
41694
41710
  function collectPaths(node, result) {
41695
41711
  for (const [key, value] of Object.entries(node)) if (key === "$path" && typeof value === "string") result.push(value.replaceAll("\\", "/"));
41696
41712
  else if (typeof value === "object" && !Array.isArray(value) && !key.startsWith("$")) collectPaths(value, result);
41697
41713
  }
41714
+ function inlineNestedProject(projectPath, currentDirectory, originalRoot, visited) {
41715
+ const chain = new Set(visited);
41716
+ chain.add(projectPath);
41717
+ let content;
41718
+ try {
41719
+ content = (0, node_fs.readFileSync)(projectPath, "utf-8");
41720
+ } catch (err) {
41721
+ const relativePath = (0, node_path.relative)(currentDirectory, projectPath);
41722
+ throw new Error(`Could not read nested Rojo project: ${relativePath}`, { cause: err });
41723
+ }
41724
+ return resolveTree(JSON.parse(content).tree, (0, node_path.dirname)(projectPath), originalRoot, chain);
41725
+ }
41726
+ function resolveRootRelativePath(currentDirectory, value, originalRoot) {
41727
+ return (0, node_path.relative)(originalRoot, (0, node_path.join)(currentDirectory, value)).replaceAll("\\", "/");
41728
+ }
41729
+ function resolveTree(node, currentDirectory, originalRoot, visited) {
41730
+ const resolved = {};
41731
+ for (const [key, value] of Object.entries(node)) {
41732
+ if (key === "$path" && typeof value === "string" && value.endsWith(".project.json")) {
41733
+ const projectPath = (0, node_path.join)(currentDirectory, value);
41734
+ if (visited.has(projectPath)) throw new Error(`Circular project reference: ${value}`);
41735
+ const innerTree = inlineNestedProject(projectPath, currentDirectory, originalRoot, visited);
41736
+ for (const [innerKey, innerValue] of Object.entries(innerTree)) resolved[innerKey] = innerValue;
41737
+ continue;
41738
+ }
41739
+ if (key === "$path" && typeof value === "string") {
41740
+ resolved[key] = resolveRootRelativePath(currentDirectory, value, originalRoot);
41741
+ continue;
41742
+ }
41743
+ if (key.startsWith("$") || typeof value !== "object" || Array.isArray(value)) {
41744
+ resolved[key] = value;
41745
+ continue;
41746
+ }
41747
+ resolved[key] = resolveTree(value, currentDirectory, originalRoot, visited);
41748
+ }
41749
+ return resolved;
41750
+ }
41751
+ //#endregion
41752
+ //#region src/luau/eval-literals.ts
41753
+ /**
41754
+ * Evaluate the first return expression in a Lute-stripped AST root block,
41755
+ * supporting only literal values (string, boolean, number, nil, table, cast).
41756
+ *
41757
+ * Accepts `unknown` and narrows safely — no type casts on JSON.parse needed.
41758
+ */
41759
+ function evalLuauReturnLiterals(root) {
41760
+ if (!isObject(root) || !Array.isArray(root["statements"])) throw new Error("Config file has no return statement");
41761
+ const returnStat = root["statements"].find((stat) => isObject(stat) && stat["tag"] === "return");
41762
+ if (!isObject(returnStat) || !Array.isArray(returnStat["expressions"])) throw new Error("Config file has no return statement");
41763
+ const first = returnStat["expressions"][0];
41764
+ if (!isObject(first) || !("node" in first)) throw new Error("Return statement has no expressions");
41765
+ return evalExpr(first["node"]);
41766
+ }
41767
+ function isObject(value) {
41768
+ return typeof value === "object" && value !== null;
41769
+ }
41770
+ function evalExpr(node) {
41771
+ if (!isObject(node)) return;
41772
+ let current = node;
41773
+ while (current["tag"] === "cast" && isObject(current["operand"])) current = current["operand"];
41774
+ const { tag } = current;
41775
+ if (tag === "boolean" || tag === "number") return current["value"];
41776
+ if (tag === "string") return current["text"];
41777
+ if (tag === "table" && Array.isArray(current["entries"])) return evalTable(current["entries"]);
41778
+ }
41779
+ function evalTable(entries) {
41780
+ if (entries.length === 0) return {};
41781
+ const first = entries[0];
41782
+ if (isObject(first) && first["kind"] === "list") return entries.map((entry) => isObject(entry) ? evalExpr(entry["value"]) : void 0);
41783
+ const result = {};
41784
+ for (const entry of entries) {
41785
+ if (!isObject(entry) || entry["kind"] !== "record") continue;
41786
+ const { key, value } = entry;
41787
+ if (isObject(key) && typeof key["text"] === "string") result[key["text"]] = evalExpr(value);
41788
+ }
41789
+ return result;
41790
+ }
41791
+ //#endregion
41792
+ //#region src/luau/parse-ast.luau
41793
+ var parse_ast_default = "local fs = require(\"@std/fs\")\nlocal json = require(\"@std/json\")\nlocal process = require(\"@std/process\")\nlocal syntax = require(\"@std/syntax\")\n\nlocal rawArgs = process.args\nlocal userArgs: { string } = {}\nlocal pastSeparator = false\n\nfor _, arg in rawArgs do\n if pastSeparator then\n table.insert(userArgs, arg)\n elseif arg == \"--\" then\n pastSeparator = true\n end\nend\n\nlocal luauRoot = userArgs[1]\nif not luauRoot then\n error(\"Usage: lute run parse-ast.luau -- <file.luau | luau-root> [output-dir]\")\nend\n\nluauRoot = string.gsub(luauRoot, \"\\\\\", \"/\")\n\n-- Fields to keep per AST tag (beyond tag/kind/location which are always kept).\n-- Tags shared by stat/expr variants are merged — nil fields are harmless.\nlocal KEEP: { [string]: { string } } = {\n assign = { \"values\", \"variables\" },\n binary = { \"lhsoperand\", \"rhsoperand\" },\n block = { \"statements\" },\n boolean = { \"value\" },\n call = { \"arguments\", \"func\" },\n cast = { \"operand\" },\n compoundassign = { \"value\", \"variable\" },\n conditional = { \"condition\", \"thenblock\", \"elseifs\", \"elseblock\", \"thenexpr\", \"elseexpr\" },\n [\"do\"] = { \"body\" },\n expression = { \"expression\" },\n [\"for\"] = { \"body\", \"from\", \"to\", \"step\" },\n forin = { \"body\", \"values\" },\n [\"function\"] = { \"body\", \"name\", \"func\" },\n global = { \"name\" },\n group = { \"expression\" },\n index = { \"expression\", \"index\" },\n indexname = { \"expression\", \"accessor\", \"index\" },\n instantiate = { \"expr\" },\n interpolatedstring = { \"expressions\" },\n [\"local\"] = { \"values\", \"variables\" },\n localfunction = { \"name\", \"func\" },\n number = { \"value\" },\n [\"repeat\"] = { \"body\", \"condition\" },\n [\"return\"] = { \"expressions\" },\n string = { \"text\" },\n table = { \"entries\" },\n unary = { \"operand\" },\n [\"while\"] = { \"body\", \"condition\" },\n}\n\nlocal function strip(value: any): any\n if type(value) ~= \"table\" then\n return value\n end\n\n -- LuauSpan — has beginline, no tag\n if value.beginline ~= nil then\n return value\n end\n\n -- Token — has text but no tag, reduce to {text}\n if value.text ~= nil and value.tag == nil then\n return { text = value.text }\n end\n\n -- AST node — has tag, keep only allowlisted fields\n if value.tag ~= nil then\n local result = { tag = value.tag, kind = value.kind, location = value.location }\n local fields = KEEP[value.tag :: string]\n if fields then\n for _, field in fields do\n if value[field] ~= nil then\n result[field] = strip(value[field])\n end\n end\n end\n\n return result\n end\n\n -- Other tables (arrays, Pairs, ElseIf structs) — recurse all fields\n local result: any = {}\n for k, v in value do\n result[k] = strip(v)\n end\n\n return result\nend\n\n-- Single-file mode: parse one file, print stripped AST to stdout\nif string.sub(luauRoot, -5) == \".luau\" or string.sub(luauRoot, -4) == \".lua\" then\n local source = fs.readfiletostring(luauRoot)\n local parseResult = syntax.parse(source)\n print(json.serialize(strip(parseResult.root)))\n return\nend\n\nlocal outputDir = userArgs[2]\nif not outputDir then\n error(\"Usage: lute run parse-ast.luau -- <luau-root> <output-dir>\")\nend\n\noutputDir = string.gsub(outputDir, \"\\\\\", \"/\")\n\n-- Discover .luau files recursively, skipping node_modules, dot dirs, spec/test files\nlocal function discoverFiles(directory: string, relativeTo: string, results: { string })\n local entries = fs.listdirectory(directory)\n for _, entry in entries do\n local fullPath = directory .. \"/\" .. entry.name\n if entry.type == \"dir\" then\n if entry.name == \"node_modules\" or entry.name == \".jest-roblox-coverage\" then\n continue\n end\n\n if string.sub(entry.name, 1, 1) == \".\" then\n continue\n end\n\n discoverFiles(fullPath, relativeTo, results)\n elseif\n entry.type == \"file\"\n and (string.sub(entry.name, -5) == \".luau\" or string.sub(entry.name, -4) == \".lua\")\n then\n if\n string.sub(entry.name, -10) == \".spec.luau\"\n or string.sub(entry.name, -10) == \".test.luau\"\n or string.sub(entry.name, -9) == \".spec.lua\"\n or string.sub(entry.name, -9) == \".test.lua\"\n or string.sub(entry.name, -10) == \".snap.luau\"\n or string.sub(entry.name, -9) == \".snap.lua\"\n then\n continue\n end\n\n -- Compute relative path\n local relative = string.sub(fullPath, #relativeTo + 2)\n table.insert(results, relative)\n end\n end\nend\n\nlocal function dirname(filepath: string): string\n local pos = string.find(filepath, \"/[^/]*$\")\n if pos then\n return string.sub(filepath, 1, pos - 1)\n end\n\n return \"\"\nend\n\nlocal files: { string } = {}\ndiscoverFiles(luauRoot, luauRoot, files)\n\n-- Parse, strip, and write per-file AST JSON\nfor _, relativePath in files do\n local fullPath = luauRoot .. \"/\" .. relativePath\n local source = fs.readfiletostring(fullPath)\n local parseResult = syntax.parse(source)\n local stripped = strip(parseResult.root)\n\n local outPath = outputDir .. \"/\" .. relativePath .. \".json\"\n local dir = dirname(outPath)\n if dir ~= \"\" then\n fs.createdirectory(dir, { makeparents = true })\n end\n\n fs.writestringtofile(outPath, json.serialize(stripped))\nend\n\n-- Print file list to stdout (tiny — just paths)\nprint(json.serialize(files :: json.array))\n";
41794
+ //#endregion
41795
+ //#region src/config/luau-config-loader.ts
41796
+ let cachedTemporaryDirectory$1;
41797
+ /**
41798
+ * Parse a .luau config file via Lute and evaluate its return expression.
41799
+ */
41800
+ function loadLuauConfig(filePath) {
41801
+ const temporaryDirectory = getTemporaryDirectory$1();
41802
+ const scriptPath = node_path.join(temporaryDirectory, "parse-ast.luau");
41803
+ node_fs.writeFileSync(scriptPath, parse_ast_default);
41804
+ let stdout;
41805
+ try {
41806
+ stdout = node_child_process.execFileSync("lute", [
41807
+ "run",
41808
+ scriptPath,
41809
+ "--",
41810
+ node_path.resolve(filePath)
41811
+ ], {
41812
+ encoding: "utf-8",
41813
+ maxBuffer: 1024 * 1024
41814
+ });
41815
+ } catch (err) {
41816
+ if (err instanceof Error && "code" in err && err.code === "ENOENT") throw new Error("lute is required to load .luau config files but was not found on PATH");
41817
+ throw new Error(`Failed to evaluate Luau config ${filePath}`, { cause: err });
41818
+ }
41819
+ let ast;
41820
+ try {
41821
+ ast = JSON.parse(stdout);
41822
+ } catch (err) {
41823
+ throw new Error(`Failed to parse AST JSON from Luau config ${filePath}`, { cause: err });
41824
+ }
41825
+ const result = evalLuauReturnLiterals(ast);
41826
+ if (typeof result !== "object" || result === null) throw new Error(`Luau config ${filePath} must return a table`);
41827
+ return result;
41828
+ }
41829
+ /**
41830
+ * Check if `<cwd>/<directoryOrFile>/jest.config.luau` exists. Returns the
41831
+ * resolved path if found, undefined otherwise.
41832
+ */
41833
+ function findLuauConfigFile(directoryOrFile, cwd) {
41834
+ const resolved = node_path.resolve(cwd, directoryOrFile, "jest.config.luau");
41835
+ if (node_fs.existsSync(resolved)) return resolved;
41836
+ }
41837
+ function getTemporaryDirectory$1() {
41838
+ if (cachedTemporaryDirectory$1 !== void 0 && node_fs.existsSync(cachedTemporaryDirectory$1)) return cachedTemporaryDirectory$1;
41839
+ cachedTemporaryDirectory$1 = node_fs.mkdtempSync(node_path.join(node_os.tmpdir(), "jest-roblox-luau-config-"));
41840
+ return cachedTemporaryDirectory$1;
41841
+ }
41698
41842
  //#endregion
41699
41843
  //#region src/config/projects.ts
41700
41844
  function extractStaticRoot(pattern) {
@@ -41795,6 +41939,8 @@ function resolveProjectConfig(project, rootConfig, rojoTree) {
41795
41939
  };
41796
41940
  }
41797
41941
  async function loadProjectConfigFile(filePath, cwd) {
41942
+ const luauConfigPath = findLuauConfigFile(filePath, cwd);
41943
+ if (luauConfigPath !== void 0) return buildProjectConfigFromLuau(luauConfigPath, filePath);
41798
41944
  let result;
41799
41945
  try {
41800
41946
  result = await loadConfig$1({
@@ -41836,6 +41982,38 @@ function mergeProjectConfig(rootConfig, project) {
41836
41982
  for (const [key, value] of Object.entries(project)) if (!PROJECT_ONLY_KEYS.has(key) && value !== void 0) merged[key] = value;
41837
41983
  return merged;
41838
41984
  }
41985
+ const LUAU_BOOLEAN_KEYS = [
41986
+ "automock",
41987
+ "clearMocks",
41988
+ "injectGlobals",
41989
+ "mockDataModel",
41990
+ "resetMocks",
41991
+ "resetModules",
41992
+ "restoreMocks"
41993
+ ];
41994
+ const LUAU_NUMBER_KEYS = ["slowTestThreshold", "testTimeout"];
41995
+ const LUAU_STRING_KEYS = ["testEnvironment"];
41996
+ const LUAU_STRING_ARRAY_KEYS = ["setupFiles", "setupFilesAfterEnv"];
41997
+ function copyLuauOptionalFields(raw, config) {
41998
+ const record = config;
41999
+ for (const key of LUAU_BOOLEAN_KEYS) if (typeof raw[key] === "boolean") record[key] = raw[key];
42000
+ for (const key of LUAU_NUMBER_KEYS) if (typeof raw[key] === "number") record[key] = raw[key];
42001
+ for (const key of LUAU_STRING_KEYS) if (typeof raw[key] === "string") record[key] = raw[key];
42002
+ for (const key of LUAU_STRING_ARRAY_KEYS) if (Array.isArray(raw[key])) record[key] = raw[key];
42003
+ }
42004
+ function buildProjectConfigFromLuau(luauConfigPath, directoryPath) {
42005
+ const raw = loadLuauConfig(luauConfigPath);
42006
+ const { displayName } = raw;
42007
+ if (typeof displayName !== "string" || displayName === "") throw new Error(`Luau config file "${luauConfigPath}" must have a displayName string`);
42008
+ const testMatch = Array.isArray(raw["testMatch"]) ? raw["testMatch"] : void 0;
42009
+ const config = {
42010
+ displayName,
42011
+ include: testMatch !== void 0 ? testMatch.map((pattern) => node_path.posix.join(directoryPath, `${pattern}.luau`)) : [node_path.posix.join(directoryPath, "**/*.spec.luau")]
42012
+ };
42013
+ if (testMatch !== void 0) config.testMatch = testMatch;
42014
+ copyLuauOptionalFields(raw, config);
42015
+ return config;
42016
+ }
41839
42017
  function matchNodePath(childNode, targetPath, childDataModelPath) {
41840
42018
  const nodePath = childNode.$path;
41841
42019
  if (typeof nodePath !== "string") return;
@@ -49643,8 +49821,8 @@ var import_RojoResolver = (/* @__PURE__ */ __commonJSMin(((exports) => {
49643
49821
  this.value = value;
49644
49822
  }
49645
49823
  };
49646
- const SCHEMA_PATH = path_1.default.join(PACKAGE_ROOT, "rojo-schema.json");
49647
- const validateRojo = new Lazy(() => ajv.compile(JSON.parse(fs_extra_1.default.readFileSync(SCHEMA_PATH).toString())));
49824
+ path_1.default.join(PACKAGE_ROOT, "rojo-schema.json");
49825
+ const validateRojo = new Lazy(() => ajv.compile(JSON.parse("{\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"required\": [\"name\", \"tree\"],\n \"type\": \"object\",\n \"properties\": {\n \"name\": {\n \"type\": \"string\"\n },\n \"servePort\": {\n \"type\": \"integer\"\n },\n \"tree\": {\n \"$id\": \"tree\",\n \"type\": \"object\",\n \"properties\": {\n \"$className\": {\n \"type\": \"string\"\n },\n \"$ignoreUnknownInstances\": {\n \"type\": \"boolean\"\n },\n \"$path\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"optional\": {\n \"type\": \"string\"\n }\n }\n }\n ]\n },\n \"$properties\": {\n \"type\": \"object\"\n }\n },\n \"patternProperties\": {\n \"^[^\\\\$].*$\": { \"$ref\": \"tree\" }\n }\n }\n }\n}\n")));
49648
49826
  function isValidRojoConfig(value) {
49649
49827
  return validateRojo.get()(value) === true;
49650
49828
  }
@@ -49873,6 +50051,8 @@ function serializeToLuau(config) {
49873
50051
  }
49874
50052
  function generateProjectConfigs(projects) {
49875
50053
  for (const project of projects) {
50054
+ const luauConfigPath = project.outputPath.replace(/\.lua$/, ".luau");
50055
+ if (node_fs.default.existsSync(luauConfigPath)) continue;
49876
50056
  const content = serializeToLuau(project.config);
49877
50057
  const directory = node_path.default.dirname(project.outputPath);
49878
50058
  node_fs.default.mkdirSync(directory, { recursive: true });
@@ -50412,27 +50592,133 @@ function mapCoverageToTypeScript(coverageData, manifest) {
50412
50592
  if (record === void 0) continue;
50413
50593
  const resources = loadFileResources(record);
50414
50594
  if (resources === void 0) continue;
50415
- mapFileFunctions(resources, fileCoverage, pendingFunctions, mapFileStatements(resources, fileCoverage, pendingStatements));
50416
- mapFileBranches(resources, fileCoverage, pendingBranches);
50595
+ if (resources.traceMap === void 0) {
50596
+ passthroughFileStatements(resources, fileCoverage, pendingStatements);
50597
+ passthroughFileFunctions(resources, fileCoverage, pendingFunctions);
50598
+ passthroughFileBranches(resources, fileCoverage, pendingBranches);
50599
+ } else {
50600
+ const mapped = {
50601
+ coverageMap: resources.coverageMap,
50602
+ traceMap: resources.traceMap
50603
+ };
50604
+ mapFileFunctions(mapped, fileCoverage, pendingFunctions, mapFileStatements(mapped, fileCoverage, pendingStatements));
50605
+ mapFileBranches(mapped, fileCoverage, pendingBranches);
50606
+ }
50417
50607
  }
50418
50608
  return buildResult(pendingStatements, pendingFunctions, pendingBranches);
50419
50609
  }
50420
50610
  function loadFileResources(record) {
50421
50611
  let coverageMapRaw;
50422
- let sourceMapRaw;
50423
50612
  try {
50424
50613
  coverageMapRaw = node_fs.readFileSync(record.coverageMapPath, "utf-8");
50425
- sourceMapRaw = node_fs.readFileSync(record.sourceMapPath, "utf-8");
50426
50614
  } catch {
50427
50615
  return;
50428
50616
  }
50429
50617
  const parsed = coverageMapSchema(JSON.parse(coverageMapRaw));
50430
50618
  if (parsed instanceof type.errors) return;
50619
+ let traceMap;
50620
+ try {
50621
+ traceMap = new TraceMap(node_fs.readFileSync(record.sourceMapPath, "utf-8"));
50622
+ } catch {}
50431
50623
  return {
50432
50624
  coverageMap: parsed,
50433
- traceMap: new TraceMap(sourceMapRaw)
50625
+ sourceKey: record.key,
50626
+ traceMap
50434
50627
  };
50435
50628
  }
50629
+ function toIstanbulColumn(luauColumn) {
50630
+ return Math.max(0, luauColumn - 1);
50631
+ }
50632
+ function passthroughFileStatements(resources, fileCoverage, pending) {
50633
+ let fileStatements = pending.get(resources.sourceKey);
50634
+ if (fileStatements === void 0) {
50635
+ fileStatements = /* @__PURE__ */ new Map();
50636
+ pending.set(resources.sourceKey, fileStatements);
50637
+ }
50638
+ for (const [statementId, rawSpan] of Object.entries(resources.coverageMap.statementMap)) {
50639
+ const span = spanSchema(rawSpan);
50640
+ if (span instanceof type.errors) continue;
50641
+ const hitCount = fileCoverage.s[statementId] ?? 0;
50642
+ fileStatements.set(statementId, {
50643
+ end: {
50644
+ column: toIstanbulColumn(span.end.column),
50645
+ line: span.end.line
50646
+ },
50647
+ hitCount,
50648
+ start: {
50649
+ column: toIstanbulColumn(span.start.column),
50650
+ line: span.start.line
50651
+ }
50652
+ });
50653
+ }
50654
+ }
50655
+ function passthroughFileFunctions(resources, fileCoverage, pendingFunctions) {
50656
+ if (resources.coverageMap.functionMap === void 0) return;
50657
+ let fileFunctions = pendingFunctions.get(resources.sourceKey);
50658
+ if (fileFunctions === void 0) {
50659
+ fileFunctions = [];
50660
+ pendingFunctions.set(resources.sourceKey, fileFunctions);
50661
+ }
50662
+ for (const [functionId, rawEntry] of Object.entries(resources.coverageMap.functionMap)) {
50663
+ const entry = functionEntrySchema(rawEntry);
50664
+ if (entry instanceof type.errors) continue;
50665
+ fileFunctions.push({
50666
+ name: entry.name,
50667
+ hitCount: fileCoverage.f?.[functionId] ?? 0,
50668
+ loc: {
50669
+ end: {
50670
+ column: toIstanbulColumn(entry.location.end.column),
50671
+ line: entry.location.end.line
50672
+ },
50673
+ start: {
50674
+ column: toIstanbulColumn(entry.location.start.column),
50675
+ line: entry.location.start.line
50676
+ }
50677
+ }
50678
+ });
50679
+ }
50680
+ }
50681
+ function passthroughFileBranches(resources, fileCoverage, pendingBranches) {
50682
+ if (resources.coverageMap.branchMap === void 0) return;
50683
+ let fileBranches = pendingBranches.get(resources.sourceKey);
50684
+ if (fileBranches === void 0) {
50685
+ fileBranches = [];
50686
+ pendingBranches.set(resources.sourceKey, fileBranches);
50687
+ }
50688
+ for (const [branchId, rawEntry] of Object.entries(resources.coverageMap.branchMap)) {
50689
+ const entry = branchEntrySchema(rawEntry);
50690
+ if (entry instanceof type.errors) continue;
50691
+ const armHitCounts = fileCoverage.b?.[branchId] ?? [];
50692
+ const locations = [];
50693
+ for (const rawLocation of entry.locations) {
50694
+ const location = spanSchema(rawLocation);
50695
+ if (location instanceof type.errors) continue;
50696
+ locations.push({
50697
+ end: {
50698
+ column: toIstanbulColumn(location.end.column),
50699
+ line: location.end.line
50700
+ },
50701
+ start: {
50702
+ column: toIstanbulColumn(location.start.column),
50703
+ line: location.start.line
50704
+ }
50705
+ });
50706
+ }
50707
+ if (locations.length === 0) continue;
50708
+ const firstLocation = locations[0];
50709
+ const lastLocation = locations[locations.length - 1];
50710
+ (0, node_assert.default)(firstLocation !== void 0 && lastLocation !== void 0, "Branch locations must not be empty after filtering");
50711
+ fileBranches.push({
50712
+ armHitCounts: entry.locations.map((_, index) => armHitCounts[index] ?? 0),
50713
+ loc: {
50714
+ end: lastLocation.end,
50715
+ start: firstLocation.start
50716
+ },
50717
+ locations,
50718
+ type: entry.type
50719
+ });
50720
+ }
50721
+ }
50436
50722
  function mapStatement(traceMap, span) {
50437
50723
  const mappedStart = originalPositionFor(traceMap, {
50438
50724
  column: Math.max(0, span.start.column - 1),
@@ -53179,26 +53465,7 @@ const rojoProjectSchema = type({
53179
53465
  "tree": "object"
53180
53466
  }).as();
53181
53467
  //#endregion
53182
- //#region src/types/luau-ast.ts
53183
- const INSTRUMENTABLE_STATEMENT_TAGS = new Set([
53184
- "assign",
53185
- "break",
53186
- "compoundassign",
53187
- "conditional",
53188
- "continue",
53189
- "do",
53190
- "expression",
53191
- "for",
53192
- "forin",
53193
- "function",
53194
- "local",
53195
- "localfunction",
53196
- "repeat",
53197
- "return",
53198
- "while"
53199
- ]);
53200
- //#endregion
53201
- //#region src/coverage/luau-visitor.ts
53468
+ //#region src/luau/visitor.ts
53202
53469
  function visitExpression(expression, visitor) {
53203
53470
  if (visitor.visitExpr?.(expression) === false) return;
53204
53471
  const { tag } = expression;
@@ -53469,6 +53736,23 @@ function visitExprInstantiate(node, visitor) {
53469
53736
  }
53470
53737
  //#endregion
53471
53738
  //#region src/coverage/coverage-collector.ts
53739
+ const INSTRUMENTABLE_STATEMENT_TAGS = new Set([
53740
+ "assign",
53741
+ "break",
53742
+ "compoundassign",
53743
+ "conditional",
53744
+ "continue",
53745
+ "do",
53746
+ "expression",
53747
+ "for",
53748
+ "forin",
53749
+ "function",
53750
+ "local",
53751
+ "localfunction",
53752
+ "repeat",
53753
+ "return",
53754
+ "while"
53755
+ ]);
53472
53756
  const END_KEYWORD_LENGTH = 3;
53473
53757
  function collectCoverage(root) {
53474
53758
  let statementIndex = 1;
@@ -53708,9 +53992,6 @@ function buildCoverageMap$1(result) {
53708
53992
  };
53709
53993
  }
53710
53994
  //#endregion
53711
- //#region src/coverage/parse-ast.luau
53712
- var parse_ast_default = "local fs = require(\"@std/fs\")\nlocal json = require(\"@std/json\")\nlocal process = require(\"@std/process\")\nlocal syntax = require(\"@std/syntax\")\n\nlocal rawArgs = process.args\nlocal userArgs: { string } = {}\nlocal pastSeparator = false\n\nfor _, arg in rawArgs do\n if pastSeparator then\n table.insert(userArgs, arg)\n elseif arg == \"--\" then\n pastSeparator = true\n end\nend\n\nlocal luauRoot = userArgs[1]\nlocal outputDir = userArgs[2]\nif not luauRoot or not outputDir then\n error(\"Usage: lute run parse-ast.luau -- <luau-root> <output-dir>\")\nend\n\nluauRoot = string.gsub(luauRoot, \"\\\\\", \"/\")\noutputDir = string.gsub(outputDir, \"\\\\\", \"/\")\n\n-- Fields to keep per AST tag (beyond tag/kind/location which are always kept).\n-- Tags shared by stat/expr variants are merged — nil fields are harmless.\nlocal KEEP: { [string]: { string } } = {\n assign = { \"values\", \"variables\" },\n binary = { \"lhsoperand\", \"rhsoperand\" },\n block = { \"statements\" },\n call = { \"arguments\", \"func\" },\n cast = { \"operand\" },\n compoundassign = { \"value\", \"variable\" },\n conditional = { \"condition\", \"thenblock\", \"elseifs\", \"elseblock\", \"thenexpr\", \"elseexpr\" },\n [\"do\"] = { \"body\" },\n expression = { \"expression\" },\n [\"for\"] = { \"body\", \"from\", \"to\", \"step\" },\n forin = { \"body\", \"values\" },\n [\"function\"] = { \"body\", \"name\", \"func\" },\n global = { \"name\" },\n group = { \"expression\" },\n index = { \"expression\", \"index\" },\n indexname = { \"expression\", \"accessor\", \"index\" },\n instantiate = { \"expr\" },\n interpolatedstring = { \"expressions\" },\n [\"local\"] = { \"values\", \"variables\" },\n localfunction = { \"name\", \"func\" },\n [\"repeat\"] = { \"body\", \"condition\" },\n [\"return\"] = { \"expressions\" },\n table = { \"entries\" },\n unary = { \"operand\" },\n [\"while\"] = { \"body\", \"condition\" },\n}\n\nlocal function strip(value: any): any\n if type(value) ~= \"table\" then\n return value\n end\n\n -- LuauSpan — has beginline, no tag\n if value.beginline ~= nil then\n return value\n end\n\n -- Token — has text but no tag, reduce to {text}\n if value.text ~= nil and value.tag == nil then\n return { text = value.text }\n end\n\n -- AST node — has tag, keep only allowlisted fields\n if value.tag ~= nil then\n local result = { tag = value.tag, kind = value.kind, location = value.location }\n local fields = KEEP[value.tag :: string]\n if fields then\n for _, field in fields do\n if value[field] ~= nil then\n result[field] = strip(value[field])\n end\n end\n end\n\n return result\n end\n\n -- Other tables (arrays, Pairs, ElseIf structs) — recurse all fields\n local result: any = {}\n for k, v in value do\n result[k] = strip(v)\n end\n\n return result\nend\n\n-- Discover .luau files recursively, skipping node_modules, dot dirs, spec/test files\nlocal function discoverFiles(directory: string, relativeTo: string, results: { string })\n local entries = fs.listdirectory(directory)\n for _, entry in entries do\n local fullPath = directory .. \"/\" .. entry.name\n if entry.type == \"dir\" then\n if entry.name == \"node_modules\" or entry.name == \".jest-roblox-coverage\" then\n continue\n end\n\n if string.sub(entry.name, 1, 1) == \".\" then\n continue\n end\n\n discoverFiles(fullPath, relativeTo, results)\n elseif\n entry.type == \"file\"\n and (string.sub(entry.name, -5) == \".luau\" or string.sub(entry.name, -4) == \".lua\")\n then\n if\n string.sub(entry.name, -10) == \".spec.luau\"\n or string.sub(entry.name, -10) == \".test.luau\"\n or string.sub(entry.name, -9) == \".spec.lua\"\n or string.sub(entry.name, -9) == \".test.lua\"\n or string.sub(entry.name, -10) == \".snap.luau\"\n or string.sub(entry.name, -9) == \".snap.lua\"\n then\n continue\n end\n\n -- Compute relative path\n local relative = string.sub(fullPath, #relativeTo + 2)\n table.insert(results, relative)\n end\n end\nend\n\nlocal function dirname(filepath: string): string\n local pos = string.find(filepath, \"/[^/]*$\")\n if pos then\n return string.sub(filepath, 1, pos - 1)\n end\n\n return \"\"\nend\n\nlocal files: { string } = {}\ndiscoverFiles(luauRoot, luauRoot, files)\n\n-- Parse, strip, and write per-file AST JSON\nfor _, relativePath in files do\n local fullPath = luauRoot .. \"/\" .. relativePath\n local source = fs.readfiletostring(fullPath)\n local parseResult = syntax.parse(source)\n local stripped = strip(parseResult.root)\n\n local outPath = outputDir .. \"/\" .. relativePath .. \".json\"\n local dir = dirname(outPath)\n if dir ~= \"\" then\n fs.createdirectory(dir, { makeparents = true })\n end\n\n fs.writestringtofile(outPath, json.serialize(stripped))\nend\n\n-- Print file list to stdout (tiny — just paths)\nprint(json.serialize(files :: json.array))\n";
53713
- //#endregion
53714
53995
  //#region src/coverage/probe-inserter.ts
53715
53996
  function insertProbes(source, result, fileKey) {
53716
53997
  const lines = splitLines(source);
@@ -53765,7 +54046,7 @@ function applyProbes(mutableLines, probes) {
53765
54046
  (0, node_assert.default)(line !== void 0, `Invalid probe line number: ${probeLine}`);
53766
54047
  const before = line.slice(0, column - 1);
53767
54048
  const after = line.slice(column - 1);
53768
- mutableLines[lineIndex] = before + text + after;
54049
+ mutableLines[lineIndex] = before + (before.length > 0 && !/\s$/.test(before) && /^[a-zA-Z_]/.test(text) ? " " : "") + text + after;
53769
54050
  }
53770
54051
  }
53771
54052
  function extractModeDirective(lines) {
@@ -54130,8 +54411,12 @@ function writeManifest(manifestPath, allFiles, luauRoots, placeFile) {
54130
54411
  function buildRojoProject(rojoProjectPath, roots, placeFile) {
54131
54412
  const rojoProjectRaw = rojoProjectSchema(JSON.parse(node_fs.readFileSync(rojoProjectPath, "utf-8")));
54132
54413
  if (rojoProjectRaw instanceof type.errors) throw new Error(`Malformed Rojo project JSON: ${rojoProjectRaw.toString()}`);
54133
- const rewritten = rewriteRojoProject(rojoProjectRaw, {
54134
- projectRelocation: node_path.relative(COVERAGE_DIR, node_path.dirname(rojoProjectPath)).replaceAll("\\", "/"),
54414
+ const projectRelocation = node_path.relative(COVERAGE_DIR, node_path.dirname(rojoProjectPath)).replaceAll("\\", "/");
54415
+ const rewritten = rewriteRojoProject({
54416
+ ...rojoProjectRaw,
54417
+ tree: resolveNestedProjects(rojoProjectRaw.tree, node_path.dirname(rojoProjectPath))
54418
+ }, {
54419
+ projectRelocation,
54135
54420
  roots
54136
54421
  });
54137
54422
  const rewrittenProjectPath = node_path.join(COVERAGE_DIR, node_path.basename(rojoProjectPath));
@@ -60258,9 +60543,13 @@ function buildSourceMapper(config, tsconfigMappings) {
60258
60543
  try {
60259
60544
  const rojoResult = rojoProjectSchema(JSON.parse(node_fs.readFileSync(rojoProjectPath, "utf-8")));
60260
60545
  if (rojoResult instanceof type.errors) return;
60546
+ const resolvedTree = resolveNestedProjects(rojoResult.tree, node_path.dirname(rojoProjectPath));
60261
60547
  return createSourceMapper({
60262
60548
  mappings: tsconfigMappings,
60263
- rojoProject: rojoResult
60549
+ rojoProject: {
60550
+ ...rojoResult,
60551
+ tree: resolvedTree
60552
+ }
60264
60553
  });
60265
60554
  } catch {
60266
60555
  return;
@@ -60338,9 +60627,13 @@ function writeSnapshots(snapshotWrites, config, tsconfigMappings) {
60338
60627
  node_process.default.stderr.write("Warning: Cannot write snapshots - invalid rojo project\n");
60339
60628
  return;
60340
60629
  }
60630
+ const resolvedTree = resolveNestedProjects(rojoResult.tree, node_path.dirname(rojoProjectPath));
60341
60631
  const resolver = createSnapshotPathResolver({
60342
60632
  mappings: tsconfigMappings,
60343
- rojoProject: rojoResult
60633
+ rojoProject: {
60634
+ ...rojoResult,
60635
+ tree: resolvedTree
60636
+ }
60344
60637
  });
60345
60638
  let written = 0;
60346
60639
  for (const [virtualPath, content] of Object.entries(snapshotWrites)) {
@@ -60807,7 +61100,7 @@ function globSync(pattern, options = {}) {
60807
61100
  return walkDirectory(cwd, cwd).filter((file) => matchesGlobPattern(file, pattern));
60808
61101
  }
60809
61102
  function matchesGlobPattern(filePath, pattern) {
60810
- const regexPattern = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{DOUBLESTAR}}").replace(/\*/g, "[^/]*").replace(/\{\{DOUBLESTAR\}\}/g, ".*");
61103
+ const regexPattern = pattern.replace(/\./g, "\\.").replace(/\*\*\//g, "{{DOUBLESTAR_SLASH}}").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*").replace(/\{\{DOUBLESTAR_SLASH\}\}/g, "(.+/)?");
60811
61104
  return new RegExp(`^${regexPattern}$`).test(filePath);
60812
61105
  }
60813
61106
  function walkDirectory(directoryPath, baseDirectory) {
@@ -60842,6 +61135,7 @@ Options:
60842
61135
  --gameOutput <path> Write game output (print/warn/error) to file
60843
61136
  --sourceMap Map Luau stack traces to TypeScript source
60844
61137
  --rojoProject <path> Path to rojo project file (auto-detected if not set)
61138
+ --passWithNoTests Exit with 0 when no test files are found
60845
61139
  --verbose Show individual test results
60846
61140
  --silent Suppress output
60847
61141
  --no-color Disable colored output
@@ -60903,6 +61197,7 @@ function parseArgs(args) {
60903
61197
  "no-color": { type: "boolean" },
60904
61198
  "no-show-luau": { type: "boolean" },
60905
61199
  "outputFile": { type: "string" },
61200
+ "passWithNoTests": { type: "boolean" },
60906
61201
  "pollInterval": { type: "string" },
60907
61202
  "port": { type: "string" },
60908
61203
  "project": {
@@ -60958,6 +61253,7 @@ function parseArgs(args) {
60958
61253
  gameOutput: values.gameOutput,
60959
61254
  help: values.help,
60960
61255
  outputFile: values.outputFile,
61256
+ passWithNoTests: values.passWithNoTests,
60961
61257
  pollInterval,
60962
61258
  port,
60963
61259
  project: values.project,
@@ -61104,7 +61400,11 @@ function printFinalStatus(passed) {
61104
61400
  node_process.default.stdout.write(`${badge}\n`);
61105
61401
  }
61106
61402
  function processCoverage(config, coverageData) {
61107
- if (!config.collectCoverage || coverageData === void 0) return true;
61403
+ if (!config.collectCoverage) return true;
61404
+ if (coverageData === void 0) {
61405
+ if (!config.silent) node_process.default.stderr.write("Warning: coverage data was empty — the Rojo project may point at uninstrumented source\n");
61406
+ return true;
61407
+ }
61108
61408
  const manifest = loadCoverageManifest(config.rootDir);
61109
61409
  if (manifest === void 0) {
61110
61410
  if (!config.silent) node_process.default.stderr.write("Warning: Coverage manifest not found, skipping TS mapping\n");
@@ -61286,7 +61586,7 @@ function loadRojoTree(config) {
61286
61586
  const content = node_fs.readFileSync(rojoPath, "utf8");
61287
61587
  const validated = rojoProjectSchema(JSON.parse(content));
61288
61588
  if (validated instanceof type.errors) throw new Error(`Invalid Rojo project: ${validated.summary}`);
61289
- return validated.tree;
61589
+ return resolveNestedProjects(validated.tree, node_path.dirname(rojoPath));
61290
61590
  }
61291
61591
  const STUB_SKIP_KEYS = new Set([
61292
61592
  "outDir",
@@ -61395,6 +61695,7 @@ async function runMultiProject(cli, rootConfig, projectEntries) {
61395
61695
  tsconfig: rootConfig.typecheckTsconfig
61396
61696
  }) : void 0;
61397
61697
  if (projectResults.length === 0 && typecheckResult === void 0) {
61698
+ if (rootConfig.passWithNoTests) return 0;
61398
61699
  console.error("No test files found in any project");
61399
61700
  return 2;
61400
61701
  }
@@ -61423,12 +61724,14 @@ async function runSingleProject(config, cliFiles) {
61423
61724
  resolveSetupFilePaths(config);
61424
61725
  const discovery = discoverTestFiles(config, cliFiles);
61425
61726
  if (discovery.files.length === 0) {
61727
+ if (config.passWithNoTests) return 0;
61426
61728
  console.error("No test files found");
61427
61729
  return 2;
61428
61730
  }
61429
61731
  const typeTestFiles = config.typecheck ? discovery.files.filter((file) => TYPE_TEST_PATTERN.test(file)) : [];
61430
61732
  const runtimeTestFiles = config.typecheckOnly ? [] : discovery.files.filter((file) => !TYPE_TEST_PATTERN.test(file));
61431
61733
  if (typeTestFiles.length === 0 && runtimeTestFiles.length === 0) {
61734
+ if (config.passWithNoTests) return 0;
61432
61735
  console.error("No test files found for the selected mode");
61433
61736
  return 2;
61434
61737
  }
@@ -61540,6 +61843,7 @@ function mergeCliWithConfig(cli, config) {
61540
61843
  formatters: resolveFormatters(cli, config),
61541
61844
  gameOutput: cli.gameOutput ?? config.gameOutput,
61542
61845
  outputFile: cli.outputFile ?? config.outputFile,
61846
+ passWithNoTests: cli.passWithNoTests ?? config.passWithNoTests,
61543
61847
  pollInterval: cli.pollInterval ?? config.pollInterval,
61544
61848
  port: cli.port ?? config.port,
61545
61849
  rojoProject: cli.rojoProject ?? config.rojoProject,