qunitx-cli 0.22.2 → 0.23.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/README.md +104 -31
- package/dist/cli.js +1777 -947
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -287,14 +287,35 @@ var init_perf_logger = __esm({
|
|
|
287
287
|
}
|
|
288
288
|
});
|
|
289
289
|
|
|
290
|
+
// lib/utils/daemon-socket-path.ts
|
|
291
|
+
import { createHash } from "node:crypto";
|
|
292
|
+
import os2 from "node:os";
|
|
293
|
+
import path2 from "node:path";
|
|
294
|
+
function cwdHash(cwd) {
|
|
295
|
+
return createHash("sha256").update(cwd).digest("hex").slice(0, 12);
|
|
296
|
+
}
|
|
297
|
+
function daemonSocketPath(cwd = process.cwd(), platform = process.platform) {
|
|
298
|
+
const name = `qunitx-daemon-${cwdHash(cwd)}`;
|
|
299
|
+
if (platform === "win32") return `\\\\.\\pipe\\${name}`;
|
|
300
|
+
return path2.join(os2.tmpdir(), `${name}.sock`);
|
|
301
|
+
}
|
|
302
|
+
function daemonInfoPath(cwd = process.cwd()) {
|
|
303
|
+
return path2.join(os2.tmpdir(), `qunitx-daemon-${cwdHash(cwd)}.json`);
|
|
304
|
+
}
|
|
305
|
+
var init_daemon_socket_path = __esm({
|
|
306
|
+
"lib/utils/daemon-socket-path.ts"() {
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
290
310
|
// lib/utils/chrome-prelaunch.ts
|
|
311
|
+
import { existsSync } from "node:fs";
|
|
291
312
|
async function shutdownPrelaunch() {
|
|
292
313
|
if (!earlyChrome) return;
|
|
293
314
|
const { shutdown } = earlyChrome;
|
|
294
315
|
earlyChrome = null;
|
|
295
316
|
await shutdown();
|
|
296
317
|
}
|
|
297
|
-
var NON_RUN_COMMANDS, isRunCommand, browserFromArgv, openFromArgv, watchFromArgv, openWatchMode, earlyChrome, prelaunchPromise;
|
|
318
|
+
var NON_RUN_COMMANDS, cmd, isDaemonControlCmd, isRunCommand, browserFromArgv, openFromArgv, watchFromArgv, isDaemonClientRun, openWatchMode, earlyChrome, prelaunchPromise;
|
|
298
319
|
var init_chrome_prelaunch = __esm({
|
|
299
320
|
"lib/utils/chrome-prelaunch.ts"() {
|
|
300
321
|
init_find_chrome();
|
|
@@ -302,8 +323,11 @@ var init_chrome_prelaunch = __esm({
|
|
|
302
323
|
init_kill_process_group();
|
|
303
324
|
init_chromium_args();
|
|
304
325
|
init_perf_logger();
|
|
326
|
+
init_daemon_socket_path();
|
|
305
327
|
NON_RUN_COMMANDS = /* @__PURE__ */ new Set(["help", "h", "p", "print", "new", "n", "g", "generate", "init"]);
|
|
306
|
-
|
|
328
|
+
cmd = process.argv[2];
|
|
329
|
+
isDaemonControlCmd = cmd === "daemon" && process.argv[3] !== "_serve";
|
|
330
|
+
isRunCommand = Boolean(cmd) && !NON_RUN_COMMANDS.has(cmd) && !isDaemonControlCmd;
|
|
307
331
|
({ browserFromArgv, openFromArgv, watchFromArgv } = process.argv.reduce(
|
|
308
332
|
(flags, arg) => {
|
|
309
333
|
if (arg.startsWith("--browser=")) flags.browserFromArgv = arg.slice(10);
|
|
@@ -319,6 +343,10 @@ var init_chrome_prelaunch = __esm({
|
|
|
319
343
|
watchFromArgv: false
|
|
320
344
|
}
|
|
321
345
|
));
|
|
346
|
+
isDaemonClientRun = isRunCommand && cmd !== "daemon" && !watchFromArgv && !openFromArgv && !process.env.QUNITX_NO_DAEMON && !process.argv.includes("--no-daemon") && (!process.env.CI || Boolean(process.env.QUNITX_DAEMON)) && // Check the info file rather than the socket path: on Windows the socket is a named
|
|
347
|
+
// pipe (\\.\pipe\...), which existsSync cannot see. The info file is always a regular
|
|
348
|
+
// file in os.tmpdir() and is created/removed in lockstep with the daemon's lifetime.
|
|
349
|
+
(Boolean(process.env.QUNITX_DAEMON) || existsSync(daemonInfoPath()));
|
|
322
350
|
openWatchMode = openFromArgv && watchFromArgv;
|
|
323
351
|
earlyChrome = null;
|
|
324
352
|
if (!openWatchMode) {
|
|
@@ -328,7 +356,7 @@ var init_chrome_prelaunch = __esm({
|
|
|
328
356
|
});
|
|
329
357
|
}
|
|
330
358
|
perfLog("chrome-prelaunch.ts: module evaluated");
|
|
331
|
-
prelaunchPromise = isRunCommand && browserFromArgv === "chromium" && process.platform !== "darwin" ? findChrome().then((chromePath) => {
|
|
359
|
+
prelaunchPromise = isRunCommand && !isDaemonClientRun && browserFromArgv === "chromium" && process.platform !== "darwin" ? findChrome().then((chromePath) => {
|
|
332
360
|
perfLog("chrome-prelaunch.ts: findChrome resolved", chromePath);
|
|
333
361
|
return preLaunchChrome(chromePath, CHROMIUM_ARGS, !openWatchMode);
|
|
334
362
|
}).then((info) => {
|
|
@@ -339,6 +367,91 @@ var init_chrome_prelaunch = __esm({
|
|
|
339
367
|
}
|
|
340
368
|
});
|
|
341
369
|
|
|
370
|
+
// package.json
|
|
371
|
+
var package_default;
|
|
372
|
+
var init_package = __esm({
|
|
373
|
+
"package.json"() {
|
|
374
|
+
package_default = {
|
|
375
|
+
name: "qunitx-cli",
|
|
376
|
+
type: "module",
|
|
377
|
+
version: "0.23.0",
|
|
378
|
+
description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
|
|
379
|
+
author: "Izel Nakri",
|
|
380
|
+
license: "MIT",
|
|
381
|
+
keywords: [
|
|
382
|
+
"test runner",
|
|
383
|
+
"testing",
|
|
384
|
+
"browser",
|
|
385
|
+
"ci",
|
|
386
|
+
"qunit",
|
|
387
|
+
"qunitx"
|
|
388
|
+
],
|
|
389
|
+
files: [
|
|
390
|
+
"bin/",
|
|
391
|
+
"dist/",
|
|
392
|
+
"templates/"
|
|
393
|
+
],
|
|
394
|
+
scripts: {
|
|
395
|
+
build: "node scripts/build-cli.js",
|
|
396
|
+
bin: "chmod +x cli.ts && ./cli.ts",
|
|
397
|
+
prepublishOnly: "npm run build",
|
|
398
|
+
format: 'prettier --check "lib/**/*.ts" "test/**/*.ts" "scripts/**/*.js" "bin/**/*.js" "*.ts" "package.json" ".github/**/*.yml"',
|
|
399
|
+
"format:fix": 'prettier --write "lib/**/*.ts" "test/**/*.ts" "scripts/**/*.js" "bin/**/*.js" "*.ts" "package.json" ".github/**/*.yml"',
|
|
400
|
+
lint: "deno lint lib/ bin/ cli.ts",
|
|
401
|
+
"lint:docs": "node scripts/lint-docs.js",
|
|
402
|
+
docs: `deno doc --html --name="qunitx-cli" --output=docs/lib 'lib/**/*.ts' README.md`,
|
|
403
|
+
"changelog:unreleased": "git-cliff --unreleased --strip all",
|
|
404
|
+
"changelog:preview": "git-cliff",
|
|
405
|
+
"changelog:update": "git-cliff --output CHANGELOG.md",
|
|
406
|
+
postinstall: "PLAYWRIGHT_SKIP_DOWNLOAD=true deno install --allow-scripts=npm:playwright-core || true",
|
|
407
|
+
test: "node test/runner.ts",
|
|
408
|
+
"test:debug": "QUNITX_DEBUG=1 node test/runner.ts",
|
|
409
|
+
dev: "node test/runner.ts --watch",
|
|
410
|
+
"test:browser": "node test/runner.ts test/flags/*-test.ts test/inputs/*-test.ts",
|
|
411
|
+
"test:release": "bash scripts/test-release.sh",
|
|
412
|
+
"test:sanity-first": "./cli.ts test/helpers/failing-tests.js test/helpers/failing-tests.ts",
|
|
413
|
+
"test:sanity-second": "./cli.ts test/helpers/passing-tests.js test/helpers/passing-tests.ts"
|
|
414
|
+
},
|
|
415
|
+
engines: {
|
|
416
|
+
node: ">=24.0.0",
|
|
417
|
+
deno: ">=2.7.0"
|
|
418
|
+
},
|
|
419
|
+
bin: {
|
|
420
|
+
qunitx: "bin/qunitx.js"
|
|
421
|
+
},
|
|
422
|
+
repository: {
|
|
423
|
+
type: "git",
|
|
424
|
+
url: "git+https://github.com/izelnakri/qunitx-cli.git"
|
|
425
|
+
},
|
|
426
|
+
dependencies: {
|
|
427
|
+
esbuild: "^0.28.0",
|
|
428
|
+
"playwright-core": "^1.59.1",
|
|
429
|
+
ws: "^8.20.0"
|
|
430
|
+
},
|
|
431
|
+
devDependencies: {
|
|
432
|
+
"js-yaml": "^4.1.1",
|
|
433
|
+
prettier: "^3.8.3",
|
|
434
|
+
qunitx: "^1.2.9",
|
|
435
|
+
react: "^19.2.5",
|
|
436
|
+
"react-dom": "^19.2.5",
|
|
437
|
+
typescript: "^6.0.3",
|
|
438
|
+
vue: "^3.5.33"
|
|
439
|
+
},
|
|
440
|
+
volta: {
|
|
441
|
+
node: "24.14.0"
|
|
442
|
+
},
|
|
443
|
+
prettier: {
|
|
444
|
+
printWidth: 100,
|
|
445
|
+
singleQuote: true,
|
|
446
|
+
arrowParens: "always"
|
|
447
|
+
},
|
|
448
|
+
optionalDependencies: {
|
|
449
|
+
"qunitx-cli-linux-x64": "*"
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
|
|
342
455
|
// lib/utils/color.ts
|
|
343
456
|
function createColors(enabled2) {
|
|
344
457
|
const makeColor = (open, close) => (text) => enabled2 ? `\x1B[${open}m${text}\x1B[${close}m` : String(text);
|
|
@@ -377,18 +490,109 @@ var init_color = __esm({
|
|
|
377
490
|
}
|
|
378
491
|
});
|
|
379
492
|
|
|
380
|
-
// lib/
|
|
381
|
-
var
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
493
|
+
// lib/commands/help.ts
|
|
494
|
+
var help_exports = {};
|
|
495
|
+
__export(help_exports, {
|
|
496
|
+
default: () => displayHelpOutput,
|
|
497
|
+
displayHelpOutput: () => displayHelpOutput
|
|
498
|
+
});
|
|
499
|
+
function displayHelpOutput() {
|
|
500
|
+
const config = package_default;
|
|
501
|
+
console.log(`${highlight("[qunitx v" + config.version + "] Usage:")} qunitx ${color("[targets] --$flags")}
|
|
502
|
+
|
|
503
|
+
${highlight("Input options:")}
|
|
504
|
+
- File: $ ${color("qunitx test/foo.js")}
|
|
505
|
+
- Folder: $ ${color("qunitx test/login")}
|
|
506
|
+
- Globs: $ ${color("qunitx test/**/*-test.js")}
|
|
507
|
+
- Combination: $ ${color("qunitx test/foo.js test/bar.js test/*-test.js test/logout")}
|
|
508
|
+
|
|
509
|
+
${highlight("Optional flags:")}
|
|
510
|
+
${color("--debug")} : print console output when tests run in browser
|
|
511
|
+
${color("--watch")} : run the target file or folders, watch them for continuous run and expose http server under localhost
|
|
512
|
+
${color("--open")} : run tests in a visible browser window instead of headless; keeps the server alive (short: ${color("-o")})
|
|
513
|
+
${color("--timeout")} : change default timeout per test case
|
|
514
|
+
${color("--output")} : folder to distribute built qunitx html and js that a webservers can run[default: tmp]
|
|
515
|
+
${color("--failFast")} : run the target file or folders with immediate abort if a single test fails
|
|
516
|
+
${color("--port")} : HTTP server port (auto-selects a free port if the given port is taken)[default: 1234]
|
|
517
|
+
${color("--extensions")} : comma-separated file extensions to track for discovery and watch-mode rebuilds[default: js,ts,jsx,tsx]
|
|
518
|
+
${color("--browser")} : browser engine to run tests in: chromium, firefox, webkit[default: chromium]
|
|
519
|
+
${color("--before")} : run a script before the tests(i.e start a new web server before tests)
|
|
520
|
+
${color("--after")} : run a script after the tests(i.e save test results to a file)
|
|
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
|
+
|
|
523
|
+
${highlight("Example:")} $ ${color("qunitx test/foo.ts app/e2e --debug --watch --before=scripts/start-new-webserver.js --after=scripts/write-test-results.js")}
|
|
524
|
+
|
|
525
|
+
${highlight("Commands:")}
|
|
526
|
+
${color("$ qunitx init")} # Bootstraps qunitx base html and add qunitx config to package.json if needed
|
|
527
|
+
${color("$ qunitx new $testFileName")} # Creates a qunitx test file
|
|
528
|
+
${color("$ qunitx daemon <start|stop|status>")} # Optional persistent daemon \u2014 ~2\xD7 faster repeated runs
|
|
529
|
+
|
|
530
|
+
${highlight("Environment:")}
|
|
531
|
+
${color("QUNITX_DAEMON=1")} : auto-spawn the daemon on the first qunitx run; reuse it on every run after (overrides the CI=1 bypass)
|
|
532
|
+
${color("QUNITX_NO_DAEMON=1")} : never use the daemon for this run
|
|
533
|
+
`);
|
|
534
|
+
}
|
|
535
|
+
var highlight, color;
|
|
536
|
+
var init_help = __esm({
|
|
537
|
+
"lib/commands/help.ts"() {
|
|
538
|
+
init_color();
|
|
539
|
+
init_package();
|
|
540
|
+
highlight = (text) => magenta().bold(text);
|
|
541
|
+
color = (text) => blue(text);
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
// lib/utils/path-exists.ts
|
|
546
|
+
import fs2 from "node:fs/promises";
|
|
547
|
+
async function pathExists(path14) {
|
|
548
|
+
try {
|
|
549
|
+
await fs2.access(path14);
|
|
550
|
+
return true;
|
|
551
|
+
} catch {
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
var init_path_exists = __esm({
|
|
556
|
+
"lib/utils/path-exists.ts"() {
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
// lib/utils/search-in-parent-directories.ts
|
|
561
|
+
async function searchInParentDirectories(directory, targetEntry) {
|
|
562
|
+
const resolvedDirectory = directory === "." ? process.cwd() : directory;
|
|
563
|
+
if (await pathExists(`${resolvedDirectory}/${targetEntry}`)) {
|
|
564
|
+
return `${resolvedDirectory}/${targetEntry}`;
|
|
565
|
+
} else if (resolvedDirectory === "") {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
return await searchInParentDirectories(
|
|
569
|
+
resolvedDirectory.slice(0, resolvedDirectory.lastIndexOf("/")),
|
|
570
|
+
targetEntry
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
var init_search_in_parent_directories = __esm({
|
|
574
|
+
"lib/utils/search-in-parent-directories.ts"() {
|
|
575
|
+
init_path_exists();
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// lib/utils/find-project-root.ts
|
|
580
|
+
import process2 from "node:process";
|
|
581
|
+
async function findProjectRoot() {
|
|
582
|
+
try {
|
|
583
|
+
const absolutePath = await searchInParentDirectories(".", "package.json");
|
|
584
|
+
if (!absolutePath.includes("package.json")) {
|
|
585
|
+
throw new Error("package.json mising");
|
|
586
|
+
}
|
|
587
|
+
return absolutePath.replace("/package.json", "");
|
|
588
|
+
} catch (_error) {
|
|
589
|
+
console.log("couldnt find projects package.json, did you run $ npm init ??");
|
|
590
|
+
process2.exit(1);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
var init_find_project_root = __esm({
|
|
594
|
+
"lib/utils/find-project-root.ts"() {
|
|
595
|
+
init_search_in_parent_directories();
|
|
392
596
|
}
|
|
393
597
|
});
|
|
394
598
|
|
|
@@ -415,280 +619,892 @@ var init_read_template = __esm({
|
|
|
415
619
|
}
|
|
416
620
|
});
|
|
417
621
|
|
|
418
|
-
// lib/utils/
|
|
419
|
-
function
|
|
420
|
-
|
|
421
|
-
const scripts = [...htmlContent.matchAll(SCRIPT_SRC_REGEX)].map((match) => match[1]).filter((uri) => !ABSOLUTE_URL_REGEX.test(uri));
|
|
422
|
-
return links.concat(scripts);
|
|
622
|
+
// lib/utils/convert-to-pascal-case.ts
|
|
623
|
+
function convertToPascalCase(str) {
|
|
624
|
+
return str.split(/[-_]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
423
625
|
}
|
|
424
|
-
var
|
|
425
|
-
|
|
426
|
-
"lib/utils/find-internal-assets-from-html.ts"() {
|
|
427
|
-
ABSOLUTE_URL_REGEX = /^(?:[a-z]+:)?\/\//i;
|
|
428
|
-
SCRIPT_SRC_REGEX = /<script[^>]+\bsrc=['"]([^'"]+)['"]/gi;
|
|
429
|
-
LINK_HREF_REGEX = /<link[^>]+\bhref=['"]([^'"]+)['"]/gi;
|
|
626
|
+
var init_convert_to_pascal_case = __esm({
|
|
627
|
+
"lib/utils/convert-to-pascal-case.ts"() {
|
|
430
628
|
}
|
|
431
629
|
});
|
|
432
630
|
|
|
433
|
-
// lib/
|
|
434
|
-
|
|
435
|
-
|
|
631
|
+
// lib/commands/generate.ts
|
|
632
|
+
var generate_exports = {};
|
|
633
|
+
__export(generate_exports, {
|
|
634
|
+
default: () => generateTestFiles,
|
|
635
|
+
generateTestFiles: () => generateTestFiles
|
|
636
|
+
});
|
|
637
|
+
import fs4 from "node:fs/promises";
|
|
638
|
+
async function generateTestFiles() {
|
|
639
|
+
const projectRoot = await findProjectRoot();
|
|
640
|
+
const moduleName = pathToModuleName(process.argv[3]);
|
|
641
|
+
const path14 = process.argv[3].endsWith(".js") || process.argv[3].endsWith(".ts") ? `${projectRoot}/${process.argv[3]}` : `${projectRoot}/${process.argv[3]}.js`;
|
|
642
|
+
if (await pathExists(path14)) {
|
|
643
|
+
console.log(`${path14} already exists!`);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
const testJSContent = await readTemplate("test.js");
|
|
647
|
+
const targetFolderPaths = path14.split("/");
|
|
648
|
+
targetFolderPaths.pop();
|
|
649
|
+
await fs4.mkdir(targetFolderPaths.join("/"), { recursive: true });
|
|
650
|
+
await fs4.writeFile(path14, testJSContent.replace("{{moduleName}}", moduleName));
|
|
651
|
+
console.log(green(`${path14} written`));
|
|
436
652
|
}
|
|
437
|
-
function
|
|
438
|
-
|
|
653
|
+
function pathToModuleName(filePath) {
|
|
654
|
+
const withoutExt = filePath.replace(/\.(js|ts)$/, "");
|
|
655
|
+
const segments = withoutExt.split("/");
|
|
656
|
+
const targetNames = segments[0] === "test" || segments[0] === "tests" ? segments.slice(1) : segments;
|
|
657
|
+
return targetNames.map(convertToPascalCase).join(" | ");
|
|
439
658
|
}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
return html.replace("</body>", `${content}</body>`);
|
|
448
|
-
}
|
|
449
|
-
if (html.includes("</html>")) {
|
|
450
|
-
return html.replace("</html>", `${content}</html>`);
|
|
451
|
-
}
|
|
452
|
-
return `${html}${content}`;
|
|
659
|
+
var init_generate = __esm({
|
|
660
|
+
"lib/commands/generate.ts"() {
|
|
661
|
+
init_color();
|
|
662
|
+
init_find_project_root();
|
|
663
|
+
init_path_exists();
|
|
664
|
+
init_read_template();
|
|
665
|
+
init_convert_to_pascal_case();
|
|
453
666
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
var
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
// lib/setup/default-project-config-values.ts
|
|
670
|
+
var defaultProjectConfigValues;
|
|
671
|
+
var init_default_project_config_values = __esm({
|
|
672
|
+
"lib/setup/default-project-config-values.ts"() {
|
|
673
|
+
defaultProjectConfigValues = {
|
|
674
|
+
output: "tmp",
|
|
675
|
+
timeout: 2e4,
|
|
676
|
+
failFast: false,
|
|
677
|
+
port: 1234,
|
|
678
|
+
extensions: ["js", "ts", "jsx", "tsx"],
|
|
679
|
+
browser: "chromium"
|
|
680
|
+
};
|
|
461
681
|
}
|
|
462
682
|
});
|
|
463
683
|
|
|
464
|
-
// lib/
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
684
|
+
// lib/commands/init.ts
|
|
685
|
+
var init_exports = {};
|
|
686
|
+
__export(init_exports, {
|
|
687
|
+
default: () => initializeProject,
|
|
688
|
+
initializeProject: () => initializeProject
|
|
689
|
+
});
|
|
690
|
+
import fs5 from "node:fs/promises";
|
|
691
|
+
import path3 from "node:path";
|
|
692
|
+
async function initializeProject() {
|
|
693
|
+
const projectRoot = await findProjectRoot();
|
|
694
|
+
const oldPackageJSON = JSON.parse(await fs5.readFile(`${projectRoot}/package.json`));
|
|
695
|
+
const existingQunitx = oldPackageJSON.qunitx || {};
|
|
696
|
+
const cliHtmlPaths = process.argv.slice(2).filter((arg) => arg.endsWith(".html"));
|
|
697
|
+
const config = Object.assign({}, defaultProjectConfigValues, existingQunitx, {
|
|
698
|
+
htmlPaths: cliHtmlPaths.length > 0 ? cliHtmlPaths : existingQunitx.htmlPaths || ["test/tests.html"]
|
|
699
|
+
});
|
|
700
|
+
await Promise.all([
|
|
701
|
+
writeTestsHTML(projectRoot, config, oldPackageJSON),
|
|
702
|
+
rewritePackageJSON(projectRoot, config, oldPackageJSON),
|
|
703
|
+
writeTSConfigIfNeeded(projectRoot)
|
|
704
|
+
]);
|
|
476
705
|
}
|
|
477
|
-
function
|
|
478
|
-
|
|
706
|
+
async function writeTestsHTML(projectRoot, config, oldPackageJSON) {
|
|
707
|
+
const testHTMLTemplateBuffer = await readTemplate("setup/tests.hbs");
|
|
708
|
+
return await Promise.all(
|
|
709
|
+
config.htmlPaths.map(async (htmlPath) => {
|
|
710
|
+
const targetPath = `${projectRoot}/${htmlPath}`;
|
|
711
|
+
if (await pathExists(targetPath)) {
|
|
712
|
+
return console.log(`${htmlPath} already exists`);
|
|
713
|
+
} else {
|
|
714
|
+
const targetDirectory = path3.dirname(targetPath);
|
|
715
|
+
const _targetOutputPath = path3.relative(
|
|
716
|
+
targetDirectory,
|
|
717
|
+
path3.join(path3.resolve(projectRoot, config.output), "tests.js")
|
|
718
|
+
);
|
|
719
|
+
const testHTMLTemplate = testHTMLTemplateBuffer.replace(
|
|
720
|
+
"{{applicationName}}",
|
|
721
|
+
oldPackageJSON.name
|
|
722
|
+
);
|
|
723
|
+
await fs5.mkdir(targetDirectory, { recursive: true });
|
|
724
|
+
await fs5.writeFile(targetPath, testHTMLTemplate);
|
|
725
|
+
console.log(`${targetPath} written`);
|
|
726
|
+
}
|
|
727
|
+
})
|
|
728
|
+
);
|
|
479
729
|
}
|
|
480
|
-
function
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
730
|
+
async function rewritePackageJSON(projectRoot, config, oldPackageJSON) {
|
|
731
|
+
const newPackageJSON = Object.assign(oldPackageJSON, { qunitx: config });
|
|
732
|
+
await fs5.writeFile(`${projectRoot}/package.json`, JSON.stringify(newPackageJSON, null, 2));
|
|
733
|
+
}
|
|
734
|
+
async function writeTSConfigIfNeeded(projectRoot) {
|
|
735
|
+
const targetPath = `${projectRoot}/tsconfig.json`;
|
|
736
|
+
if (!await pathExists(targetPath)) {
|
|
737
|
+
const tsConfigTemplate = await readTemplate("setup/tsconfig.json");
|
|
738
|
+
await fs5.writeFile(targetPath, tsConfigTemplate);
|
|
739
|
+
console.log(`${targetPath} written`);
|
|
484
740
|
}
|
|
485
|
-
if (needsQuoting(str)) return `'${str.replace(/'/g, "''")}'`;
|
|
486
|
-
return str;
|
|
487
741
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
const next2 = `${indent} `;
|
|
495
|
-
return "\n" + value.map((item) => {
|
|
496
|
-
const v = dumpValue(item, next2);
|
|
497
|
-
return v[0] === "\n" ? `${next2}-${v}` : `${next2}- ${v}`;
|
|
498
|
-
}).join("\n");
|
|
499
|
-
}
|
|
500
|
-
const entries = Object.entries(value);
|
|
501
|
-
if (entries.length === 0) return "{}";
|
|
502
|
-
const next = `${indent} `;
|
|
503
|
-
return "\n" + entries.map(([entryKey, entryValue]) => {
|
|
504
|
-
const v = dumpValue(entryValue, next);
|
|
505
|
-
return v[0] === "\n" ? `${next}${entryKey}:${v}` : `${next}${entryKey}: ${v}`;
|
|
506
|
-
}).join("\n");
|
|
507
|
-
}
|
|
508
|
-
function yamlLine(key, value) {
|
|
509
|
-
const serialized = dumpValue(value, "");
|
|
510
|
-
return serialized[0] === "\n" ? `${key}:${serialized}
|
|
511
|
-
` : `${key}: ${serialized}
|
|
512
|
-
`;
|
|
513
|
-
}
|
|
514
|
-
var NEEDS_QUOTING;
|
|
515
|
-
var init_dump_yaml = __esm({
|
|
516
|
-
"lib/tap/dump-yaml.ts"() {
|
|
517
|
-
NEEDS_QUOTING = /^$|^\s|^(null|true|false|~|yes|no|on|off|y|n)$|^[{[!|>'"#%@`]|^[-?:](\s|$)|^---|^[-+]?(\d|\.\d)|^\d{4}-\d{2}-\d{2}|: |#/i;
|
|
742
|
+
var init_init = __esm({
|
|
743
|
+
"lib/commands/init.ts"() {
|
|
744
|
+
init_find_project_root();
|
|
745
|
+
init_path_exists();
|
|
746
|
+
init_default_project_config_values();
|
|
747
|
+
init_read_template();
|
|
518
748
|
}
|
|
519
749
|
});
|
|
520
750
|
|
|
521
|
-
// lib/utils/
|
|
522
|
-
function
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
751
|
+
// lib/utils/close-with-grace.ts
|
|
752
|
+
function closeWithGrace(closes, graceMs = CLEANUP_GRACE_MS) {
|
|
753
|
+
return new Promise((resolve) => {
|
|
754
|
+
const timer = setTimeout(() => {
|
|
755
|
+
process.stderr.write(`# qunitx: cleanup timed out after ${graceMs} ms \u2014 exiting anyway
|
|
756
|
+
`);
|
|
757
|
+
resolve();
|
|
758
|
+
}, graceMs);
|
|
759
|
+
Promise.allSettled(closes).then(() => {
|
|
760
|
+
clearTimeout(timer);
|
|
761
|
+
resolve();
|
|
762
|
+
});
|
|
763
|
+
});
|
|
529
764
|
}
|
|
530
|
-
var
|
|
531
|
-
|
|
765
|
+
var CLEANUP_GRACE_MS;
|
|
766
|
+
var init_close_with_grace = __esm({
|
|
767
|
+
"lib/utils/close-with-grace.ts"() {
|
|
768
|
+
CLEANUP_GRACE_MS = 1e4;
|
|
532
769
|
}
|
|
533
770
|
});
|
|
534
771
|
|
|
535
|
-
// lib/
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
sourceIndex += readVlqAt(mappings, cursor);
|
|
549
|
-
sourceLine += readVlqAt(mappings, cursor);
|
|
550
|
-
sourceCol += readVlqAt(mappings, cursor);
|
|
551
|
-
segments.push({ generatedCol, sourceIndex, sourceLine, sourceCol });
|
|
552
|
-
if (atFieldStart(mappings, cursor.position)) readVlqAt(mappings, cursor);
|
|
772
|
+
// lib/commands/daemon/socket-utils.ts
|
|
773
|
+
import net from "node:net";
|
|
774
|
+
function attachLineParser(socket, onLine) {
|
|
775
|
+
let buf = "";
|
|
776
|
+
socket.on("data", (chunk) => {
|
|
777
|
+
buf += chunk.toString("utf8");
|
|
778
|
+
const lines = buf.split("\n");
|
|
779
|
+
buf = lines.pop() ?? "";
|
|
780
|
+
for (const line of lines) {
|
|
781
|
+
if (!line) continue;
|
|
782
|
+
try {
|
|
783
|
+
onLine(JSON.parse(line));
|
|
784
|
+
} catch {
|
|
553
785
|
}
|
|
554
|
-
} else if (charCode === COMMA) {
|
|
555
|
-
cursor.position++;
|
|
556
|
-
} else {
|
|
557
|
-
result.push(segments);
|
|
558
|
-
segments = [];
|
|
559
|
-
generatedCol = 0;
|
|
560
|
-
cursor.position++;
|
|
561
786
|
}
|
|
562
|
-
}
|
|
563
|
-
result.push(segments);
|
|
564
|
-
return result;
|
|
787
|
+
});
|
|
565
788
|
}
|
|
566
|
-
function
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
789
|
+
function probeSocket(socketPath, timeoutMs) {
|
|
790
|
+
return new Promise((resolve) => {
|
|
791
|
+
const sock = net.createConnection(socketPath);
|
|
792
|
+
const timer = setTimeout(() => {
|
|
793
|
+
sock.destroy();
|
|
794
|
+
resolve(null);
|
|
795
|
+
}, timeoutMs);
|
|
796
|
+
sock.once("connect", () => {
|
|
797
|
+
clearTimeout(timer);
|
|
798
|
+
resolve(sock);
|
|
799
|
+
});
|
|
800
|
+
sock.once("error", () => {
|
|
801
|
+
clearTimeout(timer);
|
|
802
|
+
resolve(null);
|
|
803
|
+
});
|
|
804
|
+
});
|
|
575
805
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
806
|
+
var init_socket_utils = __esm({
|
|
807
|
+
"lib/commands/daemon/socket-utils.ts"() {
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
// lib/commands/daemon/client.ts
|
|
812
|
+
var client_exports = {};
|
|
813
|
+
__export(client_exports, {
|
|
814
|
+
pingDaemon: () => pingDaemon,
|
|
815
|
+
runViaDaemon: () => runViaDaemon,
|
|
816
|
+
shouldAutoSpawnDaemon: () => shouldAutoSpawnDaemon,
|
|
817
|
+
shouldUseDaemon: () => shouldUseDaemon,
|
|
818
|
+
shutdownDaemon: () => shutdownDaemon,
|
|
819
|
+
tryConnect: () => tryConnect
|
|
820
|
+
});
|
|
821
|
+
import fs6 from "node:fs/promises";
|
|
822
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
823
|
+
function isDaemonEligible() {
|
|
824
|
+
if (process.env.QUNITX_NO_DAEMON) return false;
|
|
825
|
+
if (process.env.CI && !process.env.QUNITX_DAEMON) return false;
|
|
826
|
+
for (const arg of process.argv) {
|
|
827
|
+
if (arg === "--no-daemon") return false;
|
|
828
|
+
if (arg === "--watch" || arg === "-w") return false;
|
|
829
|
+
if (arg === "--open" || arg === "-o" || arg.startsWith("--open=")) return false;
|
|
584
830
|
}
|
|
831
|
+
return true;
|
|
585
832
|
}
|
|
586
|
-
function
|
|
587
|
-
|
|
588
|
-
if (!segments?.length) return null;
|
|
589
|
-
const targetCol = generatedCol - 1;
|
|
590
|
-
const segment = segments.findLast((s) => s.generatedCol <= targetCol);
|
|
591
|
-
if (!segment) return null;
|
|
592
|
-
const rawSource = decoder.sources[segment.sourceIndex];
|
|
593
|
-
if (!rawSource) return null;
|
|
594
|
-
return {
|
|
595
|
-
absolutePath: toAbsolutePath(rawSource, decoder.outDir, decoder.sourceRoot),
|
|
596
|
-
line: segment.sourceLine + 1,
|
|
597
|
-
col: segment.sourceCol + 1,
|
|
598
|
-
sourceText: extractSourceLine(decoder.sourcesContent[segment.sourceIndex], segment.sourceLine)
|
|
599
|
-
};
|
|
833
|
+
function shouldUseDaemon() {
|
|
834
|
+
return isDaemonEligible() && existsSync2(daemonInfoPath());
|
|
600
835
|
}
|
|
601
|
-
function
|
|
602
|
-
|
|
603
|
-
return match ? { url: match[1], line: +match[2], col: +match[3] } : null;
|
|
836
|
+
function shouldAutoSpawnDaemon() {
|
|
837
|
+
return Boolean(process.env.QUNITX_DAEMON) && isDaemonEligible() && !existsSync2(daemonInfoPath());
|
|
604
838
|
}
|
|
605
|
-
function
|
|
606
|
-
return
|
|
839
|
+
function tryConnect(cwd = process.cwd()) {
|
|
840
|
+
return probeSocket(daemonSocketPath(cwd), CONNECT_TIMEOUT_MS);
|
|
607
841
|
}
|
|
608
|
-
function
|
|
609
|
-
|
|
610
|
-
({ match }) => match !== null
|
|
611
|
-
);
|
|
612
|
-
if (!hit?.match) return null;
|
|
613
|
-
const original = tryResolve(hit.match[hit.format.urlGroup], decoder, projectRoot);
|
|
614
|
-
return original && {
|
|
615
|
-
resolved: hit.format.format(hit.match, original.display),
|
|
616
|
-
userPath: original.userPath,
|
|
617
|
-
sourceText: original.sourceText
|
|
618
|
-
};
|
|
842
|
+
function send(socket, req) {
|
|
843
|
+
socket.write(JSON.stringify(req) + "\n");
|
|
619
844
|
}
|
|
620
|
-
function
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
userPath: resolution?.userPath ?? null,
|
|
626
|
-
sourceText: resolution?.sourceText ?? null
|
|
627
|
-
};
|
|
845
|
+
function awaitClose(socket) {
|
|
846
|
+
return new Promise((resolve) => {
|
|
847
|
+
socket.once("end", () => resolve());
|
|
848
|
+
socket.once("close", () => resolve());
|
|
849
|
+
socket.once("error", () => resolve());
|
|
628
850
|
});
|
|
629
|
-
const firstUser = decoratedFrames.find((entry) => entry.userPath !== null);
|
|
630
|
-
return {
|
|
631
|
-
resolvedStack: decoratedFrames.map((entry) => entry.line).join("\n"),
|
|
632
|
-
firstUserFrame: firstUser?.userPath ?? null,
|
|
633
|
-
firstUserSourceText: firstUser?.sourceText ?? null
|
|
634
|
-
};
|
|
635
|
-
}
|
|
636
|
-
function readVlqAt(text, cursor) {
|
|
637
|
-
let value = 0, bitShift = 0, position = cursor.position;
|
|
638
|
-
for (; ; ) {
|
|
639
|
-
const digit = BASE64_LOOKUP[text.charCodeAt(position++)];
|
|
640
|
-
value |= (digit & VLQ_DATA_MASK) << bitShift;
|
|
641
|
-
if (!(digit & VLQ_CONTINUATION)) break;
|
|
642
|
-
bitShift += 5;
|
|
643
|
-
}
|
|
644
|
-
cursor.position = position;
|
|
645
|
-
return value & 1 ? -(value >>> 1) : value >>> 1;
|
|
646
851
|
}
|
|
647
|
-
function
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
852
|
+
async function pingDaemon() {
|
|
853
|
+
const socket = await tryConnect();
|
|
854
|
+
if (!socket) return null;
|
|
855
|
+
const result = new Promise((resolve) => {
|
|
856
|
+
attachLineParser(socket, (chunk) => {
|
|
857
|
+
if (chunk.type === "pong") resolve(chunk);
|
|
858
|
+
});
|
|
859
|
+
socket.once("close", () => resolve(null));
|
|
860
|
+
socket.once("error", () => resolve(null));
|
|
861
|
+
});
|
|
862
|
+
send(socket, { type: "ping" });
|
|
863
|
+
const pong = await result;
|
|
864
|
+
socket.end();
|
|
865
|
+
return pong;
|
|
866
|
+
}
|
|
867
|
+
async function shutdownDaemon() {
|
|
868
|
+
const pid = await readDaemonPid();
|
|
869
|
+
const socket = await tryConnect();
|
|
870
|
+
if (!socket) return false;
|
|
871
|
+
attachLineParser(socket, () => {
|
|
872
|
+
});
|
|
873
|
+
send(socket, { type: "shutdown" });
|
|
874
|
+
await awaitClose(socket);
|
|
875
|
+
if (pid !== null) await waitForPidExit(pid, SHUTDOWN_PID_WAIT_MS);
|
|
876
|
+
return true;
|
|
651
877
|
}
|
|
652
|
-
function
|
|
653
|
-
|
|
654
|
-
|
|
878
|
+
async function readDaemonPid() {
|
|
879
|
+
try {
|
|
880
|
+
const info = JSON.parse(await fs6.readFile(daemonInfoPath(), "utf8"));
|
|
881
|
+
return typeof info.pid === "number" ? info.pid : null;
|
|
882
|
+
} catch {
|
|
883
|
+
return null;
|
|
884
|
+
}
|
|
655
885
|
}
|
|
656
|
-
function
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
886
|
+
function waitForPidExit(pid, timeoutMs) {
|
|
887
|
+
return new Promise((resolve) => {
|
|
888
|
+
const deadline = Date.now() + timeoutMs;
|
|
889
|
+
const poll = () => {
|
|
890
|
+
if (!pidIsAlive(pid) || Date.now() >= deadline) return resolve();
|
|
891
|
+
setTimeout(poll, SHUTDOWN_PID_POLL_MS);
|
|
892
|
+
};
|
|
893
|
+
poll();
|
|
894
|
+
});
|
|
660
895
|
}
|
|
661
|
-
function
|
|
662
|
-
|
|
896
|
+
function pidIsAlive(pid) {
|
|
897
|
+
try {
|
|
898
|
+
process.kill(pid, 0);
|
|
899
|
+
return true;
|
|
900
|
+
} catch (err) {
|
|
901
|
+
return err.code === "EPERM";
|
|
902
|
+
}
|
|
663
903
|
}
|
|
664
|
-
function
|
|
665
|
-
const
|
|
666
|
-
if (
|
|
667
|
-
const
|
|
668
|
-
|
|
669
|
-
|
|
904
|
+
async function runViaDaemon(argv) {
|
|
905
|
+
const socket = await tryConnect();
|
|
906
|
+
if (!socket) throw new Error("daemon connect failed");
|
|
907
|
+
const exitCode = new Promise((resolve) => {
|
|
908
|
+
attachLineParser(socket, (chunk) => {
|
|
909
|
+
if (chunk.type === "stdout") process.stdout.write(chunk.data);
|
|
910
|
+
else if (chunk.type === "stderr") process.stderr.write(chunk.data);
|
|
911
|
+
else if (chunk.type === "done") resolve(chunk.exitCode);
|
|
912
|
+
else if (chunk.type === "fatal") {
|
|
913
|
+
process.stderr.write(`# [qunitx daemon] ${chunk.message}
|
|
914
|
+
`);
|
|
915
|
+
resolve(1);
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
socket.once("close", () => resolve(1));
|
|
919
|
+
socket.once("error", () => resolve(1));
|
|
920
|
+
});
|
|
921
|
+
const onSigint = () => {
|
|
922
|
+
socket.end();
|
|
923
|
+
process.exit(SIGINT_EXIT_CODE);
|
|
924
|
+
};
|
|
925
|
+
process.once("SIGINT", onSigint);
|
|
926
|
+
send(socket, {
|
|
927
|
+
type: "run",
|
|
928
|
+
argv,
|
|
929
|
+
cwd: process.cwd(),
|
|
930
|
+
env: { ...process.env },
|
|
931
|
+
nodeVersion: process.version
|
|
932
|
+
});
|
|
933
|
+
try {
|
|
934
|
+
return await exitCode;
|
|
935
|
+
} finally {
|
|
936
|
+
process.removeListener("SIGINT", onSigint);
|
|
937
|
+
}
|
|
670
938
|
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
939
|
+
var CONNECT_TIMEOUT_MS, SIGINT_EXIT_CODE, SHUTDOWN_PID_WAIT_MS, SHUTDOWN_PID_POLL_MS;
|
|
940
|
+
var init_client = __esm({
|
|
941
|
+
"lib/commands/daemon/client.ts"() {
|
|
942
|
+
init_daemon_socket_path();
|
|
943
|
+
init_close_with_grace();
|
|
944
|
+
init_socket_utils();
|
|
945
|
+
CONNECT_TIMEOUT_MS = 1e3;
|
|
946
|
+
SIGINT_EXIT_CODE = 130;
|
|
947
|
+
SHUTDOWN_PID_WAIT_MS = CLEANUP_GRACE_MS;
|
|
948
|
+
SHUTDOWN_PID_POLL_MS = 50;
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
// lib/setup/fs-tree.ts
|
|
953
|
+
import fs7, { glob as fsGlob } from "node:fs/promises";
|
|
954
|
+
import path4 from "node:path";
|
|
955
|
+
async function buildFSTree(fileAbsolutePaths, config = {}) {
|
|
956
|
+
const targetExtensions = config.extensions || defaultProjectConfigValues.extensions;
|
|
957
|
+
const fsTree = {};
|
|
958
|
+
await Promise.all(
|
|
959
|
+
fileAbsolutePaths.map(async (fileAbsolutePath) => {
|
|
960
|
+
try {
|
|
961
|
+
if (isGlob(fileAbsolutePath)) {
|
|
962
|
+
for await (const fileName of fsGlob(fileAbsolutePath)) {
|
|
963
|
+
if (targetExtensions.some((ext) => fileName.endsWith(`.${ext}`))) {
|
|
964
|
+
fsTree[fileName] = null;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
} else {
|
|
968
|
+
const entry = await fs7.stat(fileAbsolutePath);
|
|
969
|
+
if (entry.isFile()) {
|
|
970
|
+
fsTree[fileAbsolutePath] = null;
|
|
971
|
+
} else if (entry.isDirectory()) {
|
|
972
|
+
const fileNames = await readDirRecursive(fileAbsolutePath, (name) => {
|
|
973
|
+
return targetExtensions.some((extension) => name.endsWith(`.${extension}`));
|
|
974
|
+
});
|
|
975
|
+
fileNames.forEach((fileName) => {
|
|
976
|
+
fsTree[fileName] = null;
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
} catch (error) {
|
|
981
|
+
console.error(error);
|
|
982
|
+
return process.exit(1);
|
|
983
|
+
}
|
|
984
|
+
})
|
|
985
|
+
);
|
|
986
|
+
return fsTree;
|
|
679
987
|
}
|
|
680
|
-
function
|
|
681
|
-
|
|
682
|
-
const line = content.split("\n", lineIndex + 1)[lineIndex];
|
|
683
|
-
return line?.trim() || null;
|
|
988
|
+
function isGlob(str) {
|
|
989
|
+
return /[*?{[]/.test(str);
|
|
684
990
|
}
|
|
685
|
-
function
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
991
|
+
async function readDirRecursive(dir, filter) {
|
|
992
|
+
const entries = await fs7.readdir(dir, { recursive: true, withFileTypes: true });
|
|
993
|
+
const candidates = entries.filter(
|
|
994
|
+
(dirent) => (dirent.isFile() || dirent.isSymbolicLink()) && filter(dirent.name)
|
|
995
|
+
);
|
|
996
|
+
const resolvedPaths = await Promise.all(
|
|
997
|
+
candidates.map(async (dirent) => {
|
|
998
|
+
const fullPath = path4.join(dirent.parentPath, dirent.name);
|
|
999
|
+
if (dirent.isFile()) return fullPath;
|
|
1000
|
+
try {
|
|
1001
|
+
const statResult = await fs7.stat(fullPath);
|
|
1002
|
+
return statResult.isFile() ? fullPath : null;
|
|
1003
|
+
} catch {
|
|
1004
|
+
return null;
|
|
1005
|
+
}
|
|
1006
|
+
})
|
|
1007
|
+
);
|
|
1008
|
+
return resolvedPaths.filter((resolvedPath) => resolvedPath !== null);
|
|
1009
|
+
}
|
|
1010
|
+
var init_fs_tree = __esm({
|
|
1011
|
+
"lib/setup/fs-tree.ts"() {
|
|
1012
|
+
init_default_project_config_values();
|
|
1013
|
+
}
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
// lib/setup/test-file-paths.ts
|
|
1017
|
+
import { matchesGlob } from "node:path";
|
|
1018
|
+
function setupTestFilePaths(inputs2) {
|
|
1019
|
+
const folders = [];
|
|
1020
|
+
const filesWithGlob = [];
|
|
1021
|
+
const filesWithoutGlob = [];
|
|
1022
|
+
inputs2.forEach((input) => {
|
|
1023
|
+
if (!pathIsFile(input)) {
|
|
1024
|
+
folders.push({ input, globFormat: `${input}/**` });
|
|
1025
|
+
} else if (isGlob2(input)) {
|
|
1026
|
+
filesWithGlob.push({ input, globFormat: input });
|
|
1027
|
+
} else {
|
|
1028
|
+
filesWithoutGlob.push({ input, globFormat: input });
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1031
|
+
const dedupedFolders = folders.filter((folder) => !isIncludedIn(folders, folder));
|
|
1032
|
+
const dedupedGlobFiles = filesWithGlob.filter(
|
|
1033
|
+
(file) => !isIncludedIn(dedupedFolders, file) && !isIncludedIn(filesWithGlob, file)
|
|
1034
|
+
);
|
|
1035
|
+
const dedupedPlainFiles = filesWithoutGlob.reduce((acc, file) => {
|
|
1036
|
+
if (!isIncludedIn(dedupedFolders, file) && !isIncludedIn(dedupedGlobFiles, file) && !isIncludedIn(acc, file)) {
|
|
1037
|
+
acc.push(file);
|
|
1038
|
+
}
|
|
1039
|
+
return acc;
|
|
1040
|
+
}, []);
|
|
1041
|
+
return dedupedFolders.concat(dedupedGlobFiles, dedupedPlainFiles).map((meta) => meta.input);
|
|
1042
|
+
}
|
|
1043
|
+
function pathIsFile(path14) {
|
|
1044
|
+
return path14.includes(".", path14.lastIndexOf("/") + 1);
|
|
1045
|
+
}
|
|
1046
|
+
function isIncludedIn(paths, target) {
|
|
1047
|
+
return paths.some((path14) => path14 !== target && matchesGlob(target.input, path14.globFormat));
|
|
1048
|
+
}
|
|
1049
|
+
function isGlob2(str) {
|
|
1050
|
+
return GLOB_CHARS.test(str);
|
|
1051
|
+
}
|
|
1052
|
+
var GLOB_CHARS;
|
|
1053
|
+
var init_test_file_paths = __esm({
|
|
1054
|
+
"lib/setup/test-file-paths.ts"() {
|
|
1055
|
+
GLOB_CHARS = /[*?{[]/;
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
|
|
1059
|
+
// lib/utils/parse-cli-flags.ts
|
|
1060
|
+
import path5 from "node:path";
|
|
1061
|
+
function parseCliFlags(projectRoot) {
|
|
1062
|
+
const providedFlags = process.argv.slice(2).reduce(
|
|
1063
|
+
(result, arg) => {
|
|
1064
|
+
if (arg.startsWith("--debug")) {
|
|
1065
|
+
return Object.assign(result, { debug: parseBoolean(arg.split("=")[1]) });
|
|
1066
|
+
} else if (arg.startsWith("--watch")) {
|
|
1067
|
+
return Object.assign(result, { watch: parseBoolean(arg.split("=")[1]) });
|
|
1068
|
+
} else if (arg === "-o" || arg.startsWith("-o=") || arg.startsWith("--open")) {
|
|
1069
|
+
const value = arg.split("=")[1];
|
|
1070
|
+
const open = value === void 0 || value === "true" ? true : value === "false" ? false : value;
|
|
1071
|
+
return Object.assign(result, { open });
|
|
1072
|
+
} else if (arg.startsWith("--failfast") || arg.startsWith("--failFast")) {
|
|
1073
|
+
return Object.assign(result, { failFast: parseBoolean(arg.split("=")[1]) });
|
|
1074
|
+
} else if (arg.startsWith("--timeout")) {
|
|
1075
|
+
return Object.assign(result, { timeout: Number(arg.split("=")[1]) || FALLBACK_TIMEOUT_MS });
|
|
1076
|
+
} else if (arg.startsWith("--output")) {
|
|
1077
|
+
return Object.assign(result, { output: arg.split("=")[1] });
|
|
1078
|
+
} else if (arg.endsWith(".html")) {
|
|
1079
|
+
if (result.htmlPaths) {
|
|
1080
|
+
result.htmlPaths.push(arg);
|
|
1081
|
+
} else {
|
|
1082
|
+
result.htmlPaths = [arg];
|
|
1083
|
+
}
|
|
1084
|
+
return result;
|
|
1085
|
+
} else if (arg.startsWith("--port")) {
|
|
1086
|
+
return Object.assign(result, { port: Number(arg.split("=")[1]), portExplicit: true });
|
|
1087
|
+
} else if (arg.startsWith("--extensions")) {
|
|
1088
|
+
return Object.assign(result, {
|
|
1089
|
+
extensions: arg.split("=")[1].split(",").map((extension) => extension.trim())
|
|
1090
|
+
});
|
|
1091
|
+
} else if (arg.startsWith("--browser")) {
|
|
1092
|
+
const value = arg.split("=")[1];
|
|
1093
|
+
if (!["chromium", "firefox", "webkit"].includes(value)) {
|
|
1094
|
+
console.error(
|
|
1095
|
+
`Invalid --browser value: "${value}". Must be one of: chromium, firefox, webkit`
|
|
1096
|
+
);
|
|
1097
|
+
process.exit(1);
|
|
1098
|
+
}
|
|
1099
|
+
return Object.assign(result, { browser: value });
|
|
1100
|
+
} else if (arg.startsWith("--before")) {
|
|
1101
|
+
return Object.assign(result, { before: parseModule(arg.split("=")[1]) });
|
|
1102
|
+
} else if (arg.startsWith("--after")) {
|
|
1103
|
+
return Object.assign(result, { after: parseModule(arg.split("=")[1]) });
|
|
1104
|
+
} else if (arg === "--trace-perf") {
|
|
1105
|
+
return result;
|
|
1106
|
+
}
|
|
1107
|
+
if (arg.startsWith("-")) {
|
|
1108
|
+
console.warn(`# Warning: Unknown flag "${arg}" \u2014 ignored`);
|
|
1109
|
+
return result;
|
|
1110
|
+
}
|
|
1111
|
+
result.inputs.add(
|
|
1112
|
+
arg.startsWith(projectRoot) || arg.startsWith("/") ? arg : path5.join(process.cwd(), arg)
|
|
1113
|
+
);
|
|
1114
|
+
return result;
|
|
1115
|
+
},
|
|
1116
|
+
{ inputs: /* @__PURE__ */ new Set([]) }
|
|
1117
|
+
);
|
|
1118
|
+
if (!providedFlags.browser && process.env.QUNITX_BROWSER) {
|
|
1119
|
+
const envBrowser = process.env.QUNITX_BROWSER;
|
|
1120
|
+
if (!["chromium", "firefox", "webkit"].includes(envBrowser)) {
|
|
1121
|
+
console.error(
|
|
1122
|
+
`Invalid QUNITX_BROWSER value: "${envBrowser}". Must be one of: chromium, firefox, webkit`
|
|
1123
|
+
);
|
|
1124
|
+
process.exit(1);
|
|
1125
|
+
}
|
|
1126
|
+
providedFlags.browser = envBrowser;
|
|
1127
|
+
}
|
|
1128
|
+
return { ...providedFlags, inputs: Array.from(providedFlags.inputs) };
|
|
1129
|
+
}
|
|
1130
|
+
function parseBoolean(result, defaultValue = true) {
|
|
1131
|
+
if (result === "true") {
|
|
1132
|
+
return true;
|
|
1133
|
+
} else if (result === "false") {
|
|
1134
|
+
return false;
|
|
1135
|
+
}
|
|
1136
|
+
return defaultValue;
|
|
1137
|
+
}
|
|
1138
|
+
function parseModule(value) {
|
|
1139
|
+
if (["false", "'false'", '"false"', ""].includes(value)) {
|
|
1140
|
+
return false;
|
|
1141
|
+
}
|
|
1142
|
+
return value;
|
|
1143
|
+
}
|
|
1144
|
+
var FALLBACK_TIMEOUT_MS;
|
|
1145
|
+
var init_parse_cli_flags = __esm({
|
|
1146
|
+
"lib/utils/parse-cli-flags.ts"() {
|
|
1147
|
+
FALLBACK_TIMEOUT_MS = 1e4;
|
|
1148
|
+
}
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
// lib/setup/config.ts
|
|
1152
|
+
var config_exports = {};
|
|
1153
|
+
__export(config_exports, {
|
|
1154
|
+
default: () => setupConfig,
|
|
1155
|
+
setupConfig: () => setupConfig
|
|
1156
|
+
});
|
|
1157
|
+
import fs8 from "node:fs/promises";
|
|
1158
|
+
import { createRequire } from "node:module";
|
|
1159
|
+
import { pathToFileURL } from "node:url";
|
|
1160
|
+
async function setupConfig() {
|
|
1161
|
+
const projectRoot = await findProjectRoot();
|
|
1162
|
+
const cliConfigFlags = parseCliFlags(projectRoot);
|
|
1163
|
+
const projectPackageJSON = await readConfigFromPackageJSON(projectRoot);
|
|
1164
|
+
const { plugins: rawPlugins, ...userQunitx } = projectPackageJSON.qunitx ?? {};
|
|
1165
|
+
const pluginsPromise = resolvePlugins(rawPlugins, projectRoot);
|
|
1166
|
+
const inputs2 = cliConfigFlags.inputs.concat(readInputsFromPackageJSON(projectPackageJSON));
|
|
1167
|
+
const config = {
|
|
1168
|
+
...defaultProjectConfigValues,
|
|
1169
|
+
htmlPaths: [],
|
|
1170
|
+
...userQunitx,
|
|
1171
|
+
...cliConfigFlags,
|
|
1172
|
+
projectRoot,
|
|
1173
|
+
inputs: inputs2,
|
|
1174
|
+
testFileLookupPaths: setupTestFilePaths(inputs2),
|
|
1175
|
+
lastFailedTestFiles: null,
|
|
1176
|
+
lastRanTestFiles: null,
|
|
1177
|
+
COUNTER: {
|
|
1178
|
+
testCount: 0,
|
|
1179
|
+
failCount: 0,
|
|
1180
|
+
skipCount: 0,
|
|
1181
|
+
todoCount: 0,
|
|
1182
|
+
passCount: 0,
|
|
1183
|
+
errorCount: 0
|
|
1184
|
+
},
|
|
1185
|
+
_testRunDone: null,
|
|
1186
|
+
_resetTestTimeout: null,
|
|
1187
|
+
_onWsOpen: null,
|
|
1188
|
+
_onTestsJsServed: null
|
|
1189
|
+
};
|
|
1190
|
+
config.htmlPaths = normalizeHTMLPaths(config.projectRoot, config.htmlPaths);
|
|
1191
|
+
[config.fsTree, config.plugins] = await Promise.all([
|
|
1192
|
+
buildFSTree(config.testFileLookupPaths, config),
|
|
1193
|
+
pluginsPromise
|
|
1194
|
+
]);
|
|
1195
|
+
return config;
|
|
1196
|
+
}
|
|
1197
|
+
async function readConfigFromPackageJSON(projectRoot) {
|
|
1198
|
+
const packageJSON = await fs8.readFile(`${projectRoot}/package.json`);
|
|
1199
|
+
return JSON.parse(packageJSON.toString());
|
|
1200
|
+
}
|
|
1201
|
+
function normalizeHTMLPaths(projectRoot, htmlPaths) {
|
|
1202
|
+
return Array.from(new Set(htmlPaths.map((htmlPath) => `${projectRoot}/${htmlPath}`)));
|
|
1203
|
+
}
|
|
1204
|
+
function readInputsFromPackageJSON(packageJSON) {
|
|
1205
|
+
const qunitx = packageJSON.qunitx;
|
|
1206
|
+
return qunitx && qunitx.inputs ? qunitx.inputs : [];
|
|
1207
|
+
}
|
|
1208
|
+
function resolvePlugins(raw, projectRoot) {
|
|
1209
|
+
if (raw == null) return Promise.resolve([]);
|
|
1210
|
+
if (!Array.isArray(raw)) {
|
|
1211
|
+
console.error(`# qunitx: package.json#qunitx.plugins must be an array`);
|
|
1212
|
+
process.exit(1);
|
|
1213
|
+
}
|
|
1214
|
+
const projectRequire = createRequire(`${projectRoot}/package.json`);
|
|
1215
|
+
return Promise.all(
|
|
1216
|
+
raw.map(async (entry) => {
|
|
1217
|
+
const [spec, options] = Array.isArray(entry) ? entry : [entry];
|
|
1218
|
+
const mod = await import(pathToFileURL(projectRequire.resolve(spec)).href);
|
|
1219
|
+
const exported = mod.default ?? mod;
|
|
1220
|
+
return typeof exported === "function" ? exported(options) : exported;
|
|
1221
|
+
})
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
var init_config = __esm({
|
|
1225
|
+
"lib/setup/config.ts"() {
|
|
1226
|
+
init_default_project_config_values();
|
|
1227
|
+
init_find_project_root();
|
|
1228
|
+
init_fs_tree();
|
|
1229
|
+
init_test_file_paths();
|
|
1230
|
+
init_parse_cli_flags();
|
|
1231
|
+
}
|
|
1232
|
+
});
|
|
1233
|
+
|
|
1234
|
+
// lib/utils/find-internal-assets-from-html.ts
|
|
1235
|
+
function findInternalAssetsFromHTML(htmlContent) {
|
|
1236
|
+
const links = [...htmlContent.matchAll(LINK_HREF_REGEX)].map((match) => match[1]).filter((uri) => !ABSOLUTE_URL_REGEX.test(uri));
|
|
1237
|
+
const scripts = [...htmlContent.matchAll(SCRIPT_SRC_REGEX)].map((match) => match[1]).filter((uri) => !ABSOLUTE_URL_REGEX.test(uri));
|
|
1238
|
+
return links.concat(scripts);
|
|
1239
|
+
}
|
|
1240
|
+
var ABSOLUTE_URL_REGEX, SCRIPT_SRC_REGEX, LINK_HREF_REGEX;
|
|
1241
|
+
var init_find_internal_assets_from_html = __esm({
|
|
1242
|
+
"lib/utils/find-internal-assets-from-html.ts"() {
|
|
1243
|
+
ABSOLUTE_URL_REGEX = /^(?:[a-z]+:)?\/\//i;
|
|
1244
|
+
SCRIPT_SRC_REGEX = /<script[^>]+\bsrc=['"]([^'"]+)['"]/gi;
|
|
1245
|
+
LINK_HREF_REGEX = /<link[^>]+\bhref=['"]([^'"]+)['"]/gi;
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
// lib/utils/html.ts
|
|
1250
|
+
function findScriptPlaceholder(html) {
|
|
1251
|
+
return html.includes(SCRIPT_PLACEHOLDER) ? SCRIPT_PLACEHOLDER : void 0;
|
|
1252
|
+
}
|
|
1253
|
+
function isCustomTemplate(html) {
|
|
1254
|
+
return !!findScriptPlaceholder(html) || HANDLEBARS_TOKEN_REGEX.test(html);
|
|
1255
|
+
}
|
|
1256
|
+
function injectScript(html, content) {
|
|
1257
|
+
const placeholder = findScriptPlaceholder(html);
|
|
1258
|
+
if (placeholder) {
|
|
1259
|
+
return html.replace(placeholder, content);
|
|
1260
|
+
}
|
|
1261
|
+
if (isCustomTemplate(html)) {
|
|
1262
|
+
if (html.includes("</body>")) {
|
|
1263
|
+
return html.replace("</body>", `${content}</body>`);
|
|
1264
|
+
}
|
|
1265
|
+
if (html.includes("</html>")) {
|
|
1266
|
+
return html.replace("</html>", `${content}</html>`);
|
|
1267
|
+
}
|
|
1268
|
+
return `${html}${content}`;
|
|
1269
|
+
}
|
|
1270
|
+
return html;
|
|
1271
|
+
}
|
|
1272
|
+
var SCRIPT_PLACEHOLDER, HANDLEBARS_TOKEN_REGEX;
|
|
1273
|
+
var init_html = __esm({
|
|
1274
|
+
"lib/utils/html.ts"() {
|
|
1275
|
+
SCRIPT_PLACEHOLDER = "{{qunitxScript}}";
|
|
1276
|
+
HANDLEBARS_TOKEN_REGEX = /{{\s*[^}]+\s*}}/;
|
|
1277
|
+
}
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
// lib/tap/dump-yaml.ts
|
|
1281
|
+
function dumpYaml({
|
|
1282
|
+
name,
|
|
1283
|
+
actual,
|
|
1284
|
+
expected,
|
|
1285
|
+
message,
|
|
1286
|
+
stack,
|
|
1287
|
+
source,
|
|
1288
|
+
at
|
|
1289
|
+
}) {
|
|
1290
|
+
return `name: ${dumpString(name, "")}
|
|
1291
|
+
` + yamlLine("actual", actual) + yamlLine("expected", expected) + (message !== null ? yamlLine("message", message) : "") + (stack !== null ? yamlLine("stack", stack) : "") + (source !== null ? yamlLine("source", source) : "") + (at !== null ? yamlLine("at", at) : "");
|
|
1292
|
+
}
|
|
1293
|
+
function needsQuoting(str) {
|
|
1294
|
+
return NEEDS_QUOTING.test(str);
|
|
1295
|
+
}
|
|
1296
|
+
function dumpString(str, indent) {
|
|
1297
|
+
if (str === "") return "''";
|
|
1298
|
+
if (str.includes("\n")) {
|
|
1299
|
+
return "|-\n" + str.replace(/^/gm, `${indent} `);
|
|
1300
|
+
}
|
|
1301
|
+
if (needsQuoting(str)) return `'${str.replace(/'/g, "''")}'`;
|
|
1302
|
+
return str;
|
|
1303
|
+
}
|
|
1304
|
+
function dumpValue(value, indent) {
|
|
1305
|
+
if (value === null || value === void 0) return "null";
|
|
1306
|
+
if (typeof value === "boolean" || typeof value === "number") return String(value);
|
|
1307
|
+
if (typeof value === "string") return dumpString(value, indent);
|
|
1308
|
+
if (Array.isArray(value)) {
|
|
1309
|
+
if (value.length === 0) return "[]";
|
|
1310
|
+
const next2 = `${indent} `;
|
|
1311
|
+
return "\n" + value.map((item) => {
|
|
1312
|
+
const v = dumpValue(item, next2);
|
|
1313
|
+
return v[0] === "\n" ? `${next2}-${v}` : `${next2}- ${v}`;
|
|
1314
|
+
}).join("\n");
|
|
1315
|
+
}
|
|
1316
|
+
const entries = Object.entries(value);
|
|
1317
|
+
if (entries.length === 0) return "{}";
|
|
1318
|
+
const next = `${indent} `;
|
|
1319
|
+
return "\n" + entries.map(([entryKey, entryValue]) => {
|
|
1320
|
+
const v = dumpValue(entryValue, next);
|
|
1321
|
+
return v[0] === "\n" ? `${next}${entryKey}:${v}` : `${next}${entryKey}: ${v}`;
|
|
1322
|
+
}).join("\n");
|
|
1323
|
+
}
|
|
1324
|
+
function yamlLine(key, value) {
|
|
1325
|
+
const serialized = dumpValue(value, "");
|
|
1326
|
+
return serialized[0] === "\n" ? `${key}:${serialized}
|
|
1327
|
+
` : `${key}: ${serialized}
|
|
1328
|
+
`;
|
|
1329
|
+
}
|
|
1330
|
+
var NEEDS_QUOTING;
|
|
1331
|
+
var init_dump_yaml = __esm({
|
|
1332
|
+
"lib/tap/dump-yaml.ts"() {
|
|
1333
|
+
NEEDS_QUOTING = /^$|^\s|^(null|true|false|~|yes|no|on|off|y|n)$|^[{[!|>'"#%@`]|^[-?:](\s|$)|^---|^[-+]?(\d|\.\d)|^\d{4}-\d{2}-\d{2}|: |#/i;
|
|
1334
|
+
}
|
|
1335
|
+
});
|
|
1336
|
+
|
|
1337
|
+
// lib/utils/indent-string.ts
|
|
1338
|
+
function indentString(string, count = 1, options = {}) {
|
|
1339
|
+
const { indent = " ", includeEmptyLines = false } = options;
|
|
1340
|
+
if (count <= 0) {
|
|
1341
|
+
return string;
|
|
1342
|
+
}
|
|
1343
|
+
const regex = includeEmptyLines ? /^/gm : /^(?!\s*$)/gm;
|
|
1344
|
+
return string.replace(regex, indent.repeat(count));
|
|
1345
|
+
}
|
|
1346
|
+
var init_indent_string = __esm({
|
|
1347
|
+
"lib/utils/indent-string.ts"() {
|
|
1348
|
+
}
|
|
1349
|
+
});
|
|
1350
|
+
|
|
1351
|
+
// lib/utils/source-map-decoder.ts
|
|
1352
|
+
function decodeMappings(mappings) {
|
|
1353
|
+
const result = [];
|
|
1354
|
+
const cursor = { position: 0 };
|
|
1355
|
+
const mappingsLength = mappings.length;
|
|
1356
|
+
let segments = [];
|
|
1357
|
+
let generatedCol = 0, sourceIndex = 0, sourceLine = 0, sourceCol = 0;
|
|
1358
|
+
for (; ; ) {
|
|
1359
|
+
if (cursor.position >= mappingsLength) break;
|
|
1360
|
+
const charCode = mappings.charCodeAt(cursor.position);
|
|
1361
|
+
if (charCode !== COMMA && charCode !== SEMICOLON) {
|
|
1362
|
+
generatedCol += readVlqAt(mappings, cursor);
|
|
1363
|
+
if (atFieldStart(mappings, cursor.position)) {
|
|
1364
|
+
sourceIndex += readVlqAt(mappings, cursor);
|
|
1365
|
+
sourceLine += readVlqAt(mappings, cursor);
|
|
1366
|
+
sourceCol += readVlqAt(mappings, cursor);
|
|
1367
|
+
segments.push({ generatedCol, sourceIndex, sourceLine, sourceCol });
|
|
1368
|
+
if (atFieldStart(mappings, cursor.position)) readVlqAt(mappings, cursor);
|
|
1369
|
+
}
|
|
1370
|
+
} else if (charCode === COMMA) {
|
|
1371
|
+
cursor.position++;
|
|
1372
|
+
} else {
|
|
1373
|
+
result.push(segments);
|
|
1374
|
+
segments = [];
|
|
1375
|
+
generatedCol = 0;
|
|
1376
|
+
cursor.position++;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
result.push(segments);
|
|
1380
|
+
return result;
|
|
1381
|
+
}
|
|
1382
|
+
function parseSourceMap(json, outDir) {
|
|
1383
|
+
const map = JSON.parse(json);
|
|
1384
|
+
return {
|
|
1385
|
+
segmentsByLine: decodeMappings(map.mappings),
|
|
1386
|
+
sources: map.sources ?? [],
|
|
1387
|
+
sourceRoot: map.sourceRoot ?? "",
|
|
1388
|
+
outDir,
|
|
1389
|
+
sourcesContent: map.sourcesContent ?? []
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
function extractInlineSourceMap(bundle, outDir) {
|
|
1393
|
+
if (!bundle) return null;
|
|
1394
|
+
const base64Payload = readMarkerPayload(bundle);
|
|
1395
|
+
if (!base64Payload) return null;
|
|
1396
|
+
try {
|
|
1397
|
+
return parseSourceMap(decodeBase64Utf8(base64Payload), outDir);
|
|
1398
|
+
} catch {
|
|
1399
|
+
return null;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
function lookupPosition(decoder, generatedLine, generatedCol) {
|
|
1403
|
+
const segments = decoder.segmentsByLine[generatedLine - 1];
|
|
1404
|
+
if (!segments?.length) return null;
|
|
1405
|
+
const targetCol = generatedCol - 1;
|
|
1406
|
+
const segment = segments.findLast((s) => s.generatedCol <= targetCol);
|
|
1407
|
+
if (!segment) return null;
|
|
1408
|
+
const rawSource = decoder.sources[segment.sourceIndex];
|
|
1409
|
+
if (!rawSource) return null;
|
|
1410
|
+
return {
|
|
1411
|
+
absolutePath: toAbsolutePath(rawSource, decoder.outDir, decoder.sourceRoot),
|
|
1412
|
+
line: segment.sourceLine + 1,
|
|
1413
|
+
col: segment.sourceCol + 1,
|
|
1414
|
+
sourceText: extractSourceLine(decoder.sourcesContent[segment.sourceIndex], segment.sourceLine)
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
function parseFrameLocation(text) {
|
|
1418
|
+
const match = FRAME_LOCATION_RE.exec(text);
|
|
1419
|
+
return match ? { url: match[1], line: +match[2], col: +match[3] } : null;
|
|
1420
|
+
}
|
|
1421
|
+
function isBundleUrl(url) {
|
|
1422
|
+
return BUNDLE_URL_RE.test(url);
|
|
1423
|
+
}
|
|
1424
|
+
function resolveFrame(frame, decoder, projectRoot) {
|
|
1425
|
+
const hit = FRAME_FORMATS.map((format) => ({ format, match: frame.match(format.pattern) })).find(
|
|
1426
|
+
({ match }) => match !== null
|
|
1427
|
+
);
|
|
1428
|
+
if (!hit?.match) return null;
|
|
1429
|
+
const original = tryResolve(hit.match[hit.format.urlGroup], decoder, projectRoot);
|
|
1430
|
+
return original && {
|
|
1431
|
+
resolved: hit.format.format(hit.match, original.display),
|
|
1432
|
+
userPath: original.userPath,
|
|
1433
|
+
sourceText: original.sourceText
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
function resolveStack(stack, decoder, projectRoot) {
|
|
1437
|
+
const decoratedFrames = stack.split("\n").map((frame) => {
|
|
1438
|
+
const resolution = resolveFrame(frame, decoder, projectRoot);
|
|
1439
|
+
return {
|
|
1440
|
+
line: resolution?.resolved ?? frame,
|
|
1441
|
+
userPath: resolution?.userPath ?? null,
|
|
1442
|
+
sourceText: resolution?.sourceText ?? null
|
|
1443
|
+
};
|
|
1444
|
+
});
|
|
1445
|
+
const firstUser = decoratedFrames.find((entry) => entry.userPath !== null);
|
|
1446
|
+
return {
|
|
1447
|
+
resolvedStack: decoratedFrames.map((entry) => entry.line).join("\n"),
|
|
1448
|
+
firstUserFrame: firstUser?.userPath ?? null,
|
|
1449
|
+
firstUserSourceText: firstUser?.sourceText ?? null
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1452
|
+
function readVlqAt(text, cursor) {
|
|
1453
|
+
let value = 0, bitShift = 0, position = cursor.position;
|
|
1454
|
+
for (; ; ) {
|
|
1455
|
+
const digit = BASE64_LOOKUP[text.charCodeAt(position++)];
|
|
1456
|
+
value |= (digit & VLQ_DATA_MASK) << bitShift;
|
|
1457
|
+
if (!(digit & VLQ_CONTINUATION)) break;
|
|
1458
|
+
bitShift += 5;
|
|
1459
|
+
}
|
|
1460
|
+
cursor.position = position;
|
|
1461
|
+
return value & 1 ? -(value >>> 1) : value >>> 1;
|
|
1462
|
+
}
|
|
1463
|
+
function atFieldStart(text, position) {
|
|
1464
|
+
if (position >= text.length) return false;
|
|
1465
|
+
const charCode = text.charCodeAt(position);
|
|
1466
|
+
return charCode !== COMMA && charCode !== SEMICOLON;
|
|
1467
|
+
}
|
|
1468
|
+
function decodeBase64Utf8(base64) {
|
|
1469
|
+
if (Buffer2) return Buffer2.from(base64, "base64").toString("utf8");
|
|
1470
|
+
return UTF8_DECODER.decode(Uint8Array.from(atob(base64), (char) => char.charCodeAt(0)));
|
|
1471
|
+
}
|
|
1472
|
+
function readMarkerPayload(bundle) {
|
|
1473
|
+
if (typeof bundle === "string") return sliceMarkerFromString(bundle);
|
|
1474
|
+
if (Buffer2) return sliceMarkerFromBuffer(asBuffer(bundle, Buffer2));
|
|
1475
|
+
return sliceMarkerFromString(UTF8_DECODER.decode(bundle));
|
|
1476
|
+
}
|
|
1477
|
+
function asBuffer(view, BufferCtor) {
|
|
1478
|
+
return BufferCtor.isBuffer(view) ? view : BufferCtor.from(view.buffer, view.byteOffset, view.byteLength);
|
|
1479
|
+
}
|
|
1480
|
+
function sliceMarkerFromString(text) {
|
|
1481
|
+
const markerStart = text.lastIndexOf(SOURCE_MAP_MARKER);
|
|
1482
|
+
if (markerStart < 0) return null;
|
|
1483
|
+
const payloadStart = markerStart + SOURCE_MAP_MARKER.length;
|
|
1484
|
+
const payloadEnd = text.indexOf("\n", payloadStart);
|
|
1485
|
+
return (payloadEnd < 0 ? text.slice(payloadStart) : text.slice(payloadStart, payloadEnd)).trim();
|
|
1486
|
+
}
|
|
1487
|
+
function sliceMarkerFromBuffer(buffer) {
|
|
1488
|
+
const markerBytes = SOURCE_MAP_MARKER_BYTES;
|
|
1489
|
+
const markerStart = buffer.lastIndexOf(markerBytes);
|
|
1490
|
+
if (markerStart < 0) return null;
|
|
1491
|
+
const payloadStart = markerStart + markerBytes.length;
|
|
1492
|
+
const newlineIndex = buffer.indexOf(NEWLINE, payloadStart);
|
|
1493
|
+
const payloadEnd = newlineIndex < 0 ? buffer.length : newlineIndex;
|
|
1494
|
+
return buffer.toString("latin1", payloadStart, payloadEnd).trim();
|
|
1495
|
+
}
|
|
1496
|
+
function extractSourceLine(content, lineIndex) {
|
|
1497
|
+
if (!content || lineIndex < 0) return null;
|
|
1498
|
+
const line = content.split("\n", lineIndex + 1)[lineIndex];
|
|
1499
|
+
return line?.trim() || null;
|
|
1500
|
+
}
|
|
1501
|
+
function normalizePosix(path14) {
|
|
1502
|
+
const parts = path14.split("/").reduce((acc, part) => {
|
|
1503
|
+
if (part === "..") acc.pop();
|
|
1504
|
+
else if (part && part !== ".") acc.push(part);
|
|
1505
|
+
return acc;
|
|
1506
|
+
}, []);
|
|
1507
|
+
return (path14.startsWith("/") ? "/" : "") + parts.join("/");
|
|
692
1508
|
}
|
|
693
1509
|
function toAbsolutePath(rawSource, outDir, sourceRoot) {
|
|
694
1510
|
if (rawSource.startsWith("file://")) return rawSource.slice(7);
|
|
@@ -696,8 +1512,8 @@ function toAbsolutePath(rawSource, outDir, sourceRoot) {
|
|
|
696
1512
|
const base = sourceRoot ? normalizePosix(`${outDir}/${sourceRoot}`) : outDir;
|
|
697
1513
|
return normalizePosix(`${base}/${rawSource}`);
|
|
698
1514
|
}
|
|
699
|
-
function isNodeModulesPath(
|
|
700
|
-
return
|
|
1515
|
+
function isNodeModulesPath(path14) {
|
|
1516
|
+
return path14.includes("/node_modules/") || path14.includes("\\node_modules\\");
|
|
701
1517
|
}
|
|
702
1518
|
function makeDisplayPath(absolutePath, projectRoot) {
|
|
703
1519
|
const prefix = projectRoot + "/";
|
|
@@ -973,8 +1789,8 @@ var init_web = __esm({
|
|
|
973
1789
|
});
|
|
974
1790
|
}
|
|
975
1791
|
/** Registers a GET route handler. */
|
|
976
|
-
get(
|
|
977
|
-
this.#registerRouteHandler("GET",
|
|
1792
|
+
get(path14, handler) {
|
|
1793
|
+
this.#registerRouteHandler("GET", path14, handler);
|
|
978
1794
|
}
|
|
979
1795
|
/**
|
|
980
1796
|
* Starts listening on the given port (0 = OS-assigned).
|
|
@@ -1005,32 +1821,32 @@ var init_web = __esm({
|
|
|
1005
1821
|
});
|
|
1006
1822
|
}
|
|
1007
1823
|
/** Registers a POST route handler. */
|
|
1008
|
-
post(
|
|
1009
|
-
this.#registerRouteHandler("POST",
|
|
1824
|
+
post(path14, handler) {
|
|
1825
|
+
this.#registerRouteHandler("POST", path14, handler);
|
|
1010
1826
|
}
|
|
1011
1827
|
/** Registers a DELETE route handler. */
|
|
1012
|
-
delete(
|
|
1013
|
-
this.#registerRouteHandler("DELETE",
|
|
1828
|
+
delete(path14, handler) {
|
|
1829
|
+
this.#registerRouteHandler("DELETE", path14, handler);
|
|
1014
1830
|
}
|
|
1015
1831
|
/** Registers a PUT route handler. */
|
|
1016
|
-
put(
|
|
1017
|
-
this.#registerRouteHandler("PUT",
|
|
1832
|
+
put(path14, handler) {
|
|
1833
|
+
this.#registerRouteHandler("PUT", path14, handler);
|
|
1018
1834
|
}
|
|
1019
1835
|
/** Adds a middleware function to the chain. */
|
|
1020
1836
|
use(middleware) {
|
|
1021
1837
|
this.middleware.push(middleware);
|
|
1022
1838
|
}
|
|
1023
|
-
#registerRouteHandler(method,
|
|
1839
|
+
#registerRouteHandler(method, path14, handler) {
|
|
1024
1840
|
if (!this.routes[method]) {
|
|
1025
1841
|
this.routes[method] = {};
|
|
1026
1842
|
}
|
|
1027
|
-
const paramNames = this.#extractParamNames(
|
|
1028
|
-
this.routes[method][
|
|
1029
|
-
path:
|
|
1843
|
+
const paramNames = this.#extractParamNames(path14);
|
|
1844
|
+
this.routes[method][path14] = {
|
|
1845
|
+
path: path14,
|
|
1030
1846
|
handler,
|
|
1031
1847
|
paramNames,
|
|
1032
|
-
isWildcard:
|
|
1033
|
-
compiledRegex: paramNames.length > 0 ? new RegExp(`^${this.#buildRegexPattern(
|
|
1848
|
+
isWildcard: path14 === "/*",
|
|
1849
|
+
compiledRegex: paramNames.length > 0 ? new RegExp(`^${this.#buildRegexPattern(path14, paramNames)}$`) : null
|
|
1034
1850
|
};
|
|
1035
1851
|
}
|
|
1036
1852
|
#handleRequest(req, res) {
|
|
@@ -1068,11 +1884,11 @@ var init_web = __esm({
|
|
|
1068
1884
|
return null;
|
|
1069
1885
|
}
|
|
1070
1886
|
return routes[url] || Object.values(routes).find((route) => {
|
|
1071
|
-
const { path:
|
|
1072
|
-
if (!isWildcard && !
|
|
1887
|
+
const { path: path14, isWildcard } = route;
|
|
1888
|
+
if (!isWildcard && !path14.includes(":")) {
|
|
1073
1889
|
return false;
|
|
1074
1890
|
}
|
|
1075
|
-
if (isWildcard || this.#matchPathSegments(
|
|
1891
|
+
if (isWildcard || this.#matchPathSegments(path14, url)) {
|
|
1076
1892
|
if (route.compiledRegex) {
|
|
1077
1893
|
const regexMatches = route.compiledRegex.exec(url);
|
|
1078
1894
|
if (regexMatches) {
|
|
@@ -1084,8 +1900,8 @@ var init_web = __esm({
|
|
|
1084
1900
|
return false;
|
|
1085
1901
|
}) || null;
|
|
1086
1902
|
}
|
|
1087
|
-
#matchPathSegments(
|
|
1088
|
-
const pathSegments =
|
|
1903
|
+
#matchPathSegments(path14, url) {
|
|
1904
|
+
const pathSegments = path14.split("/");
|
|
1089
1905
|
const urlSegments = url.split("/");
|
|
1090
1906
|
if (pathSegments.length !== urlSegments.length) {
|
|
1091
1907
|
return false;
|
|
@@ -1102,14 +1918,14 @@ var init_web = __esm({
|
|
|
1102
1918
|
}
|
|
1103
1919
|
return true;
|
|
1104
1920
|
}
|
|
1105
|
-
#buildRegexPattern(
|
|
1106
|
-
let regexPattern =
|
|
1921
|
+
#buildRegexPattern(path14, _paramNames) {
|
|
1922
|
+
let regexPattern = path14.replace(/:[^/]+/g, "([^/]+)");
|
|
1107
1923
|
regexPattern = regexPattern.replace(/\//g, "\\/");
|
|
1108
1924
|
return regexPattern;
|
|
1109
1925
|
}
|
|
1110
|
-
#extractParamNames(
|
|
1926
|
+
#extractParamNames(path14) {
|
|
1111
1927
|
const paramRegex = /:(\w+)/g;
|
|
1112
|
-
const paramMatches =
|
|
1928
|
+
const paramMatches = path14.match(paramRegex);
|
|
1113
1929
|
return paramMatches ? paramMatches.map((match) => match.slice(1)) : [];
|
|
1114
1930
|
}
|
|
1115
1931
|
#extractParams(route, _url) {
|
|
@@ -1125,10 +1941,10 @@ var init_web = __esm({
|
|
|
1125
1941
|
});
|
|
1126
1942
|
|
|
1127
1943
|
// lib/setup/web-server.ts
|
|
1128
|
-
import
|
|
1129
|
-
import
|
|
1944
|
+
import fs9 from "node:fs";
|
|
1945
|
+
import path6 from "node:path";
|
|
1130
1946
|
function setupWebServer(config, cachedContent) {
|
|
1131
|
-
const STATIC_FILES_PATH =
|
|
1947
|
+
const STATIC_FILES_PATH = path6.resolve(config.projectRoot, config.output);
|
|
1132
1948
|
const server = new HTTPServer();
|
|
1133
1949
|
const mainHTMLWithReplacedAssets = replaceAssetPaths(
|
|
1134
1950
|
cachedContent.mainHTML.html,
|
|
@@ -1158,7 +1974,7 @@ function setupWebServer(config, cachedContent) {
|
|
|
1158
1974
|
config._onWsOpen?.();
|
|
1159
1975
|
} else if (event === "connection") {
|
|
1160
1976
|
config._phase = "running";
|
|
1161
|
-
if (!config._groupMode) process.stdout.write("TAP version 13\n");
|
|
1977
|
+
if (!config._groupMode && !config._daemonMode) process.stdout.write("TAP version 13\n");
|
|
1162
1978
|
if (config.debug && config._groupMode) debugGroupHeader(config);
|
|
1163
1979
|
config._resetTestTimeout?.();
|
|
1164
1980
|
} else if (event === "testEnd" && !abort) {
|
|
@@ -1266,7 +2082,7 @@ function setupWebServer(config, cachedContent) {
|
|
|
1266
2082
|
config._testRunDone = null;
|
|
1267
2083
|
}
|
|
1268
2084
|
return saveHTML(
|
|
1269
|
-
|
|
2085
|
+
path6.join(path6.resolve(config.projectRoot, config.output), "index.html"),
|
|
1270
2086
|
htmlContent
|
|
1271
2087
|
);
|
|
1272
2088
|
}
|
|
@@ -1277,7 +2093,7 @@ function setupWebServer(config, cachedContent) {
|
|
|
1277
2093
|
res.writeHead(200, HTML_HEADERS);
|
|
1278
2094
|
res.end(mainIndexHTML);
|
|
1279
2095
|
saveHTML(
|
|
1280
|
-
|
|
2096
|
+
path6.join(path6.resolve(config.projectRoot, config.output), "index.html"),
|
|
1281
2097
|
mainIndexHTML
|
|
1282
2098
|
);
|
|
1283
2099
|
});
|
|
@@ -1287,7 +2103,7 @@ function setupWebServer(config, cachedContent) {
|
|
|
1287
2103
|
res.writeHead(200, HTML_HEADERS);
|
|
1288
2104
|
res.end(htmlContent);
|
|
1289
2105
|
return saveHTML(
|
|
1290
|
-
|
|
2106
|
+
path6.join(path6.resolve(config.projectRoot, config.output), "qunitx.html"),
|
|
1291
2107
|
htmlContent
|
|
1292
2108
|
);
|
|
1293
2109
|
}
|
|
@@ -1298,7 +2114,7 @@ function setupWebServer(config, cachedContent) {
|
|
|
1298
2114
|
res.writeHead(200, HTML_HEADERS);
|
|
1299
2115
|
res.end(mainQunitxHTML);
|
|
1300
2116
|
saveHTML(
|
|
1301
|
-
|
|
2117
|
+
path6.join(path6.resolve(config.projectRoot, config.output), "qunitx.html"),
|
|
1302
2118
|
mainQunitxHTML
|
|
1303
2119
|
);
|
|
1304
2120
|
});
|
|
@@ -1312,14 +2128,14 @@ function setupWebServer(config, cachedContent) {
|
|
|
1312
2128
|
);
|
|
1313
2129
|
res.writeHead(200, HTML_HEADERS);
|
|
1314
2130
|
res.end(htmlContent);
|
|
1315
|
-
saveHTML(
|
|
2131
|
+
saveHTML(path6.join(path6.resolve(config.projectRoot, config.output), req.path), htmlContent);
|
|
1316
2132
|
return;
|
|
1317
2133
|
}
|
|
1318
2134
|
const url = req.url;
|
|
1319
2135
|
const requestStartedAt = Date.now();
|
|
1320
2136
|
const filePath = (url.endsWith("/") ? [STATIC_FILES_PATH, url, "index.html"] : [STATIC_FILES_PATH, url]).join("");
|
|
1321
|
-
const contentType = req.headers.accept?.includes("text/html") ? MIME_TYPES.html : MIME_TYPES[
|
|
1322
|
-
const stream =
|
|
2137
|
+
const contentType = req.headers.accept?.includes("text/html") ? MIME_TYPES.html : MIME_TYPES[path6.extname(filePath).substring(1).toLowerCase()] || MIME_TYPES.html;
|
|
2138
|
+
const stream = fs9.createReadStream(filePath);
|
|
1323
2139
|
stream.on("open", () => {
|
|
1324
2140
|
res.writeHead(200, { "Content-Type": contentType });
|
|
1325
2141
|
stream.pipe(res);
|
|
@@ -1572,7 +2388,7 @@ function registerGroupRoutes(server, groupConfig, groupCachedContent, groupId) {
|
|
|
1572
2388
|
res.writeHead(200, HTML_HEADERS);
|
|
1573
2389
|
res.end(mainGroupHTML);
|
|
1574
2390
|
saveHTML(
|
|
1575
|
-
|
|
2391
|
+
path6.join(path6.resolve(groupConfig.projectRoot, groupConfig.output), "index.html"),
|
|
1576
2392
|
mainGroupHTML
|
|
1577
2393
|
);
|
|
1578
2394
|
});
|
|
@@ -1658,11 +2474,11 @@ function registerSharedStaticHandler(server, groupConfigs) {
|
|
|
1658
2474
|
res.end("Not found");
|
|
1659
2475
|
return;
|
|
1660
2476
|
}
|
|
1661
|
-
const STATIC_FILES_PATH =
|
|
2477
|
+
const STATIC_FILES_PATH = path6.resolve(groupConfig.projectRoot, groupConfig.output);
|
|
1662
2478
|
const subPath = match[2] || "/";
|
|
1663
2479
|
const filePath = (subPath.endsWith("/") ? [STATIC_FILES_PATH, subPath, "index.html"] : [STATIC_FILES_PATH, subPath]).join("");
|
|
1664
|
-
const contentType = req.headers.accept?.includes("text/html") ? MIME_TYPES.html : MIME_TYPES[
|
|
1665
|
-
const stream =
|
|
2480
|
+
const contentType = req.headers.accept?.includes("text/html") ? MIME_TYPES.html : MIME_TYPES[path6.extname(filePath).substring(1).toLowerCase()] || MIME_TYPES.html;
|
|
2481
|
+
const stream = fs9.createReadStream(filePath);
|
|
1666
2482
|
stream.on("open", () => {
|
|
1667
2483
|
res.writeHead(200, { "Content-Type": contentType });
|
|
1668
2484
|
stream.pipe(res);
|
|
@@ -1677,7 +2493,7 @@ function replaceAssetPaths(html, htmlPath, projectRoot) {
|
|
|
1677
2493
|
const assetPaths = findInternalAssetsFromHTML(html);
|
|
1678
2494
|
const htmlDirectory = htmlPath.split("/").slice(0, -1).join("/");
|
|
1679
2495
|
return assetPaths.reduce((result, assetPath) => {
|
|
1680
|
-
const normalizedFullAbsolutePath =
|
|
2496
|
+
const normalizedFullAbsolutePath = path6.normalize(`${htmlDirectory}/${assetPath}`);
|
|
1681
2497
|
return result.replace(assetPath, normalizedFullAbsolutePath.replace(projectRoot, "."));
|
|
1682
2498
|
}, html);
|
|
1683
2499
|
}
|
|
@@ -1839,7 +2655,7 @@ var init_web_server = __esm({
|
|
|
1839
2655
|
init_display_test_result();
|
|
1840
2656
|
init_color();
|
|
1841
2657
|
init_web();
|
|
1842
|
-
fsPromise =
|
|
2658
|
+
fsPromise = fs9.promises;
|
|
1843
2659
|
HTML_HEADERS = { "Content-Type": "text/html", "Cache-Control": "no-store" };
|
|
1844
2660
|
WATCH_WS_RECONNECT_INTERVAL_MS = 1e3;
|
|
1845
2661
|
WATCH_WS_RECONNECT_MAX_RETRIES = 120;
|
|
@@ -1875,13 +2691,13 @@ var init_web_server = __esm({
|
|
|
1875
2691
|
});
|
|
1876
2692
|
|
|
1877
2693
|
// lib/setup/browser.ts
|
|
1878
|
-
async function launchBrowser(config) {
|
|
2694
|
+
async function launchBrowser(config, skipPrelaunch = false) {
|
|
1879
2695
|
const browserName = config.browser || "chromium";
|
|
1880
2696
|
if (browserName === "chromium") {
|
|
1881
2697
|
const waitStart = Date.now();
|
|
1882
2698
|
const [playwrightCore2, prelaunch] = await Promise.all([
|
|
1883
2699
|
playwrightCorePromise,
|
|
1884
|
-
prelaunchPromise
|
|
2700
|
+
skipPrelaunch ? Promise.resolve(null) : prelaunchPromise
|
|
1885
2701
|
]);
|
|
1886
2702
|
perfLog(
|
|
1887
2703
|
`browser.js: playwright-core + prelaunch resolved in ${Date.now() - waitStart}ms, prelaunch:`,
|
|
@@ -1999,44 +2815,6 @@ var init_browser = __esm({
|
|
|
1999
2815
|
}
|
|
2000
2816
|
});
|
|
2001
2817
|
|
|
2002
|
-
// lib/utils/open-output-in-browser.ts
|
|
2003
|
-
import { spawn as spawn2 } from "node:child_process";
|
|
2004
|
-
import path6 from "node:path";
|
|
2005
|
-
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
2006
|
-
async function openOutputInBrowser(config) {
|
|
2007
|
-
try {
|
|
2008
|
-
const outputFile = config.watch ? `http://localhost:${config.port}` : pathToFileURL2(path6.join(path6.resolve(config.projectRoot, config.output), "index.html")).href;
|
|
2009
|
-
if (typeof config.open === "string") {
|
|
2010
|
-
spawnDetached(config.open, [outputFile]);
|
|
2011
|
-
return;
|
|
2012
|
-
}
|
|
2013
|
-
const browserName = config.browser || "chromium";
|
|
2014
|
-
if (browserName === "firefox") {
|
|
2015
|
-
spawnDetached("firefox", [outputFile]);
|
|
2016
|
-
return;
|
|
2017
|
-
}
|
|
2018
|
-
if (browserName === "webkit") {
|
|
2019
|
-
if (process.platform === "darwin") spawnDetached("open", ["-a", "Safari", outputFile]);
|
|
2020
|
-
return;
|
|
2021
|
-
}
|
|
2022
|
-
const chromePath = await findChrome() ?? (await import("playwright-core")).chromium.executablePath();
|
|
2023
|
-
if (chromePath) spawnDetached(chromePath, [outputFile]);
|
|
2024
|
-
} catch (err) {
|
|
2025
|
-
console.error("# Warning: --open could not launch browser:", err);
|
|
2026
|
-
}
|
|
2027
|
-
}
|
|
2028
|
-
function spawnDetached(cmd, args) {
|
|
2029
|
-
const child = spawn2(cmd, args, { detached: true, stdio: "ignore" });
|
|
2030
|
-
child.on("error", () => {
|
|
2031
|
-
});
|
|
2032
|
-
child.unref();
|
|
2033
|
-
}
|
|
2034
|
-
var init_open_output_in_browser = __esm({
|
|
2035
|
-
"lib/utils/open-output-in-browser.ts"() {
|
|
2036
|
-
init_find_chrome();
|
|
2037
|
-
}
|
|
2038
|
-
});
|
|
2039
|
-
|
|
2040
2818
|
// lib/utils/time-counter.ts
|
|
2041
2819
|
function timeCounter() {
|
|
2042
2820
|
const startTime = /* @__PURE__ */ new Date();
|
|
@@ -2051,10 +2829,10 @@ var init_time_counter = __esm({
|
|
|
2051
2829
|
});
|
|
2052
2830
|
|
|
2053
2831
|
// lib/utils/run-user-module.ts
|
|
2054
|
-
import { pathToFileURL as
|
|
2832
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
2055
2833
|
async function runUserModule(modulePath, params, scriptPosition) {
|
|
2056
2834
|
try {
|
|
2057
|
-
const func = await import(
|
|
2835
|
+
const func = await import(pathToFileURL2(modulePath).href);
|
|
2058
2836
|
if (func) {
|
|
2059
2837
|
func.default ? await func.default(params) : typeof func === "function" ? await func(params) : null;
|
|
2060
2838
|
}
|
|
@@ -2096,7 +2874,7 @@ var init_display_final_result = __esm({
|
|
|
2096
2874
|
});
|
|
2097
2875
|
|
|
2098
2876
|
// lib/commands/run/tests-in-browser.ts
|
|
2099
|
-
import
|
|
2877
|
+
import fs10 from "node:fs/promises";
|
|
2100
2878
|
import path7 from "node:path";
|
|
2101
2879
|
import esbuild from "esbuild";
|
|
2102
2880
|
function deriveBuildErrorType(error) {
|
|
@@ -2127,6 +2905,9 @@ function formatBuildErrors(error) {
|
|
|
2127
2905
|
const raw = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
2128
2906
|
return raw.replace(/\x1b\[[0-9;]*[mGKH]/g, "").replace(/\r\n/g, "\n");
|
|
2129
2907
|
}
|
|
2908
|
+
function bundleCacheKey(opts, files) {
|
|
2909
|
+
return JSON.stringify({ files, outfile: opts.outfile, target: opts.target });
|
|
2910
|
+
}
|
|
2130
2911
|
async function buildTestBundle(config, cachedContent) {
|
|
2131
2912
|
const { projectRoot, output } = config;
|
|
2132
2913
|
const allTestFilePaths = Object.keys(config.fsTree);
|
|
@@ -2136,7 +2917,7 @@ async function buildTestBundle(config, cachedContent) {
|
|
|
2136
2917
|
}
|
|
2137
2918
|
const outDir = path7.resolve(projectRoot, output);
|
|
2138
2919
|
const outfile = path7.join(outDir, "tests.js");
|
|
2139
|
-
await
|
|
2920
|
+
await fs10.mkdir(outDir, { recursive: true });
|
|
2140
2921
|
const sourcemap = "inline";
|
|
2141
2922
|
const needsDisk = true;
|
|
2142
2923
|
const buildOptions = {
|
|
@@ -2168,15 +2949,17 @@ async function buildTestBundle(config, cachedContent) {
|
|
|
2168
2949
|
};
|
|
2169
2950
|
cachedContent._buildError = null;
|
|
2170
2951
|
cachedContent._noTestsWarning = null;
|
|
2952
|
+
const cacheHolder = config._daemonEsbuildCache ?? cachedContent;
|
|
2953
|
+
const fileKey = bundleCacheKey(buildOptions, allTestFilePaths);
|
|
2171
2954
|
try {
|
|
2172
2955
|
const [allTestCode] = await Promise.all([
|
|
2173
|
-
config.watch ? buildIncrementally(buildOptions,
|
|
2956
|
+
config.watch || config._daemonMode ? buildIncrementally(buildOptions, fileKey, cacheHolder, needsDisk) : buildWithOverlayfsRetry(buildOptions, needsDisk),
|
|
2174
2957
|
Promise.all(
|
|
2175
2958
|
cachedContent.htmlPathsToRunTests.map(async (htmlPath) => {
|
|
2176
2959
|
const targetPath = path7.join(outDir, htmlPath);
|
|
2177
2960
|
if (htmlPath !== "/") {
|
|
2178
|
-
await
|
|
2179
|
-
await
|
|
2961
|
+
await fs10.rm(targetPath, { force: true, recursive: true });
|
|
2962
|
+
await fs10.mkdir(path7.dirname(targetPath), { recursive: true });
|
|
2180
2963
|
}
|
|
2181
2964
|
})
|
|
2182
2965
|
)
|
|
@@ -2188,7 +2971,7 @@ async function buildTestBundle(config, cachedContent) {
|
|
|
2188
2971
|
type: deriveBuildErrorType(error),
|
|
2189
2972
|
formatted: formatBuildErrors(error)
|
|
2190
2973
|
};
|
|
2191
|
-
await
|
|
2974
|
+
await fs10.writeFile(path7.join(outDir, "index.html"), buildErrorHTML(cachedContent._buildError));
|
|
2192
2975
|
throw error;
|
|
2193
2976
|
}
|
|
2194
2977
|
}
|
|
@@ -2262,7 +3045,7 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
2262
3045
|
console.log(
|
|
2263
3046
|
`# Warning: 0 tests registered \u2014 no QUnit test cases found in ${allTestFilePaths.length} ${fileWord}`
|
|
2264
3047
|
);
|
|
2265
|
-
|
|
3048
|
+
fs10.writeFile(path7.join(outDir, "index.html"), buildNoTestsHTML(displayFiles)).catch(
|
|
2266
3049
|
() => {
|
|
2267
3050
|
}
|
|
2268
3051
|
);
|
|
@@ -2272,16 +3055,20 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
2272
3055
|
await runUserModule(`${process.cwd()}/${config.after}`, config.COUNTER, "after");
|
|
2273
3056
|
}
|
|
2274
3057
|
if (!config.watch) {
|
|
2275
|
-
await flushConsoleHandlers(config._pendingConsoleHandlers);
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
3058
|
+
await flushConsoleHandlers(config._pendingConsoleHandlers, connections.page);
|
|
3059
|
+
if (config._daemonMode) {
|
|
3060
|
+
throw new DaemonRunError(config.COUNTER.failCount > 0 ? 1 : 0);
|
|
3061
|
+
}
|
|
3062
|
+
await closeWithGrace([
|
|
3063
|
+
connections.server?.close(),
|
|
3064
|
+
connections.browser?.close(),
|
|
3065
|
+
shutdownPrelaunch()
|
|
2279
3066
|
]);
|
|
2280
|
-
await shutdownPrelaunch();
|
|
2281
3067
|
return process.exit(config.COUNTER.failCount > 0 ? 1 : 0);
|
|
2282
3068
|
}
|
|
2283
3069
|
}
|
|
2284
3070
|
} catch (error) {
|
|
3071
|
+
if (error instanceof DaemonRunError) throw error;
|
|
2285
3072
|
cachedContent._activeRebuild = null;
|
|
2286
3073
|
config.lastFailedTestFiles = config.lastRanTestFiles;
|
|
2287
3074
|
const exception = new BundleError(error);
|
|
@@ -2290,7 +3077,7 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
2290
3077
|
type: deriveBuildErrorType(error),
|
|
2291
3078
|
formatted: formatBuildErrors(error)
|
|
2292
3079
|
};
|
|
2293
|
-
|
|
3080
|
+
fs10.writeFile(
|
|
2294
3081
|
path7.join(outDir, "qunitx.html"),
|
|
2295
3082
|
buildErrorHTML(cachedContent._buildError)
|
|
2296
3083
|
).catch(
|
|
@@ -2306,12 +3093,14 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
2306
3093
|
}
|
|
2307
3094
|
return connections;
|
|
2308
3095
|
}
|
|
2309
|
-
async function flushConsoleHandlers(handlers, deadline = Date.now() + CONSOLE_FLUSH_TIMEOUT_MS) {
|
|
3096
|
+
async function flushConsoleHandlers(handlers, page, deadline = Date.now() + CONSOLE_FLUSH_TIMEOUT_MS) {
|
|
2310
3097
|
if (!handlers || Date.now() >= deadline) return;
|
|
3098
|
+
if (page) await page.evaluate(() => 0).catch(() => {
|
|
3099
|
+
});
|
|
2311
3100
|
await new Promise((resolve) => setImmediate(resolve));
|
|
2312
3101
|
if (handlers.size === 0) return;
|
|
2313
3102
|
await Promise.allSettled([...handlers]);
|
|
2314
|
-
return flushConsoleHandlers(handlers, deadline);
|
|
3103
|
+
return flushConsoleHandlers(handlers, page, deadline);
|
|
2315
3104
|
}
|
|
2316
3105
|
async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
2317
3106
|
groupCachedContents.forEach((cachedContent) => {
|
|
@@ -2339,7 +3128,7 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
2339
3128
|
);
|
|
2340
3129
|
await Promise.all(
|
|
2341
3130
|
activeGroups.map(
|
|
2342
|
-
(group) =>
|
|
3131
|
+
(group) => fs10.mkdir(path7.resolve(group.config.projectRoot, group.config.output), { recursive: true })
|
|
2343
3132
|
)
|
|
2344
3133
|
);
|
|
2345
3134
|
const sourcemap = "inline";
|
|
@@ -2412,7 +3201,7 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
2412
3201
|
esbuildOutdir
|
|
2413
3202
|
);
|
|
2414
3203
|
}
|
|
2415
|
-
return
|
|
3204
|
+
return fs10.writeFile(destPath, outputFile.contents);
|
|
2416
3205
|
})
|
|
2417
3206
|
);
|
|
2418
3207
|
} catch (error) {
|
|
@@ -2421,7 +3210,7 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
2421
3210
|
await Promise.all(
|
|
2422
3211
|
activeGroups.map((group) => {
|
|
2423
3212
|
group.cachedContent._buildError = buildError;
|
|
2424
|
-
return
|
|
3213
|
+
return fs10.writeFile(
|
|
2425
3214
|
path7.join(path7.resolve(group.config.projectRoot, group.config.output), "index.html"),
|
|
2426
3215
|
errorHtml
|
|
2427
3216
|
).catch(
|
|
@@ -2471,7 +3260,7 @@ async function runWithOverlayfsRetry(getContents, needsDisk) {
|
|
|
2471
3260
|
}
|
|
2472
3261
|
if (needsDisk) {
|
|
2473
3262
|
await Promise.all(
|
|
2474
|
-
result.outputFiles.map((outputFile) =>
|
|
3263
|
+
result.outputFiles.map((outputFile) => fs10.writeFile(outputFile.path, outputFile.contents))
|
|
2475
3264
|
);
|
|
2476
3265
|
}
|
|
2477
3266
|
return js;
|
|
@@ -2484,15 +3273,15 @@ function buildWithOverlayfsRetry(options, needsDisk) {
|
|
|
2484
3273
|
return { result, js: Buffer.from(jsFile.contents) };
|
|
2485
3274
|
}, needsDisk);
|
|
2486
3275
|
}
|
|
2487
|
-
async function buildIncrementally(options, fileKey,
|
|
3276
|
+
async function buildIncrementally(options, fileKey, cache, needsDisk) {
|
|
2488
3277
|
const buildOpts = { ...options, write: false };
|
|
2489
|
-
if (!
|
|
2490
|
-
|
|
3278
|
+
if (!cache._esbuildContext || cache._esbuildContextKey !== fileKey) {
|
|
3279
|
+
cache._esbuildContext?.dispose().catch(() => {
|
|
2491
3280
|
});
|
|
2492
|
-
|
|
2493
|
-
|
|
3281
|
+
cache._esbuildContext = await esbuild.context(buildOpts);
|
|
3282
|
+
cache._esbuildContextKey = fileKey;
|
|
2494
3283
|
}
|
|
2495
|
-
const ctx =
|
|
3284
|
+
const ctx = cache._esbuildContext;
|
|
2496
3285
|
return runWithOverlayfsRetry(async () => {
|
|
2497
3286
|
const result = await ctx.rebuild();
|
|
2498
3287
|
const jsFile = result.outputFiles.find((f) => !f.path.endsWith(".map"));
|
|
@@ -2564,9 +3353,10 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
|
|
|
2564
3353
|
console.error("BROWSER: runtime error thrown during executing tests");
|
|
2565
3354
|
await failOnNonWatchMode(
|
|
2566
3355
|
config.watch,
|
|
2567
|
-
{ server, browser },
|
|
3356
|
+
{ server, browser, page },
|
|
2568
3357
|
config._groupMode,
|
|
2569
|
-
config._pendingConsoleHandlers
|
|
3358
|
+
config._pendingConsoleHandlers,
|
|
3359
|
+
config._daemonMode
|
|
2570
3360
|
);
|
|
2571
3361
|
} else if (QUNIT_RESULT.totalTests === 0) {
|
|
2572
3362
|
return;
|
|
@@ -2579,27 +3369,30 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
|
|
|
2579
3369
|
console.error(`BROWSER: TEST TIMED OUT: ${QUNIT_RESULT.currentTest}`);
|
|
2580
3370
|
await failOnNonWatchMode(
|
|
2581
3371
|
config.watch,
|
|
2582
|
-
{ server, browser },
|
|
3372
|
+
{ server, browser, page },
|
|
2583
3373
|
config._groupMode,
|
|
2584
|
-
config._pendingConsoleHandlers
|
|
3374
|
+
config._pendingConsoleHandlers,
|
|
3375
|
+
config._daemonMode
|
|
2585
3376
|
);
|
|
2586
3377
|
} else if (QUNIT_RESULT.failedTests > config.COUNTER.failCount) {
|
|
2587
3378
|
config.COUNTER.failCount = QUNIT_RESULT.failedTests;
|
|
2588
3379
|
}
|
|
2589
3380
|
}
|
|
2590
|
-
async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode = false, pendingHandlers) {
|
|
2591
|
-
if (
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
}
|
|
2595
|
-
await flushConsoleHandlers(pendingHandlers);
|
|
2596
|
-
await Promise.all([
|
|
2597
|
-
connections.server && connections.server.close(),
|
|
2598
|
-
connections.browser && connections.browser.close()
|
|
2599
|
-
]);
|
|
2600
|
-
await shutdownPrelaunch();
|
|
2601
|
-
process.exit(1);
|
|
3381
|
+
async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode = false, pendingHandlers, daemonMode = false) {
|
|
3382
|
+
if (watchMode) return;
|
|
3383
|
+
if (groupMode) {
|
|
3384
|
+
throw new Error("Browser test run failed");
|
|
2602
3385
|
}
|
|
3386
|
+
await flushConsoleHandlers(pendingHandlers, connections.page);
|
|
3387
|
+
if (daemonMode) {
|
|
3388
|
+
throw new DaemonRunError(1);
|
|
3389
|
+
}
|
|
3390
|
+
await closeWithGrace([
|
|
3391
|
+
connections.server?.close(),
|
|
3392
|
+
connections.browser?.close(),
|
|
3393
|
+
shutdownPrelaunch()
|
|
3394
|
+
]);
|
|
3395
|
+
process.exit(1);
|
|
2603
3396
|
}
|
|
2604
3397
|
function toEsbuildImportPath(filePath) {
|
|
2605
3398
|
const rel = path7.relative(process.cwd(), filePath);
|
|
@@ -2607,11 +3400,12 @@ function toEsbuildImportPath(filePath) {
|
|
|
2607
3400
|
if (path7.isAbsolute(rel)) return filePath.replace(/\\/g, "/");
|
|
2608
3401
|
return normalized.startsWith(".") ? normalized : "./" + normalized;
|
|
2609
3402
|
}
|
|
2610
|
-
var ancestorNodeModules, ANCESTOR_NODE_MODULES, RETRY_DELAY_MS, MAX_RETRIES, EMPTY_BUNDLE_THRESHOLD, NAV_GRACE_MS, MIN_NAV_MS, STARTUP_TIMEOUT_FACTOR, TESTS_JS_TIMEOUT_FACTOR, CONSOLE_FLUSH_TIMEOUT_MS, TEST_STALL_BUFFER_MS, GROUP_OUTPUT_REGEX, BundleError;
|
|
3403
|
+
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;
|
|
2611
3404
|
var init_tests_in_browser = __esm({
|
|
2612
3405
|
"lib/commands/run/tests-in-browser.ts"() {
|
|
2613
3406
|
init_color();
|
|
2614
3407
|
init_chrome_prelaunch();
|
|
3408
|
+
init_close_with_grace();
|
|
2615
3409
|
init_time_counter();
|
|
2616
3410
|
init_run_user_module();
|
|
2617
3411
|
init_display_final_result();
|
|
@@ -2625,7 +3419,8 @@ var init_tests_in_browser = __esm({
|
|
|
2625
3419
|
MAX_RETRIES = 3;
|
|
2626
3420
|
EMPTY_BUNDLE_THRESHOLD = 500;
|
|
2627
3421
|
NAV_GRACE_MS = 1e4;
|
|
2628
|
-
|
|
3422
|
+
MAX_NAV_SLOWDOWN_FACTOR = 6;
|
|
3423
|
+
MIN_NAV_MS = NAV_GRACE_MS * MAX_NAV_SLOWDOWN_FACTOR;
|
|
2629
3424
|
STARTUP_TIMEOUT_FACTOR = 3;
|
|
2630
3425
|
TESTS_JS_TIMEOUT_FACTOR = 4;
|
|
2631
3426
|
CONSOLE_FLUSH_TIMEOUT_MS = 2e3;
|
|
@@ -2638,13 +3433,61 @@ var init_tests_in_browser = __esm({
|
|
|
2638
3433
|
this.message = `esbuild Bundle Error: ${message}`.split("\n").join("\n# ");
|
|
2639
3434
|
}
|
|
2640
3435
|
};
|
|
3436
|
+
DaemonRunError = class extends Error {
|
|
3437
|
+
/** The exit code that the run would have passed to `process.exit()` outside of daemon mode. */
|
|
3438
|
+
exitCode;
|
|
3439
|
+
/** Constructs a DaemonRunError carrying the run's exit code. */
|
|
3440
|
+
constructor(exitCode) {
|
|
3441
|
+
super(`daemon run finished with exit code ${exitCode}`);
|
|
3442
|
+
this.name = "DaemonRunError";
|
|
3443
|
+
this.exitCode = exitCode;
|
|
3444
|
+
}
|
|
3445
|
+
};
|
|
3446
|
+
}
|
|
3447
|
+
});
|
|
3448
|
+
|
|
3449
|
+
// lib/utils/open-output-in-browser.ts
|
|
3450
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
3451
|
+
import path8 from "node:path";
|
|
3452
|
+
import { pathToFileURL as pathToFileURL3 } from "node:url";
|
|
3453
|
+
async function openOutputInBrowser(config) {
|
|
3454
|
+
try {
|
|
3455
|
+
const outputFile = config.watch ? `http://localhost:${config.port}` : pathToFileURL3(path8.join(path8.resolve(config.projectRoot, config.output), "index.html")).href;
|
|
3456
|
+
if (typeof config.open === "string") {
|
|
3457
|
+
spawnDetached(config.open, [outputFile]);
|
|
3458
|
+
return;
|
|
3459
|
+
}
|
|
3460
|
+
const browserName = config.browser || "chromium";
|
|
3461
|
+
if (browserName === "firefox") {
|
|
3462
|
+
spawnDetached("firefox", [outputFile]);
|
|
3463
|
+
return;
|
|
3464
|
+
}
|
|
3465
|
+
if (browserName === "webkit") {
|
|
3466
|
+
if (process.platform === "darwin") spawnDetached("open", ["-a", "Safari", outputFile]);
|
|
3467
|
+
return;
|
|
3468
|
+
}
|
|
3469
|
+
const chromePath = await findChrome() ?? (await import("playwright-core")).chromium.executablePath();
|
|
3470
|
+
if (chromePath) spawnDetached(chromePath, [outputFile]);
|
|
3471
|
+
} catch (err) {
|
|
3472
|
+
console.error("# Warning: --open could not launch browser:", err);
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
function spawnDetached(cmd2, args) {
|
|
3476
|
+
const child = spawn2(cmd2, args, { detached: true, stdio: "ignore" });
|
|
3477
|
+
child.on("error", () => {
|
|
3478
|
+
});
|
|
3479
|
+
child.unref();
|
|
3480
|
+
}
|
|
3481
|
+
var init_open_output_in_browser = __esm({
|
|
3482
|
+
"lib/utils/open-output-in-browser.ts"() {
|
|
3483
|
+
init_find_chrome();
|
|
2641
3484
|
}
|
|
2642
3485
|
});
|
|
2643
3486
|
|
|
2644
3487
|
// lib/setup/file-watcher.ts
|
|
2645
|
-
import
|
|
3488
|
+
import fs11 from "node:fs";
|
|
2646
3489
|
import { readdir, stat, lstat } from "node:fs/promises";
|
|
2647
|
-
import
|
|
3490
|
+
import path9 from "node:path";
|
|
2648
3491
|
function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFunc) {
|
|
2649
3492
|
const extensions = config.extensions || defaultProjectConfigValues.extensions;
|
|
2650
3493
|
config._lastBuildEndMs ??= Date.now();
|
|
@@ -2657,7 +3500,7 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2657
3500
|
if (symlinkPollers.has(filePath)) return;
|
|
2658
3501
|
const handler = (curr, prev) => {
|
|
2659
3502
|
if (curr.nlink === 0) {
|
|
2660
|
-
|
|
3503
|
+
fs11.unwatchFile(filePath, handler);
|
|
2661
3504
|
symlinkPollers.delete(filePath);
|
|
2662
3505
|
if (filePath in config.fsTree) {
|
|
2663
3506
|
handleWatchEvent(config, extensions, "unlink", filePath, onEventFunc, onFinishFunc);
|
|
@@ -2668,8 +3511,8 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2668
3511
|
}
|
|
2669
3512
|
}
|
|
2670
3513
|
};
|
|
2671
|
-
|
|
2672
|
-
symlinkPollers.set(filePath, () =>
|
|
3514
|
+
fs11.watchFile(filePath, { interval: SYMLINK_POLL_INTERVAL_MS, persistent: false }, handler);
|
|
3515
|
+
symlinkPollers.set(filePath, () => fs11.unwatchFile(filePath, handler));
|
|
2673
3516
|
}
|
|
2674
3517
|
function untrackSymlink(filePath) {
|
|
2675
3518
|
symlinkPollers.get(filePath)?.();
|
|
@@ -2680,7 +3523,7 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2680
3523
|
let rescanInProgress = false;
|
|
2681
3524
|
const lastEventMs = {};
|
|
2682
3525
|
const seenMtimeMs = {};
|
|
2683
|
-
const childWatcher =
|
|
3526
|
+
const childWatcher = fs11.watch(watchPath, { recursive: true }, async (eventType, filename) => {
|
|
2684
3527
|
if (!ready) return;
|
|
2685
3528
|
if (!filename) {
|
|
2686
3529
|
if (process.platform === "darwin" && !rescanInProgress) {
|
|
@@ -2698,7 +3541,7 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2698
3541
|
}
|
|
2699
3542
|
return;
|
|
2700
3543
|
}
|
|
2701
|
-
const fullPath = filename ===
|
|
3544
|
+
const fullPath = filename === path9.basename(watchPath) ? watchPath : path9.join(watchPath, filename);
|
|
2702
3545
|
if (eventType === "change") {
|
|
2703
3546
|
const now = Date.now();
|
|
2704
3547
|
const last = lastEventMs[fullPath] ?? 0;
|
|
@@ -2727,10 +3570,10 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2727
3570
|
}
|
|
2728
3571
|
handleWatchEvent(config, extensions, event, fullPath, onEventFunc, onFinishFunc);
|
|
2729
3572
|
});
|
|
2730
|
-
const parentDir =
|
|
2731
|
-
const watchedBasename =
|
|
3573
|
+
const parentDir = path9.dirname(watchPath);
|
|
3574
|
+
const watchedBasename = path9.basename(watchPath);
|
|
2732
3575
|
let parentUnlinkFired = false;
|
|
2733
|
-
const parentWatcher =
|
|
3576
|
+
const parentWatcher = fs11.watch(parentDir, async (eventType, filename) => {
|
|
2734
3577
|
if (!ready || filename !== watchedBasename || eventType !== "rename") return;
|
|
2735
3578
|
if (parentUnlinkFired) return;
|
|
2736
3579
|
parentUnlinkFired = true;
|
|
@@ -2845,11 +3688,11 @@ async function rescanDirectoryForDelta(watchPath, config, extensions, onEventFun
|
|
|
2845
3688
|
const trackedToRecheck = [];
|
|
2846
3689
|
for (const entry of entries) {
|
|
2847
3690
|
if (entry.isDirectory()) {
|
|
2848
|
-
presentDirs.add(
|
|
3691
|
+
presentDirs.add(path9.join(entry.parentPath, entry.name));
|
|
2849
3692
|
continue;
|
|
2850
3693
|
}
|
|
2851
3694
|
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
2852
|
-
const entryPath =
|
|
3695
|
+
const entryPath = path9.join(entry.parentPath, entry.name);
|
|
2853
3696
|
presentDirs.add(entry.parentPath);
|
|
2854
3697
|
if (!extensions.some((ext) => entryPath.endsWith(`.${ext}`))) continue;
|
|
2855
3698
|
presentPaths.add(entryPath);
|
|
@@ -2872,13 +3715,13 @@ async function rescanDirectoryForDelta(watchPath, config, extensions, onEventFun
|
|
|
2872
3715
|
}
|
|
2873
3716
|
})
|
|
2874
3717
|
);
|
|
2875
|
-
const watchPrefix = watchPath +
|
|
3718
|
+
const watchPrefix = watchPath + path9.sep;
|
|
2876
3719
|
const firedDirPrefixes = [];
|
|
2877
3720
|
for (const trackedPath of Object.keys(config.fsTree)) {
|
|
2878
3721
|
if (!trackedPath.startsWith(watchPrefix) || presentPaths.has(trackedPath)) continue;
|
|
2879
|
-
if (firedDirPrefixes.some((p) => trackedPath.startsWith(p +
|
|
2880
|
-
const parts = trackedPath.slice(watchPrefix.length).split(
|
|
2881
|
-
const goneDirPath = parts.slice(0, -1).map((_, i) => watchPrefix + parts.slice(0, i + 1).join(
|
|
3722
|
+
if (firedDirPrefixes.some((p) => trackedPath.startsWith(p + path9.sep))) continue;
|
|
3723
|
+
const parts = trackedPath.slice(watchPrefix.length).split(path9.sep);
|
|
3724
|
+
const goneDirPath = parts.slice(0, -1).map((_, i) => watchPrefix + parts.slice(0, i + 1).join(path9.sep)).find((p) => !presentDirs.has(p)) ?? null;
|
|
2882
3725
|
if (goneDirPath !== null) {
|
|
2883
3726
|
firedDirPrefixes.push(goneDirPath);
|
|
2884
3727
|
handleWatchEvent(config, extensions, "unlinkDir", goneDirPath, onEventFunc, onFinishFunc);
|
|
@@ -3010,48 +3853,90 @@ var init_keyboard_events = __esm({
|
|
|
3010
3853
|
});
|
|
3011
3854
|
|
|
3012
3855
|
// lib/setup/write-output-static-files.ts
|
|
3013
|
-
import
|
|
3014
|
-
import
|
|
3856
|
+
import fs12 from "node:fs/promises";
|
|
3857
|
+
import path10 from "node:path";
|
|
3015
3858
|
async function writeOutputStaticFiles({ projectRoot, output }, cachedContent) {
|
|
3016
3859
|
const staticHTMLPromises = Object.keys(cachedContent.staticHTMLs).map(async (staticHTMLKey) => {
|
|
3017
|
-
const htmlRelativePath =
|
|
3018
|
-
const outDir =
|
|
3019
|
-
await ensureFolderExists(
|
|
3020
|
-
await
|
|
3021
|
-
|
|
3860
|
+
const htmlRelativePath = path10.relative(projectRoot, staticHTMLKey);
|
|
3861
|
+
const outDir = path10.resolve(projectRoot, output);
|
|
3862
|
+
await ensureFolderExists(path10.join(outDir, htmlRelativePath));
|
|
3863
|
+
await fs12.writeFile(
|
|
3864
|
+
path10.join(outDir, htmlRelativePath),
|
|
3022
3865
|
cachedContent.staticHTMLs[staticHTMLKey]
|
|
3023
3866
|
);
|
|
3024
3867
|
});
|
|
3025
3868
|
const assetPromises = Array.from(cachedContent.assets).map(async (assetAbsolutePath) => {
|
|
3026
|
-
const assetRelativePath =
|
|
3027
|
-
const outDir =
|
|
3028
|
-
await ensureFolderExists(
|
|
3029
|
-
await
|
|
3869
|
+
const assetRelativePath = path10.relative(projectRoot, assetAbsolutePath);
|
|
3870
|
+
const outDir = path10.resolve(projectRoot, output);
|
|
3871
|
+
await ensureFolderExists(path10.join(outDir, assetRelativePath));
|
|
3872
|
+
await fs12.copyFile(assetAbsolutePath, path10.join(outDir, assetRelativePath));
|
|
3030
3873
|
});
|
|
3031
3874
|
await Promise.all(staticHTMLPromises.concat(assetPromises));
|
|
3032
3875
|
}
|
|
3033
|
-
async function ensureFolderExists(assetPath) {
|
|
3034
|
-
await
|
|
3876
|
+
async function ensureFolderExists(assetPath) {
|
|
3877
|
+
await fs12.mkdir(path10.dirname(assetPath), { recursive: true });
|
|
3878
|
+
}
|
|
3879
|
+
var init_write_output_static_files = __esm({
|
|
3880
|
+
"lib/setup/write-output-static-files.ts"() {
|
|
3881
|
+
}
|
|
3882
|
+
});
|
|
3883
|
+
|
|
3884
|
+
// lib/utils/daemon-hint.ts
|
|
3885
|
+
import fs13 from "node:fs/promises";
|
|
3886
|
+
import os3 from "node:os";
|
|
3887
|
+
import path11 from "node:path";
|
|
3888
|
+
function shouldShowDaemonHint(ctx) {
|
|
3889
|
+
const env = ctx.env ?? process.env;
|
|
3890
|
+
if (ctx.watch) return false;
|
|
3891
|
+
if (ctx.daemonMode) return false;
|
|
3892
|
+
if (env.CI) return false;
|
|
3893
|
+
if (env.QUNITX_DAEMON) return false;
|
|
3894
|
+
if (env.QUNITX_NO_DAEMON) return false;
|
|
3895
|
+
if (env.QUNITX_HINT_SHOWN) return false;
|
|
3896
|
+
if (ctx.durationMs < FAST_RUN_THRESHOLD_MS) return false;
|
|
3897
|
+
if (ctx.isTTY === false) return false;
|
|
3898
|
+
if (ctx.isTTY === void 0 && !process.stderr.isTTY) return false;
|
|
3899
|
+
return true;
|
|
3900
|
+
}
|
|
3901
|
+
async function maybePrintDaemonHint(ctx, opts = {}) {
|
|
3902
|
+
if (!shouldShowDaemonHint(ctx)) return;
|
|
3903
|
+
const sentinel = opts.sentinelPath ?? DEFAULT_SENTINEL;
|
|
3904
|
+
try {
|
|
3905
|
+
await fs13.access(sentinel);
|
|
3906
|
+
return;
|
|
3907
|
+
} catch {
|
|
3908
|
+
}
|
|
3909
|
+
(opts.write ?? ((t) => process.stderr.write(t)))(HINT_TEXT);
|
|
3910
|
+
try {
|
|
3911
|
+
await fs13.mkdir(path11.dirname(sentinel), { recursive: true });
|
|
3912
|
+
await fs13.writeFile(sentinel, (/* @__PURE__ */ new Date()).toISOString());
|
|
3913
|
+
} catch {
|
|
3914
|
+
}
|
|
3035
3915
|
}
|
|
3036
|
-
var
|
|
3037
|
-
|
|
3916
|
+
var FAST_RUN_THRESHOLD_MS, HINT_TEXT, DEFAULT_SENTINEL;
|
|
3917
|
+
var init_daemon_hint = __esm({
|
|
3918
|
+
"lib/utils/daemon-hint.ts"() {
|
|
3919
|
+
FAST_RUN_THRESHOLD_MS = 500;
|
|
3920
|
+
HINT_TEXT = "\n\x1B[34m\u2139\x1B[39m Tip: export QUNITX_DAEMON=1 for ~2\xD7 faster repeated runs (qunitx daemon --help)\n";
|
|
3921
|
+
DEFAULT_SENTINEL = path11.join(os3.homedir(), ".cache", "qunitx", "hint-shown");
|
|
3038
3922
|
}
|
|
3039
3923
|
});
|
|
3040
3924
|
|
|
3041
3925
|
// lib/commands/run.ts
|
|
3042
3926
|
var run_exports = {};
|
|
3043
3927
|
__export(run_exports, {
|
|
3928
|
+
buildCachedContent: () => buildCachedContent,
|
|
3044
3929
|
computeFileTimes: () => computeFileTimes,
|
|
3045
3930
|
default: () => run,
|
|
3046
3931
|
readTimingCache: () => readTimingCache,
|
|
3047
3932
|
run: () => run
|
|
3048
3933
|
});
|
|
3049
|
-
import
|
|
3934
|
+
import fs14 from "node:fs/promises";
|
|
3050
3935
|
import { join as join3, normalize } from "node:path";
|
|
3051
3936
|
import { createRequire as createRequire2 } from "node:module";
|
|
3052
3937
|
import { availableParallelism } from "node:os";
|
|
3053
3938
|
async function run(config) {
|
|
3054
|
-
const browserPromise = config.watch ? null : launchBrowser(config);
|
|
3939
|
+
const browserPromise = config._daemonBrowser ? Promise.resolve(config._daemonBrowser) : config.watch ? null : launchBrowser(config);
|
|
3055
3940
|
const [cachedContent, timings] = await Promise.all([
|
|
3056
3941
|
buildCachedContent(config, config.htmlPaths),
|
|
3057
3942
|
config.watch ? Promise.resolve(null) : readTimingCache(config.projectRoot)
|
|
@@ -3068,7 +3953,7 @@ async function run(config) {
|
|
|
3068
3953
|
config.webServer = connections.server;
|
|
3069
3954
|
setupKeyboardEvents(config, cachedContent, connections);
|
|
3070
3955
|
process.once("SIGTERM", () => {
|
|
3071
|
-
connections.server.close().finally(() => process.exit(EXIT_CODE_SIGTERM));
|
|
3956
|
+
closeWithGrace([connections.server.close()]).finally(() => process.exit(EXIT_CODE_SIGTERM));
|
|
3072
3957
|
});
|
|
3073
3958
|
const isHeadedWatchMode = config.open === true && config.watch;
|
|
3074
3959
|
if (config.open && !isHeadedWatchMode) {
|
|
@@ -3080,10 +3965,7 @@ async function run(config) {
|
|
|
3080
3965
|
try {
|
|
3081
3966
|
await runTestsInBrowser(config, cachedContent, connections);
|
|
3082
3967
|
} catch (error) {
|
|
3083
|
-
await
|
|
3084
|
-
connections.server && connections.server.close(),
|
|
3085
|
-
connections.browser && connections.browser.close()
|
|
3086
|
-
]);
|
|
3968
|
+
await closeWithGrace([connections.server?.close(), connections.browser?.close()]);
|
|
3087
3969
|
throw error;
|
|
3088
3970
|
}
|
|
3089
3971
|
if (isHeadedWatchMode && (cachedContent._buildError || cachedContent._noTestsWarning)) {
|
|
@@ -3165,7 +4047,7 @@ async function run(config) {
|
|
|
3165
4047
|
})() : null;
|
|
3166
4048
|
process.stdout.write("TAP version 13\n");
|
|
3167
4049
|
process.stdout.write(
|
|
3168
|
-
`# Running ${allFiles.length} test file${allFiles.length === 1 ? "" : "s"} across ${groupCount} group${groupCount === 1 ? "" : "s"}
|
|
4050
|
+
`# Running ${allFiles.length} test file${allFiles.length === 1 ? "" : "s"} across ${groupCount} group${groupCount === 1 ? "" : "s"}${config._daemonMode ? " (daemon)" : ""}
|
|
3169
4051
|
`
|
|
3170
4052
|
);
|
|
3171
4053
|
const [browser] = await Promise.all([
|
|
@@ -3223,19 +4105,10 @@ async function run(config) {
|
|
|
3223
4105
|
try {
|
|
3224
4106
|
await runTestsInBrowser(groupConfig, groupCachedContents[i], connections);
|
|
3225
4107
|
} finally {
|
|
3226
|
-
await flushConsoleHandlers(groupConfig._pendingConsoleHandlers);
|
|
3227
|
-
await
|
|
3228
|
-
|
|
3229
|
-
connections.page
|
|
3230
|
-
// timer still fires if page.close() hangs, without preventing process exit later.
|
|
3231
|
-
Promise.race([
|
|
3232
|
-
connections.page.close(),
|
|
3233
|
-
new Promise((resolve) => {
|
|
3234
|
-
const pageCloseTimeoutId = setTimeout(resolve, PAGE_CLOSE_GRACE_MS);
|
|
3235
|
-
pageCloseTimeoutId.unref();
|
|
3236
|
-
})
|
|
3237
|
-
]).catch(() => {
|
|
3238
|
-
})
|
|
4108
|
+
await flushConsoleHandlers(groupConfig._pendingConsoleHandlers, connections.page);
|
|
4109
|
+
await closeWithGrace([
|
|
4110
|
+
sharedServer ? void 0 : connections.server?.close(),
|
|
4111
|
+
connections.page?.close()
|
|
3239
4112
|
]);
|
|
3240
4113
|
}
|
|
3241
4114
|
})();
|
|
@@ -3269,11 +4142,22 @@ async function run(config) {
|
|
|
3269
4142
|
if (config.after) {
|
|
3270
4143
|
await runUserModule(`${process.cwd()}/${config.after}`, config.COUNTER, "after");
|
|
3271
4144
|
}
|
|
4145
|
+
if (config._daemonMode) {
|
|
4146
|
+
clearInterval(keepAlive);
|
|
4147
|
+
await closeWithGrace([
|
|
4148
|
+
sharedServer?.close().catch(
|
|
4149
|
+
(err) => config.debug && process.stderr.write(`# [qunitx] server.close: ${err.message}
|
|
4150
|
+
`)
|
|
4151
|
+
)
|
|
4152
|
+
]);
|
|
4153
|
+
throw new DaemonRunError(exitCode);
|
|
4154
|
+
}
|
|
4155
|
+
await maybePrintDaemonHint({ durationMs: process.uptime() * 1e3 });
|
|
3272
4156
|
const exitTimer = setTimeout(() => process.exit(exitCode), STDOUT_FLUSH_GRACE_MS);
|
|
3273
4157
|
exitTimer.unref();
|
|
3274
4158
|
process.stdout.write("\n", async () => {
|
|
3275
4159
|
clearTimeout(exitTimer);
|
|
3276
|
-
await
|
|
4160
|
+
await closeWithGrace([
|
|
3277
4161
|
sharedServer?.close().catch(
|
|
3278
4162
|
(err) => config.debug && process.stderr.write(`# [qunitx] server.close: ${err.message}
|
|
3279
4163
|
`)
|
|
@@ -3281,9 +4165,9 @@ async function run(config) {
|
|
|
3281
4165
|
browser.close().catch(
|
|
3282
4166
|
(err) => config.debug && process.stderr.write(`# [qunitx] browser.close: ${err.message}
|
|
3283
4167
|
`)
|
|
3284
|
-
)
|
|
4168
|
+
),
|
|
4169
|
+
shutdownPrelaunch()
|
|
3285
4170
|
]);
|
|
3286
|
-
await shutdownPrelaunch();
|
|
3287
4171
|
clearInterval(keepAlive);
|
|
3288
4172
|
process.exit(exitCode);
|
|
3289
4173
|
});
|
|
@@ -3291,7 +4175,7 @@ async function run(config) {
|
|
|
3291
4175
|
}
|
|
3292
4176
|
async function buildCachedContent(config, htmlPaths) {
|
|
3293
4177
|
const htmlBuffers = await Promise.all(
|
|
3294
|
-
config.htmlPaths.map((htmlPath) =>
|
|
4178
|
+
config.htmlPaths.map((htmlPath) => fs14.readFile(htmlPath).catch(() => null))
|
|
3295
4179
|
);
|
|
3296
4180
|
const cachedContent = htmlPaths.reduce(
|
|
3297
4181
|
(result, _htmlPath, index) => {
|
|
@@ -3346,7 +4230,7 @@ async function addCachedContentMainHTML(projectRoot, cachedContent) {
|
|
|
3346
4230
|
}
|
|
3347
4231
|
async function readTimingCache(projectRoot) {
|
|
3348
4232
|
try {
|
|
3349
|
-
const parsed = JSON.parse(await
|
|
4233
|
+
const parsed = JSON.parse(await fs14.readFile(`${projectRoot}/tmp/test-timings.json`, "utf8"));
|
|
3350
4234
|
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : {};
|
|
3351
4235
|
} catch {
|
|
3352
4236
|
return {};
|
|
@@ -3365,7 +4249,7 @@ function computeFileTimes(groups, weights, wallTimes) {
|
|
|
3365
4249
|
return result;
|
|
3366
4250
|
}
|
|
3367
4251
|
async function persistTimings(fileTimes, projectRoot) {
|
|
3368
|
-
await
|
|
4252
|
+
await fs14.writeFile(
|
|
3369
4253
|
`${projectRoot}/tmp/test-timings.json`,
|
|
3370
4254
|
JSON.stringify(Object.fromEntries(fileTimes), null, 2)
|
|
3371
4255
|
);
|
|
@@ -3380,7 +4264,7 @@ ${lines.join("\n")}
|
|
|
3380
4264
|
async function splitIntoGroups(files, groupCount, timings) {
|
|
3381
4265
|
const sizes = await Promise.all(
|
|
3382
4266
|
files.map(
|
|
3383
|
-
(f) => timings[f] > 0 ? Promise.resolve(0) :
|
|
4267
|
+
(f) => timings[f] > 0 ? Promise.resolve(0) : fs14.stat(f).then((s) => s.size).catch(() => 0)
|
|
3384
4268
|
)
|
|
3385
4269
|
);
|
|
3386
4270
|
const knownRates = files.map((f, i) => ({ ms: timings[f], size: sizes[i] })).filter(({ ms, size }) => ms > 0 && size > 0);
|
|
@@ -3419,7 +4303,7 @@ function resolveQunitxRoot(projectRoot) {
|
|
|
3419
4303
|
if (!match) throw new Error(`Could not derive qunitx root from ${mainEntry}`);
|
|
3420
4304
|
return match[1];
|
|
3421
4305
|
}
|
|
3422
|
-
var WATCH_NAV_TIMEOUT_MS,
|
|
4306
|
+
var WATCH_NAV_TIMEOUT_MS, STDOUT_FLUSH_GRACE_MS, KEEP_ALIVE_INTERVAL_MS, EXIT_CODE_SIGTERM;
|
|
3423
4307
|
var init_run = __esm({
|
|
3424
4308
|
"lib/commands/run.ts"() {
|
|
3425
4309
|
init_browser();
|
|
@@ -3437,542 +4321,488 @@ var init_run = __esm({
|
|
|
3437
4321
|
init_write_output_static_files();
|
|
3438
4322
|
init_time_counter();
|
|
3439
4323
|
init_display_final_result();
|
|
3440
|
-
init_read_template();
|
|
3441
|
-
init_html();
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
}
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
"
|
|
3470
|
-
"
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
],
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
};
|
|
3536
|
-
|
|
3537
|
-
// lib/commands/help.ts
|
|
3538
|
-
var highlight = (text) => magenta().bold(text);
|
|
3539
|
-
var color = (text) => blue(text);
|
|
3540
|
-
function displayHelpOutput() {
|
|
3541
|
-
const config = package_default;
|
|
3542
|
-
console.log(`${highlight("[qunitx v" + config.version + "] Usage:")} qunitx ${color("[targets] --$flags")}
|
|
3543
|
-
|
|
3544
|
-
${highlight("Input options:")}
|
|
3545
|
-
- File: $ ${color("qunitx test/foo.js")}
|
|
3546
|
-
- Folder: $ ${color("qunitx test/login")}
|
|
3547
|
-
- Globs: $ ${color("qunitx test/**/*-test.js")}
|
|
3548
|
-
- Combination: $ ${color("qunitx test/foo.js test/bar.js test/*-test.js test/logout")}
|
|
3549
|
-
|
|
3550
|
-
${highlight("Optional flags:")}
|
|
3551
|
-
${color("--debug")} : print console output when tests run in browser
|
|
3552
|
-
${color("--watch")} : run the target file or folders, watch them for continuous run and expose http server under localhost
|
|
3553
|
-
${color("--open")} : run tests in a visible browser window instead of headless; keeps the server alive (short: ${color("-o")})
|
|
3554
|
-
${color("--timeout")} : change default timeout per test case
|
|
3555
|
-
${color("--output")} : folder to distribute built qunitx html and js that a webservers can run[default: tmp]
|
|
3556
|
-
${color("--failFast")} : run the target file or folders with immediate abort if a single test fails
|
|
3557
|
-
${color("--port")} : HTTP server port (auto-selects a free port if the given port is taken)[default: 1234]
|
|
3558
|
-
${color("--extensions")} : comma-separated file extensions to track for discovery and watch-mode rebuilds[default: js,ts,jsx,tsx]
|
|
3559
|
-
${color("--browser")} : browser engine to run tests in: chromium, firefox, webkit[default: chromium]
|
|
3560
|
-
${color("--before")} : run a script before the tests(i.e start a new web server before tests)
|
|
3561
|
-
${color("--after")} : run a script after the tests(i.e save test results to a file)
|
|
3562
|
-
|
|
3563
|
-
${highlight("Example:")} $ ${color("qunitx test/foo.ts app/e2e --debug --watch --before=scripts/start-new-webserver.js --after=scripts/write-test-results.js")}
|
|
3564
|
-
|
|
3565
|
-
${highlight("Commands:")}
|
|
3566
|
-
${color("$ qunitx init")} # Bootstraps qunitx base html and add qunitx config to package.json if needed
|
|
3567
|
-
${color("$ qunitx new $testFileName")} # Creates a qunitx test file
|
|
3568
|
-
`);
|
|
3569
|
-
}
|
|
3570
|
-
|
|
3571
|
-
// lib/commands/init.ts
|
|
3572
|
-
import fs4 from "node:fs/promises";
|
|
3573
|
-
import path2 from "node:path";
|
|
3574
|
-
|
|
3575
|
-
// lib/utils/find-project-root.ts
|
|
3576
|
-
import process2 from "node:process";
|
|
3577
|
-
|
|
3578
|
-
// lib/utils/path-exists.ts
|
|
3579
|
-
import fs2 from "node:fs/promises";
|
|
3580
|
-
async function pathExists(path10) {
|
|
3581
|
-
try {
|
|
3582
|
-
await fs2.access(path10);
|
|
3583
|
-
return true;
|
|
3584
|
-
} catch {
|
|
3585
|
-
return false;
|
|
3586
|
-
}
|
|
3587
|
-
}
|
|
3588
|
-
|
|
3589
|
-
// lib/utils/search-in-parent-directories.ts
|
|
3590
|
-
async function searchInParentDirectories(directory, targetEntry) {
|
|
3591
|
-
const resolvedDirectory = directory === "." ? process.cwd() : directory;
|
|
3592
|
-
if (await pathExists(`${resolvedDirectory}/${targetEntry}`)) {
|
|
3593
|
-
return `${resolvedDirectory}/${targetEntry}`;
|
|
3594
|
-
} else if (resolvedDirectory === "") {
|
|
3595
|
-
return;
|
|
3596
|
-
}
|
|
3597
|
-
return await searchInParentDirectories(
|
|
3598
|
-
resolvedDirectory.slice(0, resolvedDirectory.lastIndexOf("/")),
|
|
3599
|
-
targetEntry
|
|
3600
|
-
);
|
|
3601
|
-
}
|
|
3602
|
-
|
|
3603
|
-
// lib/utils/find-project-root.ts
|
|
3604
|
-
async function findProjectRoot() {
|
|
3605
|
-
try {
|
|
3606
|
-
const absolutePath = await searchInParentDirectories(".", "package.json");
|
|
3607
|
-
if (!absolutePath.includes("package.json")) {
|
|
3608
|
-
throw new Error("package.json mising");
|
|
3609
|
-
}
|
|
3610
|
-
return absolutePath.replace("/package.json", "");
|
|
3611
|
-
} catch (_error) {
|
|
3612
|
-
console.log("couldnt find projects package.json, did you run $ npm init ??");
|
|
3613
|
-
process2.exit(1);
|
|
3614
|
-
}
|
|
3615
|
-
}
|
|
3616
|
-
|
|
3617
|
-
// lib/commands/init.ts
|
|
3618
|
-
init_default_project_config_values();
|
|
3619
|
-
init_read_template();
|
|
3620
|
-
async function initializeProject() {
|
|
3621
|
-
const projectRoot = await findProjectRoot();
|
|
3622
|
-
const oldPackageJSON = JSON.parse(await fs4.readFile(`${projectRoot}/package.json`));
|
|
3623
|
-
const existingQunitx = oldPackageJSON.qunitx || {};
|
|
3624
|
-
const cliHtmlPaths = process.argv.slice(2).filter((arg) => arg.endsWith(".html"));
|
|
3625
|
-
const config = Object.assign({}, defaultProjectConfigValues, existingQunitx, {
|
|
3626
|
-
htmlPaths: cliHtmlPaths.length > 0 ? cliHtmlPaths : existingQunitx.htmlPaths || ["test/tests.html"]
|
|
3627
|
-
});
|
|
3628
|
-
await Promise.all([
|
|
3629
|
-
writeTestsHTML(projectRoot, config, oldPackageJSON),
|
|
3630
|
-
rewritePackageJSON(projectRoot, config, oldPackageJSON),
|
|
3631
|
-
writeTSConfigIfNeeded(projectRoot)
|
|
3632
|
-
]);
|
|
3633
|
-
}
|
|
3634
|
-
async function writeTestsHTML(projectRoot, config, oldPackageJSON) {
|
|
3635
|
-
const testHTMLTemplateBuffer = await readTemplate("setup/tests.hbs");
|
|
3636
|
-
return await Promise.all(
|
|
3637
|
-
config.htmlPaths.map(async (htmlPath) => {
|
|
3638
|
-
const targetPath = `${projectRoot}/${htmlPath}`;
|
|
3639
|
-
if (await pathExists(targetPath)) {
|
|
3640
|
-
return console.log(`${htmlPath} already exists`);
|
|
3641
|
-
} else {
|
|
3642
|
-
const targetDirectory = path2.dirname(targetPath);
|
|
3643
|
-
const _targetOutputPath = path2.relative(
|
|
3644
|
-
targetDirectory,
|
|
3645
|
-
path2.join(path2.resolve(projectRoot, config.output), "tests.js")
|
|
3646
|
-
);
|
|
3647
|
-
const testHTMLTemplate = testHTMLTemplateBuffer.replace(
|
|
3648
|
-
"{{applicationName}}",
|
|
3649
|
-
oldPackageJSON.name
|
|
3650
|
-
);
|
|
3651
|
-
await fs4.mkdir(targetDirectory, { recursive: true });
|
|
3652
|
-
await fs4.writeFile(targetPath, testHTMLTemplate);
|
|
3653
|
-
console.log(`${targetPath} written`);
|
|
3654
|
-
}
|
|
3655
|
-
})
|
|
3656
|
-
);
|
|
3657
|
-
}
|
|
3658
|
-
async function rewritePackageJSON(projectRoot, config, oldPackageJSON) {
|
|
3659
|
-
const newPackageJSON = Object.assign(oldPackageJSON, { qunitx: config });
|
|
3660
|
-
await fs4.writeFile(`${projectRoot}/package.json`, JSON.stringify(newPackageJSON, null, 2));
|
|
3661
|
-
}
|
|
3662
|
-
async function writeTSConfigIfNeeded(projectRoot) {
|
|
3663
|
-
const targetPath = `${projectRoot}/tsconfig.json`;
|
|
3664
|
-
if (!await pathExists(targetPath)) {
|
|
3665
|
-
const tsConfigTemplate = await readTemplate("setup/tsconfig.json");
|
|
3666
|
-
await fs4.writeFile(targetPath, tsConfigTemplate);
|
|
3667
|
-
console.log(`${targetPath} written`);
|
|
3668
|
-
}
|
|
3669
|
-
}
|
|
3670
|
-
|
|
3671
|
-
// lib/commands/generate.ts
|
|
3672
|
-
init_color();
|
|
3673
|
-
import fs5 from "node:fs/promises";
|
|
3674
|
-
init_read_template();
|
|
3675
|
-
|
|
3676
|
-
// lib/utils/convert-to-pascal-case.ts
|
|
3677
|
-
function convertToPascalCase(str) {
|
|
3678
|
-
return str.split(/[-_]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
3679
|
-
}
|
|
3680
|
-
|
|
3681
|
-
// lib/commands/generate.ts
|
|
3682
|
-
async function generateTestFiles() {
|
|
3683
|
-
const projectRoot = await findProjectRoot();
|
|
3684
|
-
const moduleName = pathToModuleName(process.argv[3]);
|
|
3685
|
-
const path10 = process.argv[3].endsWith(".js") || process.argv[3].endsWith(".ts") ? `${projectRoot}/${process.argv[3]}` : `${projectRoot}/${process.argv[3]}.js`;
|
|
3686
|
-
if (await pathExists(path10)) {
|
|
3687
|
-
console.log(`${path10} already exists!`);
|
|
3688
|
-
return;
|
|
3689
|
-
}
|
|
3690
|
-
const testJSContent = await readTemplate("test.js");
|
|
3691
|
-
const targetFolderPaths = path10.split("/");
|
|
3692
|
-
targetFolderPaths.pop();
|
|
3693
|
-
await fs5.mkdir(targetFolderPaths.join("/"), { recursive: true });
|
|
3694
|
-
await fs5.writeFile(path10, testJSContent.replace("{{moduleName}}", moduleName));
|
|
3695
|
-
console.log(green(`${path10} written`));
|
|
3696
|
-
}
|
|
3697
|
-
function pathToModuleName(filePath) {
|
|
3698
|
-
const withoutExt = filePath.replace(/\.(js|ts)$/, "");
|
|
3699
|
-
const segments = withoutExt.split("/");
|
|
3700
|
-
const targetNames = segments[0] === "test" || segments[0] === "tests" ? segments.slice(1) : segments;
|
|
3701
|
-
return targetNames.map(convertToPascalCase).join(" | ");
|
|
3702
|
-
}
|
|
3703
|
-
|
|
3704
|
-
// lib/setup/config.ts
|
|
3705
|
-
init_default_project_config_values();
|
|
3706
|
-
import fs7 from "node:fs/promises";
|
|
3707
|
-
import { createRequire } from "node:module";
|
|
3708
|
-
import { pathToFileURL } from "node:url";
|
|
3709
|
-
|
|
3710
|
-
// lib/setup/fs-tree.ts
|
|
3711
|
-
init_default_project_config_values();
|
|
3712
|
-
import fs6, { glob as fsGlob } from "node:fs/promises";
|
|
3713
|
-
import path3 from "node:path";
|
|
3714
|
-
async function buildFSTree(fileAbsolutePaths, config = {}) {
|
|
3715
|
-
const targetExtensions = config.extensions || defaultProjectConfigValues.extensions;
|
|
3716
|
-
const fsTree = {};
|
|
3717
|
-
await Promise.all(
|
|
3718
|
-
fileAbsolutePaths.map(async (fileAbsolutePath) => {
|
|
3719
|
-
try {
|
|
3720
|
-
if (isGlob(fileAbsolutePath)) {
|
|
3721
|
-
for await (const fileName of fsGlob(fileAbsolutePath)) {
|
|
3722
|
-
if (targetExtensions.some((ext) => fileName.endsWith(`.${ext}`))) {
|
|
3723
|
-
fsTree[fileName] = null;
|
|
3724
|
-
}
|
|
3725
|
-
}
|
|
3726
|
-
} else {
|
|
3727
|
-
const entry = await fs6.stat(fileAbsolutePath);
|
|
3728
|
-
if (entry.isFile()) {
|
|
3729
|
-
fsTree[fileAbsolutePath] = null;
|
|
3730
|
-
} else if (entry.isDirectory()) {
|
|
3731
|
-
const fileNames = await readDirRecursive(fileAbsolutePath, (name) => {
|
|
3732
|
-
return targetExtensions.some((extension) => name.endsWith(`.${extension}`));
|
|
3733
|
-
});
|
|
3734
|
-
fileNames.forEach((fileName) => {
|
|
3735
|
-
fsTree[fileName] = null;
|
|
3736
|
-
});
|
|
3737
|
-
}
|
|
3738
|
-
}
|
|
3739
|
-
} catch (error) {
|
|
3740
|
-
console.error(error);
|
|
3741
|
-
return process.exit(1);
|
|
3742
|
-
}
|
|
3743
|
-
})
|
|
3744
|
-
);
|
|
3745
|
-
return fsTree;
|
|
3746
|
-
}
|
|
3747
|
-
function isGlob(str) {
|
|
3748
|
-
return /[*?{[]/.test(str);
|
|
3749
|
-
}
|
|
3750
|
-
async function readDirRecursive(dir, filter) {
|
|
3751
|
-
const entries = await fs6.readdir(dir, { recursive: true, withFileTypes: true });
|
|
3752
|
-
const candidates = entries.filter(
|
|
3753
|
-
(dirent) => (dirent.isFile() || dirent.isSymbolicLink()) && filter(dirent.name)
|
|
3754
|
-
);
|
|
3755
|
-
const resolvedPaths = await Promise.all(
|
|
3756
|
-
candidates.map(async (dirent) => {
|
|
3757
|
-
const fullPath = path3.join(dirent.parentPath, dirent.name);
|
|
3758
|
-
if (dirent.isFile()) return fullPath;
|
|
3759
|
-
try {
|
|
3760
|
-
const statResult = await fs6.stat(fullPath);
|
|
3761
|
-
return statResult.isFile() ? fullPath : null;
|
|
3762
|
-
} catch {
|
|
3763
|
-
return null;
|
|
3764
|
-
}
|
|
3765
|
-
})
|
|
3766
|
-
);
|
|
3767
|
-
return resolvedPaths.filter((resolvedPath) => resolvedPath !== null);
|
|
3768
|
-
}
|
|
3769
|
-
|
|
3770
|
-
// lib/setup/test-file-paths.ts
|
|
3771
|
-
import { matchesGlob } from "node:path";
|
|
3772
|
-
var GLOB_CHARS = /[*?{[]/;
|
|
3773
|
-
function setupTestFilePaths(inputs2) {
|
|
3774
|
-
const folders = [];
|
|
3775
|
-
const filesWithGlob = [];
|
|
3776
|
-
const filesWithoutGlob = [];
|
|
3777
|
-
inputs2.forEach((input) => {
|
|
3778
|
-
if (!pathIsFile(input)) {
|
|
3779
|
-
folders.push({ input, globFormat: `${input}/**` });
|
|
3780
|
-
} else if (isGlob2(input)) {
|
|
3781
|
-
filesWithGlob.push({ input, globFormat: input });
|
|
3782
|
-
} else {
|
|
3783
|
-
filesWithoutGlob.push({ input, globFormat: input });
|
|
3784
|
-
}
|
|
4324
|
+
init_read_template();
|
|
4325
|
+
init_html();
|
|
4326
|
+
init_close_with_grace();
|
|
4327
|
+
init_daemon_hint();
|
|
4328
|
+
WATCH_NAV_TIMEOUT_MS = 5e3;
|
|
4329
|
+
STDOUT_FLUSH_GRACE_MS = 5e3;
|
|
4330
|
+
KEEP_ALIVE_INTERVAL_MS = 1e4;
|
|
4331
|
+
EXIT_CODE_SIGTERM = 128 + 15;
|
|
4332
|
+
}
|
|
4333
|
+
});
|
|
4334
|
+
|
|
4335
|
+
// lib/commands/daemon/server.ts
|
|
4336
|
+
var server_exports = {};
|
|
4337
|
+
__export(server_exports, {
|
|
4338
|
+
runDaemonServer: () => runDaemonServer
|
|
4339
|
+
});
|
|
4340
|
+
import net2 from "node:net";
|
|
4341
|
+
import fs15 from "node:fs";
|
|
4342
|
+
import { writeFile, unlink, stat as stat2, chmod } from "node:fs/promises";
|
|
4343
|
+
import path12 from "node:path";
|
|
4344
|
+
async function runDaemonServer() {
|
|
4345
|
+
const cwd = process.cwd();
|
|
4346
|
+
const socketPath = daemonSocketPath(cwd);
|
|
4347
|
+
const infoPath = daemonInfoPath(cwd);
|
|
4348
|
+
if (fs15.existsSync(infoPath) && await isLiveSocket(socketPath)) process.exit(0);
|
|
4349
|
+
await unlink(socketPath).catch(() => {
|
|
4350
|
+
});
|
|
4351
|
+
const logPath = process.env.QUNITX_DAEMON_LOG;
|
|
4352
|
+
if (logPath) {
|
|
4353
|
+
const log = fs15.createWriteStream(logPath, { flags: "a" });
|
|
4354
|
+
log.on("error", () => {
|
|
4355
|
+
});
|
|
4356
|
+
const forward = log.write.bind(log);
|
|
4357
|
+
process.stdout.write = forward;
|
|
4358
|
+
process.stderr.write = forward;
|
|
4359
|
+
}
|
|
4360
|
+
const argvSnapshot = process.argv;
|
|
4361
|
+
process.argv = [argvSnapshot[0], argvSnapshot[1] ?? "cli.ts"];
|
|
4362
|
+
let baseConfig;
|
|
4363
|
+
try {
|
|
4364
|
+
baseConfig = await setupConfig();
|
|
4365
|
+
} finally {
|
|
4366
|
+
process.argv = argvSnapshot;
|
|
4367
|
+
}
|
|
4368
|
+
baseConfig._daemonMode = true;
|
|
4369
|
+
baseConfig.watch = false;
|
|
4370
|
+
baseConfig.open = false;
|
|
4371
|
+
const [browser, pkgMtime] = await Promise.all([launchBrowser(baseConfig), readPkgMtime(cwd)]);
|
|
4372
|
+
const state = {
|
|
4373
|
+
browser,
|
|
4374
|
+
baseConfig,
|
|
4375
|
+
cwd,
|
|
4376
|
+
startedAt: Date.now(),
|
|
4377
|
+
pkgMtime,
|
|
4378
|
+
runQueue: Promise.resolve(),
|
|
4379
|
+
shuttingDown: false,
|
|
4380
|
+
pendingClients: /* @__PURE__ */ new Set(),
|
|
4381
|
+
socketServer: null,
|
|
4382
|
+
idleTimer: null,
|
|
4383
|
+
socketPath,
|
|
4384
|
+
infoPath,
|
|
4385
|
+
consecutiveCrashes: 0,
|
|
4386
|
+
listenSucceeded: false,
|
|
4387
|
+
esbuildCache: { _esbuildContext: null }
|
|
4388
|
+
};
|
|
4389
|
+
const shutdown = (reason) => shutdownDaemon2(state, reason);
|
|
4390
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
4391
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
4392
|
+
process.on("unhandledRejection", (err) => {
|
|
4393
|
+
process.stderr.write(`# [qunitx daemon] unhandledRejection: ${err}
|
|
4394
|
+
`);
|
|
4395
|
+
void shutdown("unhandledRejection");
|
|
4396
|
+
});
|
|
4397
|
+
state.socketServer = net2.createServer((socket) => handleConnection(socket, state));
|
|
4398
|
+
state.socketServer.on("error", (err) => {
|
|
4399
|
+
process.stderr.write(`# [qunitx daemon] server error: ${err.message}
|
|
4400
|
+
`);
|
|
4401
|
+
void shutdown("server error");
|
|
4402
|
+
});
|
|
4403
|
+
await listen(state.socketServer, socketPath);
|
|
4404
|
+
state.listenSucceeded = true;
|
|
4405
|
+
if (process.platform !== "win32") await chmod(socketPath, 384).catch(() => {
|
|
4406
|
+
});
|
|
4407
|
+
const info = {
|
|
4408
|
+
pid: process.pid,
|
|
4409
|
+
socketPath,
|
|
4410
|
+
cwd,
|
|
4411
|
+
nodeVersion: process.version,
|
|
4412
|
+
startedAt: state.startedAt
|
|
4413
|
+
};
|
|
4414
|
+
await writeFile(infoPath, JSON.stringify(info, null, 2));
|
|
4415
|
+
resetIdleTimer(state);
|
|
4416
|
+
process.stderr.write(`# [qunitx daemon] listening on ${socketPath} (pid ${process.pid})
|
|
4417
|
+
`);
|
|
4418
|
+
return new Promise(() => {
|
|
3785
4419
|
});
|
|
3786
|
-
const dedupedFolders = folders.filter((folder) => !isIncludedIn(folders, folder));
|
|
3787
|
-
const dedupedGlobFiles = filesWithGlob.filter(
|
|
3788
|
-
(file) => !isIncludedIn(dedupedFolders, file) && !isIncludedIn(filesWithGlob, file)
|
|
3789
|
-
);
|
|
3790
|
-
const dedupedPlainFiles = filesWithoutGlob.reduce((acc, file) => {
|
|
3791
|
-
if (!isIncludedIn(dedupedFolders, file) && !isIncludedIn(dedupedGlobFiles, file) && !isIncludedIn(acc, file)) {
|
|
3792
|
-
acc.push(file);
|
|
3793
|
-
}
|
|
3794
|
-
return acc;
|
|
3795
|
-
}, []);
|
|
3796
|
-
return dedupedFolders.concat(dedupedGlobFiles, dedupedPlainFiles).map((meta) => meta.input);
|
|
3797
4420
|
}
|
|
3798
|
-
function
|
|
3799
|
-
return
|
|
4421
|
+
function listen(server, socketPath) {
|
|
4422
|
+
return new Promise((resolve, reject) => {
|
|
4423
|
+
const onError = (err) => reject(err);
|
|
4424
|
+
server.once("error", onError);
|
|
4425
|
+
server.listen(socketPath, () => {
|
|
4426
|
+
server.removeListener("error", onError);
|
|
4427
|
+
resolve();
|
|
4428
|
+
});
|
|
4429
|
+
});
|
|
3800
4430
|
}
|
|
3801
|
-
function
|
|
3802
|
-
|
|
4431
|
+
async function shutdownDaemon2(state, reason) {
|
|
4432
|
+
if (state.shuttingDown) return;
|
|
4433
|
+
state.shuttingDown = true;
|
|
4434
|
+
process.stderr.write(`# [qunitx daemon] shutting down: ${reason}
|
|
4435
|
+
`);
|
|
4436
|
+
if (state.idleTimer) clearTimeout(state.idleTimer);
|
|
4437
|
+
for (const sock of state.pendingClients) {
|
|
4438
|
+
writeChunk(sock, { type: "fatal", message: `daemon shutting down: ${reason}` });
|
|
4439
|
+
sock.end();
|
|
4440
|
+
}
|
|
4441
|
+
await new Promise((resolve) => state.socketServer.close(() => resolve()));
|
|
4442
|
+
await Promise.all([
|
|
4443
|
+
state.listenSucceeded ? unlink(state.socketPath).catch(() => {
|
|
4444
|
+
}) : null,
|
|
4445
|
+
state.listenSucceeded ? unlink(state.infoPath).catch(() => {
|
|
4446
|
+
}) : null,
|
|
4447
|
+
state.browser.close().catch(() => {
|
|
4448
|
+
}),
|
|
4449
|
+
state.esbuildCache._esbuildContext?.dispose().catch(() => {
|
|
4450
|
+
})
|
|
4451
|
+
]);
|
|
4452
|
+
process.exit(0);
|
|
3803
4453
|
}
|
|
3804
|
-
function
|
|
3805
|
-
|
|
4454
|
+
function resetIdleTimer(state) {
|
|
4455
|
+
if (state.idleTimer) clearTimeout(state.idleTimer);
|
|
4456
|
+
state.idleTimer = setTimeout(() => void shutdownDaemon2(state, "idle timeout"), IDLE_TIMEOUT_MS);
|
|
4457
|
+
state.idleTimer.unref();
|
|
3806
4458
|
}
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
} else if (arg.endsWith(".html")) {
|
|
3829
|
-
if (result.htmlPaths) {
|
|
3830
|
-
result.htmlPaths.push(arg);
|
|
3831
|
-
} else {
|
|
3832
|
-
result.htmlPaths = [arg];
|
|
3833
|
-
}
|
|
3834
|
-
return result;
|
|
3835
|
-
} else if (arg.startsWith("--port")) {
|
|
3836
|
-
return Object.assign(result, { port: Number(arg.split("=")[1]), portExplicit: true });
|
|
3837
|
-
} else if (arg.startsWith("--extensions")) {
|
|
3838
|
-
return Object.assign(result, {
|
|
3839
|
-
extensions: arg.split("=")[1].split(",").map((extension) => extension.trim())
|
|
3840
|
-
});
|
|
3841
|
-
} else if (arg.startsWith("--browser")) {
|
|
3842
|
-
const value = arg.split("=")[1];
|
|
3843
|
-
if (!["chromium", "firefox", "webkit"].includes(value)) {
|
|
3844
|
-
console.error(
|
|
3845
|
-
`Invalid --browser value: "${value}". Must be one of: chromium, firefox, webkit`
|
|
3846
|
-
);
|
|
3847
|
-
process.exit(1);
|
|
3848
|
-
}
|
|
3849
|
-
return Object.assign(result, { browser: value });
|
|
3850
|
-
} else if (arg.startsWith("--before")) {
|
|
3851
|
-
return Object.assign(result, { before: parseModule(arg.split("=")[1]) });
|
|
3852
|
-
} else if (arg.startsWith("--after")) {
|
|
3853
|
-
return Object.assign(result, { after: parseModule(arg.split("=")[1]) });
|
|
3854
|
-
} else if (arg === "--trace-perf") {
|
|
3855
|
-
return result;
|
|
3856
|
-
}
|
|
3857
|
-
if (arg.startsWith("-")) {
|
|
3858
|
-
console.warn(`# Warning: Unknown flag "${arg}" \u2014 ignored`);
|
|
3859
|
-
return result;
|
|
3860
|
-
}
|
|
3861
|
-
result.inputs.add(
|
|
3862
|
-
arg.startsWith(projectRoot) || arg.startsWith("/") ? arg : path4.join(process.cwd(), arg)
|
|
3863
|
-
);
|
|
3864
|
-
return result;
|
|
3865
|
-
},
|
|
3866
|
-
{ inputs: /* @__PURE__ */ new Set([]) }
|
|
3867
|
-
);
|
|
3868
|
-
if (!providedFlags.browser && process.env.QUNITX_BROWSER) {
|
|
3869
|
-
const envBrowser = process.env.QUNITX_BROWSER;
|
|
3870
|
-
if (!["chromium", "firefox", "webkit"].includes(envBrowser)) {
|
|
3871
|
-
console.error(
|
|
3872
|
-
`Invalid QUNITX_BROWSER value: "${envBrowser}". Must be one of: chromium, firefox, webkit`
|
|
3873
|
-
);
|
|
3874
|
-
process.exit(1);
|
|
4459
|
+
function handleConnection(socket, state) {
|
|
4460
|
+
state.pendingClients.add(socket);
|
|
4461
|
+
socket.on("close", () => state.pendingClients.delete(socket));
|
|
4462
|
+
socket.on("error", () => {
|
|
4463
|
+
});
|
|
4464
|
+
attachLineParser(socket, (req) => void dispatch(req, socket, state));
|
|
4465
|
+
}
|
|
4466
|
+
async function dispatch(req, socket, state) {
|
|
4467
|
+
if (req.type === "ping") {
|
|
4468
|
+
writeChunk(socket, {
|
|
4469
|
+
type: "pong",
|
|
4470
|
+
pid: process.pid,
|
|
4471
|
+
nodeVersion: process.version,
|
|
4472
|
+
cwd: state.cwd,
|
|
4473
|
+
startedAt: state.startedAt
|
|
4474
|
+
});
|
|
4475
|
+
socket.end();
|
|
4476
|
+
} else if (req.type === "shutdown") {
|
|
4477
|
+
try {
|
|
4478
|
+
fs15.unlinkSync(state.infoPath);
|
|
4479
|
+
} catch {
|
|
3875
4480
|
}
|
|
3876
|
-
|
|
4481
|
+
writeChunk(socket, { type: "done", exitCode: 0 });
|
|
4482
|
+
socket.end();
|
|
4483
|
+
void shutdownDaemon2(state, "shutdown request");
|
|
4484
|
+
} else if (req.type === "run") {
|
|
4485
|
+
state.runQueue = state.runQueue.then(() => handleRun(req, socket, state));
|
|
4486
|
+
await state.runQueue;
|
|
3877
4487
|
}
|
|
3878
|
-
return { ...providedFlags, inputs: Array.from(providedFlags.inputs) };
|
|
3879
4488
|
}
|
|
3880
|
-
function
|
|
3881
|
-
if (
|
|
4489
|
+
function writeChunk(socket, chunk) {
|
|
4490
|
+
if (socket.destroyed) return;
|
|
4491
|
+
try {
|
|
4492
|
+
socket.write(JSON.stringify(chunk) + "\n");
|
|
4493
|
+
} catch {
|
|
4494
|
+
}
|
|
4495
|
+
}
|
|
4496
|
+
function makeInterceptor(socket, type) {
|
|
4497
|
+
return ((chunk, ...args) => {
|
|
4498
|
+
if (!socket.destroyed) {
|
|
4499
|
+
const str = typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
4500
|
+
writeChunk(socket, { type, data: str });
|
|
4501
|
+
}
|
|
4502
|
+
const cb = args[args.length - 1];
|
|
4503
|
+
if (typeof cb === "function") queueMicrotask(cb);
|
|
3882
4504
|
return true;
|
|
3883
|
-
}
|
|
3884
|
-
|
|
4505
|
+
});
|
|
4506
|
+
}
|
|
4507
|
+
async function handleRun(req, socket, state) {
|
|
4508
|
+
if (state.shuttingDown) {
|
|
4509
|
+
writeChunk(socket, { type: "fatal", message: "daemon shutting down" });
|
|
4510
|
+
return void socket.end();
|
|
4511
|
+
} else if (req.cwd !== state.cwd) {
|
|
4512
|
+
writeChunk(socket, {
|
|
4513
|
+
type: "fatal",
|
|
4514
|
+
message: `cwd mismatch: daemon=${state.cwd} client=${req.cwd}`
|
|
4515
|
+
});
|
|
4516
|
+
return void socket.end();
|
|
4517
|
+
} else if (req.nodeVersion !== process.version) {
|
|
4518
|
+
writeChunk(socket, {
|
|
4519
|
+
type: "fatal",
|
|
4520
|
+
message: `node version mismatch: daemon=${process.version} client=${req.nodeVersion}`
|
|
4521
|
+
});
|
|
4522
|
+
socket.end();
|
|
4523
|
+
return void shutdownDaemon2(state, "node version mismatch");
|
|
3885
4524
|
}
|
|
3886
|
-
|
|
4525
|
+
const currentMtime = await readPkgMtime(state.cwd);
|
|
4526
|
+
if (currentMtime !== state.pkgMtime) {
|
|
4527
|
+
writeChunk(socket, { type: "fatal", message: "package.json changed; restarting daemon" });
|
|
4528
|
+
socket.end();
|
|
4529
|
+
return void shutdownDaemon2(state, "package.json changed");
|
|
4530
|
+
}
|
|
4531
|
+
if (state.idleTimer) clearTimeout(state.idleTimer);
|
|
4532
|
+
if (!state.browser.isConnected()) {
|
|
4533
|
+
await recoverBrowser(state);
|
|
4534
|
+
if (state.shuttingDown) {
|
|
4535
|
+
writeChunk(socket, { type: "fatal", message: "browser recovery failed" });
|
|
4536
|
+
return void socket.end();
|
|
4537
|
+
}
|
|
4538
|
+
}
|
|
4539
|
+
const origStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
4540
|
+
const origStderrWrite = process.stderr.write.bind(process.stderr);
|
|
4541
|
+
process.stdout.write = makeInterceptor(socket, "stdout");
|
|
4542
|
+
process.stderr.write = makeInterceptor(socket, "stderr");
|
|
4543
|
+
let exitCode = 0;
|
|
4544
|
+
try {
|
|
4545
|
+
exitCode = await runOnce(req.argv, req.env, state);
|
|
4546
|
+
} catch (err) {
|
|
4547
|
+
process.stderr.write = origStderrWrite;
|
|
4548
|
+
origStderrWrite(`# [qunitx daemon] run error: ${err.stack || err}
|
|
4549
|
+
`);
|
|
4550
|
+
if (!socket.destroyed)
|
|
4551
|
+
writeChunk(socket, { type: "fatal", message: err.message || String(err) });
|
|
4552
|
+
exitCode = 1;
|
|
4553
|
+
} finally {
|
|
4554
|
+
process.stdout.write = origStdoutWrite;
|
|
4555
|
+
process.stderr.write = origStderrWrite;
|
|
4556
|
+
}
|
|
4557
|
+
if (state.browser.isConnected()) state.consecutiveCrashes = 0;
|
|
4558
|
+
else await recoverBrowser(state);
|
|
4559
|
+
if (!socket.destroyed) {
|
|
4560
|
+
writeChunk(socket, { type: "done", exitCode });
|
|
4561
|
+
socket.end();
|
|
4562
|
+
}
|
|
4563
|
+
resetIdleTimer(state);
|
|
3887
4564
|
}
|
|
3888
|
-
function
|
|
3889
|
-
if (
|
|
3890
|
-
return
|
|
4565
|
+
async function recoverBrowser(state) {
|
|
4566
|
+
if (++state.consecutiveCrashes > MAX_CONSECUTIVE_CRASHES) {
|
|
4567
|
+
return void shutdownDaemon2(state, `${state.consecutiveCrashes} consecutive browser crashes`);
|
|
4568
|
+
}
|
|
4569
|
+
process.stderr.write(
|
|
4570
|
+
`# [qunitx daemon] browser crashed; relaunching (${state.consecutiveCrashes}/${MAX_CONSECUTIVE_CRASHES})
|
|
4571
|
+
`
|
|
4572
|
+
);
|
|
4573
|
+
state.browser.close().catch(() => {
|
|
4574
|
+
});
|
|
4575
|
+
try {
|
|
4576
|
+
state.browser = await launchBrowser(state.baseConfig, true);
|
|
4577
|
+
} catch (err) {
|
|
4578
|
+
void shutdownDaemon2(state, `browser relaunch failed: ${err.message || err}`);
|
|
3891
4579
|
}
|
|
3892
|
-
return value;
|
|
3893
4580
|
}
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
const
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
_onTestsJsServed: null
|
|
3925
|
-
};
|
|
3926
|
-
config.htmlPaths = normalizeHTMLPaths(config.projectRoot, config.htmlPaths);
|
|
3927
|
-
[config.fsTree, config.plugins] = await Promise.all([
|
|
3928
|
-
buildFSTree(config.testFileLookupPaths, config),
|
|
3929
|
-
pluginsPromise
|
|
3930
|
-
]);
|
|
3931
|
-
return config;
|
|
4581
|
+
async function runOnce(argv, env, state) {
|
|
4582
|
+
const envSnapshot = { ...process.env };
|
|
4583
|
+
for (const [key, value] of Object.entries(env)) {
|
|
4584
|
+
if (value !== void 0) process.env[key] = value;
|
|
4585
|
+
}
|
|
4586
|
+
const argvSnapshot = process.argv;
|
|
4587
|
+
process.argv = ["node", argvSnapshot[1] ?? "cli.ts", ...argv];
|
|
4588
|
+
let config;
|
|
4589
|
+
try {
|
|
4590
|
+
config = await setupConfig();
|
|
4591
|
+
} finally {
|
|
4592
|
+
process.argv = argvSnapshot;
|
|
4593
|
+
}
|
|
4594
|
+
config._daemonMode = true;
|
|
4595
|
+
config._daemonBrowser = state.browser;
|
|
4596
|
+
config._daemonEsbuildCache = state.esbuildCache;
|
|
4597
|
+
config.watch = false;
|
|
4598
|
+
config.open = false;
|
|
4599
|
+
try {
|
|
4600
|
+
await run(config);
|
|
4601
|
+
return config.COUNTER.failCount > 0 ? 1 : 0;
|
|
4602
|
+
} catch (err) {
|
|
4603
|
+
if (err instanceof DaemonRunError) return err.exitCode;
|
|
4604
|
+
throw err;
|
|
4605
|
+
} finally {
|
|
4606
|
+
for (const key of Object.keys(process.env)) {
|
|
4607
|
+
if (!(key in envSnapshot)) delete process.env[key];
|
|
4608
|
+
}
|
|
4609
|
+
Object.assign(process.env, envSnapshot);
|
|
4610
|
+
}
|
|
3932
4611
|
}
|
|
3933
|
-
async function
|
|
3934
|
-
const
|
|
3935
|
-
|
|
4612
|
+
async function isLiveSocket(socketPath) {
|
|
4613
|
+
const sock = await probeSocket(socketPath, LIVENESS_PROBE_TIMEOUT_MS);
|
|
4614
|
+
if (!sock) return false;
|
|
4615
|
+
sock.destroy();
|
|
4616
|
+
return true;
|
|
3936
4617
|
}
|
|
3937
|
-
function
|
|
3938
|
-
|
|
4618
|
+
async function readPkgMtime(cwd) {
|
|
4619
|
+
try {
|
|
4620
|
+
return (await stat2(path12.join(cwd, "package.json"))).mtimeMs;
|
|
4621
|
+
} catch {
|
|
4622
|
+
return 0;
|
|
4623
|
+
}
|
|
3939
4624
|
}
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
4625
|
+
var IDLE_TIMEOUT_MS, LIVENESS_PROBE_TIMEOUT_MS, MAX_CONSECUTIVE_CRASHES;
|
|
4626
|
+
var init_server = __esm({
|
|
4627
|
+
"lib/commands/daemon/server.ts"() {
|
|
4628
|
+
init_daemon_socket_path();
|
|
4629
|
+
init_socket_utils();
|
|
4630
|
+
init_config();
|
|
4631
|
+
init_browser();
|
|
4632
|
+
init_tests_in_browser();
|
|
4633
|
+
init_run();
|
|
4634
|
+
IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
4635
|
+
LIVENESS_PROBE_TIMEOUT_MS = 500;
|
|
4636
|
+
MAX_CONSECUTIVE_CRASHES = 2;
|
|
4637
|
+
}
|
|
4638
|
+
});
|
|
4639
|
+
|
|
4640
|
+
// lib/commands/daemon/index.ts
|
|
4641
|
+
var daemon_exports = {};
|
|
4642
|
+
__export(daemon_exports, {
|
|
4643
|
+
ensureDaemonRunning: () => ensureDaemonRunning,
|
|
4644
|
+
runDaemonCommand: () => runDaemonCommand
|
|
4645
|
+
});
|
|
4646
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
4647
|
+
import fs16, { existsSync as existsSync3 } from "node:fs";
|
|
4648
|
+
import path13 from "node:path";
|
|
4649
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
4650
|
+
function runDaemonCommand() {
|
|
4651
|
+
const sub = process.argv[3];
|
|
4652
|
+
if (sub === "_serve") return runServeMode();
|
|
4653
|
+
if (sub === "start") return startDaemon();
|
|
4654
|
+
if (sub === "stop") return stopDaemon();
|
|
4655
|
+
if (sub === "status") return statusDaemon();
|
|
4656
|
+
const helpRequested = !sub || sub === "--help" || sub === "-h" || sub === "help";
|
|
4657
|
+
const out = helpRequested ? process.stdout : process.stderr;
|
|
4658
|
+
out.write(USAGE);
|
|
4659
|
+
return Promise.resolve(helpRequested ? 0 : 1);
|
|
4660
|
+
}
|
|
4661
|
+
async function runServeMode() {
|
|
4662
|
+
const { runDaemonServer: runDaemonServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
4663
|
+
await runDaemonServer2();
|
|
4664
|
+
return 0;
|
|
4665
|
+
}
|
|
4666
|
+
function waitForFile(filePath, timeoutMs) {
|
|
4667
|
+
if (existsSync3(filePath)) return Promise.resolve(true);
|
|
4668
|
+
return new Promise((resolve) => {
|
|
4669
|
+
const dir = path13.dirname(filePath);
|
|
4670
|
+
const fileName = path13.basename(filePath);
|
|
4671
|
+
const settle = (ok) => {
|
|
4672
|
+
clearTimeout(timer);
|
|
4673
|
+
watcher.close();
|
|
4674
|
+
resolve(ok);
|
|
4675
|
+
};
|
|
4676
|
+
const timer = setTimeout(() => settle(false), timeoutMs);
|
|
4677
|
+
const watcher = fs16.watch(dir, (_event, name) => {
|
|
4678
|
+
if (name === fileName && existsSync3(filePath)) settle(true);
|
|
4679
|
+
});
|
|
4680
|
+
watcher.on("error", () => settle(false));
|
|
4681
|
+
if (existsSync3(filePath)) settle(true);
|
|
4682
|
+
});
|
|
3943
4683
|
}
|
|
3944
|
-
function
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
process.
|
|
4684
|
+
async function spawnAndWaitForDaemon() {
|
|
4685
|
+
spawn3(process.execPath, [CLI_ENTRY, "daemon", "_serve"], {
|
|
4686
|
+
detached: true,
|
|
4687
|
+
stdio: "ignore",
|
|
4688
|
+
env: { ...process.env, QUNITX_DAEMON_CWD: process.cwd() }
|
|
4689
|
+
}).unref();
|
|
4690
|
+
if (!await waitForFile(daemonInfoPath(), SPAWN_TIMEOUT_MS)) return null;
|
|
4691
|
+
const pong = await pingDaemon();
|
|
4692
|
+
return pong?.type === "pong" ? { pid: pong.pid } : null;
|
|
4693
|
+
}
|
|
4694
|
+
async function ensureDaemonRunning() {
|
|
4695
|
+
if ((await pingDaemon())?.type === "pong") return true;
|
|
4696
|
+
return Boolean(await spawnAndWaitForDaemon());
|
|
4697
|
+
}
|
|
4698
|
+
async function startDaemon() {
|
|
4699
|
+
const existing = await pingDaemon();
|
|
4700
|
+
if (existing?.type === "pong") {
|
|
4701
|
+
process.stdout.write(`Daemon already running (pid ${existing.pid})
|
|
4702
|
+
`);
|
|
4703
|
+
return 0;
|
|
3949
4704
|
}
|
|
3950
|
-
const
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
4705
|
+
const result = await spawnAndWaitForDaemon();
|
|
4706
|
+
if (result) {
|
|
4707
|
+
process.stdout.write(`Daemon started (pid ${result.pid})
|
|
4708
|
+
`);
|
|
4709
|
+
return 0;
|
|
4710
|
+
}
|
|
4711
|
+
process.stderr.write(`Daemon did not start within ${SPAWN_TIMEOUT_MS / 1e3}s
|
|
4712
|
+
`);
|
|
4713
|
+
return 1;
|
|
4714
|
+
}
|
|
4715
|
+
async function stopDaemon() {
|
|
4716
|
+
const stopped = await shutdownDaemon();
|
|
4717
|
+
process.stdout.write(stopped ? "Daemon stopped\n" : "No daemon was running\n");
|
|
4718
|
+
return 0;
|
|
4719
|
+
}
|
|
4720
|
+
async function statusDaemon() {
|
|
4721
|
+
const pong = await pingDaemon();
|
|
4722
|
+
if (pong?.type !== "pong") {
|
|
4723
|
+
process.stdout.write("No daemon running for this project\n");
|
|
4724
|
+
return 1;
|
|
4725
|
+
}
|
|
4726
|
+
const ageMin = Math.round((Date.now() - pong.startedAt) / 6e4);
|
|
4727
|
+
process.stdout.write(
|
|
4728
|
+
`Daemon running
|
|
4729
|
+
pid: ${pong.pid}
|
|
4730
|
+
cwd: ${pong.cwd}
|
|
4731
|
+
node: ${pong.nodeVersion}
|
|
4732
|
+
uptime: ${ageMin} min
|
|
4733
|
+
socket: ${daemonSocketPath(pong.cwd)}
|
|
4734
|
+
`
|
|
3958
4735
|
);
|
|
4736
|
+
return 0;
|
|
3959
4737
|
}
|
|
4738
|
+
var SPAWN_TIMEOUT_MS, highlight2, color2, USAGE, __filename, CLI_ENTRY;
|
|
4739
|
+
var init_daemon = __esm({
|
|
4740
|
+
"lib/commands/daemon/index.ts"() {
|
|
4741
|
+
init_color();
|
|
4742
|
+
init_daemon_socket_path();
|
|
4743
|
+
init_client();
|
|
4744
|
+
init_package();
|
|
4745
|
+
SPAWN_TIMEOUT_MS = 3e4;
|
|
4746
|
+
highlight2 = (text) => magenta().bold(text);
|
|
4747
|
+
color2 = (text) => blue(text);
|
|
4748
|
+
USAGE = `${highlight2(`[qunitx v${package_default.version}] Usage:`)} qunitx ${color2("daemon <subcommand>")}
|
|
4749
|
+
|
|
4750
|
+
${highlight2("Subcommands:")}
|
|
4751
|
+
${color2("$ qunitx daemon start")} # Spawn a persistent daemon for this project (~2\xD7 faster repeated runs)
|
|
4752
|
+
${color2("$ qunitx daemon stop")} # Stop the running daemon
|
|
4753
|
+
${color2("$ qunitx daemon status")} # Print pid, socket, and uptime
|
|
4754
|
+
|
|
4755
|
+
${highlight2("Environment:")}
|
|
4756
|
+
${color2("QUNITX_DAEMON=1")} : auto-spawn the daemon on the first qunitx run; reuse it on every run after (overrides the CI=1 bypass)
|
|
4757
|
+
${color2("QUNITX_NO_DAEMON=1")} : never use the daemon for this run
|
|
4758
|
+
|
|
4759
|
+
${highlight2("Tip:")} set ${color2("QUNITX_DAEMON=1")} to auto-spawn the daemon on the first qunitx run; ${color2("$ qunitx --help")} for top-level options.
|
|
4760
|
+
`;
|
|
4761
|
+
__filename = fileURLToPath2(import.meta.url);
|
|
4762
|
+
CLI_ENTRY = path13.resolve(path13.dirname(__filename), "..", "..", "..", "cli.ts");
|
|
4763
|
+
}
|
|
4764
|
+
});
|
|
3960
4765
|
|
|
3961
4766
|
// cli.ts
|
|
4767
|
+
init_chrome_prelaunch();
|
|
4768
|
+
init_package();
|
|
4769
|
+
import process4 from "node:process";
|
|
3962
4770
|
process4.title = "qunitx";
|
|
3963
4771
|
(async () => {
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
4772
|
+
const cmd2 = process4.argv[2];
|
|
4773
|
+
if (!cmd2) {
|
|
4774
|
+
return await (await Promise.resolve().then(() => (init_help(), help_exports))).displayHelpOutput();
|
|
4775
|
+
} else if (["--version", "-v", "version"].includes(cmd2)) {
|
|
3967
4776
|
return process4.stdout.write(package_default.version + "\n");
|
|
3968
|
-
} else if (["help", "h", "p", "print"].includes(
|
|
3969
|
-
return await displayHelpOutput();
|
|
3970
|
-
} else if (["new", "n", "g", "generate"].includes(
|
|
3971
|
-
return await generateTestFiles();
|
|
3972
|
-
} else if (
|
|
3973
|
-
return await initializeProject();
|
|
3974
|
-
}
|
|
3975
|
-
|
|
4777
|
+
} else if (["help", "h", "p", "print"].includes(cmd2)) {
|
|
4778
|
+
return await (await Promise.resolve().then(() => (init_help(), help_exports))).displayHelpOutput();
|
|
4779
|
+
} else if (["new", "n", "g", "generate"].includes(cmd2)) {
|
|
4780
|
+
return await (await Promise.resolve().then(() => (init_generate(), generate_exports))).generateTestFiles();
|
|
4781
|
+
} else if (cmd2 === "init") {
|
|
4782
|
+
return await (await Promise.resolve().then(() => (init_init(), init_exports))).initializeProject();
|
|
4783
|
+
} else if (cmd2 === "daemon") {
|
|
4784
|
+
const { runDaemonCommand: runDaemonCommand2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
|
|
4785
|
+
process4.exit(await runDaemonCommand2());
|
|
4786
|
+
}
|
|
4787
|
+
const { shouldUseDaemon: shouldUseDaemon2, shouldAutoSpawnDaemon: shouldAutoSpawnDaemon2, runViaDaemon: runViaDaemon2 } = await Promise.resolve().then(() => (init_client(), client_exports));
|
|
4788
|
+
let useDaemon = shouldUseDaemon2();
|
|
4789
|
+
if (!useDaemon && shouldAutoSpawnDaemon2()) {
|
|
4790
|
+
const { ensureDaemonRunning: ensureDaemonRunning2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
|
|
4791
|
+
useDaemon = await ensureDaemonRunning2();
|
|
4792
|
+
}
|
|
4793
|
+
if (useDaemon) {
|
|
4794
|
+
try {
|
|
4795
|
+
const exitCode = await runViaDaemon2(process4.argv.slice(2));
|
|
4796
|
+
process4.stdout.write("", () => process4.exit(exitCode));
|
|
4797
|
+
return;
|
|
4798
|
+
} catch {
|
|
4799
|
+
}
|
|
4800
|
+
}
|
|
4801
|
+
const [{ setupConfig: setupConfig2 }, { run: run2 }] = await Promise.all([
|
|
4802
|
+
Promise.resolve().then(() => (init_config(), config_exports)),
|
|
4803
|
+
Promise.resolve().then(() => (init_run(), run_exports))
|
|
4804
|
+
]);
|
|
4805
|
+
const config = await setupConfig2();
|
|
3976
4806
|
try {
|
|
3977
4807
|
return await run2(config);
|
|
3978
4808
|
} catch (error) {
|