qunitx-cli 0.23.7 → 0.24.0
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.js +338 -142
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -374,7 +374,7 @@ var init_package = __esm({
|
|
|
374
374
|
package_default = {
|
|
375
375
|
name: "qunitx-cli",
|
|
376
376
|
type: "module",
|
|
377
|
-
version: "0.
|
|
377
|
+
version: "0.24.0",
|
|
378
378
|
description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
|
|
379
379
|
author: "Izel Nakri",
|
|
380
380
|
license: "MIT",
|
|
@@ -519,6 +519,8 @@ ${color("--browser")} : browser engine to run tests in: chromium, firefox, webki
|
|
|
519
519
|
${color("--before")} : run a script before the tests(i.e start a new web server before tests)
|
|
520
520
|
${color("--after")} : run a script after the tests(i.e save test results to a file)
|
|
521
521
|
${color("--no-daemon")} : don't use the daemon for this run \u2014 skips a running daemon and prevents ${color("QUNITX_DAEMON")} auto-spawn
|
|
522
|
+
${color("--changed")} : run only test files affected by changes since ${color("HEAD")} (requires git; falls back to running all on first use)
|
|
523
|
+
${color("--since")} : run only test files affected by changes since the given git ref (e.g. ${color("--since=main")}); ${color("--changed")} = ${color("--since=HEAD")}
|
|
522
524
|
${color("--trace-perf")} : write timestamped startup-perf trace lines to stderr (Chrome pre-launch, module load, browser bind)
|
|
523
525
|
|
|
524
526
|
${highlight("Example:")} $ ${color("qunitx test/foo.ts app/e2e --debug --watch --before=scripts/start-new-webserver.js --after=scripts/write-test-results.js")}
|
|
@@ -547,9 +549,9 @@ var init_help = __esm({
|
|
|
547
549
|
|
|
548
550
|
// lib/utils/path-exists.ts
|
|
549
551
|
import fs2 from "node:fs/promises";
|
|
550
|
-
async function pathExists(
|
|
552
|
+
async function pathExists(path17) {
|
|
551
553
|
try {
|
|
552
|
-
await fs2.access(
|
|
554
|
+
await fs2.access(path17);
|
|
553
555
|
return true;
|
|
554
556
|
} catch {
|
|
555
557
|
return false;
|
|
@@ -641,17 +643,17 @@ import fs4 from "node:fs/promises";
|
|
|
641
643
|
async function generateTestFiles() {
|
|
642
644
|
const projectRoot = await findProjectRoot();
|
|
643
645
|
const moduleName = pathToModuleName(process.argv[3]);
|
|
644
|
-
const
|
|
645
|
-
if (await pathExists(
|
|
646
|
-
console.log(`${
|
|
646
|
+
const path17 = process.argv[3].endsWith(".js") || process.argv[3].endsWith(".ts") ? `${projectRoot}/${process.argv[3]}` : `${projectRoot}/${process.argv[3]}.js`;
|
|
647
|
+
if (await pathExists(path17)) {
|
|
648
|
+
console.log(`${path17} already exists!`);
|
|
647
649
|
return;
|
|
648
650
|
}
|
|
649
651
|
const testJSContent = await readTemplate("test.js");
|
|
650
|
-
const targetFolderPaths =
|
|
652
|
+
const targetFolderPaths = path17.split("/");
|
|
651
653
|
targetFolderPaths.pop();
|
|
652
654
|
await fs4.mkdir(targetFolderPaths.join("/"), { recursive: true });
|
|
653
|
-
await fs4.writeFile(
|
|
654
|
-
console.log(green(`${
|
|
655
|
+
await fs4.writeFile(path17, testJSContent.replace("{{moduleName}}", moduleName));
|
|
656
|
+
console.log(green(`${path17} written`));
|
|
655
657
|
}
|
|
656
658
|
function pathToModuleName(filePath) {
|
|
657
659
|
const withoutExt = filePath.replace(/\.(js|ts)$/, "");
|
|
@@ -1068,11 +1070,11 @@ function setupTestFilePaths(inputs2) {
|
|
|
1068
1070
|
}, []);
|
|
1069
1071
|
return dedupedFolders.concat(dedupedGlobFiles, dedupedPlainFiles).map((meta) => meta.input);
|
|
1070
1072
|
}
|
|
1071
|
-
function pathIsFile(
|
|
1072
|
-
return
|
|
1073
|
+
function pathIsFile(path17) {
|
|
1074
|
+
return path17.includes(".", path17.lastIndexOf("/") + 1);
|
|
1073
1075
|
}
|
|
1074
1076
|
function isIncludedIn(paths, target) {
|
|
1075
|
-
return paths.some((
|
|
1077
|
+
return paths.some((path17) => path17 !== target && matchesGlob(target.input, path17.globFormat));
|
|
1076
1078
|
}
|
|
1077
1079
|
function isGlob2(str) {
|
|
1078
1080
|
return GLOB_CHARS.test(str);
|
|
@@ -1084,8 +1086,162 @@ var init_test_file_paths = __esm({
|
|
|
1084
1086
|
}
|
|
1085
1087
|
});
|
|
1086
1088
|
|
|
1087
|
-
// lib/utils/
|
|
1089
|
+
// lib/utils/get-changed-files.ts
|
|
1088
1090
|
import path5 from "node:path";
|
|
1091
|
+
function getChangedFiles(metafile, esbuildCwd, changedAbsPaths, testFiles) {
|
|
1092
|
+
const deps = new Map(
|
|
1093
|
+
Object.entries(metafile.inputs).map(([relPath, info]) => [
|
|
1094
|
+
path5.resolve(esbuildCwd, relPath),
|
|
1095
|
+
(info.imports ?? []).map((i) => path5.resolve(esbuildCwd, i.path))
|
|
1096
|
+
])
|
|
1097
|
+
);
|
|
1098
|
+
const memo = /* @__PURE__ */ new Map();
|
|
1099
|
+
return new Set(
|
|
1100
|
+
testFiles.filter((test) => reachesChange(test, deps, changedAbsPaths, memo, /* @__PURE__ */ new Set()))
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
function reachesChange(node, deps, changedAbsPaths, memo, stack) {
|
|
1104
|
+
const cached = memo.get(node);
|
|
1105
|
+
if (cached !== void 0) return cached;
|
|
1106
|
+
if (stack.has(node)) return false;
|
|
1107
|
+
stack.add(node);
|
|
1108
|
+
const result2 = changedAbsPaths.has(node) || (deps.get(node) ?? []).some((dep) => reachesChange(dep, deps, changedAbsPaths, memo, stack));
|
|
1109
|
+
stack.delete(node);
|
|
1110
|
+
memo.set(node, result2);
|
|
1111
|
+
return result2;
|
|
1112
|
+
}
|
|
1113
|
+
var init_get_changed_files = __esm({
|
|
1114
|
+
"lib/utils/get-changed-files.ts"() {
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
// lib/utils/get-changed-file-paths-in-git-since.ts
|
|
1119
|
+
import { execFile } from "node:child_process";
|
|
1120
|
+
import { promisify } from "node:util";
|
|
1121
|
+
import path6 from "node:path";
|
|
1122
|
+
async function getChangedFilePathsInGitSince(projectRoot, ref) {
|
|
1123
|
+
const [diffOut, statusOut] = await Promise.all([
|
|
1124
|
+
execFileAsync("git", ["diff", "--name-only", "--no-renames", ref, "--", projectRoot], {
|
|
1125
|
+
cwd: projectRoot,
|
|
1126
|
+
maxBuffer: 16 * 1024 * 1024
|
|
1127
|
+
}).then((r) => r.stdout),
|
|
1128
|
+
execFileAsync("git", ["status", "--porcelain", "--untracked-files=all"], {
|
|
1129
|
+
cwd: projectRoot,
|
|
1130
|
+
maxBuffer: 16 * 1024 * 1024
|
|
1131
|
+
}).then((r) => r.stdout)
|
|
1132
|
+
]);
|
|
1133
|
+
const fromStatus = (line) => {
|
|
1134
|
+
const rest = line.slice(3);
|
|
1135
|
+
const arrow = rest.indexOf(" -> ");
|
|
1136
|
+
return arrow === -1 ? rest : rest.slice(arrow + 4);
|
|
1137
|
+
};
|
|
1138
|
+
const relPaths = /* @__PURE__ */ new Set([
|
|
1139
|
+
...diffOut.split("\n").filter(Boolean),
|
|
1140
|
+
...statusOut.split("\n").filter((l) => l.length >= 4).map(fromStatus)
|
|
1141
|
+
]);
|
|
1142
|
+
const isBlastRadius = (rel) => {
|
|
1143
|
+
const base = path6.basename(rel);
|
|
1144
|
+
return BLAST_RADIUS_FILES.has(base) || BLAST_RADIUS_PATTERNS.some((re) => re.test(base));
|
|
1145
|
+
};
|
|
1146
|
+
if (Array.from(relPaths).some(isBlastRadius)) return null;
|
|
1147
|
+
return new Set(Array.from(relPaths, (rel) => path6.resolve(projectRoot, rel)));
|
|
1148
|
+
}
|
|
1149
|
+
var execFileAsync, BLAST_RADIUS_FILES, BLAST_RADIUS_PATTERNS;
|
|
1150
|
+
var init_get_changed_file_paths_in_git_since = __esm({
|
|
1151
|
+
"lib/utils/get-changed-file-paths-in-git-since.ts"() {
|
|
1152
|
+
execFileAsync = promisify(execFile);
|
|
1153
|
+
BLAST_RADIUS_FILES = /* @__PURE__ */ new Set(["package.json", "package-lock.json", "deno.json", "deno.lock"]);
|
|
1154
|
+
BLAST_RADIUS_PATTERNS = [/^tsconfig.*\.json$/];
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
// lib/utils/metafile-cache.ts
|
|
1159
|
+
import fs8 from "node:fs/promises";
|
|
1160
|
+
import path7 from "node:path";
|
|
1161
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1162
|
+
function metafileCachePath(projectRoot) {
|
|
1163
|
+
const tag = createHash2("sha1").update(projectRoot).digest("hex").slice(0, 12);
|
|
1164
|
+
return path7.join(projectRoot, "node_modules", ".cache", "qunitx", tag, CACHE_FILE);
|
|
1165
|
+
}
|
|
1166
|
+
async function writeMetafileCache(projectRoot, esbuildCwd, metafile) {
|
|
1167
|
+
const file = metafileCachePath(projectRoot);
|
|
1168
|
+
try {
|
|
1169
|
+
await fs8.mkdir(path7.dirname(file), { recursive: true });
|
|
1170
|
+
await fs8.writeFile(
|
|
1171
|
+
file,
|
|
1172
|
+
JSON.stringify({ esbuildCwd, metafile })
|
|
1173
|
+
);
|
|
1174
|
+
} catch {
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
async function readMetafileCache(projectRoot) {
|
|
1178
|
+
try {
|
|
1179
|
+
const raw = await fs8.readFile(metafileCachePath(projectRoot), "utf8");
|
|
1180
|
+
const parsed = JSON.parse(raw);
|
|
1181
|
+
if (typeof parsed?.esbuildCwd !== "string" || !parsed.metafile?.inputs) return null;
|
|
1182
|
+
return parsed;
|
|
1183
|
+
} catch {
|
|
1184
|
+
return null;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
var CACHE_FILE;
|
|
1188
|
+
var init_metafile_cache = __esm({
|
|
1189
|
+
"lib/utils/metafile-cache.ts"() {
|
|
1190
|
+
CACHE_FILE = "metafile.json";
|
|
1191
|
+
}
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
// lib/setup/get-changed-fs-tree.ts
|
|
1195
|
+
async function getChangedFsTree(fsTree, projectRoot, changedSince) {
|
|
1196
|
+
const testFiles = Object.keys(fsTree);
|
|
1197
|
+
if (testFiles.length === 0) return fsTree;
|
|
1198
|
+
const cache = await readMetafileCache(projectRoot);
|
|
1199
|
+
if (!cache) {
|
|
1200
|
+
process.stdout.write(
|
|
1201
|
+
`# --changed: no metafile cache yet \u2014 running all ${testFiles.length} test files (cache populates on this run)
|
|
1202
|
+
`
|
|
1203
|
+
);
|
|
1204
|
+
return fsTree;
|
|
1205
|
+
}
|
|
1206
|
+
const changed = await getChangedFilePathsInGitSince(projectRoot, changedSince).catch(
|
|
1207
|
+
(err) => err
|
|
1208
|
+
);
|
|
1209
|
+
if (changed instanceof Error) {
|
|
1210
|
+
process.stdout.write(
|
|
1211
|
+
`# --changed: git lookup failed (${changed.message.split("\n")[0]}) \u2014 running all ${testFiles.length} test files
|
|
1212
|
+
`
|
|
1213
|
+
);
|
|
1214
|
+
return fsTree;
|
|
1215
|
+
} else if (changed === null) {
|
|
1216
|
+
process.stdout.write(
|
|
1217
|
+
`# --changed: blast-radius file changed (package.json / tsconfig.json / lockfile) \u2014 running all ${testFiles.length} test files
|
|
1218
|
+
`
|
|
1219
|
+
);
|
|
1220
|
+
return fsTree;
|
|
1221
|
+
} else if (changed.size === 0) {
|
|
1222
|
+
process.stdout.write(
|
|
1223
|
+
`# --changed: 0 files changed since ${changedSince} \u2014 running 0 test files
|
|
1224
|
+
`
|
|
1225
|
+
);
|
|
1226
|
+
return {};
|
|
1227
|
+
}
|
|
1228
|
+
const affected = getChangedFiles(cache.metafile, cache.esbuildCwd, changed, testFiles);
|
|
1229
|
+
process.stdout.write(
|
|
1230
|
+
`# --changed: ${affected.size} of ${testFiles.length} test files affected by changes since ${changedSince}
|
|
1231
|
+
`
|
|
1232
|
+
);
|
|
1233
|
+
return Object.fromEntries(testFiles.filter((f) => affected.has(f)).map((f) => [f, null]));
|
|
1234
|
+
}
|
|
1235
|
+
var init_get_changed_fs_tree = __esm({
|
|
1236
|
+
"lib/setup/get-changed-fs-tree.ts"() {
|
|
1237
|
+
init_get_changed_files();
|
|
1238
|
+
init_get_changed_file_paths_in_git_since();
|
|
1239
|
+
init_metafile_cache();
|
|
1240
|
+
}
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1243
|
+
// lib/utils/parse-cli-flags.ts
|
|
1244
|
+
import path8 from "node:path";
|
|
1089
1245
|
function parseCliFlags(projectRoot) {
|
|
1090
1246
|
const providedFlags = process.argv.slice(2).reduce(
|
|
1091
1247
|
(result2, arg) => {
|
|
@@ -1129,6 +1285,15 @@ function parseCliFlags(projectRoot) {
|
|
|
1129
1285
|
return Object.assign(result2, { before: parseModule(arg.split("=")[1]) });
|
|
1130
1286
|
} else if (arg.startsWith("--after")) {
|
|
1131
1287
|
return Object.assign(result2, { after: parseModule(arg.split("=")[1]) });
|
|
1288
|
+
} else if (arg === "--changed") {
|
|
1289
|
+
return Object.assign(result2, { changedSince: "HEAD" });
|
|
1290
|
+
} else if (arg.startsWith("--since")) {
|
|
1291
|
+
const ref = arg.split("=")[1];
|
|
1292
|
+
if (!ref) {
|
|
1293
|
+
console.error(`Invalid --since value: empty. Expected --since=<git-ref>.`);
|
|
1294
|
+
process.exit(1);
|
|
1295
|
+
}
|
|
1296
|
+
return Object.assign(result2, { changedSince: ref });
|
|
1132
1297
|
} else if (arg === "--trace-perf") {
|
|
1133
1298
|
return result2;
|
|
1134
1299
|
}
|
|
@@ -1137,7 +1302,7 @@ function parseCliFlags(projectRoot) {
|
|
|
1137
1302
|
return result2;
|
|
1138
1303
|
}
|
|
1139
1304
|
result2.inputs.add(
|
|
1140
|
-
arg.startsWith(projectRoot) ||
|
|
1305
|
+
arg.startsWith(projectRoot) || path8.isAbsolute(arg) ? arg : path8.join(process.cwd(), arg)
|
|
1141
1306
|
);
|
|
1142
1307
|
return result2;
|
|
1143
1308
|
},
|
|
@@ -1185,7 +1350,7 @@ __export(config_exports, {
|
|
|
1185
1350
|
default: () => setupConfig,
|
|
1186
1351
|
setupConfig: () => setupConfig
|
|
1187
1352
|
});
|
|
1188
|
-
import
|
|
1353
|
+
import fs9 from "node:fs/promises";
|
|
1189
1354
|
import { createRequire } from "node:module";
|
|
1190
1355
|
import { pathToFileURL } from "node:url";
|
|
1191
1356
|
async function setupConfig() {
|
|
@@ -1223,10 +1388,13 @@ async function setupConfig() {
|
|
|
1223
1388
|
buildFSTree(config.testFileLookupPaths, config),
|
|
1224
1389
|
pluginsPromise
|
|
1225
1390
|
]);
|
|
1391
|
+
if (config.changedSince && !config.watch) {
|
|
1392
|
+
config.fsTree = await getChangedFsTree(config.fsTree, config.projectRoot, config.changedSince);
|
|
1393
|
+
}
|
|
1226
1394
|
return config;
|
|
1227
1395
|
}
|
|
1228
1396
|
async function readConfigFromPackageJSON(projectRoot) {
|
|
1229
|
-
const packageJSON = await
|
|
1397
|
+
const packageJSON = await fs9.readFile(`${projectRoot}/package.json`);
|
|
1230
1398
|
return JSON.parse(packageJSON.toString());
|
|
1231
1399
|
}
|
|
1232
1400
|
function normalizeHTMLPaths(projectRoot, htmlPaths) {
|
|
@@ -1258,6 +1426,7 @@ var init_config = __esm({
|
|
|
1258
1426
|
init_find_project_root();
|
|
1259
1427
|
init_fs_tree();
|
|
1260
1428
|
init_test_file_paths();
|
|
1429
|
+
init_get_changed_fs_tree();
|
|
1261
1430
|
init_parse_cli_flags();
|
|
1262
1431
|
}
|
|
1263
1432
|
});
|
|
@@ -1529,13 +1698,13 @@ function extractSourceLine(content, lineIndex) {
|
|
|
1529
1698
|
const line = content.split("\n", lineIndex + 1)[lineIndex];
|
|
1530
1699
|
return line?.trim() || null;
|
|
1531
1700
|
}
|
|
1532
|
-
function normalizePosix(
|
|
1533
|
-
const parts =
|
|
1701
|
+
function normalizePosix(path17) {
|
|
1702
|
+
const parts = path17.split("/").reduce((acc, part) => {
|
|
1534
1703
|
if (part === "..") acc.pop();
|
|
1535
1704
|
else if (part && part !== ".") acc.push(part);
|
|
1536
1705
|
return acc;
|
|
1537
1706
|
}, []);
|
|
1538
|
-
return (
|
|
1707
|
+
return (path17.startsWith("/") ? "/" : "") + parts.join("/");
|
|
1539
1708
|
}
|
|
1540
1709
|
function toAbsolutePath(rawSource, outDir, sourceRoot) {
|
|
1541
1710
|
if (rawSource.startsWith("file://")) return rawSource.slice(7);
|
|
@@ -1543,8 +1712,8 @@ function toAbsolutePath(rawSource, outDir, sourceRoot) {
|
|
|
1543
1712
|
const base = sourceRoot ? normalizePosix(`${outDir}/${sourceRoot}`) : outDir;
|
|
1544
1713
|
return normalizePosix(`${base}/${rawSource}`);
|
|
1545
1714
|
}
|
|
1546
|
-
function isNodeModulesPath(
|
|
1547
|
-
return
|
|
1715
|
+
function isNodeModulesPath(path17) {
|
|
1716
|
+
return path17.includes("/node_modules/") || path17.includes("\\node_modules\\");
|
|
1548
1717
|
}
|
|
1549
1718
|
function makeDisplayPath(absolutePath, projectRoot) {
|
|
1550
1719
|
const prefix = projectRoot + "/";
|
|
@@ -1820,8 +1989,8 @@ var init_web = __esm({
|
|
|
1820
1989
|
});
|
|
1821
1990
|
}
|
|
1822
1991
|
/** Registers a GET route handler. */
|
|
1823
|
-
get(
|
|
1824
|
-
this.#registerRouteHandler("GET",
|
|
1992
|
+
get(path17, handler) {
|
|
1993
|
+
this.#registerRouteHandler("GET", path17, handler);
|
|
1825
1994
|
}
|
|
1826
1995
|
/**
|
|
1827
1996
|
* Starts listening on the given port (0 = OS-assigned).
|
|
@@ -1852,32 +2021,32 @@ var init_web = __esm({
|
|
|
1852
2021
|
});
|
|
1853
2022
|
}
|
|
1854
2023
|
/** Registers a POST route handler. */
|
|
1855
|
-
post(
|
|
1856
|
-
this.#registerRouteHandler("POST",
|
|
2024
|
+
post(path17, handler) {
|
|
2025
|
+
this.#registerRouteHandler("POST", path17, handler);
|
|
1857
2026
|
}
|
|
1858
2027
|
/** Registers a DELETE route handler. */
|
|
1859
|
-
delete(
|
|
1860
|
-
this.#registerRouteHandler("DELETE",
|
|
2028
|
+
delete(path17, handler) {
|
|
2029
|
+
this.#registerRouteHandler("DELETE", path17, handler);
|
|
1861
2030
|
}
|
|
1862
2031
|
/** Registers a PUT route handler. */
|
|
1863
|
-
put(
|
|
1864
|
-
this.#registerRouteHandler("PUT",
|
|
2032
|
+
put(path17, handler) {
|
|
2033
|
+
this.#registerRouteHandler("PUT", path17, handler);
|
|
1865
2034
|
}
|
|
1866
2035
|
/** Adds a middleware function to the chain. */
|
|
1867
2036
|
use(middleware) {
|
|
1868
2037
|
this.middleware.push(middleware);
|
|
1869
2038
|
}
|
|
1870
|
-
#registerRouteHandler(method,
|
|
2039
|
+
#registerRouteHandler(method, path17, handler) {
|
|
1871
2040
|
if (!this.routes[method]) {
|
|
1872
2041
|
this.routes[method] = {};
|
|
1873
2042
|
}
|
|
1874
|
-
const paramNames = this.#extractParamNames(
|
|
1875
|
-
this.routes[method][
|
|
1876
|
-
path:
|
|
2043
|
+
const paramNames = this.#extractParamNames(path17);
|
|
2044
|
+
this.routes[method][path17] = {
|
|
2045
|
+
path: path17,
|
|
1877
2046
|
handler,
|
|
1878
2047
|
paramNames,
|
|
1879
|
-
isWildcard:
|
|
1880
|
-
compiledRegex: paramNames.length > 0 ? new RegExp(`^${this.#buildRegexPattern(
|
|
2048
|
+
isWildcard: path17 === "/*",
|
|
2049
|
+
compiledRegex: paramNames.length > 0 ? new RegExp(`^${this.#buildRegexPattern(path17, paramNames)}$`) : null
|
|
1881
2050
|
};
|
|
1882
2051
|
}
|
|
1883
2052
|
#handleRequest(req, res) {
|
|
@@ -1915,11 +2084,11 @@ var init_web = __esm({
|
|
|
1915
2084
|
return null;
|
|
1916
2085
|
}
|
|
1917
2086
|
return routes[url] || Object.values(routes).find((route) => {
|
|
1918
|
-
const { path:
|
|
1919
|
-
if (!isWildcard && !
|
|
2087
|
+
const { path: path17, isWildcard } = route;
|
|
2088
|
+
if (!isWildcard && !path17.includes(":")) {
|
|
1920
2089
|
return false;
|
|
1921
2090
|
}
|
|
1922
|
-
if (isWildcard || this.#matchPathSegments(
|
|
2091
|
+
if (isWildcard || this.#matchPathSegments(path17, url)) {
|
|
1923
2092
|
if (route.compiledRegex) {
|
|
1924
2093
|
const regexMatches = route.compiledRegex.exec(url);
|
|
1925
2094
|
if (regexMatches) {
|
|
@@ -1931,8 +2100,8 @@ var init_web = __esm({
|
|
|
1931
2100
|
return false;
|
|
1932
2101
|
}) || null;
|
|
1933
2102
|
}
|
|
1934
|
-
#matchPathSegments(
|
|
1935
|
-
const pathSegments =
|
|
2103
|
+
#matchPathSegments(path17, url) {
|
|
2104
|
+
const pathSegments = path17.split("/");
|
|
1936
2105
|
const urlSegments = url.split("/");
|
|
1937
2106
|
if (pathSegments.length !== urlSegments.length) {
|
|
1938
2107
|
return false;
|
|
@@ -1949,14 +2118,14 @@ var init_web = __esm({
|
|
|
1949
2118
|
}
|
|
1950
2119
|
return true;
|
|
1951
2120
|
}
|
|
1952
|
-
#buildRegexPattern(
|
|
1953
|
-
let regexPattern =
|
|
2121
|
+
#buildRegexPattern(path17, _paramNames) {
|
|
2122
|
+
let regexPattern = path17.replace(/:[^/]+/g, "([^/]+)");
|
|
1954
2123
|
regexPattern = regexPattern.replace(/\//g, "\\/");
|
|
1955
2124
|
return regexPattern;
|
|
1956
2125
|
}
|
|
1957
|
-
#extractParamNames(
|
|
2126
|
+
#extractParamNames(path17) {
|
|
1958
2127
|
const paramRegex = /:(\w+)/g;
|
|
1959
|
-
const paramMatches =
|
|
2128
|
+
const paramMatches = path17.match(paramRegex);
|
|
1960
2129
|
return paramMatches ? paramMatches.map((match) => match.slice(1)) : [];
|
|
1961
2130
|
}
|
|
1962
2131
|
#extractParams(route, _url) {
|
|
@@ -1972,10 +2141,10 @@ var init_web = __esm({
|
|
|
1972
2141
|
});
|
|
1973
2142
|
|
|
1974
2143
|
// lib/setup/web-server.ts
|
|
1975
|
-
import
|
|
1976
|
-
import
|
|
2144
|
+
import fs10 from "node:fs";
|
|
2145
|
+
import path9 from "node:path";
|
|
1977
2146
|
function setupWebServer(config, cachedContent) {
|
|
1978
|
-
const STATIC_FILES_PATH =
|
|
2147
|
+
const STATIC_FILES_PATH = path9.resolve(config.projectRoot, config.output);
|
|
1979
2148
|
const server = new HTTPServer();
|
|
1980
2149
|
const mainHTMLWithReplacedAssets = replaceAssetPaths(
|
|
1981
2150
|
cachedContent.mainHTML.html,
|
|
@@ -2113,7 +2282,7 @@ function setupWebServer(config, cachedContent) {
|
|
|
2113
2282
|
config._testRunDone = null;
|
|
2114
2283
|
}
|
|
2115
2284
|
return saveHTML(
|
|
2116
|
-
|
|
2285
|
+
path9.join(path9.resolve(config.projectRoot, config.output), "index.html"),
|
|
2117
2286
|
htmlContent
|
|
2118
2287
|
);
|
|
2119
2288
|
}
|
|
@@ -2124,7 +2293,7 @@ function setupWebServer(config, cachedContent) {
|
|
|
2124
2293
|
res.writeHead(200, HTML_HEADERS);
|
|
2125
2294
|
res.end(mainIndexHTML);
|
|
2126
2295
|
saveHTML(
|
|
2127
|
-
|
|
2296
|
+
path9.join(path9.resolve(config.projectRoot, config.output), "index.html"),
|
|
2128
2297
|
mainIndexHTML
|
|
2129
2298
|
);
|
|
2130
2299
|
});
|
|
@@ -2134,7 +2303,7 @@ function setupWebServer(config, cachedContent) {
|
|
|
2134
2303
|
res.writeHead(200, HTML_HEADERS);
|
|
2135
2304
|
res.end(htmlContent);
|
|
2136
2305
|
return saveHTML(
|
|
2137
|
-
|
|
2306
|
+
path9.join(path9.resolve(config.projectRoot, config.output), "qunitx.html"),
|
|
2138
2307
|
htmlContent
|
|
2139
2308
|
);
|
|
2140
2309
|
}
|
|
@@ -2145,7 +2314,7 @@ function setupWebServer(config, cachedContent) {
|
|
|
2145
2314
|
res.writeHead(200, HTML_HEADERS);
|
|
2146
2315
|
res.end(mainQunitxHTML);
|
|
2147
2316
|
saveHTML(
|
|
2148
|
-
|
|
2317
|
+
path9.join(path9.resolve(config.projectRoot, config.output), "qunitx.html"),
|
|
2149
2318
|
mainQunitxHTML
|
|
2150
2319
|
);
|
|
2151
2320
|
});
|
|
@@ -2159,14 +2328,14 @@ function setupWebServer(config, cachedContent) {
|
|
|
2159
2328
|
);
|
|
2160
2329
|
res.writeHead(200, HTML_HEADERS);
|
|
2161
2330
|
res.end(htmlContent);
|
|
2162
|
-
saveHTML(
|
|
2331
|
+
saveHTML(path9.join(path9.resolve(config.projectRoot, config.output), req.path), htmlContent);
|
|
2163
2332
|
return;
|
|
2164
2333
|
}
|
|
2165
2334
|
const url = req.url;
|
|
2166
2335
|
const requestStartedAt = Date.now();
|
|
2167
2336
|
const filePath = (url.endsWith("/") ? [STATIC_FILES_PATH, url, "index.html"] : [STATIC_FILES_PATH, url]).join("");
|
|
2168
|
-
const contentType = req.headers.accept?.includes("text/html") ? MIME_TYPES.html : MIME_TYPES[
|
|
2169
|
-
const stream =
|
|
2337
|
+
const contentType = req.headers.accept?.includes("text/html") ? MIME_TYPES.html : MIME_TYPES[path9.extname(filePath).substring(1).toLowerCase()] || MIME_TYPES.html;
|
|
2338
|
+
const stream = fs10.createReadStream(filePath);
|
|
2170
2339
|
stream.on("open", () => {
|
|
2171
2340
|
res.writeHead(200, { "Content-Type": contentType });
|
|
2172
2341
|
stream.pipe(res);
|
|
@@ -2419,7 +2588,7 @@ function registerGroupRoutes(server, groupConfig, groupCachedContent, groupId) {
|
|
|
2419
2588
|
res.writeHead(200, HTML_HEADERS);
|
|
2420
2589
|
res.end(mainGroupHTML);
|
|
2421
2590
|
saveHTML(
|
|
2422
|
-
|
|
2591
|
+
path9.join(path9.resolve(groupConfig.projectRoot, groupConfig.output), "index.html"),
|
|
2423
2592
|
mainGroupHTML
|
|
2424
2593
|
);
|
|
2425
2594
|
});
|
|
@@ -2505,11 +2674,11 @@ function registerSharedStaticHandler(server, groupConfigs) {
|
|
|
2505
2674
|
res.end("Not found");
|
|
2506
2675
|
return;
|
|
2507
2676
|
}
|
|
2508
|
-
const STATIC_FILES_PATH =
|
|
2677
|
+
const STATIC_FILES_PATH = path9.resolve(groupConfig.projectRoot, groupConfig.output);
|
|
2509
2678
|
const subPath = match[2] || "/";
|
|
2510
2679
|
const filePath = (subPath.endsWith("/") ? [STATIC_FILES_PATH, subPath, "index.html"] : [STATIC_FILES_PATH, subPath]).join("");
|
|
2511
|
-
const contentType = req.headers.accept?.includes("text/html") ? MIME_TYPES.html : MIME_TYPES[
|
|
2512
|
-
const stream =
|
|
2680
|
+
const contentType = req.headers.accept?.includes("text/html") ? MIME_TYPES.html : MIME_TYPES[path9.extname(filePath).substring(1).toLowerCase()] || MIME_TYPES.html;
|
|
2681
|
+
const stream = fs10.createReadStream(filePath);
|
|
2513
2682
|
stream.on("open", () => {
|
|
2514
2683
|
res.writeHead(200, { "Content-Type": contentType });
|
|
2515
2684
|
stream.pipe(res);
|
|
@@ -2524,7 +2693,7 @@ function replaceAssetPaths(html, htmlPath, projectRoot) {
|
|
|
2524
2693
|
const assetPaths = findInternalAssetsFromHTML(html);
|
|
2525
2694
|
const htmlDirectory = htmlPath.split("/").slice(0, -1).join("/");
|
|
2526
2695
|
return assetPaths.reduce((result2, assetPath) => {
|
|
2527
|
-
const normalizedFullAbsolutePath =
|
|
2696
|
+
const normalizedFullAbsolutePath = path9.normalize(`${htmlDirectory}/${assetPath}`);
|
|
2528
2697
|
return result2.replace(assetPath, normalizedFullAbsolutePath.replace(projectRoot, "."));
|
|
2529
2698
|
}, html);
|
|
2530
2699
|
}
|
|
@@ -2686,7 +2855,7 @@ var init_web_server = __esm({
|
|
|
2686
2855
|
init_display_test_result();
|
|
2687
2856
|
init_color();
|
|
2688
2857
|
init_web();
|
|
2689
|
-
fsPromise =
|
|
2858
|
+
fsPromise = fs10.promises;
|
|
2690
2859
|
HTML_HEADERS = { "Content-Type": "text/html", "Cache-Control": "no-store" };
|
|
2691
2860
|
WATCH_WS_RECONNECT_INTERVAL_MS = 1e3;
|
|
2692
2861
|
WATCH_WS_RECONNECT_MAX_RETRIES = 120;
|
|
@@ -2905,8 +3074,8 @@ var init_display_final_result = __esm({
|
|
|
2905
3074
|
});
|
|
2906
3075
|
|
|
2907
3076
|
// lib/commands/run/tests-in-browser.ts
|
|
2908
|
-
import
|
|
2909
|
-
import
|
|
3077
|
+
import fs11 from "node:fs/promises";
|
|
3078
|
+
import path10 from "node:path";
|
|
2910
3079
|
import esbuild from "esbuild";
|
|
2911
3080
|
function deriveBuildErrorType(error) {
|
|
2912
3081
|
const msgs = error?.errors ?? [];
|
|
@@ -2946,9 +3115,9 @@ async function buildTestBundle(config, cachedContent) {
|
|
|
2946
3115
|
console.log("# [buildTestBundle] fsTree is empty \u2014 skipping build (no test files found)");
|
|
2947
3116
|
return;
|
|
2948
3117
|
}
|
|
2949
|
-
const outDir =
|
|
2950
|
-
const outfile =
|
|
2951
|
-
await
|
|
3118
|
+
const outDir = path10.resolve(projectRoot, output);
|
|
3119
|
+
const outfile = path10.join(outDir, "tests.js");
|
|
3120
|
+
await fs11.mkdir(outDir, { recursive: true });
|
|
2952
3121
|
const sourcemap = "inline";
|
|
2953
3122
|
const needsDisk = true;
|
|
2954
3123
|
const buildOptions = {
|
|
@@ -2972,6 +3141,10 @@ async function buildTestBundle(config, cachedContent) {
|
|
|
2972
3141
|
// tsconfig's `jsxImportSource` or a `@jsxImportSource <pkg>` pragma cover Vue/Preact/Solid.
|
|
2973
3142
|
jsx: "automatic",
|
|
2974
3143
|
plugins: config.plugins,
|
|
3144
|
+
// Required for --changed/--since dep-graph filter on subsequent runs (cache
|
|
3145
|
+
// populates here, reads in setupConfig). Inputs map carries the full reverse-dep
|
|
3146
|
+
// graph; output cost is negligible.
|
|
3147
|
+
metafile: true,
|
|
2975
3148
|
// Signal the runtime that all test modules are registered. The runtime's maybeStart()
|
|
2976
3149
|
// waits for both this event and the WebSocket 'open' event before calling QUnit.start().
|
|
2977
3150
|
// Dispatching from the bundle (rather than from a script onload attr) is reliable across
|
|
@@ -2983,32 +3156,33 @@ async function buildTestBundle(config, cachedContent) {
|
|
|
2983
3156
|
const cacheHolder = config._daemonEsbuildCache ?? cachedContent;
|
|
2984
3157
|
const fileKey = bundleCacheKey(buildOptions, allTestFilePaths);
|
|
2985
3158
|
try {
|
|
2986
|
-
const [allTestCode] = await Promise.all([
|
|
3159
|
+
const [{ js: allTestCode, metafile }] = await Promise.all([
|
|
2987
3160
|
config.watch || config._daemonMode ? buildIncrementally(buildOptions, fileKey, cacheHolder, needsDisk) : buildWithOverlayfsRetry(buildOptions, needsDisk),
|
|
2988
3161
|
Promise.all(
|
|
2989
3162
|
cachedContent.htmlPathsToRunTests.map(async (htmlPath) => {
|
|
2990
|
-
const targetPath =
|
|
3163
|
+
const targetPath = path10.join(outDir, htmlPath);
|
|
2991
3164
|
if (htmlPath !== "/") {
|
|
2992
|
-
await
|
|
2993
|
-
await
|
|
3165
|
+
await fs11.rm(targetPath, { force: true, recursive: true });
|
|
3166
|
+
await fs11.mkdir(path10.dirname(targetPath), { recursive: true });
|
|
2994
3167
|
}
|
|
2995
3168
|
})
|
|
2996
3169
|
)
|
|
2997
3170
|
]);
|
|
2998
3171
|
cachedContent.allTestCode = allTestCode;
|
|
2999
3172
|
config._sourceMapDecoder = extractInlineSourceMap(allTestCode, outDir);
|
|
3173
|
+
if (metafile) void writeMetafileCache(projectRoot, process.cwd(), metafile);
|
|
3000
3174
|
} catch (error) {
|
|
3001
3175
|
cachedContent._buildError = {
|
|
3002
3176
|
type: deriveBuildErrorType(error),
|
|
3003
3177
|
formatted: formatBuildErrors(error)
|
|
3004
3178
|
};
|
|
3005
|
-
await
|
|
3179
|
+
await fs11.writeFile(path10.join(outDir, "index.html"), buildErrorHTML(cachedContent._buildError));
|
|
3006
3180
|
throw error;
|
|
3007
3181
|
}
|
|
3008
3182
|
}
|
|
3009
3183
|
async function runTestsInBrowser(config, cachedContent = {}, connections, targetTestFilesToFilter) {
|
|
3010
3184
|
const { projectRoot, output } = config;
|
|
3011
|
-
const outDir =
|
|
3185
|
+
const outDir = path10.resolve(projectRoot, output);
|
|
3012
3186
|
const allTestFilePaths = Object.keys(config.fsTree);
|
|
3013
3187
|
const runHasFilter = !!targetTestFilesToFilter;
|
|
3014
3188
|
if (!config._groupMode) {
|
|
@@ -3036,7 +3210,7 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
3036
3210
|
return connections;
|
|
3037
3211
|
}
|
|
3038
3212
|
if (runHasFilter) {
|
|
3039
|
-
const outputPath =
|
|
3213
|
+
const outputPath = path10.join(outDir, "filtered-tests.js");
|
|
3040
3214
|
cachedContent.filteredTestCode = await buildFilteredTests(
|
|
3041
3215
|
targetTestFilesToFilter,
|
|
3042
3216
|
outputPath,
|
|
@@ -3076,7 +3250,7 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
3076
3250
|
console.log(
|
|
3077
3251
|
`# Warning: 0 tests registered \u2014 no QUnit test cases found in ${allTestFilePaths.length} ${fileWord}`
|
|
3078
3252
|
);
|
|
3079
|
-
|
|
3253
|
+
fs11.writeFile(path10.join(outDir, "index.html"), buildNoTestsHTML(displayFiles)).catch(
|
|
3080
3254
|
() => {
|
|
3081
3255
|
}
|
|
3082
3256
|
);
|
|
@@ -3108,8 +3282,8 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
3108
3282
|
type: deriveBuildErrorType(error),
|
|
3109
3283
|
formatted: formatBuildErrors(error)
|
|
3110
3284
|
};
|
|
3111
|
-
|
|
3112
|
-
|
|
3285
|
+
fs11.writeFile(
|
|
3286
|
+
path10.join(outDir, "qunitx.html"),
|
|
3113
3287
|
buildErrorHTML(cachedContent._buildError)
|
|
3114
3288
|
).catch(
|
|
3115
3289
|
(err) => config.debug && process.stderr.write(`# [qunitx] writeFile qunitx.html: ${err.message}
|
|
@@ -3159,7 +3333,7 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
3159
3333
|
);
|
|
3160
3334
|
await Promise.all(
|
|
3161
3335
|
activeGroups.map(
|
|
3162
|
-
(group) =>
|
|
3336
|
+
(group) => fs11.mkdir(path10.resolve(group.config.projectRoot, group.config.output), { recursive: true })
|
|
3163
3337
|
)
|
|
3164
3338
|
);
|
|
3165
3339
|
const sourcemap = "inline";
|
|
@@ -3179,7 +3353,7 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
3179
3353
|
});
|
|
3180
3354
|
}
|
|
3181
3355
|
};
|
|
3182
|
-
const esbuildOutdir =
|
|
3356
|
+
const esbuildOutdir = path10.join(projectRoot, "tmp");
|
|
3183
3357
|
const buildOptions = {
|
|
3184
3358
|
entryPoints: activeGroups.map((_, slotIndex) => ({
|
|
3185
3359
|
in: `group-entry-${slotIndex}`,
|
|
@@ -3201,6 +3375,9 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
3201
3375
|
sourcemap,
|
|
3202
3376
|
write: false,
|
|
3203
3377
|
jsx: "automatic",
|
|
3378
|
+
// Required for --changed/--since dep-graph filter on subsequent runs. Same
|
|
3379
|
+
// contract as the single-group path in `buildTestBundle`.
|
|
3380
|
+
metafile: true,
|
|
3204
3381
|
footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
|
|
3205
3382
|
};
|
|
3206
3383
|
const hasSmallOutput = (result2) => (result2.outputFiles ?? []).some(
|
|
@@ -3221,8 +3398,8 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
3221
3398
|
const slotIndex = parseInt(match[1]);
|
|
3222
3399
|
const isMap = Boolean(match[2]);
|
|
3223
3400
|
const { config, cachedContent } = activeGroups[slotIndex];
|
|
3224
|
-
const destPath =
|
|
3225
|
-
|
|
3401
|
+
const destPath = path10.join(
|
|
3402
|
+
path10.resolve(config.projectRoot, config.output),
|
|
3226
3403
|
"tests.js" + (isMap ? ".map" : "")
|
|
3227
3404
|
);
|
|
3228
3405
|
if (!isMap) {
|
|
@@ -3232,17 +3409,20 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
3232
3409
|
esbuildOutdir
|
|
3233
3410
|
);
|
|
3234
3411
|
}
|
|
3235
|
-
return
|
|
3412
|
+
return fs11.writeFile(destPath, outputFile.contents);
|
|
3236
3413
|
})
|
|
3237
3414
|
);
|
|
3415
|
+
if (result2.metafile) {
|
|
3416
|
+
void writeMetafileCache(projectRoot, process.cwd(), result2.metafile);
|
|
3417
|
+
}
|
|
3238
3418
|
} catch (error) {
|
|
3239
3419
|
const buildError = { type: deriveBuildErrorType(error), formatted: formatBuildErrors(error) };
|
|
3240
3420
|
const errorHtml = buildErrorHTML(buildError);
|
|
3241
3421
|
await Promise.all(
|
|
3242
3422
|
activeGroups.map((group) => {
|
|
3243
3423
|
group.cachedContent._buildError = buildError;
|
|
3244
|
-
return
|
|
3245
|
-
|
|
3424
|
+
return fs11.writeFile(
|
|
3425
|
+
path10.join(path10.resolve(group.config.projectRoot, group.config.output), "index.html"),
|
|
3246
3426
|
errorHtml
|
|
3247
3427
|
).catch(
|
|
3248
3428
|
(err) => debug && process.stderr.write(`# [qunitx] writeFile index.html: ${err.message}
|
|
@@ -3274,7 +3454,7 @@ function buildFilteredTests(filteredTests, outputPath, config) {
|
|
|
3274
3454
|
footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
|
|
3275
3455
|
},
|
|
3276
3456
|
needsDisk
|
|
3277
|
-
);
|
|
3457
|
+
).then((r) => r.js);
|
|
3278
3458
|
}
|
|
3279
3459
|
async function runWithOverlayfsRetry(getContents, needsDisk) {
|
|
3280
3460
|
let { result: result2, js } = await getContents();
|
|
@@ -3291,10 +3471,10 @@ async function runWithOverlayfsRetry(getContents, needsDisk) {
|
|
|
3291
3471
|
}
|
|
3292
3472
|
if (needsDisk) {
|
|
3293
3473
|
await Promise.all(
|
|
3294
|
-
result2.outputFiles.map((outputFile) =>
|
|
3474
|
+
result2.outputFiles.map((outputFile) => fs11.writeFile(outputFile.path, outputFile.contents))
|
|
3295
3475
|
);
|
|
3296
3476
|
}
|
|
3297
|
-
return js;
|
|
3477
|
+
return { js, metafile: result2.metafile };
|
|
3298
3478
|
}
|
|
3299
3479
|
function buildWithOverlayfsRetry(options, needsDisk) {
|
|
3300
3480
|
const buildOpts = { ...options, write: false };
|
|
@@ -3428,9 +3608,9 @@ async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode
|
|
|
3428
3608
|
process.exit(1);
|
|
3429
3609
|
}
|
|
3430
3610
|
function toEsbuildImportPath(filePath) {
|
|
3431
|
-
const rel =
|
|
3611
|
+
const rel = path10.relative(process.cwd(), filePath);
|
|
3432
3612
|
const normalized = rel.replace(/\\/g, "/");
|
|
3433
|
-
if (
|
|
3613
|
+
if (path10.isAbsolute(rel)) return filePath.replace(/\\/g, "/");
|
|
3434
3614
|
return normalized.startsWith(".") ? normalized : "./" + normalized;
|
|
3435
3615
|
}
|
|
3436
3616
|
var ancestorNodeModules, ANCESTOR_NODE_MODULES, RETRY_DELAY_MS, MAX_RETRIES, EMPTY_BUNDLE_THRESHOLD, NAV_GRACE_MS, MAX_NAV_SLOWDOWN_FACTOR, MIN_NAV_MS, STARTUP_TIMEOUT_FACTOR, TESTS_JS_TIMEOUT_FACTOR, CONSOLE_FLUSH_TIMEOUT_MS, TEST_STALL_BUFFER_MS, GROUP_OUTPUT_REGEX, BundleError, DaemonRunError;
|
|
@@ -3444,8 +3624,9 @@ var init_tests_in_browser = __esm({
|
|
|
3444
3624
|
init_display_final_result();
|
|
3445
3625
|
init_web_server();
|
|
3446
3626
|
init_source_map_decoder();
|
|
3447
|
-
|
|
3448
|
-
|
|
3627
|
+
init_metafile_cache();
|
|
3628
|
+
ancestorNodeModules = (dir) => dir.split(path10.sep).map(
|
|
3629
|
+
(_, i, parts) => path10.join(parts.slice(0, parts.length - i).join(path10.sep) || path10.sep, "node_modules")
|
|
3449
3630
|
);
|
|
3450
3631
|
ANCESTOR_NODE_MODULES = ancestorNodeModules(process.cwd());
|
|
3451
3632
|
RETRY_DELAY_MS = 100;
|
|
@@ -3481,11 +3662,11 @@ var init_tests_in_browser = __esm({
|
|
|
3481
3662
|
|
|
3482
3663
|
// lib/utils/open-output-in-browser.ts
|
|
3483
3664
|
import { spawn as spawn2 } from "node:child_process";
|
|
3484
|
-
import
|
|
3665
|
+
import path11 from "node:path";
|
|
3485
3666
|
import { pathToFileURL as pathToFileURL3 } from "node:url";
|
|
3486
3667
|
async function openOutputInBrowser(config) {
|
|
3487
3668
|
try {
|
|
3488
|
-
const outputFile = config.watch ? `http://localhost:${config.port}` : pathToFileURL3(
|
|
3669
|
+
const outputFile = config.watch ? `http://localhost:${config.port}` : pathToFileURL3(path11.join(path11.resolve(config.projectRoot, config.output), "index.html")).href;
|
|
3489
3670
|
if (typeof config.open === "string") {
|
|
3490
3671
|
spawnDetached(config.open, [outputFile]);
|
|
3491
3672
|
return;
|
|
@@ -3518,9 +3699,9 @@ var init_open_output_in_browser = __esm({
|
|
|
3518
3699
|
});
|
|
3519
3700
|
|
|
3520
3701
|
// lib/setup/file-watcher.ts
|
|
3521
|
-
import
|
|
3702
|
+
import fs12 from "node:fs";
|
|
3522
3703
|
import { readdir, stat, lstat } from "node:fs/promises";
|
|
3523
|
-
import
|
|
3704
|
+
import path12 from "node:path";
|
|
3524
3705
|
function recordJustAdded(config, filePath) {
|
|
3525
3706
|
let map = justAddedAt.get(config);
|
|
3526
3707
|
if (!map) justAddedAt.set(config, map = /* @__PURE__ */ new Map());
|
|
@@ -3539,7 +3720,7 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
3539
3720
|
if (symlinkPollers.has(filePath)) return;
|
|
3540
3721
|
const handler = (curr, prev) => {
|
|
3541
3722
|
if (curr.nlink === 0) {
|
|
3542
|
-
|
|
3723
|
+
fs12.unwatchFile(filePath, handler);
|
|
3543
3724
|
symlinkPollers.delete(filePath);
|
|
3544
3725
|
if (filePath in config.fsTree) {
|
|
3545
3726
|
handleWatchEvent(config, extensions, "unlink", filePath, onEventFunc, onFinishFunc);
|
|
@@ -3558,8 +3739,8 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
3558
3739
|
}
|
|
3559
3740
|
}
|
|
3560
3741
|
};
|
|
3561
|
-
|
|
3562
|
-
symlinkPollers.set(filePath, () =>
|
|
3742
|
+
fs12.watchFile(filePath, { interval: SYMLINK_POLL_INTERVAL_MS, persistent: false }, handler);
|
|
3743
|
+
symlinkPollers.set(filePath, () => fs12.unwatchFile(filePath, handler));
|
|
3563
3744
|
}
|
|
3564
3745
|
function untrackSymlink(filePath) {
|
|
3565
3746
|
symlinkPollers.get(filePath)?.();
|
|
@@ -3570,7 +3751,7 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
3570
3751
|
let rescanInProgress = false;
|
|
3571
3752
|
const lastEventMs = {};
|
|
3572
3753
|
const seenMtimeMs = {};
|
|
3573
|
-
const childWatcher =
|
|
3754
|
+
const childWatcher = fs12.watch(watchPath, { recursive: true }, async (eventType, filename) => {
|
|
3574
3755
|
if (!ready) return;
|
|
3575
3756
|
if (!filename) {
|
|
3576
3757
|
if (process.platform === "darwin" && !rescanInProgress) {
|
|
@@ -3588,7 +3769,7 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
3588
3769
|
}
|
|
3589
3770
|
return;
|
|
3590
3771
|
}
|
|
3591
|
-
const fullPath = filename ===
|
|
3772
|
+
const fullPath = filename === path12.basename(watchPath) ? watchPath : path12.join(watchPath, filename);
|
|
3592
3773
|
if (eventType === "change") {
|
|
3593
3774
|
const now = Date.now();
|
|
3594
3775
|
const last = lastEventMs[fullPath] ?? 0;
|
|
@@ -3626,8 +3807,8 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
3626
3807
|
}
|
|
3627
3808
|
handleWatchEvent(config, extensions, event, fullPath, onEventFunc, onFinishFunc);
|
|
3628
3809
|
});
|
|
3629
|
-
const parentDir =
|
|
3630
|
-
const watchedBasename =
|
|
3810
|
+
const parentDir = path12.dirname(watchPath);
|
|
3811
|
+
const watchedBasename = path12.basename(watchPath);
|
|
3631
3812
|
let parentUnlinkFired = false;
|
|
3632
3813
|
let rescanTimer = null;
|
|
3633
3814
|
const tryFireParentUnlink = async () => {
|
|
@@ -3646,7 +3827,7 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
3646
3827
|
return true;
|
|
3647
3828
|
}
|
|
3648
3829
|
};
|
|
3649
|
-
const parentWatcher =
|
|
3830
|
+
const parentWatcher = fs12.watch(parentDir, async (eventType, filename) => {
|
|
3650
3831
|
if (!ready || filename !== watchedBasename || eventType !== "rename") return;
|
|
3651
3832
|
await tryFireParentUnlink();
|
|
3652
3833
|
});
|
|
@@ -3760,11 +3941,11 @@ async function rescanDirectoryForDelta(watchPath, config, extensions, onEventFun
|
|
|
3760
3941
|
const trackedToRecheck = [];
|
|
3761
3942
|
for (const entry of entries) {
|
|
3762
3943
|
if (entry.isDirectory()) {
|
|
3763
|
-
presentDirs.add(
|
|
3944
|
+
presentDirs.add(path12.join(entry.parentPath, entry.name));
|
|
3764
3945
|
continue;
|
|
3765
3946
|
}
|
|
3766
3947
|
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
3767
|
-
const entryPath =
|
|
3948
|
+
const entryPath = path12.join(entry.parentPath, entry.name);
|
|
3768
3949
|
presentDirs.add(entry.parentPath);
|
|
3769
3950
|
if (!extensions.some((ext) => entryPath.endsWith(`.${ext}`))) continue;
|
|
3770
3951
|
presentPaths.add(entryPath);
|
|
@@ -3787,13 +3968,13 @@ async function rescanDirectoryForDelta(watchPath, config, extensions, onEventFun
|
|
|
3787
3968
|
}
|
|
3788
3969
|
})
|
|
3789
3970
|
);
|
|
3790
|
-
const watchPrefix = watchPath +
|
|
3971
|
+
const watchPrefix = watchPath + path12.sep;
|
|
3791
3972
|
const firedDirPrefixes = [];
|
|
3792
3973
|
for (const trackedPath of Object.keys(config.fsTree)) {
|
|
3793
3974
|
if (!trackedPath.startsWith(watchPrefix) || presentPaths.has(trackedPath)) continue;
|
|
3794
|
-
if (firedDirPrefixes.some((p) => trackedPath.startsWith(p +
|
|
3795
|
-
const parts = trackedPath.slice(watchPrefix.length).split(
|
|
3796
|
-
const goneDirPath = parts.slice(0, -1).map((_, i) => watchPrefix + parts.slice(0, i + 1).join(
|
|
3975
|
+
if (firedDirPrefixes.some((p) => trackedPath.startsWith(p + path12.sep))) continue;
|
|
3976
|
+
const parts = trackedPath.slice(watchPrefix.length).split(path12.sep);
|
|
3977
|
+
const goneDirPath = parts.slice(0, -1).map((_, i) => watchPrefix + parts.slice(0, i + 1).join(path12.sep)).find((p) => !presentDirs.has(p)) ?? null;
|
|
3797
3978
|
if (goneDirPath !== null) {
|
|
3798
3979
|
firedDirPrefixes.push(goneDirPath);
|
|
3799
3980
|
handleWatchEvent(config, extensions, "unlinkDir", goneDirPath, onEventFunc, onFinishFunc);
|
|
@@ -3846,7 +4027,7 @@ var init_file_watcher = __esm({
|
|
|
3846
4027
|
SYMLINK_POLL_INTERVAL_MS = 500;
|
|
3847
4028
|
OVERLAYFS_RENAME_RETRY_MS = 50;
|
|
3848
4029
|
RESCAN_INTERVAL_MS = 1e3;
|
|
3849
|
-
CHANGE_COALESCE_MS =
|
|
4030
|
+
CHANGE_COALESCE_MS = 75;
|
|
3850
4031
|
ADD_SUPPRESS_WINDOW_MS = 1e3;
|
|
3851
4032
|
justAddedAt = /* @__PURE__ */ new WeakMap();
|
|
3852
4033
|
}
|
|
@@ -3928,28 +4109,28 @@ var init_keyboard_events = __esm({
|
|
|
3928
4109
|
});
|
|
3929
4110
|
|
|
3930
4111
|
// lib/setup/write-output-static-files.ts
|
|
3931
|
-
import
|
|
3932
|
-
import
|
|
4112
|
+
import fs13 from "node:fs/promises";
|
|
4113
|
+
import path13 from "node:path";
|
|
3933
4114
|
async function writeOutputStaticFiles({ projectRoot, output }, cachedContent) {
|
|
3934
4115
|
const staticHTMLPromises = Object.keys(cachedContent.staticHTMLs).map(async (staticHTMLKey) => {
|
|
3935
|
-
const htmlRelativePath =
|
|
3936
|
-
const outDir =
|
|
3937
|
-
await ensureFolderExists(
|
|
3938
|
-
await
|
|
3939
|
-
|
|
4116
|
+
const htmlRelativePath = path13.relative(projectRoot, staticHTMLKey);
|
|
4117
|
+
const outDir = path13.resolve(projectRoot, output);
|
|
4118
|
+
await ensureFolderExists(path13.join(outDir, htmlRelativePath));
|
|
4119
|
+
await fs13.writeFile(
|
|
4120
|
+
path13.join(outDir, htmlRelativePath),
|
|
3940
4121
|
cachedContent.staticHTMLs[staticHTMLKey]
|
|
3941
4122
|
);
|
|
3942
4123
|
});
|
|
3943
4124
|
const assetPromises = Array.from(cachedContent.assets).map(async (assetAbsolutePath) => {
|
|
3944
|
-
const assetRelativePath =
|
|
3945
|
-
const outDir =
|
|
3946
|
-
await ensureFolderExists(
|
|
3947
|
-
await
|
|
4125
|
+
const assetRelativePath = path13.relative(projectRoot, assetAbsolutePath).replace(/^(?:\.\.[\\/])+/, "");
|
|
4126
|
+
const outDir = path13.resolve(projectRoot, output);
|
|
4127
|
+
await ensureFolderExists(path13.join(outDir, assetRelativePath));
|
|
4128
|
+
await fs13.copyFile(assetAbsolutePath, path13.join(outDir, assetRelativePath));
|
|
3948
4129
|
});
|
|
3949
4130
|
await Promise.all(staticHTMLPromises.concat(assetPromises));
|
|
3950
4131
|
}
|
|
3951
4132
|
async function ensureFolderExists(assetPath) {
|
|
3952
|
-
await
|
|
4133
|
+
await fs13.mkdir(path13.dirname(assetPath), { recursive: true });
|
|
3953
4134
|
}
|
|
3954
4135
|
var init_write_output_static_files = __esm({
|
|
3955
4136
|
"lib/setup/write-output-static-files.ts"() {
|
|
@@ -3957,9 +4138,9 @@ var init_write_output_static_files = __esm({
|
|
|
3957
4138
|
});
|
|
3958
4139
|
|
|
3959
4140
|
// lib/utils/daemon-hint.ts
|
|
3960
|
-
import
|
|
4141
|
+
import fs14 from "node:fs/promises";
|
|
3961
4142
|
import os3 from "node:os";
|
|
3962
|
-
import
|
|
4143
|
+
import path14 from "node:path";
|
|
3963
4144
|
function shouldShowDaemonHint(ctx) {
|
|
3964
4145
|
const env = ctx.env ?? process.env;
|
|
3965
4146
|
if (ctx.watch) return false;
|
|
@@ -3977,14 +4158,14 @@ async function maybePrintDaemonHint(ctx, opts = {}) {
|
|
|
3977
4158
|
if (!shouldShowDaemonHint(ctx)) return;
|
|
3978
4159
|
const sentinel = opts.sentinelPath ?? DEFAULT_SENTINEL;
|
|
3979
4160
|
try {
|
|
3980
|
-
await
|
|
4161
|
+
await fs14.access(sentinel);
|
|
3981
4162
|
return;
|
|
3982
4163
|
} catch {
|
|
3983
4164
|
}
|
|
3984
4165
|
(opts.write ?? ((t) => process.stderr.write(t)))(HINT_TEXT);
|
|
3985
4166
|
try {
|
|
3986
|
-
await
|
|
3987
|
-
await
|
|
4167
|
+
await fs14.mkdir(path14.dirname(sentinel), { recursive: true });
|
|
4168
|
+
await fs14.writeFile(sentinel, (/* @__PURE__ */ new Date()).toISOString());
|
|
3988
4169
|
} catch {
|
|
3989
4170
|
}
|
|
3990
4171
|
}
|
|
@@ -3993,7 +4174,7 @@ var init_daemon_hint = __esm({
|
|
|
3993
4174
|
"lib/utils/daemon-hint.ts"() {
|
|
3994
4175
|
FAST_RUN_THRESHOLD_MS = 500;
|
|
3995
4176
|
HINT_TEXT = "\n\x1B[34m\u2139\x1B[39m Tip: export QUNITX_DAEMON=1 for ~2\xD7 faster repeated runs (qunitx daemon --help)\n";
|
|
3996
|
-
DEFAULT_SENTINEL =
|
|
4177
|
+
DEFAULT_SENTINEL = path14.join(os3.homedir(), ".cache", "qunitx", "hint-shown");
|
|
3997
4178
|
}
|
|
3998
4179
|
});
|
|
3999
4180
|
|
|
@@ -4006,7 +4187,7 @@ __export(run_exports, {
|
|
|
4006
4187
|
readTimingCache: () => readTimingCache,
|
|
4007
4188
|
run: () => run
|
|
4008
4189
|
});
|
|
4009
|
-
import
|
|
4190
|
+
import fs15 from "node:fs/promises";
|
|
4010
4191
|
import { join as join3, normalize } from "node:path";
|
|
4011
4192
|
import { createRequire as createRequire2 } from "node:module";
|
|
4012
4193
|
import { availableParallelism } from "node:os";
|
|
@@ -4093,6 +4274,21 @@ async function run(config) {
|
|
|
4093
4274
|
logWatcherAndKeyboardShortcutInfo(config, connections.server);
|
|
4094
4275
|
} else {
|
|
4095
4276
|
const allFiles = Object.keys(config.fsTree);
|
|
4277
|
+
if (allFiles.length === 0) {
|
|
4278
|
+
process.stdout.write("TAP version 13\n");
|
|
4279
|
+
process.stdout.write(
|
|
4280
|
+
`# Running 0 test files${config._daemonMode ? " (daemon)" : ""}
|
|
4281
|
+
1..0
|
|
4282
|
+
`
|
|
4283
|
+
);
|
|
4284
|
+
if (config._daemonMode) throw new DaemonRunError(0);
|
|
4285
|
+
if (!config.watch) {
|
|
4286
|
+
const browser2 = config._daemonBrowser ? null : await browserPromise;
|
|
4287
|
+
await closeWithGrace([browser2?.close(), shutdownPrelaunch()]);
|
|
4288
|
+
return process.exit(0);
|
|
4289
|
+
}
|
|
4290
|
+
return;
|
|
4291
|
+
}
|
|
4096
4292
|
const groupCount = Math.min(allFiles.length, availableParallelism());
|
|
4097
4293
|
const { groups, weights } = await splitIntoGroups(allFiles, groupCount, timings ?? {});
|
|
4098
4294
|
config.COUNTER = {
|
|
@@ -4250,7 +4446,7 @@ async function run(config) {
|
|
|
4250
4446
|
}
|
|
4251
4447
|
async function buildCachedContent(config, htmlPaths) {
|
|
4252
4448
|
const htmlBuffers = await Promise.all(
|
|
4253
|
-
config.htmlPaths.map((htmlPath) =>
|
|
4449
|
+
config.htmlPaths.map((htmlPath) => fs15.readFile(htmlPath).catch(() => null))
|
|
4254
4450
|
);
|
|
4255
4451
|
const cachedContent = htmlPaths.reduce(
|
|
4256
4452
|
(result2, _htmlPath, index) => {
|
|
@@ -4305,7 +4501,7 @@ async function addCachedContentMainHTML(projectRoot, cachedContent) {
|
|
|
4305
4501
|
}
|
|
4306
4502
|
async function readTimingCache(projectRoot) {
|
|
4307
4503
|
try {
|
|
4308
|
-
const parsed = JSON.parse(await
|
|
4504
|
+
const parsed = JSON.parse(await fs15.readFile(`${projectRoot}/tmp/test-timings.json`, "utf8"));
|
|
4309
4505
|
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : {};
|
|
4310
4506
|
} catch {
|
|
4311
4507
|
return {};
|
|
@@ -4324,7 +4520,7 @@ function computeFileTimes(groups, weights, wallTimes) {
|
|
|
4324
4520
|
return result2;
|
|
4325
4521
|
}
|
|
4326
4522
|
async function persistTimings(fileTimes, projectRoot) {
|
|
4327
|
-
await
|
|
4523
|
+
await fs15.writeFile(
|
|
4328
4524
|
`${projectRoot}/tmp/test-timings.json`,
|
|
4329
4525
|
JSON.stringify(Object.fromEntries(fileTimes), null, 2)
|
|
4330
4526
|
);
|
|
@@ -4339,7 +4535,7 @@ ${lines.join("\n")}
|
|
|
4339
4535
|
async function splitIntoGroups(files, groupCount, timings) {
|
|
4340
4536
|
const sizes = await Promise.all(
|
|
4341
4537
|
files.map(
|
|
4342
|
-
(f) => timings[f] > 0 ? Promise.resolve(0) :
|
|
4538
|
+
(f) => timings[f] > 0 ? Promise.resolve(0) : fs15.stat(f).then((s) => s.size).catch(() => 0)
|
|
4343
4539
|
)
|
|
4344
4540
|
);
|
|
4345
4541
|
const knownRates = files.map((f, i) => ({ ms: timings[f], size: sizes[i] })).filter(({ ms, size }) => ms > 0 && size > 0);
|
|
@@ -4413,19 +4609,19 @@ __export(server_exports, {
|
|
|
4413
4609
|
runDaemonServer: () => runDaemonServer
|
|
4414
4610
|
});
|
|
4415
4611
|
import net2 from "node:net";
|
|
4416
|
-
import
|
|
4612
|
+
import fs16 from "node:fs";
|
|
4417
4613
|
import { writeFile, unlink, stat as stat2, chmod } from "node:fs/promises";
|
|
4418
|
-
import
|
|
4614
|
+
import path15 from "node:path";
|
|
4419
4615
|
async function runDaemonServer() {
|
|
4420
4616
|
const cwd = process.cwd();
|
|
4421
4617
|
const socketPath = daemonSocketPath(cwd);
|
|
4422
4618
|
const infoPath = daemonInfoPath(cwd);
|
|
4423
|
-
if (
|
|
4619
|
+
if (fs16.existsSync(infoPath) && await isLiveSocket(socketPath)) process.exit(0);
|
|
4424
4620
|
await unlink(socketPath).catch(() => {
|
|
4425
4621
|
});
|
|
4426
4622
|
const logPath = process.env.QUNITX_DAEMON_LOG;
|
|
4427
4623
|
if (logPath) {
|
|
4428
|
-
const log =
|
|
4624
|
+
const log = fs16.createWriteStream(logPath, { flags: "a" });
|
|
4429
4625
|
log.on("error", () => {
|
|
4430
4626
|
});
|
|
4431
4627
|
const forward = log.write.bind(log);
|
|
@@ -4551,7 +4747,7 @@ async function dispatch(req, socket, state) {
|
|
|
4551
4747
|
socket.end();
|
|
4552
4748
|
} else if (req.type === "shutdown") {
|
|
4553
4749
|
try {
|
|
4554
|
-
|
|
4750
|
+
fs16.unlinkSync(state.infoPath);
|
|
4555
4751
|
} catch {
|
|
4556
4752
|
}
|
|
4557
4753
|
writeChunk(socket, { type: "done", exitCode: 0 });
|
|
@@ -4693,7 +4889,7 @@ async function isLiveSocket(socketPath) {
|
|
|
4693
4889
|
}
|
|
4694
4890
|
async function readPkgMtime(cwd) {
|
|
4695
4891
|
try {
|
|
4696
|
-
return (await stat2(
|
|
4892
|
+
return (await stat2(path15.join(cwd, "package.json"))).mtimeMs;
|
|
4697
4893
|
} catch {
|
|
4698
4894
|
return 0;
|
|
4699
4895
|
}
|
|
@@ -4721,8 +4917,8 @@ __export(daemon_exports, {
|
|
|
4721
4917
|
runDaemonCommand: () => runDaemonCommand
|
|
4722
4918
|
});
|
|
4723
4919
|
import { spawn as spawn3 } from "node:child_process";
|
|
4724
|
-
import
|
|
4725
|
-
import
|
|
4920
|
+
import fs17, { existsSync as existsSync3 } from "node:fs";
|
|
4921
|
+
import path16 from "node:path";
|
|
4726
4922
|
async function buildDaemonSpawn() {
|
|
4727
4923
|
const sea = await import("node:sea").catch(() => null);
|
|
4728
4924
|
if (sea?.isSea()) return { bin: process.execPath, args: ["daemon", "_serve"] };
|
|
@@ -4747,15 +4943,15 @@ async function runServeMode() {
|
|
|
4747
4943
|
function waitForFile(filePath, timeoutMs) {
|
|
4748
4944
|
if (existsSync3(filePath)) return Promise.resolve(true);
|
|
4749
4945
|
return new Promise((resolve) => {
|
|
4750
|
-
const dir =
|
|
4751
|
-
const fileName =
|
|
4946
|
+
const dir = path16.dirname(filePath);
|
|
4947
|
+
const fileName = path16.basename(filePath);
|
|
4752
4948
|
const settle = (ok) => {
|
|
4753
4949
|
clearTimeout(timer);
|
|
4754
4950
|
watcher.close();
|
|
4755
4951
|
resolve(ok);
|
|
4756
4952
|
};
|
|
4757
4953
|
const timer = setTimeout(() => settle(false), timeoutMs);
|
|
4758
|
-
const watcher =
|
|
4954
|
+
const watcher = fs17.watch(dir, (_event, name) => {
|
|
4759
4955
|
if (name === fileName && existsSync3(filePath)) settle(true);
|
|
4760
4956
|
});
|
|
4761
4957
|
watcher.on("error", () => settle(false));
|