qunitx-cli 0.23.7 → 0.25.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 +363 -147
- 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.25.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;
|
|
@@ -2778,9 +2947,16 @@ async function launchBrowser(config, skipPrelaunch = false) {
|
|
|
2778
2947
|
}
|
|
2779
2948
|
async function setupBrowser(config, cachedContent, existingBrowser = null, sharedServer = null) {
|
|
2780
2949
|
const setupStart = Date.now();
|
|
2950
|
+
const slot = config._daemonPageSlot;
|
|
2951
|
+
const slotPage = slot?.page && !slot.page.isClosed() ? slot.page : null;
|
|
2952
|
+
if (slotPage) {
|
|
2953
|
+
slotPage.removeAllListeners("console");
|
|
2954
|
+
slotPage.removeAllListeners("pageerror");
|
|
2955
|
+
slot.page = null;
|
|
2956
|
+
}
|
|
2781
2957
|
const [server, browser, page] = await (async () => {
|
|
2782
2958
|
if (sharedServer) {
|
|
2783
|
-
const newPage2 = await existingBrowser.newPage();
|
|
2959
|
+
const newPage2 = slotPage ?? await existingBrowser.newPage();
|
|
2784
2960
|
perfLog(`browser.js: newPage (shared server) took ${Date.now() - setupStart}ms`);
|
|
2785
2961
|
return [sharedServer, existingBrowser, newPage2];
|
|
2786
2962
|
}
|
|
@@ -2789,12 +2965,12 @@ async function setupBrowser(config, cachedContent, existingBrowser = null, share
|
|
|
2789
2965
|
const activeBrowser = existingBrowser ?? await launchBrowser(config);
|
|
2790
2966
|
const pageStart = Date.now();
|
|
2791
2967
|
const isHeadedWatchMode = config.open === true && config.watch;
|
|
2792
|
-
const getPage = isHeadedWatchMode ? () => activeBrowser.contexts()[0]?.pages()[0] ?? activeBrowser.newPage() : () => activeBrowser.newPage();
|
|
2968
|
+
const getPage = slotPage ? () => Promise.resolve(slotPage) : isHeadedWatchMode ? () => activeBrowser.contexts()[0]?.pages()[0] ?? activeBrowser.newPage() : () => activeBrowser.newPage();
|
|
2793
2969
|
const [newPage] = await Promise.all([getPage(), bindServerToPort(newServer, config)]);
|
|
2794
2970
|
perfLog(`browser.js: newPage + bindServerToPort took ${Date.now() - pageStart}ms`);
|
|
2795
2971
|
return [newServer, activeBrowser, newPage];
|
|
2796
2972
|
})();
|
|
2797
|
-
if (config.browser === "firefox") {
|
|
2973
|
+
if (config.browser === "firefox" && !slotPage) {
|
|
2798
2974
|
await page.addInitScript(() => {
|
|
2799
2975
|
const preSerialize = (arg) => {
|
|
2800
2976
|
if (arg === null || typeof arg !== "object") return arg;
|
|
@@ -2905,8 +3081,8 @@ var init_display_final_result = __esm({
|
|
|
2905
3081
|
});
|
|
2906
3082
|
|
|
2907
3083
|
// lib/commands/run/tests-in-browser.ts
|
|
2908
|
-
import
|
|
2909
|
-
import
|
|
3084
|
+
import fs11 from "node:fs/promises";
|
|
3085
|
+
import path10 from "node:path";
|
|
2910
3086
|
import esbuild from "esbuild";
|
|
2911
3087
|
function deriveBuildErrorType(error) {
|
|
2912
3088
|
const msgs = error?.errors ?? [];
|
|
@@ -2946,9 +3122,9 @@ async function buildTestBundle(config, cachedContent) {
|
|
|
2946
3122
|
console.log("# [buildTestBundle] fsTree is empty \u2014 skipping build (no test files found)");
|
|
2947
3123
|
return;
|
|
2948
3124
|
}
|
|
2949
|
-
const outDir =
|
|
2950
|
-
const outfile =
|
|
2951
|
-
await
|
|
3125
|
+
const outDir = path10.resolve(projectRoot, output);
|
|
3126
|
+
const outfile = path10.join(outDir, "tests.js");
|
|
3127
|
+
await fs11.mkdir(outDir, { recursive: true });
|
|
2952
3128
|
const sourcemap = "inline";
|
|
2953
3129
|
const needsDisk = true;
|
|
2954
3130
|
const buildOptions = {
|
|
@@ -2972,6 +3148,10 @@ async function buildTestBundle(config, cachedContent) {
|
|
|
2972
3148
|
// tsconfig's `jsxImportSource` or a `@jsxImportSource <pkg>` pragma cover Vue/Preact/Solid.
|
|
2973
3149
|
jsx: "automatic",
|
|
2974
3150
|
plugins: config.plugins,
|
|
3151
|
+
// Required for --changed/--since dep-graph filter on subsequent runs (cache
|
|
3152
|
+
// populates here, reads in setupConfig). Inputs map carries the full reverse-dep
|
|
3153
|
+
// graph; output cost is negligible.
|
|
3154
|
+
metafile: true,
|
|
2975
3155
|
// Signal the runtime that all test modules are registered. The runtime's maybeStart()
|
|
2976
3156
|
// waits for both this event and the WebSocket 'open' event before calling QUnit.start().
|
|
2977
3157
|
// Dispatching from the bundle (rather than from a script onload attr) is reliable across
|
|
@@ -2983,32 +3163,33 @@ async function buildTestBundle(config, cachedContent) {
|
|
|
2983
3163
|
const cacheHolder = config._daemonEsbuildCache ?? cachedContent;
|
|
2984
3164
|
const fileKey = bundleCacheKey(buildOptions, allTestFilePaths);
|
|
2985
3165
|
try {
|
|
2986
|
-
const [allTestCode] = await Promise.all([
|
|
3166
|
+
const [{ js: allTestCode, metafile }] = await Promise.all([
|
|
2987
3167
|
config.watch || config._daemonMode ? buildIncrementally(buildOptions, fileKey, cacheHolder, needsDisk) : buildWithOverlayfsRetry(buildOptions, needsDisk),
|
|
2988
3168
|
Promise.all(
|
|
2989
3169
|
cachedContent.htmlPathsToRunTests.map(async (htmlPath) => {
|
|
2990
|
-
const targetPath =
|
|
3170
|
+
const targetPath = path10.join(outDir, htmlPath);
|
|
2991
3171
|
if (htmlPath !== "/") {
|
|
2992
|
-
await
|
|
2993
|
-
await
|
|
3172
|
+
await fs11.rm(targetPath, { force: true, recursive: true });
|
|
3173
|
+
await fs11.mkdir(path10.dirname(targetPath), { recursive: true });
|
|
2994
3174
|
}
|
|
2995
3175
|
})
|
|
2996
3176
|
)
|
|
2997
3177
|
]);
|
|
2998
3178
|
cachedContent.allTestCode = allTestCode;
|
|
2999
3179
|
config._sourceMapDecoder = extractInlineSourceMap(allTestCode, outDir);
|
|
3180
|
+
if (metafile) void writeMetafileCache(projectRoot, process.cwd(), metafile);
|
|
3000
3181
|
} catch (error) {
|
|
3001
3182
|
cachedContent._buildError = {
|
|
3002
3183
|
type: deriveBuildErrorType(error),
|
|
3003
3184
|
formatted: formatBuildErrors(error)
|
|
3004
3185
|
};
|
|
3005
|
-
await
|
|
3186
|
+
await fs11.writeFile(path10.join(outDir, "index.html"), buildErrorHTML(cachedContent._buildError));
|
|
3006
3187
|
throw error;
|
|
3007
3188
|
}
|
|
3008
3189
|
}
|
|
3009
3190
|
async function runTestsInBrowser(config, cachedContent = {}, connections, targetTestFilesToFilter) {
|
|
3010
3191
|
const { projectRoot, output } = config;
|
|
3011
|
-
const outDir =
|
|
3192
|
+
const outDir = path10.resolve(projectRoot, output);
|
|
3012
3193
|
const allTestFilePaths = Object.keys(config.fsTree);
|
|
3013
3194
|
const runHasFilter = !!targetTestFilesToFilter;
|
|
3014
3195
|
if (!config._groupMode) {
|
|
@@ -3036,7 +3217,7 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
3036
3217
|
return connections;
|
|
3037
3218
|
}
|
|
3038
3219
|
if (runHasFilter) {
|
|
3039
|
-
const outputPath =
|
|
3220
|
+
const outputPath = path10.join(outDir, "filtered-tests.js");
|
|
3040
3221
|
cachedContent.filteredTestCode = await buildFilteredTests(
|
|
3041
3222
|
targetTestFilesToFilter,
|
|
3042
3223
|
outputPath,
|
|
@@ -3076,7 +3257,7 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
3076
3257
|
console.log(
|
|
3077
3258
|
`# Warning: 0 tests registered \u2014 no QUnit test cases found in ${allTestFilePaths.length} ${fileWord}`
|
|
3078
3259
|
);
|
|
3079
|
-
|
|
3260
|
+
fs11.writeFile(path10.join(outDir, "index.html"), buildNoTestsHTML(displayFiles)).catch(
|
|
3080
3261
|
() => {
|
|
3081
3262
|
}
|
|
3082
3263
|
);
|
|
@@ -3108,8 +3289,8 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
3108
3289
|
type: deriveBuildErrorType(error),
|
|
3109
3290
|
formatted: formatBuildErrors(error)
|
|
3110
3291
|
};
|
|
3111
|
-
|
|
3112
|
-
|
|
3292
|
+
fs11.writeFile(
|
|
3293
|
+
path10.join(outDir, "qunitx.html"),
|
|
3113
3294
|
buildErrorHTML(cachedContent._buildError)
|
|
3114
3295
|
).catch(
|
|
3115
3296
|
(err) => config.debug && process.stderr.write(`# [qunitx] writeFile qunitx.html: ${err.message}
|
|
@@ -3159,7 +3340,7 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
3159
3340
|
);
|
|
3160
3341
|
await Promise.all(
|
|
3161
3342
|
activeGroups.map(
|
|
3162
|
-
(group) =>
|
|
3343
|
+
(group) => fs11.mkdir(path10.resolve(group.config.projectRoot, group.config.output), { recursive: true })
|
|
3163
3344
|
)
|
|
3164
3345
|
);
|
|
3165
3346
|
const sourcemap = "inline";
|
|
@@ -3179,7 +3360,7 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
3179
3360
|
});
|
|
3180
3361
|
}
|
|
3181
3362
|
};
|
|
3182
|
-
const esbuildOutdir =
|
|
3363
|
+
const esbuildOutdir = path10.join(projectRoot, "tmp");
|
|
3183
3364
|
const buildOptions = {
|
|
3184
3365
|
entryPoints: activeGroups.map((_, slotIndex) => ({
|
|
3185
3366
|
in: `group-entry-${slotIndex}`,
|
|
@@ -3201,6 +3382,9 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
3201
3382
|
sourcemap,
|
|
3202
3383
|
write: false,
|
|
3203
3384
|
jsx: "automatic",
|
|
3385
|
+
// Required for --changed/--since dep-graph filter on subsequent runs. Same
|
|
3386
|
+
// contract as the single-group path in `buildTestBundle`.
|
|
3387
|
+
metafile: true,
|
|
3204
3388
|
footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
|
|
3205
3389
|
};
|
|
3206
3390
|
const hasSmallOutput = (result2) => (result2.outputFiles ?? []).some(
|
|
@@ -3221,8 +3405,8 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
3221
3405
|
const slotIndex = parseInt(match[1]);
|
|
3222
3406
|
const isMap = Boolean(match[2]);
|
|
3223
3407
|
const { config, cachedContent } = activeGroups[slotIndex];
|
|
3224
|
-
const destPath =
|
|
3225
|
-
|
|
3408
|
+
const destPath = path10.join(
|
|
3409
|
+
path10.resolve(config.projectRoot, config.output),
|
|
3226
3410
|
"tests.js" + (isMap ? ".map" : "")
|
|
3227
3411
|
);
|
|
3228
3412
|
if (!isMap) {
|
|
@@ -3232,17 +3416,20 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
3232
3416
|
esbuildOutdir
|
|
3233
3417
|
);
|
|
3234
3418
|
}
|
|
3235
|
-
return
|
|
3419
|
+
return fs11.writeFile(destPath, outputFile.contents);
|
|
3236
3420
|
})
|
|
3237
3421
|
);
|
|
3422
|
+
if (result2.metafile) {
|
|
3423
|
+
void writeMetafileCache(projectRoot, process.cwd(), result2.metafile);
|
|
3424
|
+
}
|
|
3238
3425
|
} catch (error) {
|
|
3239
3426
|
const buildError = { type: deriveBuildErrorType(error), formatted: formatBuildErrors(error) };
|
|
3240
3427
|
const errorHtml = buildErrorHTML(buildError);
|
|
3241
3428
|
await Promise.all(
|
|
3242
3429
|
activeGroups.map((group) => {
|
|
3243
3430
|
group.cachedContent._buildError = buildError;
|
|
3244
|
-
return
|
|
3245
|
-
|
|
3431
|
+
return fs11.writeFile(
|
|
3432
|
+
path10.join(path10.resolve(group.config.projectRoot, group.config.output), "index.html"),
|
|
3246
3433
|
errorHtml
|
|
3247
3434
|
).catch(
|
|
3248
3435
|
(err) => debug && process.stderr.write(`# [qunitx] writeFile index.html: ${err.message}
|
|
@@ -3274,7 +3461,7 @@ function buildFilteredTests(filteredTests, outputPath, config) {
|
|
|
3274
3461
|
footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
|
|
3275
3462
|
},
|
|
3276
3463
|
needsDisk
|
|
3277
|
-
);
|
|
3464
|
+
).then((r) => r.js);
|
|
3278
3465
|
}
|
|
3279
3466
|
async function runWithOverlayfsRetry(getContents, needsDisk) {
|
|
3280
3467
|
let { result: result2, js } = await getContents();
|
|
@@ -3291,10 +3478,10 @@ async function runWithOverlayfsRetry(getContents, needsDisk) {
|
|
|
3291
3478
|
}
|
|
3292
3479
|
if (needsDisk) {
|
|
3293
3480
|
await Promise.all(
|
|
3294
|
-
result2.outputFiles.map((outputFile) =>
|
|
3481
|
+
result2.outputFiles.map((outputFile) => fs11.writeFile(outputFile.path, outputFile.contents))
|
|
3295
3482
|
);
|
|
3296
3483
|
}
|
|
3297
|
-
return js;
|
|
3484
|
+
return { js, metafile: result2.metafile };
|
|
3298
3485
|
}
|
|
3299
3486
|
function buildWithOverlayfsRetry(options, needsDisk) {
|
|
3300
3487
|
const buildOpts = { ...options, write: false };
|
|
@@ -3428,9 +3615,9 @@ async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode
|
|
|
3428
3615
|
process.exit(1);
|
|
3429
3616
|
}
|
|
3430
3617
|
function toEsbuildImportPath(filePath) {
|
|
3431
|
-
const rel =
|
|
3618
|
+
const rel = path10.relative(process.cwd(), filePath);
|
|
3432
3619
|
const normalized = rel.replace(/\\/g, "/");
|
|
3433
|
-
if (
|
|
3620
|
+
if (path10.isAbsolute(rel)) return filePath.replace(/\\/g, "/");
|
|
3434
3621
|
return normalized.startsWith(".") ? normalized : "./" + normalized;
|
|
3435
3622
|
}
|
|
3436
3623
|
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 +3631,9 @@ var init_tests_in_browser = __esm({
|
|
|
3444
3631
|
init_display_final_result();
|
|
3445
3632
|
init_web_server();
|
|
3446
3633
|
init_source_map_decoder();
|
|
3447
|
-
|
|
3448
|
-
|
|
3634
|
+
init_metafile_cache();
|
|
3635
|
+
ancestorNodeModules = (dir) => dir.split(path10.sep).map(
|
|
3636
|
+
(_, i, parts) => path10.join(parts.slice(0, parts.length - i).join(path10.sep) || path10.sep, "node_modules")
|
|
3449
3637
|
);
|
|
3450
3638
|
ANCESTOR_NODE_MODULES = ancestorNodeModules(process.cwd());
|
|
3451
3639
|
RETRY_DELAY_MS = 100;
|
|
@@ -3481,11 +3669,11 @@ var init_tests_in_browser = __esm({
|
|
|
3481
3669
|
|
|
3482
3670
|
// lib/utils/open-output-in-browser.ts
|
|
3483
3671
|
import { spawn as spawn2 } from "node:child_process";
|
|
3484
|
-
import
|
|
3672
|
+
import path11 from "node:path";
|
|
3485
3673
|
import { pathToFileURL as pathToFileURL3 } from "node:url";
|
|
3486
3674
|
async function openOutputInBrowser(config) {
|
|
3487
3675
|
try {
|
|
3488
|
-
const outputFile = config.watch ? `http://localhost:${config.port}` : pathToFileURL3(
|
|
3676
|
+
const outputFile = config.watch ? `http://localhost:${config.port}` : pathToFileURL3(path11.join(path11.resolve(config.projectRoot, config.output), "index.html")).href;
|
|
3489
3677
|
if (typeof config.open === "string") {
|
|
3490
3678
|
spawnDetached(config.open, [outputFile]);
|
|
3491
3679
|
return;
|
|
@@ -3518,9 +3706,9 @@ var init_open_output_in_browser = __esm({
|
|
|
3518
3706
|
});
|
|
3519
3707
|
|
|
3520
3708
|
// lib/setup/file-watcher.ts
|
|
3521
|
-
import
|
|
3709
|
+
import fs12 from "node:fs";
|
|
3522
3710
|
import { readdir, stat, lstat } from "node:fs/promises";
|
|
3523
|
-
import
|
|
3711
|
+
import path12 from "node:path";
|
|
3524
3712
|
function recordJustAdded(config, filePath) {
|
|
3525
3713
|
let map = justAddedAt.get(config);
|
|
3526
3714
|
if (!map) justAddedAt.set(config, map = /* @__PURE__ */ new Map());
|
|
@@ -3539,7 +3727,7 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
3539
3727
|
if (symlinkPollers.has(filePath)) return;
|
|
3540
3728
|
const handler = (curr, prev) => {
|
|
3541
3729
|
if (curr.nlink === 0) {
|
|
3542
|
-
|
|
3730
|
+
fs12.unwatchFile(filePath, handler);
|
|
3543
3731
|
symlinkPollers.delete(filePath);
|
|
3544
3732
|
if (filePath in config.fsTree) {
|
|
3545
3733
|
handleWatchEvent(config, extensions, "unlink", filePath, onEventFunc, onFinishFunc);
|
|
@@ -3558,8 +3746,8 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
3558
3746
|
}
|
|
3559
3747
|
}
|
|
3560
3748
|
};
|
|
3561
|
-
|
|
3562
|
-
symlinkPollers.set(filePath, () =>
|
|
3749
|
+
fs12.watchFile(filePath, { interval: SYMLINK_POLL_INTERVAL_MS, persistent: false }, handler);
|
|
3750
|
+
symlinkPollers.set(filePath, () => fs12.unwatchFile(filePath, handler));
|
|
3563
3751
|
}
|
|
3564
3752
|
function untrackSymlink(filePath) {
|
|
3565
3753
|
symlinkPollers.get(filePath)?.();
|
|
@@ -3570,7 +3758,7 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
3570
3758
|
let rescanInProgress = false;
|
|
3571
3759
|
const lastEventMs = {};
|
|
3572
3760
|
const seenMtimeMs = {};
|
|
3573
|
-
const childWatcher =
|
|
3761
|
+
const childWatcher = fs12.watch(watchPath, { recursive: true }, async (eventType, filename) => {
|
|
3574
3762
|
if (!ready) return;
|
|
3575
3763
|
if (!filename) {
|
|
3576
3764
|
if (process.platform === "darwin" && !rescanInProgress) {
|
|
@@ -3588,7 +3776,7 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
3588
3776
|
}
|
|
3589
3777
|
return;
|
|
3590
3778
|
}
|
|
3591
|
-
const fullPath = filename ===
|
|
3779
|
+
const fullPath = filename === path12.basename(watchPath) ? watchPath : path12.join(watchPath, filename);
|
|
3592
3780
|
if (eventType === "change") {
|
|
3593
3781
|
const now = Date.now();
|
|
3594
3782
|
const last = lastEventMs[fullPath] ?? 0;
|
|
@@ -3626,8 +3814,8 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
3626
3814
|
}
|
|
3627
3815
|
handleWatchEvent(config, extensions, event, fullPath, onEventFunc, onFinishFunc);
|
|
3628
3816
|
});
|
|
3629
|
-
const parentDir =
|
|
3630
|
-
const watchedBasename =
|
|
3817
|
+
const parentDir = path12.dirname(watchPath);
|
|
3818
|
+
const watchedBasename = path12.basename(watchPath);
|
|
3631
3819
|
let parentUnlinkFired = false;
|
|
3632
3820
|
let rescanTimer = null;
|
|
3633
3821
|
const tryFireParentUnlink = async () => {
|
|
@@ -3646,7 +3834,7 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
3646
3834
|
return true;
|
|
3647
3835
|
}
|
|
3648
3836
|
};
|
|
3649
|
-
const parentWatcher =
|
|
3837
|
+
const parentWatcher = fs12.watch(parentDir, async (eventType, filename) => {
|
|
3650
3838
|
if (!ready || filename !== watchedBasename || eventType !== "rename") return;
|
|
3651
3839
|
await tryFireParentUnlink();
|
|
3652
3840
|
});
|
|
@@ -3760,11 +3948,11 @@ async function rescanDirectoryForDelta(watchPath, config, extensions, onEventFun
|
|
|
3760
3948
|
const trackedToRecheck = [];
|
|
3761
3949
|
for (const entry of entries) {
|
|
3762
3950
|
if (entry.isDirectory()) {
|
|
3763
|
-
presentDirs.add(
|
|
3951
|
+
presentDirs.add(path12.join(entry.parentPath, entry.name));
|
|
3764
3952
|
continue;
|
|
3765
3953
|
}
|
|
3766
3954
|
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
3767
|
-
const entryPath =
|
|
3955
|
+
const entryPath = path12.join(entry.parentPath, entry.name);
|
|
3768
3956
|
presentDirs.add(entry.parentPath);
|
|
3769
3957
|
if (!extensions.some((ext) => entryPath.endsWith(`.${ext}`))) continue;
|
|
3770
3958
|
presentPaths.add(entryPath);
|
|
@@ -3787,13 +3975,13 @@ async function rescanDirectoryForDelta(watchPath, config, extensions, onEventFun
|
|
|
3787
3975
|
}
|
|
3788
3976
|
})
|
|
3789
3977
|
);
|
|
3790
|
-
const watchPrefix = watchPath +
|
|
3978
|
+
const watchPrefix = watchPath + path12.sep;
|
|
3791
3979
|
const firedDirPrefixes = [];
|
|
3792
3980
|
for (const trackedPath of Object.keys(config.fsTree)) {
|
|
3793
3981
|
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(
|
|
3982
|
+
if (firedDirPrefixes.some((p) => trackedPath.startsWith(p + path12.sep))) continue;
|
|
3983
|
+
const parts = trackedPath.slice(watchPrefix.length).split(path12.sep);
|
|
3984
|
+
const goneDirPath = parts.slice(0, -1).map((_, i) => watchPrefix + parts.slice(0, i + 1).join(path12.sep)).find((p) => !presentDirs.has(p)) ?? null;
|
|
3797
3985
|
if (goneDirPath !== null) {
|
|
3798
3986
|
firedDirPrefixes.push(goneDirPath);
|
|
3799
3987
|
handleWatchEvent(config, extensions, "unlinkDir", goneDirPath, onEventFunc, onFinishFunc);
|
|
@@ -3846,7 +4034,7 @@ var init_file_watcher = __esm({
|
|
|
3846
4034
|
SYMLINK_POLL_INTERVAL_MS = 500;
|
|
3847
4035
|
OVERLAYFS_RENAME_RETRY_MS = 50;
|
|
3848
4036
|
RESCAN_INTERVAL_MS = 1e3;
|
|
3849
|
-
CHANGE_COALESCE_MS =
|
|
4037
|
+
CHANGE_COALESCE_MS = 75;
|
|
3850
4038
|
ADD_SUPPRESS_WINDOW_MS = 1e3;
|
|
3851
4039
|
justAddedAt = /* @__PURE__ */ new WeakMap();
|
|
3852
4040
|
}
|
|
@@ -3928,28 +4116,28 @@ var init_keyboard_events = __esm({
|
|
|
3928
4116
|
});
|
|
3929
4117
|
|
|
3930
4118
|
// lib/setup/write-output-static-files.ts
|
|
3931
|
-
import
|
|
3932
|
-
import
|
|
4119
|
+
import fs13 from "node:fs/promises";
|
|
4120
|
+
import path13 from "node:path";
|
|
3933
4121
|
async function writeOutputStaticFiles({ projectRoot, output }, cachedContent) {
|
|
3934
4122
|
const staticHTMLPromises = Object.keys(cachedContent.staticHTMLs).map(async (staticHTMLKey) => {
|
|
3935
|
-
const htmlRelativePath =
|
|
3936
|
-
const outDir =
|
|
3937
|
-
await ensureFolderExists(
|
|
3938
|
-
await
|
|
3939
|
-
|
|
4123
|
+
const htmlRelativePath = path13.relative(projectRoot, staticHTMLKey);
|
|
4124
|
+
const outDir = path13.resolve(projectRoot, output);
|
|
4125
|
+
await ensureFolderExists(path13.join(outDir, htmlRelativePath));
|
|
4126
|
+
await fs13.writeFile(
|
|
4127
|
+
path13.join(outDir, htmlRelativePath),
|
|
3940
4128
|
cachedContent.staticHTMLs[staticHTMLKey]
|
|
3941
4129
|
);
|
|
3942
4130
|
});
|
|
3943
4131
|
const assetPromises = Array.from(cachedContent.assets).map(async (assetAbsolutePath) => {
|
|
3944
|
-
const assetRelativePath =
|
|
3945
|
-
const outDir =
|
|
3946
|
-
await ensureFolderExists(
|
|
3947
|
-
await
|
|
4132
|
+
const assetRelativePath = path13.relative(projectRoot, assetAbsolutePath).replace(/^(?:\.\.[\\/])+/, "");
|
|
4133
|
+
const outDir = path13.resolve(projectRoot, output);
|
|
4134
|
+
await ensureFolderExists(path13.join(outDir, assetRelativePath));
|
|
4135
|
+
await fs13.copyFile(assetAbsolutePath, path13.join(outDir, assetRelativePath));
|
|
3948
4136
|
});
|
|
3949
4137
|
await Promise.all(staticHTMLPromises.concat(assetPromises));
|
|
3950
4138
|
}
|
|
3951
4139
|
async function ensureFolderExists(assetPath) {
|
|
3952
|
-
await
|
|
4140
|
+
await fs13.mkdir(path13.dirname(assetPath), { recursive: true });
|
|
3953
4141
|
}
|
|
3954
4142
|
var init_write_output_static_files = __esm({
|
|
3955
4143
|
"lib/setup/write-output-static-files.ts"() {
|
|
@@ -3957,9 +4145,9 @@ var init_write_output_static_files = __esm({
|
|
|
3957
4145
|
});
|
|
3958
4146
|
|
|
3959
4147
|
// lib/utils/daemon-hint.ts
|
|
3960
|
-
import
|
|
4148
|
+
import fs14 from "node:fs/promises";
|
|
3961
4149
|
import os3 from "node:os";
|
|
3962
|
-
import
|
|
4150
|
+
import path14 from "node:path";
|
|
3963
4151
|
function shouldShowDaemonHint(ctx) {
|
|
3964
4152
|
const env = ctx.env ?? process.env;
|
|
3965
4153
|
if (ctx.watch) return false;
|
|
@@ -3977,14 +4165,14 @@ async function maybePrintDaemonHint(ctx, opts = {}) {
|
|
|
3977
4165
|
if (!shouldShowDaemonHint(ctx)) return;
|
|
3978
4166
|
const sentinel = opts.sentinelPath ?? DEFAULT_SENTINEL;
|
|
3979
4167
|
try {
|
|
3980
|
-
await
|
|
4168
|
+
await fs14.access(sentinel);
|
|
3981
4169
|
return;
|
|
3982
4170
|
} catch {
|
|
3983
4171
|
}
|
|
3984
4172
|
(opts.write ?? ((t) => process.stderr.write(t)))(HINT_TEXT);
|
|
3985
4173
|
try {
|
|
3986
|
-
await
|
|
3987
|
-
await
|
|
4174
|
+
await fs14.mkdir(path14.dirname(sentinel), { recursive: true });
|
|
4175
|
+
await fs14.writeFile(sentinel, (/* @__PURE__ */ new Date()).toISOString());
|
|
3988
4176
|
} catch {
|
|
3989
4177
|
}
|
|
3990
4178
|
}
|
|
@@ -3993,7 +4181,7 @@ var init_daemon_hint = __esm({
|
|
|
3993
4181
|
"lib/utils/daemon-hint.ts"() {
|
|
3994
4182
|
FAST_RUN_THRESHOLD_MS = 500;
|
|
3995
4183
|
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 =
|
|
4184
|
+
DEFAULT_SENTINEL = path14.join(os3.homedir(), ".cache", "qunitx", "hint-shown");
|
|
3997
4185
|
}
|
|
3998
4186
|
});
|
|
3999
4187
|
|
|
@@ -4006,7 +4194,7 @@ __export(run_exports, {
|
|
|
4006
4194
|
readTimingCache: () => readTimingCache,
|
|
4007
4195
|
run: () => run
|
|
4008
4196
|
});
|
|
4009
|
-
import
|
|
4197
|
+
import fs15 from "node:fs/promises";
|
|
4010
4198
|
import { join as join3, normalize } from "node:path";
|
|
4011
4199
|
import { createRequire as createRequire2 } from "node:module";
|
|
4012
4200
|
import { availableParallelism } from "node:os";
|
|
@@ -4093,6 +4281,21 @@ async function run(config) {
|
|
|
4093
4281
|
logWatcherAndKeyboardShortcutInfo(config, connections.server);
|
|
4094
4282
|
} else {
|
|
4095
4283
|
const allFiles = Object.keys(config.fsTree);
|
|
4284
|
+
if (allFiles.length === 0) {
|
|
4285
|
+
process.stdout.write("TAP version 13\n");
|
|
4286
|
+
process.stdout.write(
|
|
4287
|
+
`# Running 0 test files${config._daemonMode ? " (daemon)" : ""}
|
|
4288
|
+
1..0
|
|
4289
|
+
`
|
|
4290
|
+
);
|
|
4291
|
+
if (config._daemonMode) throw new DaemonRunError(0);
|
|
4292
|
+
if (!config.watch) {
|
|
4293
|
+
const browser2 = config._daemonBrowser ? null : await browserPromise;
|
|
4294
|
+
await closeWithGrace([browser2?.close(), shutdownPrelaunch()]);
|
|
4295
|
+
return process.exit(0);
|
|
4296
|
+
}
|
|
4297
|
+
return;
|
|
4298
|
+
}
|
|
4096
4299
|
const groupCount = Math.min(allFiles.length, availableParallelism());
|
|
4097
4300
|
const { groups, weights } = await splitIntoGroups(allFiles, groupCount, timings ?? {});
|
|
4098
4301
|
config.COUNTER = {
|
|
@@ -4109,6 +4312,12 @@ async function run(config) {
|
|
|
4109
4312
|
fsTree: Object.fromEntries(groupFiles.map((filePath) => [filePath, config.fsTree[filePath]])),
|
|
4110
4313
|
// Single group keeps the root output dir for backward-compatible file paths.
|
|
4111
4314
|
output: groupCount === 1 ? config.output : `${config.output}/group-${i}`,
|
|
4315
|
+
// Page reuse is single-group only: in concurrent group mode group 0 would
|
|
4316
|
+
// otherwise drain `slot.page` (setupBrowser consumes it) without re-stashing
|
|
4317
|
+
// (cleanup's `groupCount === 1` guard rejects), leaving the slot empty for
|
|
4318
|
+
// the next single-file run. Withhold the slot here so the warm page survives
|
|
4319
|
+
// a transient multi-file invocation untouched.
|
|
4320
|
+
_daemonPageSlot: groupCount === 1 ? config._daemonPageSlot : void 0,
|
|
4112
4321
|
_groupMode: true,
|
|
4113
4322
|
_phase: "bundling"
|
|
4114
4323
|
}));
|
|
@@ -4181,9 +4390,11 @@ async function run(config) {
|
|
|
4181
4390
|
await runTestsInBrowser(groupConfig, groupCachedContents[i], connections);
|
|
4182
4391
|
} finally {
|
|
4183
4392
|
await flushConsoleHandlers(groupConfig._pendingConsoleHandlers, connections.page);
|
|
4393
|
+
const reusePage = groupCount === 1 && groupConfig._daemonPageSlot && connections.page && !connections.page.isClosed();
|
|
4394
|
+
if (reusePage) groupConfig._daemonPageSlot.page = connections.page;
|
|
4184
4395
|
await closeWithGrace([
|
|
4185
4396
|
sharedServer ? void 0 : connections.server?.close(),
|
|
4186
|
-
connections.page?.close()
|
|
4397
|
+
reusePage ? void 0 : connections.page?.close()
|
|
4187
4398
|
]);
|
|
4188
4399
|
}
|
|
4189
4400
|
})();
|
|
@@ -4250,7 +4461,7 @@ async function run(config) {
|
|
|
4250
4461
|
}
|
|
4251
4462
|
async function buildCachedContent(config, htmlPaths) {
|
|
4252
4463
|
const htmlBuffers = await Promise.all(
|
|
4253
|
-
config.htmlPaths.map((htmlPath) =>
|
|
4464
|
+
config.htmlPaths.map((htmlPath) => fs15.readFile(htmlPath).catch(() => null))
|
|
4254
4465
|
);
|
|
4255
4466
|
const cachedContent = htmlPaths.reduce(
|
|
4256
4467
|
(result2, _htmlPath, index) => {
|
|
@@ -4305,7 +4516,7 @@ async function addCachedContentMainHTML(projectRoot, cachedContent) {
|
|
|
4305
4516
|
}
|
|
4306
4517
|
async function readTimingCache(projectRoot) {
|
|
4307
4518
|
try {
|
|
4308
|
-
const parsed = JSON.parse(await
|
|
4519
|
+
const parsed = JSON.parse(await fs15.readFile(`${projectRoot}/tmp/test-timings.json`, "utf8"));
|
|
4309
4520
|
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : {};
|
|
4310
4521
|
} catch {
|
|
4311
4522
|
return {};
|
|
@@ -4324,7 +4535,7 @@ function computeFileTimes(groups, weights, wallTimes) {
|
|
|
4324
4535
|
return result2;
|
|
4325
4536
|
}
|
|
4326
4537
|
async function persistTimings(fileTimes, projectRoot) {
|
|
4327
|
-
await
|
|
4538
|
+
await fs15.writeFile(
|
|
4328
4539
|
`${projectRoot}/tmp/test-timings.json`,
|
|
4329
4540
|
JSON.stringify(Object.fromEntries(fileTimes), null, 2)
|
|
4330
4541
|
);
|
|
@@ -4339,7 +4550,7 @@ ${lines.join("\n")}
|
|
|
4339
4550
|
async function splitIntoGroups(files, groupCount, timings) {
|
|
4340
4551
|
const sizes = await Promise.all(
|
|
4341
4552
|
files.map(
|
|
4342
|
-
(f) => timings[f] > 0 ? Promise.resolve(0) :
|
|
4553
|
+
(f) => timings[f] > 0 ? Promise.resolve(0) : fs15.stat(f).then((s) => s.size).catch(() => 0)
|
|
4343
4554
|
)
|
|
4344
4555
|
);
|
|
4345
4556
|
const knownRates = files.map((f, i) => ({ ms: timings[f], size: sizes[i] })).filter(({ ms, size }) => ms > 0 && size > 0);
|
|
@@ -4413,19 +4624,19 @@ __export(server_exports, {
|
|
|
4413
4624
|
runDaemonServer: () => runDaemonServer
|
|
4414
4625
|
});
|
|
4415
4626
|
import net2 from "node:net";
|
|
4416
|
-
import
|
|
4627
|
+
import fs16 from "node:fs";
|
|
4417
4628
|
import { writeFile, unlink, stat as stat2, chmod } from "node:fs/promises";
|
|
4418
|
-
import
|
|
4629
|
+
import path15 from "node:path";
|
|
4419
4630
|
async function runDaemonServer() {
|
|
4420
4631
|
const cwd = process.cwd();
|
|
4421
4632
|
const socketPath = daemonSocketPath(cwd);
|
|
4422
4633
|
const infoPath = daemonInfoPath(cwd);
|
|
4423
|
-
if (
|
|
4634
|
+
if (fs16.existsSync(infoPath) && await isLiveSocket(socketPath)) process.exit(0);
|
|
4424
4635
|
await unlink(socketPath).catch(() => {
|
|
4425
4636
|
});
|
|
4426
4637
|
const logPath = process.env.QUNITX_DAEMON_LOG;
|
|
4427
4638
|
if (logPath) {
|
|
4428
|
-
const log =
|
|
4639
|
+
const log = fs16.createWriteStream(logPath, { flags: "a" });
|
|
4429
4640
|
log.on("error", () => {
|
|
4430
4641
|
});
|
|
4431
4642
|
const forward = log.write.bind(log);
|
|
@@ -4459,7 +4670,8 @@ async function runDaemonServer() {
|
|
|
4459
4670
|
infoPath,
|
|
4460
4671
|
consecutiveCrashes: 0,
|
|
4461
4672
|
listenSucceeded: false,
|
|
4462
|
-
esbuildCache: { _esbuildContext: null }
|
|
4673
|
+
esbuildCache: { _esbuildContext: null },
|
|
4674
|
+
pageSlot: { page: null }
|
|
4463
4675
|
};
|
|
4464
4676
|
const shutdown = (reason) => shutdownDaemon2(state, reason);
|
|
4465
4677
|
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
@@ -4519,6 +4731,8 @@ async function shutdownDaemon2(state, reason) {
|
|
|
4519
4731
|
}) : null,
|
|
4520
4732
|
state.listenSucceeded ? unlink(state.infoPath).catch(() => {
|
|
4521
4733
|
}) : null,
|
|
4734
|
+
state.pageSlot.page?.close().catch(() => {
|
|
4735
|
+
}),
|
|
4522
4736
|
state.browser.close().catch(() => {
|
|
4523
4737
|
}),
|
|
4524
4738
|
state.esbuildCache._esbuildContext?.dispose().catch(() => {
|
|
@@ -4551,7 +4765,7 @@ async function dispatch(req, socket, state) {
|
|
|
4551
4765
|
socket.end();
|
|
4552
4766
|
} else if (req.type === "shutdown") {
|
|
4553
4767
|
try {
|
|
4554
|
-
|
|
4768
|
+
fs16.unlinkSync(state.infoPath);
|
|
4555
4769
|
} catch {
|
|
4556
4770
|
}
|
|
4557
4771
|
writeChunk(socket, { type: "done", exitCode: 0 });
|
|
@@ -4648,6 +4862,7 @@ async function recoverBrowser(state) {
|
|
|
4648
4862
|
);
|
|
4649
4863
|
state.browser.close().catch(() => {
|
|
4650
4864
|
});
|
|
4865
|
+
state.pageSlot.page = null;
|
|
4651
4866
|
try {
|
|
4652
4867
|
state.browser = await launchBrowser(state.baseConfig, true);
|
|
4653
4868
|
} catch (err) {
|
|
@@ -4670,6 +4885,7 @@ async function runOnce(argv, env, state) {
|
|
|
4670
4885
|
config._daemonMode = true;
|
|
4671
4886
|
config._daemonBrowser = state.browser;
|
|
4672
4887
|
config._daemonEsbuildCache = state.esbuildCache;
|
|
4888
|
+
config._daemonPageSlot = state.pageSlot;
|
|
4673
4889
|
config.watch = false;
|
|
4674
4890
|
config.open = false;
|
|
4675
4891
|
try {
|
|
@@ -4693,7 +4909,7 @@ async function isLiveSocket(socketPath) {
|
|
|
4693
4909
|
}
|
|
4694
4910
|
async function readPkgMtime(cwd) {
|
|
4695
4911
|
try {
|
|
4696
|
-
return (await stat2(
|
|
4912
|
+
return (await stat2(path15.join(cwd, "package.json"))).mtimeMs;
|
|
4697
4913
|
} catch {
|
|
4698
4914
|
return 0;
|
|
4699
4915
|
}
|
|
@@ -4721,8 +4937,8 @@ __export(daemon_exports, {
|
|
|
4721
4937
|
runDaemonCommand: () => runDaemonCommand
|
|
4722
4938
|
});
|
|
4723
4939
|
import { spawn as spawn3 } from "node:child_process";
|
|
4724
|
-
import
|
|
4725
|
-
import
|
|
4940
|
+
import fs17, { existsSync as existsSync3 } from "node:fs";
|
|
4941
|
+
import path16 from "node:path";
|
|
4726
4942
|
async function buildDaemonSpawn() {
|
|
4727
4943
|
const sea = await import("node:sea").catch(() => null);
|
|
4728
4944
|
if (sea?.isSea()) return { bin: process.execPath, args: ["daemon", "_serve"] };
|
|
@@ -4747,15 +4963,15 @@ async function runServeMode() {
|
|
|
4747
4963
|
function waitForFile(filePath, timeoutMs) {
|
|
4748
4964
|
if (existsSync3(filePath)) return Promise.resolve(true);
|
|
4749
4965
|
return new Promise((resolve) => {
|
|
4750
|
-
const dir =
|
|
4751
|
-
const fileName =
|
|
4966
|
+
const dir = path16.dirname(filePath);
|
|
4967
|
+
const fileName = path16.basename(filePath);
|
|
4752
4968
|
const settle = (ok) => {
|
|
4753
4969
|
clearTimeout(timer);
|
|
4754
4970
|
watcher.close();
|
|
4755
4971
|
resolve(ok);
|
|
4756
4972
|
};
|
|
4757
4973
|
const timer = setTimeout(() => settle(false), timeoutMs);
|
|
4758
|
-
const watcher =
|
|
4974
|
+
const watcher = fs17.watch(dir, (_event, name) => {
|
|
4759
4975
|
if (name === fileName && existsSync3(filePath)) settle(true);
|
|
4760
4976
|
});
|
|
4761
4977
|
watcher.on("error", () => settle(false));
|