qunitx-cli 0.22.3 → 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 +1595 -775
- 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,90 +619,702 @@ 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");
|
|
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();
|
|
499
748
|
}
|
|
500
|
-
|
|
501
|
-
|
|
749
|
+
});
|
|
750
|
+
|
|
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
|
+
});
|
|
764
|
+
}
|
|
765
|
+
var CLEANUP_GRACE_MS;
|
|
766
|
+
var init_close_with_grace = __esm({
|
|
767
|
+
"lib/utils/close-with-grace.ts"() {
|
|
768
|
+
CLEANUP_GRACE_MS = 1e4;
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
|
|
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 {
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
}
|
|
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
|
+
});
|
|
805
|
+
}
|
|
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;
|
|
830
|
+
}
|
|
831
|
+
return true;
|
|
832
|
+
}
|
|
833
|
+
function shouldUseDaemon() {
|
|
834
|
+
return isDaemonEligible() && existsSync2(daemonInfoPath());
|
|
835
|
+
}
|
|
836
|
+
function shouldAutoSpawnDaemon() {
|
|
837
|
+
return Boolean(process.env.QUNITX_DAEMON) && isDaemonEligible() && !existsSync2(daemonInfoPath());
|
|
838
|
+
}
|
|
839
|
+
function tryConnect(cwd = process.cwd()) {
|
|
840
|
+
return probeSocket(daemonSocketPath(cwd), CONNECT_TIMEOUT_MS);
|
|
841
|
+
}
|
|
842
|
+
function send(socket, req) {
|
|
843
|
+
socket.write(JSON.stringify(req) + "\n");
|
|
844
|
+
}
|
|
845
|
+
function awaitClose(socket) {
|
|
846
|
+
return new Promise((resolve) => {
|
|
847
|
+
socket.once("end", () => resolve());
|
|
848
|
+
socket.once("close", () => resolve());
|
|
849
|
+
socket.once("error", () => resolve());
|
|
850
|
+
});
|
|
851
|
+
}
|
|
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;
|
|
877
|
+
}
|
|
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
|
+
}
|
|
885
|
+
}
|
|
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
|
+
});
|
|
895
|
+
}
|
|
896
|
+
function pidIsAlive(pid) {
|
|
897
|
+
try {
|
|
898
|
+
process.kill(pid, 0);
|
|
899
|
+
return true;
|
|
900
|
+
} catch (err) {
|
|
901
|
+
return err.code === "EPERM";
|
|
902
|
+
}
|
|
903
|
+
}
|
|
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
|
+
}
|
|
938
|
+
}
|
|
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;
|
|
987
|
+
}
|
|
988
|
+
function isGlob(str) {
|
|
989
|
+
return /[*?{[]/.test(str);
|
|
990
|
+
}
|
|
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 "{}";
|
|
502
1318
|
const next = `${indent} `;
|
|
503
1319
|
return "\n" + entries.map(([entryKey, entryValue]) => {
|
|
504
1320
|
const v = dumpValue(entryValue, next);
|
|
@@ -682,13 +1498,13 @@ function extractSourceLine(content, lineIndex) {
|
|
|
682
1498
|
const line = content.split("\n", lineIndex + 1)[lineIndex];
|
|
683
1499
|
return line?.trim() || null;
|
|
684
1500
|
}
|
|
685
|
-
function normalizePosix(
|
|
686
|
-
const parts =
|
|
1501
|
+
function normalizePosix(path14) {
|
|
1502
|
+
const parts = path14.split("/").reduce((acc, part) => {
|
|
687
1503
|
if (part === "..") acc.pop();
|
|
688
1504
|
else if (part && part !== ".") acc.push(part);
|
|
689
1505
|
return acc;
|
|
690
1506
|
}, []);
|
|
691
|
-
return (
|
|
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,65 +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
|
-
// lib/utils/close-with-grace.ts
|
|
2041
|
-
function closeWithGrace(closes, graceMs = CLEANUP_GRACE_MS) {
|
|
2042
|
-
return new Promise((resolve) => {
|
|
2043
|
-
const timer = setTimeout(() => {
|
|
2044
|
-
process.stderr.write(`# qunitx: cleanup timed out after ${graceMs} ms \u2014 exiting anyway
|
|
2045
|
-
`);
|
|
2046
|
-
resolve();
|
|
2047
|
-
}, graceMs);
|
|
2048
|
-
Promise.allSettled(closes).then(() => {
|
|
2049
|
-
clearTimeout(timer);
|
|
2050
|
-
resolve();
|
|
2051
|
-
});
|
|
2052
|
-
});
|
|
2053
|
-
}
|
|
2054
|
-
var CLEANUP_GRACE_MS;
|
|
2055
|
-
var init_close_with_grace = __esm({
|
|
2056
|
-
"lib/utils/close-with-grace.ts"() {
|
|
2057
|
-
CLEANUP_GRACE_MS = 1e4;
|
|
2058
|
-
}
|
|
2059
|
-
});
|
|
2060
|
-
|
|
2061
2818
|
// lib/utils/time-counter.ts
|
|
2062
2819
|
function timeCounter() {
|
|
2063
2820
|
const startTime = /* @__PURE__ */ new Date();
|
|
@@ -2072,10 +2829,10 @@ var init_time_counter = __esm({
|
|
|
2072
2829
|
});
|
|
2073
2830
|
|
|
2074
2831
|
// lib/utils/run-user-module.ts
|
|
2075
|
-
import { pathToFileURL as
|
|
2832
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
2076
2833
|
async function runUserModule(modulePath, params, scriptPosition) {
|
|
2077
2834
|
try {
|
|
2078
|
-
const func = await import(
|
|
2835
|
+
const func = await import(pathToFileURL2(modulePath).href);
|
|
2079
2836
|
if (func) {
|
|
2080
2837
|
func.default ? await func.default(params) : typeof func === "function" ? await func(params) : null;
|
|
2081
2838
|
}
|
|
@@ -2117,7 +2874,7 @@ var init_display_final_result = __esm({
|
|
|
2117
2874
|
});
|
|
2118
2875
|
|
|
2119
2876
|
// lib/commands/run/tests-in-browser.ts
|
|
2120
|
-
import
|
|
2877
|
+
import fs10 from "node:fs/promises";
|
|
2121
2878
|
import path7 from "node:path";
|
|
2122
2879
|
import esbuild from "esbuild";
|
|
2123
2880
|
function deriveBuildErrorType(error) {
|
|
@@ -2148,6 +2905,9 @@ function formatBuildErrors(error) {
|
|
|
2148
2905
|
const raw = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
2149
2906
|
return raw.replace(/\x1b\[[0-9;]*[mGKH]/g, "").replace(/\r\n/g, "\n");
|
|
2150
2907
|
}
|
|
2908
|
+
function bundleCacheKey(opts, files) {
|
|
2909
|
+
return JSON.stringify({ files, outfile: opts.outfile, target: opts.target });
|
|
2910
|
+
}
|
|
2151
2911
|
async function buildTestBundle(config, cachedContent) {
|
|
2152
2912
|
const { projectRoot, output } = config;
|
|
2153
2913
|
const allTestFilePaths = Object.keys(config.fsTree);
|
|
@@ -2157,7 +2917,7 @@ async function buildTestBundle(config, cachedContent) {
|
|
|
2157
2917
|
}
|
|
2158
2918
|
const outDir = path7.resolve(projectRoot, output);
|
|
2159
2919
|
const outfile = path7.join(outDir, "tests.js");
|
|
2160
|
-
await
|
|
2920
|
+
await fs10.mkdir(outDir, { recursive: true });
|
|
2161
2921
|
const sourcemap = "inline";
|
|
2162
2922
|
const needsDisk = true;
|
|
2163
2923
|
const buildOptions = {
|
|
@@ -2189,15 +2949,17 @@ async function buildTestBundle(config, cachedContent) {
|
|
|
2189
2949
|
};
|
|
2190
2950
|
cachedContent._buildError = null;
|
|
2191
2951
|
cachedContent._noTestsWarning = null;
|
|
2952
|
+
const cacheHolder = config._daemonEsbuildCache ?? cachedContent;
|
|
2953
|
+
const fileKey = bundleCacheKey(buildOptions, allTestFilePaths);
|
|
2192
2954
|
try {
|
|
2193
2955
|
const [allTestCode] = await Promise.all([
|
|
2194
|
-
config.watch ? buildIncrementally(buildOptions,
|
|
2956
|
+
config.watch || config._daemonMode ? buildIncrementally(buildOptions, fileKey, cacheHolder, needsDisk) : buildWithOverlayfsRetry(buildOptions, needsDisk),
|
|
2195
2957
|
Promise.all(
|
|
2196
2958
|
cachedContent.htmlPathsToRunTests.map(async (htmlPath) => {
|
|
2197
2959
|
const targetPath = path7.join(outDir, htmlPath);
|
|
2198
2960
|
if (htmlPath !== "/") {
|
|
2199
|
-
await
|
|
2200
|
-
await
|
|
2961
|
+
await fs10.rm(targetPath, { force: true, recursive: true });
|
|
2962
|
+
await fs10.mkdir(path7.dirname(targetPath), { recursive: true });
|
|
2201
2963
|
}
|
|
2202
2964
|
})
|
|
2203
2965
|
)
|
|
@@ -2209,7 +2971,7 @@ async function buildTestBundle(config, cachedContent) {
|
|
|
2209
2971
|
type: deriveBuildErrorType(error),
|
|
2210
2972
|
formatted: formatBuildErrors(error)
|
|
2211
2973
|
};
|
|
2212
|
-
await
|
|
2974
|
+
await fs10.writeFile(path7.join(outDir, "index.html"), buildErrorHTML(cachedContent._buildError));
|
|
2213
2975
|
throw error;
|
|
2214
2976
|
}
|
|
2215
2977
|
}
|
|
@@ -2283,7 +3045,7 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
2283
3045
|
console.log(
|
|
2284
3046
|
`# Warning: 0 tests registered \u2014 no QUnit test cases found in ${allTestFilePaths.length} ${fileWord}`
|
|
2285
3047
|
);
|
|
2286
|
-
|
|
3048
|
+
fs10.writeFile(path7.join(outDir, "index.html"), buildNoTestsHTML(displayFiles)).catch(
|
|
2287
3049
|
() => {
|
|
2288
3050
|
}
|
|
2289
3051
|
);
|
|
@@ -2293,7 +3055,10 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
2293
3055
|
await runUserModule(`${process.cwd()}/${config.after}`, config.COUNTER, "after");
|
|
2294
3056
|
}
|
|
2295
3057
|
if (!config.watch) {
|
|
2296
|
-
await flushConsoleHandlers(config._pendingConsoleHandlers);
|
|
3058
|
+
await flushConsoleHandlers(config._pendingConsoleHandlers, connections.page);
|
|
3059
|
+
if (config._daemonMode) {
|
|
3060
|
+
throw new DaemonRunError(config.COUNTER.failCount > 0 ? 1 : 0);
|
|
3061
|
+
}
|
|
2297
3062
|
await closeWithGrace([
|
|
2298
3063
|
connections.server?.close(),
|
|
2299
3064
|
connections.browser?.close(),
|
|
@@ -2303,6 +3068,7 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
2303
3068
|
}
|
|
2304
3069
|
}
|
|
2305
3070
|
} catch (error) {
|
|
3071
|
+
if (error instanceof DaemonRunError) throw error;
|
|
2306
3072
|
cachedContent._activeRebuild = null;
|
|
2307
3073
|
config.lastFailedTestFiles = config.lastRanTestFiles;
|
|
2308
3074
|
const exception = new BundleError(error);
|
|
@@ -2311,7 +3077,7 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
2311
3077
|
type: deriveBuildErrorType(error),
|
|
2312
3078
|
formatted: formatBuildErrors(error)
|
|
2313
3079
|
};
|
|
2314
|
-
|
|
3080
|
+
fs10.writeFile(
|
|
2315
3081
|
path7.join(outDir, "qunitx.html"),
|
|
2316
3082
|
buildErrorHTML(cachedContent._buildError)
|
|
2317
3083
|
).catch(
|
|
@@ -2327,12 +3093,14 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
|
|
|
2327
3093
|
}
|
|
2328
3094
|
return connections;
|
|
2329
3095
|
}
|
|
2330
|
-
async function flushConsoleHandlers(handlers, deadline = Date.now() + CONSOLE_FLUSH_TIMEOUT_MS) {
|
|
3096
|
+
async function flushConsoleHandlers(handlers, page, deadline = Date.now() + CONSOLE_FLUSH_TIMEOUT_MS) {
|
|
2331
3097
|
if (!handlers || Date.now() >= deadline) return;
|
|
3098
|
+
if (page) await page.evaluate(() => 0).catch(() => {
|
|
3099
|
+
});
|
|
2332
3100
|
await new Promise((resolve) => setImmediate(resolve));
|
|
2333
3101
|
if (handlers.size === 0) return;
|
|
2334
3102
|
await Promise.allSettled([...handlers]);
|
|
2335
|
-
return flushConsoleHandlers(handlers, deadline);
|
|
3103
|
+
return flushConsoleHandlers(handlers, page, deadline);
|
|
2336
3104
|
}
|
|
2337
3105
|
async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
2338
3106
|
groupCachedContents.forEach((cachedContent) => {
|
|
@@ -2360,7 +3128,7 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
2360
3128
|
);
|
|
2361
3129
|
await Promise.all(
|
|
2362
3130
|
activeGroups.map(
|
|
2363
|
-
(group) =>
|
|
3131
|
+
(group) => fs10.mkdir(path7.resolve(group.config.projectRoot, group.config.output), { recursive: true })
|
|
2364
3132
|
)
|
|
2365
3133
|
);
|
|
2366
3134
|
const sourcemap = "inline";
|
|
@@ -2433,7 +3201,7 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
2433
3201
|
esbuildOutdir
|
|
2434
3202
|
);
|
|
2435
3203
|
}
|
|
2436
|
-
return
|
|
3204
|
+
return fs10.writeFile(destPath, outputFile.contents);
|
|
2437
3205
|
})
|
|
2438
3206
|
);
|
|
2439
3207
|
} catch (error) {
|
|
@@ -2442,7 +3210,7 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
|
|
|
2442
3210
|
await Promise.all(
|
|
2443
3211
|
activeGroups.map((group) => {
|
|
2444
3212
|
group.cachedContent._buildError = buildError;
|
|
2445
|
-
return
|
|
3213
|
+
return fs10.writeFile(
|
|
2446
3214
|
path7.join(path7.resolve(group.config.projectRoot, group.config.output), "index.html"),
|
|
2447
3215
|
errorHtml
|
|
2448
3216
|
).catch(
|
|
@@ -2492,7 +3260,7 @@ async function runWithOverlayfsRetry(getContents, needsDisk) {
|
|
|
2492
3260
|
}
|
|
2493
3261
|
if (needsDisk) {
|
|
2494
3262
|
await Promise.all(
|
|
2495
|
-
result.outputFiles.map((outputFile) =>
|
|
3263
|
+
result.outputFiles.map((outputFile) => fs10.writeFile(outputFile.path, outputFile.contents))
|
|
2496
3264
|
);
|
|
2497
3265
|
}
|
|
2498
3266
|
return js;
|
|
@@ -2505,15 +3273,15 @@ function buildWithOverlayfsRetry(options, needsDisk) {
|
|
|
2505
3273
|
return { result, js: Buffer.from(jsFile.contents) };
|
|
2506
3274
|
}, needsDisk);
|
|
2507
3275
|
}
|
|
2508
|
-
async function buildIncrementally(options, fileKey,
|
|
3276
|
+
async function buildIncrementally(options, fileKey, cache, needsDisk) {
|
|
2509
3277
|
const buildOpts = { ...options, write: false };
|
|
2510
|
-
if (!
|
|
2511
|
-
|
|
3278
|
+
if (!cache._esbuildContext || cache._esbuildContextKey !== fileKey) {
|
|
3279
|
+
cache._esbuildContext?.dispose().catch(() => {
|
|
2512
3280
|
});
|
|
2513
|
-
|
|
2514
|
-
|
|
3281
|
+
cache._esbuildContext = await esbuild.context(buildOpts);
|
|
3282
|
+
cache._esbuildContextKey = fileKey;
|
|
2515
3283
|
}
|
|
2516
|
-
const ctx =
|
|
3284
|
+
const ctx = cache._esbuildContext;
|
|
2517
3285
|
return runWithOverlayfsRetry(async () => {
|
|
2518
3286
|
const result = await ctx.rebuild();
|
|
2519
3287
|
const jsFile = result.outputFiles.find((f) => !f.path.endsWith(".map"));
|
|
@@ -2585,9 +3353,10 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
|
|
|
2585
3353
|
console.error("BROWSER: runtime error thrown during executing tests");
|
|
2586
3354
|
await failOnNonWatchMode(
|
|
2587
3355
|
config.watch,
|
|
2588
|
-
{ server, browser },
|
|
3356
|
+
{ server, browser, page },
|
|
2589
3357
|
config._groupMode,
|
|
2590
|
-
config._pendingConsoleHandlers
|
|
3358
|
+
config._pendingConsoleHandlers,
|
|
3359
|
+
config._daemonMode
|
|
2591
3360
|
);
|
|
2592
3361
|
} else if (QUNIT_RESULT.totalTests === 0) {
|
|
2593
3362
|
return;
|
|
@@ -2600,27 +3369,30 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
|
|
|
2600
3369
|
console.error(`BROWSER: TEST TIMED OUT: ${QUNIT_RESULT.currentTest}`);
|
|
2601
3370
|
await failOnNonWatchMode(
|
|
2602
3371
|
config.watch,
|
|
2603
|
-
{ server, browser },
|
|
3372
|
+
{ server, browser, page },
|
|
2604
3373
|
config._groupMode,
|
|
2605
|
-
config._pendingConsoleHandlers
|
|
3374
|
+
config._pendingConsoleHandlers,
|
|
3375
|
+
config._daemonMode
|
|
2606
3376
|
);
|
|
2607
3377
|
} else if (QUNIT_RESULT.failedTests > config.COUNTER.failCount) {
|
|
2608
3378
|
config.COUNTER.failCount = QUNIT_RESULT.failedTests;
|
|
2609
3379
|
}
|
|
2610
3380
|
}
|
|
2611
|
-
async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode = false, pendingHandlers) {
|
|
2612
|
-
if (
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
}
|
|
2616
|
-
await flushConsoleHandlers(pendingHandlers);
|
|
2617
|
-
await closeWithGrace([
|
|
2618
|
-
connections.server?.close(),
|
|
2619
|
-
connections.browser?.close(),
|
|
2620
|
-
shutdownPrelaunch()
|
|
2621
|
-
]);
|
|
2622
|
-
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");
|
|
2623
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);
|
|
2624
3396
|
}
|
|
2625
3397
|
function toEsbuildImportPath(filePath) {
|
|
2626
3398
|
const rel = path7.relative(process.cwd(), filePath);
|
|
@@ -2628,7 +3400,7 @@ function toEsbuildImportPath(filePath) {
|
|
|
2628
3400
|
if (path7.isAbsolute(rel)) return filePath.replace(/\\/g, "/");
|
|
2629
3401
|
return normalized.startsWith(".") ? normalized : "./" + normalized;
|
|
2630
3402
|
}
|
|
2631
|
-
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;
|
|
2632
3404
|
var init_tests_in_browser = __esm({
|
|
2633
3405
|
"lib/commands/run/tests-in-browser.ts"() {
|
|
2634
3406
|
init_color();
|
|
@@ -2647,7 +3419,8 @@ var init_tests_in_browser = __esm({
|
|
|
2647
3419
|
MAX_RETRIES = 3;
|
|
2648
3420
|
EMPTY_BUNDLE_THRESHOLD = 500;
|
|
2649
3421
|
NAV_GRACE_MS = 1e4;
|
|
2650
|
-
|
|
3422
|
+
MAX_NAV_SLOWDOWN_FACTOR = 6;
|
|
3423
|
+
MIN_NAV_MS = NAV_GRACE_MS * MAX_NAV_SLOWDOWN_FACTOR;
|
|
2651
3424
|
STARTUP_TIMEOUT_FACTOR = 3;
|
|
2652
3425
|
TESTS_JS_TIMEOUT_FACTOR = 4;
|
|
2653
3426
|
CONSOLE_FLUSH_TIMEOUT_MS = 2e3;
|
|
@@ -2660,13 +3433,61 @@ var init_tests_in_browser = __esm({
|
|
|
2660
3433
|
this.message = `esbuild Bundle Error: ${message}`.split("\n").join("\n# ");
|
|
2661
3434
|
}
|
|
2662
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();
|
|
2663
3484
|
}
|
|
2664
3485
|
});
|
|
2665
3486
|
|
|
2666
3487
|
// lib/setup/file-watcher.ts
|
|
2667
|
-
import
|
|
3488
|
+
import fs11 from "node:fs";
|
|
2668
3489
|
import { readdir, stat, lstat } from "node:fs/promises";
|
|
2669
|
-
import
|
|
3490
|
+
import path9 from "node:path";
|
|
2670
3491
|
function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFunc) {
|
|
2671
3492
|
const extensions = config.extensions || defaultProjectConfigValues.extensions;
|
|
2672
3493
|
config._lastBuildEndMs ??= Date.now();
|
|
@@ -2679,7 +3500,7 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2679
3500
|
if (symlinkPollers.has(filePath)) return;
|
|
2680
3501
|
const handler = (curr, prev) => {
|
|
2681
3502
|
if (curr.nlink === 0) {
|
|
2682
|
-
|
|
3503
|
+
fs11.unwatchFile(filePath, handler);
|
|
2683
3504
|
symlinkPollers.delete(filePath);
|
|
2684
3505
|
if (filePath in config.fsTree) {
|
|
2685
3506
|
handleWatchEvent(config, extensions, "unlink", filePath, onEventFunc, onFinishFunc);
|
|
@@ -2690,8 +3511,8 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2690
3511
|
}
|
|
2691
3512
|
}
|
|
2692
3513
|
};
|
|
2693
|
-
|
|
2694
|
-
symlinkPollers.set(filePath, () =>
|
|
3514
|
+
fs11.watchFile(filePath, { interval: SYMLINK_POLL_INTERVAL_MS, persistent: false }, handler);
|
|
3515
|
+
symlinkPollers.set(filePath, () => fs11.unwatchFile(filePath, handler));
|
|
2695
3516
|
}
|
|
2696
3517
|
function untrackSymlink(filePath) {
|
|
2697
3518
|
symlinkPollers.get(filePath)?.();
|
|
@@ -2702,7 +3523,7 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2702
3523
|
let rescanInProgress = false;
|
|
2703
3524
|
const lastEventMs = {};
|
|
2704
3525
|
const seenMtimeMs = {};
|
|
2705
|
-
const childWatcher =
|
|
3526
|
+
const childWatcher = fs11.watch(watchPath, { recursive: true }, async (eventType, filename) => {
|
|
2706
3527
|
if (!ready) return;
|
|
2707
3528
|
if (!filename) {
|
|
2708
3529
|
if (process.platform === "darwin" && !rescanInProgress) {
|
|
@@ -2720,7 +3541,7 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2720
3541
|
}
|
|
2721
3542
|
return;
|
|
2722
3543
|
}
|
|
2723
|
-
const fullPath = filename ===
|
|
3544
|
+
const fullPath = filename === path9.basename(watchPath) ? watchPath : path9.join(watchPath, filename);
|
|
2724
3545
|
if (eventType === "change") {
|
|
2725
3546
|
const now = Date.now();
|
|
2726
3547
|
const last = lastEventMs[fullPath] ?? 0;
|
|
@@ -2749,10 +3570,10 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
|
|
|
2749
3570
|
}
|
|
2750
3571
|
handleWatchEvent(config, extensions, event, fullPath, onEventFunc, onFinishFunc);
|
|
2751
3572
|
});
|
|
2752
|
-
const parentDir =
|
|
2753
|
-
const watchedBasename =
|
|
3573
|
+
const parentDir = path9.dirname(watchPath);
|
|
3574
|
+
const watchedBasename = path9.basename(watchPath);
|
|
2754
3575
|
let parentUnlinkFired = false;
|
|
2755
|
-
const parentWatcher =
|
|
3576
|
+
const parentWatcher = fs11.watch(parentDir, async (eventType, filename) => {
|
|
2756
3577
|
if (!ready || filename !== watchedBasename || eventType !== "rename") return;
|
|
2757
3578
|
if (parentUnlinkFired) return;
|
|
2758
3579
|
parentUnlinkFired = true;
|
|
@@ -2867,11 +3688,11 @@ async function rescanDirectoryForDelta(watchPath, config, extensions, onEventFun
|
|
|
2867
3688
|
const trackedToRecheck = [];
|
|
2868
3689
|
for (const entry of entries) {
|
|
2869
3690
|
if (entry.isDirectory()) {
|
|
2870
|
-
presentDirs.add(
|
|
3691
|
+
presentDirs.add(path9.join(entry.parentPath, entry.name));
|
|
2871
3692
|
continue;
|
|
2872
3693
|
}
|
|
2873
3694
|
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
2874
|
-
const entryPath =
|
|
3695
|
+
const entryPath = path9.join(entry.parentPath, entry.name);
|
|
2875
3696
|
presentDirs.add(entry.parentPath);
|
|
2876
3697
|
if (!extensions.some((ext) => entryPath.endsWith(`.${ext}`))) continue;
|
|
2877
3698
|
presentPaths.add(entryPath);
|
|
@@ -2894,13 +3715,13 @@ async function rescanDirectoryForDelta(watchPath, config, extensions, onEventFun
|
|
|
2894
3715
|
}
|
|
2895
3716
|
})
|
|
2896
3717
|
);
|
|
2897
|
-
const watchPrefix = watchPath +
|
|
3718
|
+
const watchPrefix = watchPath + path9.sep;
|
|
2898
3719
|
const firedDirPrefixes = [];
|
|
2899
3720
|
for (const trackedPath of Object.keys(config.fsTree)) {
|
|
2900
3721
|
if (!trackedPath.startsWith(watchPrefix) || presentPaths.has(trackedPath)) continue;
|
|
2901
|
-
if (firedDirPrefixes.some((p) => trackedPath.startsWith(p +
|
|
2902
|
-
const parts = trackedPath.slice(watchPrefix.length).split(
|
|
2903
|
-
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;
|
|
2904
3725
|
if (goneDirPath !== null) {
|
|
2905
3726
|
firedDirPrefixes.push(goneDirPath);
|
|
2906
3727
|
handleWatchEvent(config, extensions, "unlinkDir", goneDirPath, onEventFunc, onFinishFunc);
|
|
@@ -3032,48 +3853,90 @@ var init_keyboard_events = __esm({
|
|
|
3032
3853
|
});
|
|
3033
3854
|
|
|
3034
3855
|
// lib/setup/write-output-static-files.ts
|
|
3035
|
-
import
|
|
3036
|
-
import
|
|
3856
|
+
import fs12 from "node:fs/promises";
|
|
3857
|
+
import path10 from "node:path";
|
|
3037
3858
|
async function writeOutputStaticFiles({ projectRoot, output }, cachedContent) {
|
|
3038
3859
|
const staticHTMLPromises = Object.keys(cachedContent.staticHTMLs).map(async (staticHTMLKey) => {
|
|
3039
|
-
const htmlRelativePath =
|
|
3040
|
-
const outDir =
|
|
3041
|
-
await ensureFolderExists(
|
|
3042
|
-
await
|
|
3043
|
-
|
|
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),
|
|
3044
3865
|
cachedContent.staticHTMLs[staticHTMLKey]
|
|
3045
3866
|
);
|
|
3046
3867
|
});
|
|
3047
3868
|
const assetPromises = Array.from(cachedContent.assets).map(async (assetAbsolutePath) => {
|
|
3048
|
-
const assetRelativePath =
|
|
3049
|
-
const outDir =
|
|
3050
|
-
await ensureFolderExists(
|
|
3051
|
-
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));
|
|
3052
3873
|
});
|
|
3053
3874
|
await Promise.all(staticHTMLPromises.concat(assetPromises));
|
|
3054
3875
|
}
|
|
3055
3876
|
async function ensureFolderExists(assetPath) {
|
|
3056
|
-
await
|
|
3877
|
+
await fs12.mkdir(path10.dirname(assetPath), { recursive: true });
|
|
3057
3878
|
}
|
|
3058
3879
|
var init_write_output_static_files = __esm({
|
|
3059
3880
|
"lib/setup/write-output-static-files.ts"() {
|
|
3060
3881
|
}
|
|
3061
3882
|
});
|
|
3062
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
|
+
}
|
|
3915
|
+
}
|
|
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");
|
|
3922
|
+
}
|
|
3923
|
+
});
|
|
3924
|
+
|
|
3063
3925
|
// lib/commands/run.ts
|
|
3064
3926
|
var run_exports = {};
|
|
3065
3927
|
__export(run_exports, {
|
|
3928
|
+
buildCachedContent: () => buildCachedContent,
|
|
3066
3929
|
computeFileTimes: () => computeFileTimes,
|
|
3067
3930
|
default: () => run,
|
|
3068
3931
|
readTimingCache: () => readTimingCache,
|
|
3069
3932
|
run: () => run
|
|
3070
3933
|
});
|
|
3071
|
-
import
|
|
3934
|
+
import fs14 from "node:fs/promises";
|
|
3072
3935
|
import { join as join3, normalize } from "node:path";
|
|
3073
3936
|
import { createRequire as createRequire2 } from "node:module";
|
|
3074
3937
|
import { availableParallelism } from "node:os";
|
|
3075
3938
|
async function run(config) {
|
|
3076
|
-
const browserPromise = config.watch ? null : launchBrowser(config);
|
|
3939
|
+
const browserPromise = config._daemonBrowser ? Promise.resolve(config._daemonBrowser) : config.watch ? null : launchBrowser(config);
|
|
3077
3940
|
const [cachedContent, timings] = await Promise.all([
|
|
3078
3941
|
buildCachedContent(config, config.htmlPaths),
|
|
3079
3942
|
config.watch ? Promise.resolve(null) : readTimingCache(config.projectRoot)
|
|
@@ -3184,7 +4047,7 @@ async function run(config) {
|
|
|
3184
4047
|
})() : null;
|
|
3185
4048
|
process.stdout.write("TAP version 13\n");
|
|
3186
4049
|
process.stdout.write(
|
|
3187
|
-
`# 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)" : ""}
|
|
3188
4051
|
`
|
|
3189
4052
|
);
|
|
3190
4053
|
const [browser] = await Promise.all([
|
|
@@ -3242,7 +4105,7 @@ async function run(config) {
|
|
|
3242
4105
|
try {
|
|
3243
4106
|
await runTestsInBrowser(groupConfig, groupCachedContents[i], connections);
|
|
3244
4107
|
} finally {
|
|
3245
|
-
await flushConsoleHandlers(groupConfig._pendingConsoleHandlers);
|
|
4108
|
+
await flushConsoleHandlers(groupConfig._pendingConsoleHandlers, connections.page);
|
|
3246
4109
|
await closeWithGrace([
|
|
3247
4110
|
sharedServer ? void 0 : connections.server?.close(),
|
|
3248
4111
|
connections.page?.close()
|
|
@@ -3279,6 +4142,17 @@ async function run(config) {
|
|
|
3279
4142
|
if (config.after) {
|
|
3280
4143
|
await runUserModule(`${process.cwd()}/${config.after}`, config.COUNTER, "after");
|
|
3281
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 });
|
|
3282
4156
|
const exitTimer = setTimeout(() => process.exit(exitCode), STDOUT_FLUSH_GRACE_MS);
|
|
3283
4157
|
exitTimer.unref();
|
|
3284
4158
|
process.stdout.write("\n", async () => {
|
|
@@ -3301,7 +4175,7 @@ async function run(config) {
|
|
|
3301
4175
|
}
|
|
3302
4176
|
async function buildCachedContent(config, htmlPaths) {
|
|
3303
4177
|
const htmlBuffers = await Promise.all(
|
|
3304
|
-
config.htmlPaths.map((htmlPath) =>
|
|
4178
|
+
config.htmlPaths.map((htmlPath) => fs14.readFile(htmlPath).catch(() => null))
|
|
3305
4179
|
);
|
|
3306
4180
|
const cachedContent = htmlPaths.reduce(
|
|
3307
4181
|
(result, _htmlPath, index) => {
|
|
@@ -3356,7 +4230,7 @@ async function addCachedContentMainHTML(projectRoot, cachedContent) {
|
|
|
3356
4230
|
}
|
|
3357
4231
|
async function readTimingCache(projectRoot) {
|
|
3358
4232
|
try {
|
|
3359
|
-
const parsed = JSON.parse(await
|
|
4233
|
+
const parsed = JSON.parse(await fs14.readFile(`${projectRoot}/tmp/test-timings.json`, "utf8"));
|
|
3360
4234
|
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : {};
|
|
3361
4235
|
} catch {
|
|
3362
4236
|
return {};
|
|
@@ -3375,7 +4249,7 @@ function computeFileTimes(groups, weights, wallTimes) {
|
|
|
3375
4249
|
return result;
|
|
3376
4250
|
}
|
|
3377
4251
|
async function persistTimings(fileTimes, projectRoot) {
|
|
3378
|
-
await
|
|
4252
|
+
await fs14.writeFile(
|
|
3379
4253
|
`${projectRoot}/tmp/test-timings.json`,
|
|
3380
4254
|
JSON.stringify(Object.fromEntries(fileTimes), null, 2)
|
|
3381
4255
|
);
|
|
@@ -3390,7 +4264,7 @@ ${lines.join("\n")}
|
|
|
3390
4264
|
async function splitIntoGroups(files, groupCount, timings) {
|
|
3391
4265
|
const sizes = await Promise.all(
|
|
3392
4266
|
files.map(
|
|
3393
|
-
(f) => timings[f] > 0 ? Promise.resolve(0) :
|
|
4267
|
+
(f) => timings[f] > 0 ? Promise.resolve(0) : fs14.stat(f).then((s) => s.size).catch(() => 0)
|
|
3394
4268
|
)
|
|
3395
4269
|
);
|
|
3396
4270
|
const knownRates = files.map((f, i) => ({ ms: timings[f], size: sizes[i] })).filter(({ ms, size }) => ms > 0 && size > 0);
|
|
@@ -3450,539 +4324,485 @@ var init_run = __esm({
|
|
|
3450
4324
|
init_read_template();
|
|
3451
4325
|
init_html();
|
|
3452
4326
|
init_close_with_grace();
|
|
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
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
};
|
|
3546
|
-
|
|
3547
|
-
// lib/commands/help.ts
|
|
3548
|
-
var highlight = (text) => magenta().bold(text);
|
|
3549
|
-
var color = (text) => blue(text);
|
|
3550
|
-
function displayHelpOutput() {
|
|
3551
|
-
const config = package_default;
|
|
3552
|
-
console.log(`${highlight("[qunitx v" + config.version + "] Usage:")} qunitx ${color("[targets] --$flags")}
|
|
3553
|
-
|
|
3554
|
-
${highlight("Input options:")}
|
|
3555
|
-
- File: $ ${color("qunitx test/foo.js")}
|
|
3556
|
-
- Folder: $ ${color("qunitx test/login")}
|
|
3557
|
-
- Globs: $ ${color("qunitx test/**/*-test.js")}
|
|
3558
|
-
- Combination: $ ${color("qunitx test/foo.js test/bar.js test/*-test.js test/logout")}
|
|
3559
|
-
|
|
3560
|
-
${highlight("Optional flags:")}
|
|
3561
|
-
${color("--debug")} : print console output when tests run in browser
|
|
3562
|
-
${color("--watch")} : run the target file or folders, watch them for continuous run and expose http server under localhost
|
|
3563
|
-
${color("--open")} : run tests in a visible browser window instead of headless; keeps the server alive (short: ${color("-o")})
|
|
3564
|
-
${color("--timeout")} : change default timeout per test case
|
|
3565
|
-
${color("--output")} : folder to distribute built qunitx html and js that a webservers can run[default: tmp]
|
|
3566
|
-
${color("--failFast")} : run the target file or folders with immediate abort if a single test fails
|
|
3567
|
-
${color("--port")} : HTTP server port (auto-selects a free port if the given port is taken)[default: 1234]
|
|
3568
|
-
${color("--extensions")} : comma-separated file extensions to track for discovery and watch-mode rebuilds[default: js,ts,jsx,tsx]
|
|
3569
|
-
${color("--browser")} : browser engine to run tests in: chromium, firefox, webkit[default: chromium]
|
|
3570
|
-
${color("--before")} : run a script before the tests(i.e start a new web server before tests)
|
|
3571
|
-
${color("--after")} : run a script after the tests(i.e save test results to a file)
|
|
3572
|
-
|
|
3573
|
-
${highlight("Example:")} $ ${color("qunitx test/foo.ts app/e2e --debug --watch --before=scripts/start-new-webserver.js --after=scripts/write-test-results.js")}
|
|
3574
|
-
|
|
3575
|
-
${highlight("Commands:")}
|
|
3576
|
-
${color("$ qunitx init")} # Bootstraps qunitx base html and add qunitx config to package.json if needed
|
|
3577
|
-
${color("$ qunitx new $testFileName")} # Creates a qunitx test file
|
|
3578
|
-
`);
|
|
3579
|
-
}
|
|
3580
|
-
|
|
3581
|
-
// lib/commands/init.ts
|
|
3582
|
-
import fs4 from "node:fs/promises";
|
|
3583
|
-
import path2 from "node:path";
|
|
3584
|
-
|
|
3585
|
-
// lib/utils/find-project-root.ts
|
|
3586
|
-
import process2 from "node:process";
|
|
3587
|
-
|
|
3588
|
-
// lib/utils/path-exists.ts
|
|
3589
|
-
import fs2 from "node:fs/promises";
|
|
3590
|
-
async function pathExists(path10) {
|
|
3591
|
-
try {
|
|
3592
|
-
await fs2.access(path10);
|
|
3593
|
-
return true;
|
|
3594
|
-
} catch {
|
|
3595
|
-
return false;
|
|
3596
|
-
}
|
|
3597
|
-
}
|
|
3598
|
-
|
|
3599
|
-
// lib/utils/search-in-parent-directories.ts
|
|
3600
|
-
async function searchInParentDirectories(directory, targetEntry) {
|
|
3601
|
-
const resolvedDirectory = directory === "." ? process.cwd() : directory;
|
|
3602
|
-
if (await pathExists(`${resolvedDirectory}/${targetEntry}`)) {
|
|
3603
|
-
return `${resolvedDirectory}/${targetEntry}`;
|
|
3604
|
-
} else if (resolvedDirectory === "") {
|
|
3605
|
-
return;
|
|
3606
|
-
}
|
|
3607
|
-
return await searchInParentDirectories(
|
|
3608
|
-
resolvedDirectory.slice(0, resolvedDirectory.lastIndexOf("/")),
|
|
3609
|
-
targetEntry
|
|
3610
|
-
);
|
|
3611
|
-
}
|
|
3612
|
-
|
|
3613
|
-
// lib/utils/find-project-root.ts
|
|
3614
|
-
async function findProjectRoot() {
|
|
3615
|
-
try {
|
|
3616
|
-
const absolutePath = await searchInParentDirectories(".", "package.json");
|
|
3617
|
-
if (!absolutePath.includes("package.json")) {
|
|
3618
|
-
throw new Error("package.json mising");
|
|
3619
|
-
}
|
|
3620
|
-
return absolutePath.replace("/package.json", "");
|
|
3621
|
-
} catch (_error) {
|
|
3622
|
-
console.log("couldnt find projects package.json, did you run $ npm init ??");
|
|
3623
|
-
process2.exit(1);
|
|
3624
|
-
}
|
|
3625
|
-
}
|
|
3626
|
-
|
|
3627
|
-
// lib/commands/init.ts
|
|
3628
|
-
init_default_project_config_values();
|
|
3629
|
-
init_read_template();
|
|
3630
|
-
async function initializeProject() {
|
|
3631
|
-
const projectRoot = await findProjectRoot();
|
|
3632
|
-
const oldPackageJSON = JSON.parse(await fs4.readFile(`${projectRoot}/package.json`));
|
|
3633
|
-
const existingQunitx = oldPackageJSON.qunitx || {};
|
|
3634
|
-
const cliHtmlPaths = process.argv.slice(2).filter((arg) => arg.endsWith(".html"));
|
|
3635
|
-
const config = Object.assign({}, defaultProjectConfigValues, existingQunitx, {
|
|
3636
|
-
htmlPaths: cliHtmlPaths.length > 0 ? cliHtmlPaths : existingQunitx.htmlPaths || ["test/tests.html"]
|
|
3637
|
-
});
|
|
3638
|
-
await Promise.all([
|
|
3639
|
-
writeTestsHTML(projectRoot, config, oldPackageJSON),
|
|
3640
|
-
rewritePackageJSON(projectRoot, config, oldPackageJSON),
|
|
3641
|
-
writeTSConfigIfNeeded(projectRoot)
|
|
3642
|
-
]);
|
|
3643
|
-
}
|
|
3644
|
-
async function writeTestsHTML(projectRoot, config, oldPackageJSON) {
|
|
3645
|
-
const testHTMLTemplateBuffer = await readTemplate("setup/tests.hbs");
|
|
3646
|
-
return await Promise.all(
|
|
3647
|
-
config.htmlPaths.map(async (htmlPath) => {
|
|
3648
|
-
const targetPath = `${projectRoot}/${htmlPath}`;
|
|
3649
|
-
if (await pathExists(targetPath)) {
|
|
3650
|
-
return console.log(`${htmlPath} already exists`);
|
|
3651
|
-
} else {
|
|
3652
|
-
const targetDirectory = path2.dirname(targetPath);
|
|
3653
|
-
const _targetOutputPath = path2.relative(
|
|
3654
|
-
targetDirectory,
|
|
3655
|
-
path2.join(path2.resolve(projectRoot, config.output), "tests.js")
|
|
3656
|
-
);
|
|
3657
|
-
const testHTMLTemplate = testHTMLTemplateBuffer.replace(
|
|
3658
|
-
"{{applicationName}}",
|
|
3659
|
-
oldPackageJSON.name
|
|
3660
|
-
);
|
|
3661
|
-
await fs4.mkdir(targetDirectory, { recursive: true });
|
|
3662
|
-
await fs4.writeFile(targetPath, testHTMLTemplate);
|
|
3663
|
-
console.log(`${targetPath} written`);
|
|
3664
|
-
}
|
|
3665
|
-
})
|
|
3666
|
-
);
|
|
3667
|
-
}
|
|
3668
|
-
async function rewritePackageJSON(projectRoot, config, oldPackageJSON) {
|
|
3669
|
-
const newPackageJSON = Object.assign(oldPackageJSON, { qunitx: config });
|
|
3670
|
-
await fs4.writeFile(`${projectRoot}/package.json`, JSON.stringify(newPackageJSON, null, 2));
|
|
3671
|
-
}
|
|
3672
|
-
async function writeTSConfigIfNeeded(projectRoot) {
|
|
3673
|
-
const targetPath = `${projectRoot}/tsconfig.json`;
|
|
3674
|
-
if (!await pathExists(targetPath)) {
|
|
3675
|
-
const tsConfigTemplate = await readTemplate("setup/tsconfig.json");
|
|
3676
|
-
await fs4.writeFile(targetPath, tsConfigTemplate);
|
|
3677
|
-
console.log(`${targetPath} written`);
|
|
3678
|
-
}
|
|
3679
|
-
}
|
|
3680
|
-
|
|
3681
|
-
// lib/commands/generate.ts
|
|
3682
|
-
init_color();
|
|
3683
|
-
import fs5 from "node:fs/promises";
|
|
3684
|
-
init_read_template();
|
|
3685
|
-
|
|
3686
|
-
// lib/utils/convert-to-pascal-case.ts
|
|
3687
|
-
function convertToPascalCase(str) {
|
|
3688
|
-
return str.split(/[-_]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
3689
|
-
}
|
|
3690
|
-
|
|
3691
|
-
// lib/commands/generate.ts
|
|
3692
|
-
async function generateTestFiles() {
|
|
3693
|
-
const projectRoot = await findProjectRoot();
|
|
3694
|
-
const moduleName = pathToModuleName(process.argv[3]);
|
|
3695
|
-
const path10 = process.argv[3].endsWith(".js") || process.argv[3].endsWith(".ts") ? `${projectRoot}/${process.argv[3]}` : `${projectRoot}/${process.argv[3]}.js`;
|
|
3696
|
-
if (await pathExists(path10)) {
|
|
3697
|
-
console.log(`${path10} already exists!`);
|
|
3698
|
-
return;
|
|
3699
|
-
}
|
|
3700
|
-
const testJSContent = await readTemplate("test.js");
|
|
3701
|
-
const targetFolderPaths = path10.split("/");
|
|
3702
|
-
targetFolderPaths.pop();
|
|
3703
|
-
await fs5.mkdir(targetFolderPaths.join("/"), { recursive: true });
|
|
3704
|
-
await fs5.writeFile(path10, testJSContent.replace("{{moduleName}}", moduleName));
|
|
3705
|
-
console.log(green(`${path10} written`));
|
|
3706
|
-
}
|
|
3707
|
-
function pathToModuleName(filePath) {
|
|
3708
|
-
const withoutExt = filePath.replace(/\.(js|ts)$/, "");
|
|
3709
|
-
const segments = withoutExt.split("/");
|
|
3710
|
-
const targetNames = segments[0] === "test" || segments[0] === "tests" ? segments.slice(1) : segments;
|
|
3711
|
-
return targetNames.map(convertToPascalCase).join(" | ");
|
|
3712
|
-
}
|
|
3713
|
-
|
|
3714
|
-
// lib/setup/config.ts
|
|
3715
|
-
init_default_project_config_values();
|
|
3716
|
-
import fs7 from "node:fs/promises";
|
|
3717
|
-
import { createRequire } from "node:module";
|
|
3718
|
-
import { pathToFileURL } from "node:url";
|
|
3719
|
-
|
|
3720
|
-
// lib/setup/fs-tree.ts
|
|
3721
|
-
init_default_project_config_values();
|
|
3722
|
-
import fs6, { glob as fsGlob } from "node:fs/promises";
|
|
3723
|
-
import path3 from "node:path";
|
|
3724
|
-
async function buildFSTree(fileAbsolutePaths, config = {}) {
|
|
3725
|
-
const targetExtensions = config.extensions || defaultProjectConfigValues.extensions;
|
|
3726
|
-
const fsTree = {};
|
|
3727
|
-
await Promise.all(
|
|
3728
|
-
fileAbsolutePaths.map(async (fileAbsolutePath) => {
|
|
3729
|
-
try {
|
|
3730
|
-
if (isGlob(fileAbsolutePath)) {
|
|
3731
|
-
for await (const fileName of fsGlob(fileAbsolutePath)) {
|
|
3732
|
-
if (targetExtensions.some((ext) => fileName.endsWith(`.${ext}`))) {
|
|
3733
|
-
fsTree[fileName] = null;
|
|
3734
|
-
}
|
|
3735
|
-
}
|
|
3736
|
-
} else {
|
|
3737
|
-
const entry = await fs6.stat(fileAbsolutePath);
|
|
3738
|
-
if (entry.isFile()) {
|
|
3739
|
-
fsTree[fileAbsolutePath] = null;
|
|
3740
|
-
} else if (entry.isDirectory()) {
|
|
3741
|
-
const fileNames = await readDirRecursive(fileAbsolutePath, (name) => {
|
|
3742
|
-
return targetExtensions.some((extension) => name.endsWith(`.${extension}`));
|
|
3743
|
-
});
|
|
3744
|
-
fileNames.forEach((fileName) => {
|
|
3745
|
-
fsTree[fileName] = null;
|
|
3746
|
-
});
|
|
3747
|
-
}
|
|
3748
|
-
}
|
|
3749
|
-
} catch (error) {
|
|
3750
|
-
console.error(error);
|
|
3751
|
-
return process.exit(1);
|
|
3752
|
-
}
|
|
3753
|
-
})
|
|
3754
|
-
);
|
|
3755
|
-
return fsTree;
|
|
3756
|
-
}
|
|
3757
|
-
function isGlob(str) {
|
|
3758
|
-
return /[*?{[]/.test(str);
|
|
3759
|
-
}
|
|
3760
|
-
async function readDirRecursive(dir, filter) {
|
|
3761
|
-
const entries = await fs6.readdir(dir, { recursive: true, withFileTypes: true });
|
|
3762
|
-
const candidates = entries.filter(
|
|
3763
|
-
(dirent) => (dirent.isFile() || dirent.isSymbolicLink()) && filter(dirent.name)
|
|
3764
|
-
);
|
|
3765
|
-
const resolvedPaths = await Promise.all(
|
|
3766
|
-
candidates.map(async (dirent) => {
|
|
3767
|
-
const fullPath = path3.join(dirent.parentPath, dirent.name);
|
|
3768
|
-
if (dirent.isFile()) return fullPath;
|
|
3769
|
-
try {
|
|
3770
|
-
const statResult = await fs6.stat(fullPath);
|
|
3771
|
-
return statResult.isFile() ? fullPath : null;
|
|
3772
|
-
} catch {
|
|
3773
|
-
return null;
|
|
3774
|
-
}
|
|
3775
|
-
})
|
|
3776
|
-
);
|
|
3777
|
-
return resolvedPaths.filter((resolvedPath) => resolvedPath !== null);
|
|
3778
|
-
}
|
|
3779
|
-
|
|
3780
|
-
// lib/setup/test-file-paths.ts
|
|
3781
|
-
import { matchesGlob } from "node:path";
|
|
3782
|
-
var GLOB_CHARS = /[*?{[]/;
|
|
3783
|
-
function setupTestFilePaths(inputs2) {
|
|
3784
|
-
const folders = [];
|
|
3785
|
-
const filesWithGlob = [];
|
|
3786
|
-
const filesWithoutGlob = [];
|
|
3787
|
-
inputs2.forEach((input) => {
|
|
3788
|
-
if (!pathIsFile(input)) {
|
|
3789
|
-
folders.push({ input, globFormat: `${input}/**` });
|
|
3790
|
-
} else if (isGlob2(input)) {
|
|
3791
|
-
filesWithGlob.push({ input, globFormat: input });
|
|
3792
|
-
} else {
|
|
3793
|
-
filesWithoutGlob.push({ input, globFormat: input });
|
|
3794
|
-
}
|
|
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(() => {
|
|
3795
4419
|
});
|
|
3796
|
-
const dedupedFolders = folders.filter((folder) => !isIncludedIn(folders, folder));
|
|
3797
|
-
const dedupedGlobFiles = filesWithGlob.filter(
|
|
3798
|
-
(file) => !isIncludedIn(dedupedFolders, file) && !isIncludedIn(filesWithGlob, file)
|
|
3799
|
-
);
|
|
3800
|
-
const dedupedPlainFiles = filesWithoutGlob.reduce((acc, file) => {
|
|
3801
|
-
if (!isIncludedIn(dedupedFolders, file) && !isIncludedIn(dedupedGlobFiles, file) && !isIncludedIn(acc, file)) {
|
|
3802
|
-
acc.push(file);
|
|
3803
|
-
}
|
|
3804
|
-
return acc;
|
|
3805
|
-
}, []);
|
|
3806
|
-
return dedupedFolders.concat(dedupedGlobFiles, dedupedPlainFiles).map((meta) => meta.input);
|
|
3807
4420
|
}
|
|
3808
|
-
function
|
|
3809
|
-
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
|
+
});
|
|
3810
4430
|
}
|
|
3811
|
-
function
|
|
3812
|
-
|
|
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);
|
|
3813
4453
|
}
|
|
3814
|
-
function
|
|
3815
|
-
|
|
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();
|
|
3816
4458
|
}
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
} else if (arg.endsWith(".html")) {
|
|
3839
|
-
if (result.htmlPaths) {
|
|
3840
|
-
result.htmlPaths.push(arg);
|
|
3841
|
-
} else {
|
|
3842
|
-
result.htmlPaths = [arg];
|
|
3843
|
-
}
|
|
3844
|
-
return result;
|
|
3845
|
-
} else if (arg.startsWith("--port")) {
|
|
3846
|
-
return Object.assign(result, { port: Number(arg.split("=")[1]), portExplicit: true });
|
|
3847
|
-
} else if (arg.startsWith("--extensions")) {
|
|
3848
|
-
return Object.assign(result, {
|
|
3849
|
-
extensions: arg.split("=")[1].split(",").map((extension) => extension.trim())
|
|
3850
|
-
});
|
|
3851
|
-
} else if (arg.startsWith("--browser")) {
|
|
3852
|
-
const value = arg.split("=")[1];
|
|
3853
|
-
if (!["chromium", "firefox", "webkit"].includes(value)) {
|
|
3854
|
-
console.error(
|
|
3855
|
-
`Invalid --browser value: "${value}". Must be one of: chromium, firefox, webkit`
|
|
3856
|
-
);
|
|
3857
|
-
process.exit(1);
|
|
3858
|
-
}
|
|
3859
|
-
return Object.assign(result, { browser: value });
|
|
3860
|
-
} else if (arg.startsWith("--before")) {
|
|
3861
|
-
return Object.assign(result, { before: parseModule(arg.split("=")[1]) });
|
|
3862
|
-
} else if (arg.startsWith("--after")) {
|
|
3863
|
-
return Object.assign(result, { after: parseModule(arg.split("=")[1]) });
|
|
3864
|
-
} else if (arg === "--trace-perf") {
|
|
3865
|
-
return result;
|
|
3866
|
-
}
|
|
3867
|
-
if (arg.startsWith("-")) {
|
|
3868
|
-
console.warn(`# Warning: Unknown flag "${arg}" \u2014 ignored`);
|
|
3869
|
-
return result;
|
|
3870
|
-
}
|
|
3871
|
-
result.inputs.add(
|
|
3872
|
-
arg.startsWith(projectRoot) || arg.startsWith("/") ? arg : path4.join(process.cwd(), arg)
|
|
3873
|
-
);
|
|
3874
|
-
return result;
|
|
3875
|
-
},
|
|
3876
|
-
{ inputs: /* @__PURE__ */ new Set([]) }
|
|
3877
|
-
);
|
|
3878
|
-
if (!providedFlags.browser && process.env.QUNITX_BROWSER) {
|
|
3879
|
-
const envBrowser = process.env.QUNITX_BROWSER;
|
|
3880
|
-
if (!["chromium", "firefox", "webkit"].includes(envBrowser)) {
|
|
3881
|
-
console.error(
|
|
3882
|
-
`Invalid QUNITX_BROWSER value: "${envBrowser}". Must be one of: chromium, firefox, webkit`
|
|
3883
|
-
);
|
|
3884
|
-
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 {
|
|
3885
4480
|
}
|
|
3886
|
-
|
|
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;
|
|
3887
4487
|
}
|
|
3888
|
-
return { ...providedFlags, inputs: Array.from(providedFlags.inputs) };
|
|
3889
4488
|
}
|
|
3890
|
-
function
|
|
3891
|
-
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);
|
|
3892
4504
|
return true;
|
|
3893
|
-
}
|
|
3894
|
-
|
|
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");
|
|
3895
4524
|
}
|
|
3896
|
-
|
|
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);
|
|
3897
4564
|
}
|
|
3898
|
-
function
|
|
3899
|
-
if (
|
|
3900
|
-
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}`);
|
|
3901
4579
|
}
|
|
3902
|
-
return value;
|
|
3903
4580
|
}
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
const
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
_onTestsJsServed: null
|
|
3935
|
-
};
|
|
3936
|
-
config.htmlPaths = normalizeHTMLPaths(config.projectRoot, config.htmlPaths);
|
|
3937
|
-
[config.fsTree, config.plugins] = await Promise.all([
|
|
3938
|
-
buildFSTree(config.testFileLookupPaths, config),
|
|
3939
|
-
pluginsPromise
|
|
3940
|
-
]);
|
|
3941
|
-
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
|
+
}
|
|
3942
4611
|
}
|
|
3943
|
-
async function
|
|
3944
|
-
const
|
|
3945
|
-
|
|
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;
|
|
3946
4617
|
}
|
|
3947
|
-
function
|
|
3948
|
-
|
|
4618
|
+
async function readPkgMtime(cwd) {
|
|
4619
|
+
try {
|
|
4620
|
+
return (await stat2(path12.join(cwd, "package.json"))).mtimeMs;
|
|
4621
|
+
} catch {
|
|
4622
|
+
return 0;
|
|
4623
|
+
}
|
|
3949
4624
|
}
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
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
|
+
});
|
|
3953
4683
|
}
|
|
3954
|
-
function
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
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;
|
|
3959
4704
|
}
|
|
3960
|
-
const
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
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
|
+
`
|
|
3968
4735
|
);
|
|
4736
|
+
return 0;
|
|
3969
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
|
+
});
|
|
3970
4765
|
|
|
3971
4766
|
// cli.ts
|
|
4767
|
+
init_chrome_prelaunch();
|
|
4768
|
+
init_package();
|
|
4769
|
+
import process4 from "node:process";
|
|
3972
4770
|
process4.title = "qunitx";
|
|
3973
4771
|
(async () => {
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
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)) {
|
|
3977
4776
|
return process4.stdout.write(package_default.version + "\n");
|
|
3978
|
-
} else if (["help", "h", "p", "print"].includes(
|
|
3979
|
-
return await displayHelpOutput();
|
|
3980
|
-
} else if (["new", "n", "g", "generate"].includes(
|
|
3981
|
-
return await generateTestFiles();
|
|
3982
|
-
} else if (
|
|
3983
|
-
return await initializeProject();
|
|
3984
|
-
}
|
|
3985
|
-
|
|
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();
|
|
3986
4806
|
try {
|
|
3987
4807
|
return await run2(config);
|
|
3988
4808
|
} catch (error) {
|