@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.
- package/dist/cli.d.mts +1 -1
- package/dist/cli.mjs +278 -41
- package/dist/{executor-DqZE3wME.d.mts → executor-D6BzDfQ_.d.mts} +3 -0
- package/dist/{game-output-C0_-YIAY.mjs → game-output-BU-9pJ93.mjs} +73 -6
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/sea/jest-roblox +0 -0
- package/dist/sea-entry.cjs +362 -58
- package/package.json +2 -2
package/dist/sea-entry.cjs
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
49647
|
-
const validateRojo = new Lazy(() => ajv.compile(JSON.parse(
|
|
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
|
-
|
|
50416
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
|
54134
|
-
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
|
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,
|