@isentinel/jest-roblox 0.2.4 → 0.2.6
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.
- package/bin/jest-roblox.js +2 -2
- package/dist/cli.d.mts +1 -1
- package/dist/cli.mjs +209 -95
- package/dist/{executor-B2IDh6bH.d.mts → executor-COuwZJJX.d.mts} +15 -2
- package/dist/{game-output-BtWj32M8.mjs → game-output-CCPIQMWm.mjs} +70 -34
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +1 -1
- package/dist/sea/jest-roblox +0 -0
- package/dist/sea-entry.cjs +1045 -5289
- package/package.json +4 -4
package/bin/jest-roblox.js
CHANGED
|
@@ -10,8 +10,8 @@ register("../loaders/luau-raw.mjs", import.meta.url);
|
|
|
10
10
|
|
|
11
11
|
if (existsSync(sourceEntry)) {
|
|
12
12
|
const { main } = await import("../src/cli.ts");
|
|
13
|
-
main();
|
|
13
|
+
await main();
|
|
14
14
|
} else {
|
|
15
15
|
const { main } = await import("../dist/cli.mjs");
|
|
16
|
-
main();
|
|
16
|
+
await main();
|
|
17
17
|
}
|
package/dist/cli.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as ResolvedProjectConfig, n as ExecuteResult, v as CliOptions } from "./executor-
|
|
1
|
+
import { _ as ResolvedProjectConfig, n as ExecuteResult, v as CliOptions } from "./executor-COuwZJJX.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/cli.d.ts
|
|
4
4
|
declare function parseArgs(args: Array<string>): CliOptions;
|
package/dist/cli.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as collectMounts, B as createOpenCloudBackend, D as formatTypecheckSummary, F as loadConfig$1, G as VALID_BACKENDS, J as isValidBackend, M as findInTree, N as pruneAncestors, O as formatBanner, P as resolveNestedProjects, R as createStudioBackend, S as formatAgentMultiProject, T as formatResult, W as ROOT_ONLY_KEYS, X as LuauScriptError, Y as hashBuffer, _ as resolveTsconfigDirectories, a as formatAnnotations, c as visitBlock, d as buildProjectJob, f as execute, g as processProjectResult, h as loadCoverageManifest, i as runTypecheck, j as collectPaths, k as combineSourceMappers, m as formatExecuteOutput, n as parseGameOutput, o as formatJobSummary, p as executeBackend, r as writeGameOutput, s as resolveGitHubActionsOptions, t as formatGameOutputNotice, v as rojoProjectSchema, w as formatMultiProjectResult, x as writeJsonFile, y as findFormatterOptions } from "./game-output-CCPIQMWm.mjs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { type } from "arktype";
|
|
4
4
|
import assert from "node:assert";
|
|
@@ -23,7 +23,7 @@ import istanbulCoverage from "istanbul-lib-coverage";
|
|
|
23
23
|
import istanbulReport from "istanbul-lib-report";
|
|
24
24
|
import istanbulReports from "istanbul-reports";
|
|
25
25
|
//#region package.json
|
|
26
|
-
var version = "0.2.
|
|
26
|
+
var version = "0.2.6";
|
|
27
27
|
//#endregion
|
|
28
28
|
//#region src/backends/auto.ts
|
|
29
29
|
var StudioWithFallback = class {
|
|
@@ -160,7 +160,7 @@ function evalTable(entries) {
|
|
|
160
160
|
}
|
|
161
161
|
//#endregion
|
|
162
162
|
//#region src/luau/parse-ast.luau
|
|
163
|
-
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.
|
|
163
|
+
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\n-- Optional 3rd arg: path to skip list JSON file.\n-- Skipped files are still included in the file list but not parsed.\nlocal skipListPath = userArgs[3]\nlocal skipSet: { [string]: boolean } = {}\nif skipListPath then\n local skipJson = fs.readFileToString(skipListPath)\n local skipList = json.deserialize(skipJson) :: { string }\n for _, entry in skipList do\n skipSet[entry] = true\n end\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 if skipSet[relativePath] then\n continue\n end\n\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";
|
|
164
164
|
//#endregion
|
|
165
165
|
//#region src/config/luau-config-loader.ts
|
|
166
166
|
let cachedTemporaryDirectory$1;
|
|
@@ -237,6 +237,19 @@ function extractStaticRoot(pattern) {
|
|
|
237
237
|
root: pattern.slice(0, lastSlash)
|
|
238
238
|
};
|
|
239
239
|
}
|
|
240
|
+
function mapFsRootToDataModel(outDirectory, rojoTree) {
|
|
241
|
+
const normalized = outDirectory.replace(/\/$/, "");
|
|
242
|
+
const result = findInTree(rojoTree, normalized, "");
|
|
243
|
+
if (result === void 0) {
|
|
244
|
+
const available = [];
|
|
245
|
+
collectPaths(rojoTree, available);
|
|
246
|
+
let message = `No Rojo tree mapping found for path: ${normalized}`;
|
|
247
|
+
if (available.length > 0) message += `\n\nAvailable $path entries: ${available.join(", ")}`;
|
|
248
|
+
const hint = normalized.startsWith("src/") ? "Path starts with \"src/\" — if using roblox-ts, set \"outDir\" in your project config to the compiled output directory (e.g. \"out/client\")" : void 0;
|
|
249
|
+
throw new ConfigError(message, hint);
|
|
250
|
+
}
|
|
251
|
+
return result;
|
|
252
|
+
}
|
|
240
253
|
function extractProjectRoots(include) {
|
|
241
254
|
const rootMap = /* @__PURE__ */ new Map();
|
|
242
255
|
for (const pattern of include) {
|
|
@@ -255,23 +268,22 @@ function extractProjectRoots(include) {
|
|
|
255
268
|
testMatch
|
|
256
269
|
}));
|
|
257
270
|
}
|
|
258
|
-
function
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}
|
|
269
|
-
return result;
|
|
271
|
+
function applyProjectRoot(include, projectRoot) {
|
|
272
|
+
if (projectRoot === void 0) return include;
|
|
273
|
+
return include.map((pattern) => path$1.posix.join(projectRoot, pattern));
|
|
274
|
+
}
|
|
275
|
+
function createFsClassifier(rootDirectory) {
|
|
276
|
+
return function classify(fsPath) {
|
|
277
|
+
const absolute = path$1.isAbsolute(fsPath) ? fsPath : path$1.resolve(rootDirectory, fsPath);
|
|
278
|
+
const stat = fs.statSync(absolute, { throwIfNoEntry: false });
|
|
279
|
+
if (stat === void 0) return "missing";
|
|
280
|
+
return stat.isDirectory() ? "directory" : "file";
|
|
281
|
+
};
|
|
270
282
|
}
|
|
271
283
|
function validateProjects(projects) {
|
|
272
284
|
const names = /* @__PURE__ */ new Set();
|
|
273
285
|
for (const project of projects) {
|
|
274
|
-
const name =
|
|
286
|
+
const name = displayNameOf(project);
|
|
275
287
|
if (name === "") throw new Error("Project must have a non-empty displayName");
|
|
276
288
|
if (names.has(name)) throw new Error(`Duplicate project displayName: ${name}`);
|
|
277
289
|
names.add(name);
|
|
@@ -285,26 +297,23 @@ const PROJECT_ONLY_KEYS = new Set([
|
|
|
285
297
|
"outDir",
|
|
286
298
|
"root"
|
|
287
299
|
]);
|
|
288
|
-
function resolveProjectConfig(project, rootConfig, rojoTree) {
|
|
289
|
-
const
|
|
300
|
+
function resolveProjectConfig(project, rootConfig, rojoTree, classify) {
|
|
301
|
+
const rootPrefixedInclude = applyProjectRoot(project.include, project.root);
|
|
302
|
+
const roots = extractProjectRoots(rootPrefixedInclude);
|
|
290
303
|
const testMatch = roots.flatMap((entry) => entry.testMatch);
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
throw new Error(`Project "${name}" has multiple include roots but no outDir. Set outDir or split into separate projects.`);
|
|
295
|
-
}
|
|
296
|
-
const resolvedOutDirectory = resolveOutDirectory(project.outDir, projectRoot, roots[0]?.root);
|
|
297
|
-
const dataModelPath = resolvedOutDirectory !== void 0 ? mapFsRootToDataModel(resolvedOutDirectory, rojoTree) : void 0;
|
|
298
|
-
const resolvedInclude = projectRoot === void 0 ? project.include : project.include.map((pattern) => path$1.posix.join(projectRoot, pattern));
|
|
304
|
+
const rojoMounts = resolveMounts(project, roots, rojoTree, classify);
|
|
305
|
+
const projects = rojoMounts.map((mount) => mount.dataModelPath);
|
|
306
|
+
const singleMount = rojoMounts.length === 1 ? rojoMounts[0] : void 0;
|
|
299
307
|
const config = mergeProjectConfig(rootConfig, project);
|
|
300
|
-
const displayName =
|
|
308
|
+
const displayName = displayNameOf(project);
|
|
301
309
|
return {
|
|
302
310
|
config,
|
|
303
311
|
displayColor: typeof project.displayName === "string" ? void 0 : project.displayName.color,
|
|
304
312
|
displayName,
|
|
305
|
-
include:
|
|
306
|
-
outDir:
|
|
307
|
-
projects
|
|
313
|
+
include: rootPrefixedInclude,
|
|
314
|
+
outDir: singleMount?.fsPath,
|
|
315
|
+
projects,
|
|
316
|
+
rojoMounts,
|
|
308
317
|
testMatch
|
|
309
318
|
};
|
|
310
319
|
}
|
|
@@ -340,19 +349,92 @@ async function resolveAllProjects(entries, rootConfig, rojoTree, cwd) {
|
|
|
340
349
|
projects.push(loaded);
|
|
341
350
|
} else projects.push(entry.test);
|
|
342
351
|
validateProjects(projects);
|
|
343
|
-
|
|
352
|
+
const classify = createFsClassifier(cwd);
|
|
353
|
+
return projects.map((project) => resolveProjectConfig(project, rootConfig, rojoTree, classify));
|
|
344
354
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const base = projectOutDirectory ?? fallbackRoot;
|
|
348
|
-
if (base === void 0) return;
|
|
349
|
-
return projectRoot !== void 0 ? path$1.posix.join(projectRoot, base) : base;
|
|
355
|
+
function displayNameOf(project) {
|
|
356
|
+
return typeof project.displayName === "string" ? project.displayName : project.displayName.name;
|
|
350
357
|
}
|
|
351
358
|
function mergeProjectConfig(rootConfig, project) {
|
|
352
359
|
const merged = { ...rootConfig };
|
|
353
360
|
for (const [key, value] of Object.entries(project)) if (!PROJECT_ONLY_KEYS.has(key) && value !== void 0) merged[key] = value;
|
|
354
361
|
return merged;
|
|
355
362
|
}
|
|
363
|
+
function dedupeMounts(mounts) {
|
|
364
|
+
const seen = /* @__PURE__ */ new Set();
|
|
365
|
+
const result = [];
|
|
366
|
+
for (const mount of mounts) if (!seen.has(mount.dataModelPath)) {
|
|
367
|
+
seen.add(mount.dataModelPath);
|
|
368
|
+
result.push(mount);
|
|
369
|
+
}
|
|
370
|
+
return result;
|
|
371
|
+
}
|
|
372
|
+
function joinProjectRoot(relativePath, projectRoot) {
|
|
373
|
+
return projectRoot !== void 0 ? path$1.posix.join(projectRoot, relativePath) : relativePath;
|
|
374
|
+
}
|
|
375
|
+
function pruneAncestorMounts(mounts) {
|
|
376
|
+
const dataModelPaths = mounts.map((mount) => mount.dataModelPath);
|
|
377
|
+
const surviving = new Set(pruneAncestors(dataModelPaths));
|
|
378
|
+
return mounts.filter((mount) => surviving.has(mount.dataModelPath));
|
|
379
|
+
}
|
|
380
|
+
function unmappableRootError(project, root, rojoTree) {
|
|
381
|
+
const name = displayNameOf(project);
|
|
382
|
+
const available = [];
|
|
383
|
+
collectPaths(rojoTree, available);
|
|
384
|
+
let message = `Project "${name}": include root "${root}" did not match any Rojo $path entry or subdirectory.`;
|
|
385
|
+
if (available.length > 0) message += `\n\nAvailable $path entries: ${available.join(", ")}`;
|
|
386
|
+
const hint = root.startsWith("src/") ? "Path starts with \"src/\" — if using roblox-ts, set \"outDir\" in your project config to the compiled output directory (e.g. \"out/client\")" : void 0;
|
|
387
|
+
return new ConfigError(message, hint);
|
|
388
|
+
}
|
|
389
|
+
function filterMountsForRoot(allMounts, root) {
|
|
390
|
+
return allMounts.filter((mount) => mount.fsPath === root || mount.fsPath.startsWith(`${root}/`));
|
|
391
|
+
}
|
|
392
|
+
function resolveMounts(project, roots, rojoTree, classify) {
|
|
393
|
+
if (project.outDir !== void 0) {
|
|
394
|
+
const resolvedOutDirectory = joinProjectRoot(project.outDir, project.root);
|
|
395
|
+
return [{
|
|
396
|
+
dataModelPath: mapFsRootToDataModel(resolvedOutDirectory, rojoTree),
|
|
397
|
+
fsPath: resolvedOutDirectory
|
|
398
|
+
}];
|
|
399
|
+
}
|
|
400
|
+
let collectedMounts;
|
|
401
|
+
const allMounts = [];
|
|
402
|
+
for (const { root } of roots) {
|
|
403
|
+
const exact = findInTree(rojoTree, root, "");
|
|
404
|
+
if (exact !== void 0) {
|
|
405
|
+
allMounts.push({
|
|
406
|
+
dataModelPath: exact,
|
|
407
|
+
fsPath: root
|
|
408
|
+
});
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
collectedMounts ??= collectMounts(rojoTree, "", classify);
|
|
412
|
+
const expanded = filterMountsForRoot(collectedMounts, root);
|
|
413
|
+
if (expanded.length === 0) throw unmappableRootError(project, root, rojoTree);
|
|
414
|
+
allMounts.push(...expanded);
|
|
415
|
+
}
|
|
416
|
+
return pruneAncestorMounts(dedupeMounts(allMounts));
|
|
417
|
+
}
|
|
418
|
+
function copyLuauOptionalFields(raw, config) {
|
|
419
|
+
const record = config;
|
|
420
|
+
for (const key of LUAU_BOOLEAN_KEYS) if (typeof raw[key] === "boolean") record[key] = raw[key];
|
|
421
|
+
for (const key of LUAU_NUMBER_KEYS) if (typeof raw[key] === "number") record[key] = raw[key];
|
|
422
|
+
for (const key of LUAU_STRING_KEYS) if (typeof raw[key] === "string") record[key] = raw[key];
|
|
423
|
+
for (const key of LUAU_STRING_ARRAY_KEYS) if (Array.isArray(raw[key])) record[key] = raw[key];
|
|
424
|
+
}
|
|
425
|
+
function buildProjectConfigFromLuau(luauConfigPath, directoryPath) {
|
|
426
|
+
const raw = loadLuauConfig(luauConfigPath);
|
|
427
|
+
const { displayName } = raw;
|
|
428
|
+
if (typeof displayName !== "string" || displayName === "") throw new Error(`Luau config file "${luauConfigPath}" must have a displayName string`);
|
|
429
|
+
const testMatch = Array.isArray(raw["testMatch"]) ? raw["testMatch"] : void 0;
|
|
430
|
+
const config = {
|
|
431
|
+
displayName,
|
|
432
|
+
include: testMatch !== void 0 ? testMatch.map((pattern) => path$1.posix.join(directoryPath, `${pattern}.luau`)) : [path$1.posix.join(directoryPath, "**/*.spec.luau")]
|
|
433
|
+
};
|
|
434
|
+
if (testMatch !== void 0) config.testMatch = testMatch;
|
|
435
|
+
copyLuauOptionalFields(raw, config);
|
|
436
|
+
return config;
|
|
437
|
+
}
|
|
356
438
|
/**
|
|
357
439
|
* When a project config provides `testMatch` but not `include`, derive
|
|
358
440
|
* `include` by appending `.ts` and `.tsx` extensions. This lets users
|
|
@@ -384,26 +466,6 @@ const LUAU_BOOLEAN_KEYS = [
|
|
|
384
466
|
const LUAU_NUMBER_KEYS = ["slowTestThreshold", "testTimeout"];
|
|
385
467
|
const LUAU_STRING_KEYS = ["testEnvironment"];
|
|
386
468
|
const LUAU_STRING_ARRAY_KEYS = ["setupFiles", "setupFilesAfterEnv"];
|
|
387
|
-
function copyLuauOptionalFields(raw, config) {
|
|
388
|
-
const record = config;
|
|
389
|
-
for (const key of LUAU_BOOLEAN_KEYS) if (typeof raw[key] === "boolean") record[key] = raw[key];
|
|
390
|
-
for (const key of LUAU_NUMBER_KEYS) if (typeof raw[key] === "number") record[key] = raw[key];
|
|
391
|
-
for (const key of LUAU_STRING_KEYS) if (typeof raw[key] === "string") record[key] = raw[key];
|
|
392
|
-
for (const key of LUAU_STRING_ARRAY_KEYS) if (Array.isArray(raw[key])) record[key] = raw[key];
|
|
393
|
-
}
|
|
394
|
-
function buildProjectConfigFromLuau(luauConfigPath, directoryPath) {
|
|
395
|
-
const raw = loadLuauConfig(luauConfigPath);
|
|
396
|
-
const { displayName } = raw;
|
|
397
|
-
if (typeof displayName !== "string" || displayName === "") throw new Error(`Luau config file "${luauConfigPath}" must have a displayName string`);
|
|
398
|
-
const testMatch = Array.isArray(raw["testMatch"]) ? raw["testMatch"] : void 0;
|
|
399
|
-
const config = {
|
|
400
|
-
displayName,
|
|
401
|
-
include: testMatch !== void 0 ? testMatch.map((pattern) => path$1.posix.join(directoryPath, `${pattern}.luau`)) : [path$1.posix.join(directoryPath, "**/*.spec.luau")]
|
|
402
|
-
};
|
|
403
|
-
if (testMatch !== void 0) config.testMatch = testMatch;
|
|
404
|
-
copyLuauOptionalFields(raw, config);
|
|
405
|
-
return config;
|
|
406
|
-
}
|
|
407
469
|
//#endregion
|
|
408
470
|
//#region src/config/setup-resolver.ts
|
|
409
471
|
const PROBE_EXTENSIONS = [
|
|
@@ -445,6 +507,8 @@ function resolvePackageSpecifier(resolve, input) {
|
|
|
445
507
|
//#endregion
|
|
446
508
|
//#region src/config/stubs.ts
|
|
447
509
|
const HEADER = "-- Auto-generated by jest-roblox (do not edit)\n";
|
|
510
|
+
const STUB_FILENAME = "jest.config.lua";
|
|
511
|
+
const USER_LUAU_FILENAME = "jest.config.luau";
|
|
448
512
|
const SKIP_FIELDS = new Set(["exclude", "include"]);
|
|
449
513
|
function serializeToLuau(config) {
|
|
450
514
|
let output = `${HEADER}return {\n`;
|
|
@@ -461,33 +525,52 @@ function serializeToLuau(config) {
|
|
|
461
525
|
}
|
|
462
526
|
function generateProjectConfigs(projects) {
|
|
463
527
|
for (const project of projects) {
|
|
464
|
-
const luauConfigPath = project.outputPath.replace(/\.lua$/, ".luau");
|
|
465
|
-
if (fs.existsSync(luauConfigPath)) continue;
|
|
466
|
-
const content = serializeToLuau(project.config);
|
|
467
528
|
const directory = path.dirname(project.outputPath);
|
|
529
|
+
if (hasUserAuthoredConfig(directory)) continue;
|
|
530
|
+
const content = serializeToLuau(project.config);
|
|
468
531
|
fs.mkdirSync(directory, { recursive: true });
|
|
469
532
|
fs.writeFileSync(project.outputPath, content, "utf8");
|
|
470
533
|
}
|
|
471
534
|
}
|
|
535
|
+
/**
|
|
536
|
+
* Refuse to let any downstream write land outside `rootDirectory`. Mount
|
|
537
|
+
* fsPaths originate from the rojo tree and `resolveNestedProjects` can emit
|
|
538
|
+
* values with `..` segments (nested projects relocate paths); combined with
|
|
539
|
+
* `path.resolve` on Windows, a permissive fsPath could send stub writes to
|
|
540
|
+
* `D:\outside\...`. Guard at every write-site.
|
|
541
|
+
*/
|
|
542
|
+
function assertMountContained(project, fsPath, rootDirectory) {
|
|
543
|
+
if (path.isAbsolute(fsPath)) throw new Error(`Project "${project.displayName}" mount fsPath must be relative, got: ${fsPath}`);
|
|
544
|
+
const rootResolved = path.resolve(rootDirectory);
|
|
545
|
+
const mountResolved = path.resolve(rootDirectory, fsPath);
|
|
546
|
+
if (mountResolved !== rootResolved && !mountResolved.startsWith(rootResolved + path.sep)) throw new Error(`Project "${project.displayName}" mount fsPath escapes root directory: ${fsPath}`);
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* For a multi-mount project, the stubs-per-mount rule requires that a
|
|
550
|
+
* user-authored config at any tracked FS path must appear at every tracked
|
|
551
|
+
* FS path (or none). Throws `ConfigError` when the project has partial
|
|
552
|
+
* coverage — some mounts have a user config and others don't.
|
|
553
|
+
*/
|
|
554
|
+
function assertStubCollisionRule(project, rootDirectory) {
|
|
555
|
+
const withUser = [];
|
|
556
|
+
const withoutUser = [];
|
|
557
|
+
for (const mount of project.rojoMounts) {
|
|
558
|
+
assertMountContained(project, mount.fsPath, rootDirectory);
|
|
559
|
+
if (hasUserAuthoredConfig(path.resolve(rootDirectory, mount.fsPath))) withUser.push(mount.fsPath);
|
|
560
|
+
else withoutUser.push(mount.fsPath);
|
|
561
|
+
}
|
|
562
|
+
if (withUser.length === 0 || withoutUser.length === 0) return;
|
|
563
|
+
const withUserList = withUser.join(", ");
|
|
564
|
+
const withoutUserList = withoutUser.join(", ");
|
|
565
|
+
throw new ConfigError(`Project "${project.displayName}": user-authored jest config present at some mounts but not others.\n\nWith user config: ${withUserList}\nWithout user config: ${withoutUserList}\n\nFor multi-mount projects, either every tracked mount must have a user config, or none.`);
|
|
566
|
+
}
|
|
472
567
|
function syncStubsToShadowDirectory(projects, rootDirectory, shadowDirectory) {
|
|
473
568
|
let changed = false;
|
|
474
569
|
const expectedPaths = /* @__PURE__ */ new Set();
|
|
475
|
-
for (const project of projects) {
|
|
476
|
-
|
|
477
|
-
if (
|
|
478
|
-
|
|
479
|
-
const sourcePath = path.resolve(rootDirectory, project.outDir, stubName);
|
|
480
|
-
if (!fs.existsSync(sourcePath)) continue;
|
|
481
|
-
const targetPath = path.resolve(shadowDirectory, project.outDir, stubName);
|
|
482
|
-
expectedPaths.add(targetPath);
|
|
483
|
-
const sourceContent = fs.readFileSync(sourcePath);
|
|
484
|
-
if (fs.existsSync(targetPath)) {
|
|
485
|
-
const targetContent = fs.readFileSync(targetPath);
|
|
486
|
-
if (Buffer.compare(sourceContent, targetContent) === 0) continue;
|
|
487
|
-
}
|
|
488
|
-
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
489
|
-
fs.copyFileSync(sourcePath, targetPath);
|
|
490
|
-
changed = true;
|
|
570
|
+
for (const project of projects) for (const mount of project.rojoMounts) {
|
|
571
|
+
const result = syncMountStub(project, mount.fsPath, rootDirectory, shadowDirectory);
|
|
572
|
+
if (result.targetPath !== void 0) expectedPaths.add(result.targetPath);
|
|
573
|
+
changed ||= result.changed;
|
|
491
574
|
}
|
|
492
575
|
for (const existing of findShadowStubs(shadowDirectory)) if (!expectedPaths.has(existing)) {
|
|
493
576
|
fs.unlinkSync(existing);
|
|
@@ -497,7 +580,7 @@ function syncStubsToShadowDirectory(projects, rootDirectory, shadowDirectory) {
|
|
|
497
580
|
return changed;
|
|
498
581
|
}
|
|
499
582
|
function escapeString(value) {
|
|
500
|
-
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
583
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
501
584
|
}
|
|
502
585
|
function serializeLuauValue(value, indent) {
|
|
503
586
|
if (typeof value === "string") return `"${escapeString(value)}"`;
|
|
@@ -510,16 +593,35 @@ function serializeLuauValue(value, indent) {
|
|
|
510
593
|
}
|
|
511
594
|
return String(value);
|
|
512
595
|
}
|
|
513
|
-
function
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
596
|
+
function hasUserAuthoredConfig(directoryPath) {
|
|
597
|
+
const luauPath = path.join(directoryPath, USER_LUAU_FILENAME);
|
|
598
|
+
if (fs.existsSync(luauPath)) return true;
|
|
599
|
+
const luaPath = path.join(directoryPath, STUB_FILENAME);
|
|
600
|
+
if (!fs.existsSync(luaPath)) return false;
|
|
601
|
+
return !fs.readFileSync(luaPath, "utf8").startsWith(HEADER);
|
|
602
|
+
}
|
|
603
|
+
function syncMountStub(project, fsPath, rootDirectory, shadowDirectory) {
|
|
604
|
+
assertMountContained(project, fsPath, rootDirectory);
|
|
605
|
+
const sourcePath = path.resolve(rootDirectory, fsPath, STUB_FILENAME);
|
|
606
|
+
if (!fs.existsSync(sourcePath)) return {
|
|
607
|
+
changed: false,
|
|
608
|
+
targetPath: void 0
|
|
609
|
+
};
|
|
610
|
+
const targetPath = path.resolve(shadowDirectory, fsPath, STUB_FILENAME);
|
|
611
|
+
const sourceContent = fs.readFileSync(sourcePath);
|
|
612
|
+
if (fs.existsSync(targetPath)) {
|
|
613
|
+
const targetContent = fs.readFileSync(targetPath);
|
|
614
|
+
if (Buffer.compare(sourceContent, targetContent) === 0) return {
|
|
615
|
+
changed: false,
|
|
616
|
+
targetPath
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
620
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
621
|
+
return {
|
|
622
|
+
changed: true,
|
|
623
|
+
targetPath
|
|
624
|
+
};
|
|
523
625
|
}
|
|
524
626
|
function findShadowStubs(directory) {
|
|
525
627
|
const results = [];
|
|
@@ -528,10 +630,21 @@ function findShadowStubs(directory) {
|
|
|
528
630
|
for (const entry of entries) {
|
|
529
631
|
const fullPath = path.resolve(directory, entry.name);
|
|
530
632
|
if (entry.isDirectory()) results.push(...findShadowStubs(fullPath));
|
|
531
|
-
else if (entry.name ===
|
|
633
|
+
else if (entry.name === STUB_FILENAME) results.push(fullPath);
|
|
532
634
|
}
|
|
533
635
|
return results;
|
|
534
636
|
}
|
|
637
|
+
function removeEmptyParents(directory, stopAt) {
|
|
638
|
+
const resolved = path.resolve(directory);
|
|
639
|
+
const resolvedStop = path.resolve(stopAt);
|
|
640
|
+
if (resolved === resolvedStop || !resolved.startsWith(resolvedStop)) return;
|
|
641
|
+
try {
|
|
642
|
+
if (fs.readdirSync(resolved).length === 0) {
|
|
643
|
+
fs.rmdirSync(resolved);
|
|
644
|
+
removeEmptyParents(path.dirname(resolved), stopAt);
|
|
645
|
+
}
|
|
646
|
+
} catch {}
|
|
647
|
+
}
|
|
535
648
|
//#endregion
|
|
536
649
|
//#region src/coverage/derive-coverage-from.ts
|
|
537
650
|
/**
|
|
@@ -2328,8 +2441,7 @@ async function run(args) {
|
|
|
2328
2441
|
}
|
|
2329
2442
|
}
|
|
2330
2443
|
async function main() {
|
|
2331
|
-
|
|
2332
|
-
process.exit(exitCode);
|
|
2444
|
+
process.exitCode = await run(process.argv.slice(2));
|
|
2333
2445
|
}
|
|
2334
2446
|
/**
|
|
2335
2447
|
* `--parallel` with no value means `"auto"`. Node's `parseArgs` can't express
|
|
@@ -2623,18 +2735,20 @@ function buildStubConfig(config) {
|
|
|
2623
2735
|
function generateProjectStubs(projects, rootDirectory) {
|
|
2624
2736
|
const entries = [];
|
|
2625
2737
|
for (const project of projects) {
|
|
2626
|
-
|
|
2627
|
-
const outputPath = path$1.resolve(rootDirectory, project.outDir, "jest.config.lua");
|
|
2738
|
+
assertStubCollisionRule(project, rootDirectory);
|
|
2628
2739
|
const stubConfig = {
|
|
2629
2740
|
...buildStubConfig(project.config),
|
|
2630
2741
|
displayName: project.displayName,
|
|
2631
2742
|
include: [],
|
|
2632
2743
|
testMatch: project.testMatch
|
|
2633
2744
|
};
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2745
|
+
for (const mount of project.rojoMounts) {
|
|
2746
|
+
const outputPath = path$1.resolve(rootDirectory, mount.fsPath, "jest.config.lua");
|
|
2747
|
+
entries.push({
|
|
2748
|
+
config: stubConfig,
|
|
2749
|
+
outputPath
|
|
2750
|
+
});
|
|
2751
|
+
}
|
|
2638
2752
|
}
|
|
2639
2753
|
generateProjectConfigs(entries);
|
|
2640
2754
|
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { ReportOptions } from "istanbul-reports";
|
|
2
2
|
|
|
3
|
+
//#region packages/rojo-utils/dist/index.d.mts
|
|
4
|
+
interface Mount {
|
|
5
|
+
dataModelPath: string;
|
|
6
|
+
fsPath: string;
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
3
9
|
//#region node_modules/.pnpm/@rbxts+jest@3.13.3-ts.1/node_modules/@rbxts/jest/src/config.d.ts
|
|
4
10
|
interface ReporterConfig {
|
|
5
11
|
reporter: string | ModuleScript;
|
|
@@ -987,10 +993,17 @@ interface ResolvedProjectConfig {
|
|
|
987
993
|
displayName: string;
|
|
988
994
|
/** Original include patterns (with TS extensions) for filesystem discovery. */
|
|
989
995
|
include: Array<string>;
|
|
990
|
-
/**
|
|
996
|
+
/**
|
|
997
|
+
* Single resolved output directory (workspace-relative). Set only when
|
|
998
|
+
* resolution produced exactly one mount; undefined when the project spans
|
|
999
|
+
* multiple rojo mounts. Kept for back-compat; new code should consume
|
|
1000
|
+
* `rojoMounts` instead.
|
|
1001
|
+
*/
|
|
991
1002
|
outDir?: string;
|
|
992
|
-
/** DataModel paths
|
|
1003
|
+
/** DataModel paths Jest walks up from to discover test configs. */
|
|
993
1004
|
projects: Array<string>;
|
|
1005
|
+
/** Internal: FS↔DataModel pairs for stub generation and shadow sync. */
|
|
1006
|
+
rojoMounts: Array<Mount>;
|
|
994
1007
|
/** Luau-side testMatch patterns (extensions stripped). */
|
|
995
1008
|
testMatch: Array<string>;
|
|
996
1009
|
}
|