qunitx-cli 0.22.3 → 0.23.1

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.
Files changed (3) hide show
  1. package/README.md +104 -31
  2. package/dist/cli.js +1621 -793
  3. 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
- isRunCommand = Boolean(process.argv[2]) && !NON_RUN_COMMANDS.has(process.argv[2]);
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.1",
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/setup/default-project-config-values.ts
381
- var defaultProjectConfigValues;
382
- var init_default_project_config_values = __esm({
383
- "lib/setup/default-project-config-values.ts"() {
384
- defaultProjectConfigValues = {
385
- output: "tmp",
386
- timeout: 2e4,
387
- failFast: false,
388
- port: 1234,
389
- extensions: ["js", "ts", "jsx", "tsx"],
390
- browser: "chromium"
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/find-internal-assets-from-html.ts
419
- function findInternalAssetsFromHTML(htmlContent) {
420
- const links = [...htmlContent.matchAll(LINK_HREF_REGEX)].map((match) => match[1]).filter((uri) => !ABSOLUTE_URL_REGEX.test(uri));
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 ABSOLUTE_URL_REGEX, SCRIPT_SRC_REGEX, LINK_HREF_REGEX;
425
- var init_find_internal_assets_from_html = __esm({
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/utils/html.ts
434
- function findScriptPlaceholder(html) {
435
- return html.includes(SCRIPT_PLACEHOLDER) ? SCRIPT_PLACEHOLDER : void 0;
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 isCustomTemplate(html) {
438
- return !!findScriptPlaceholder(html) || HANDLEBARS_TOKEN_REGEX.test(html);
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
- function injectScript(html, content) {
441
- const placeholder = findScriptPlaceholder(html);
442
- if (placeholder) {
443
- return html.replace(placeholder, content);
444
- }
445
- if (isCustomTemplate(html)) {
446
- if (html.includes("</body>")) {
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
- return html;
455
- }
456
- var SCRIPT_PLACEHOLDER, HANDLEBARS_TOKEN_REGEX;
457
- var init_html = __esm({
458
- "lib/utils/html.ts"() {
459
- SCRIPT_PLACEHOLDER = "{{qunitxScript}}";
460
- HANDLEBARS_TOKEN_REGEX = /{{\s*[^}]+\s*}}/;
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/tap/dump-yaml.ts
465
- function dumpYaml({
466
- name,
467
- actual,
468
- expected,
469
- message,
470
- stack,
471
- source,
472
- at
473
- }) {
474
- return `name: ${dumpString(name, "")}
475
- ` + 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) : "");
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 needsQuoting(str) {
478
- return NEEDS_QUOTING.test(str);
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 dumpString(str, indent) {
481
- if (str === "") return "''";
482
- if (str.includes("\n")) {
483
- return "|-\n" + str.replace(/^/gm, `${indent} `);
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
- function dumpValue(value, indent) {
489
- if (value === null || value === void 0) return "null";
490
- if (typeof value === "boolean" || typeof value === "number") return String(value);
491
- if (typeof value === "string") return dumpString(value, indent);
492
- if (Array.isArray(value)) {
493
- if (value.length === 0) return "[]";
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
- const entries = Object.entries(value);
501
- if (entries.length === 0) return "{}";
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(path10) {
686
- const parts = path10.split("/").reduce((acc, part) => {
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 (path10.startsWith("/") ? "/" : "") + parts.join("/");
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(path10) {
700
- return path10.includes("/node_modules/") || path10.includes("\\node_modules\\");
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(path10, handler) {
977
- this.#registerRouteHandler("GET", path10, handler);
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(path10, handler) {
1009
- this.#registerRouteHandler("POST", path10, handler);
1824
+ post(path14, handler) {
1825
+ this.#registerRouteHandler("POST", path14, handler);
1010
1826
  }
1011
1827
  /** Registers a DELETE route handler. */
1012
- delete(path10, handler) {
1013
- this.#registerRouteHandler("DELETE", path10, handler);
1828
+ delete(path14, handler) {
1829
+ this.#registerRouteHandler("DELETE", path14, handler);
1014
1830
  }
1015
1831
  /** Registers a PUT route handler. */
1016
- put(path10, handler) {
1017
- this.#registerRouteHandler("PUT", path10, handler);
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, path10, handler) {
1839
+ #registerRouteHandler(method, path14, handler) {
1024
1840
  if (!this.routes[method]) {
1025
1841
  this.routes[method] = {};
1026
1842
  }
1027
- const paramNames = this.#extractParamNames(path10);
1028
- this.routes[method][path10] = {
1029
- path: path10,
1843
+ const paramNames = this.#extractParamNames(path14);
1844
+ this.routes[method][path14] = {
1845
+ path: path14,
1030
1846
  handler,
1031
1847
  paramNames,
1032
- isWildcard: path10 === "/*",
1033
- compiledRegex: paramNames.length > 0 ? new RegExp(`^${this.#buildRegexPattern(path10, paramNames)}$`) : null
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: path10, isWildcard } = route;
1072
- if (!isWildcard && !path10.includes(":")) {
1887
+ const { path: path14, isWildcard } = route;
1888
+ if (!isWildcard && !path14.includes(":")) {
1073
1889
  return false;
1074
1890
  }
1075
- if (isWildcard || this.#matchPathSegments(path10, url)) {
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(path10, url) {
1088
- const pathSegments = path10.split("/");
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(path10, _paramNames) {
1106
- let regexPattern = path10.replace(/:[^/]+/g, "([^/]+)");
1921
+ #buildRegexPattern(path14, _paramNames) {
1922
+ let regexPattern = path14.replace(/:[^/]+/g, "([^/]+)");
1107
1923
  regexPattern = regexPattern.replace(/\//g, "\\/");
1108
1924
  return regexPattern;
1109
1925
  }
1110
- #extractParamNames(path10) {
1926
+ #extractParamNames(path14) {
1111
1927
  const paramRegex = /:(\w+)/g;
1112
- const paramMatches = path10.match(paramRegex);
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 fs8 from "node:fs";
1129
- import path5 from "node:path";
1944
+ import fs9 from "node:fs";
1945
+ import path6 from "node:path";
1130
1946
  function setupWebServer(config, cachedContent) {
1131
- const STATIC_FILES_PATH = path5.resolve(config.projectRoot, config.output);
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
- path5.join(path5.resolve(config.projectRoot, config.output), "index.html"),
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
- path5.join(path5.resolve(config.projectRoot, config.output), "index.html"),
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
- path5.join(path5.resolve(config.projectRoot, config.output), "qunitx.html"),
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
- path5.join(path5.resolve(config.projectRoot, config.output), "qunitx.html"),
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(path5.join(path5.resolve(config.projectRoot, config.output), req.path), htmlContent);
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[path5.extname(filePath).substring(1).toLowerCase()] || MIME_TYPES.html;
1322
- const stream = fs8.createReadStream(filePath);
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
- path5.join(path5.resolve(groupConfig.projectRoot, groupConfig.output), "index.html"),
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 = path5.resolve(groupConfig.projectRoot, groupConfig.output);
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[path5.extname(filePath).substring(1).toLowerCase()] || MIME_TYPES.html;
1665
- const stream = fs8.createReadStream(filePath);
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 = path5.normalize(`${htmlDirectory}/${assetPath}`);
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 = fs8.promises;
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 pathToFileURL3 } from "node:url";
2832
+ import { pathToFileURL as pathToFileURL2 } from "node:url";
2076
2833
  async function runUserModule(modulePath, params, scriptPosition) {
2077
2834
  try {
2078
- const func = await import(pathToFileURL3(modulePath).href);
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 fs9 from "node:fs/promises";
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 fs9.mkdir(outDir, { recursive: true });
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, allTestFilePaths.join("\0"), cachedContent, needsDisk) : buildWithOverlayfsRetry(buildOptions, needsDisk),
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 fs9.rm(targetPath, { force: true, recursive: true });
2200
- await fs9.mkdir(path7.dirname(targetPath), { recursive: true });
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 fs9.writeFile(path7.join(outDir, "index.html"), buildErrorHTML(cachedContent._buildError));
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
- fs9.writeFile(path7.join(outDir, "index.html"), buildNoTestsHTML(displayFiles)).catch(
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
- fs9.writeFile(
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) => fs9.mkdir(path7.resolve(group.config.projectRoot, group.config.output), { recursive: true })
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 fs9.writeFile(destPath, outputFile.contents);
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 fs9.writeFile(
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) => fs9.writeFile(outputFile.path, outputFile.contents))
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, cachedContent, needsDisk) {
3276
+ async function buildIncrementally(options, fileKey, cache, needsDisk) {
2509
3277
  const buildOpts = { ...options, write: false };
2510
- if (!cachedContent._esbuildContext || cachedContent._esbuildContextKey !== fileKey) {
2511
- cachedContent._esbuildContext?.dispose().catch(() => {
3278
+ if (!cache._esbuildContext || cache._esbuildContextKey !== fileKey) {
3279
+ cache._esbuildContext?.dispose().catch(() => {
2512
3280
  });
2513
- cachedContent._esbuildContext = await esbuild.context(buildOpts);
2514
- cachedContent._esbuildContextKey = fileKey;
3281
+ cache._esbuildContext = await esbuild.context(buildOpts);
3282
+ cache._esbuildContextKey = fileKey;
2515
3283
  }
2516
- const ctx = cachedContent._esbuildContext;
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 (!watchMode) {
2613
- if (groupMode) {
2614
- throw new Error("Browser test run failed");
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");
3385
+ }
3386
+ await flushConsoleHandlers(pendingHandlers, connections.page);
3387
+ if (daemonMode) {
3388
+ throw new DaemonRunError(1);
2623
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
- MIN_NAV_MS = 3e4;
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 fs10 from "node:fs";
3488
+ import fs11 from "node:fs";
2668
3489
  import { readdir, stat, lstat } from "node:fs/promises";
2669
- import path8 from "node:path";
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
- fs10.unwatchFile(filePath, handler);
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
- fs10.watchFile(filePath, { interval: SYMLINK_POLL_INTERVAL_MS, persistent: false }, handler);
2694
- symlinkPollers.set(filePath, () => fs10.unwatchFile(filePath, handler));
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 = fs10.watch(watchPath, { recursive: true }, async (eventType, filename) => {
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 === path8.basename(watchPath) ? watchPath : path8.join(watchPath, 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,22 +3570,29 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
2749
3570
  }
2750
3571
  handleWatchEvent(config, extensions, event, fullPath, onEventFunc, onFinishFunc);
2751
3572
  });
2752
- const parentDir = path8.dirname(watchPath);
2753
- const watchedBasename = path8.basename(watchPath);
3573
+ const parentDir = path9.dirname(watchPath);
3574
+ const watchedBasename = path9.basename(watchPath);
2754
3575
  let parentUnlinkFired = false;
2755
- const parentWatcher = fs10.watch(parentDir, async (eventType, filename) => {
2756
- if (!ready || filename !== watchedBasename || eventType !== "rename") return;
2757
- if (parentUnlinkFired) return;
3576
+ let rescanTimer = null;
3577
+ const tryFireParentUnlink = async () => {
3578
+ if (parentUnlinkFired) return false;
2758
3579
  parentUnlinkFired = true;
2759
3580
  try {
2760
3581
  await stat(watchPath);
2761
3582
  parentUnlinkFired = false;
3583
+ return false;
2762
3584
  } catch {
2763
3585
  handleWatchEvent(config, extensions, "unlinkDir", watchPath, onEventFunc, onFinishFunc);
2764
3586
  childWatcher.close();
2765
3587
  parentWatcher.close();
3588
+ if (rescanTimer) clearInterval(rescanTimer);
2766
3589
  delete fileWatchers[watchPath];
3590
+ return true;
2767
3591
  }
3592
+ };
3593
+ const parentWatcher = fs11.watch(parentDir, async (eventType, filename) => {
3594
+ if (!ready || filename !== watchedBasename || eventType !== "rename") return;
3595
+ await tryFireParentUnlink();
2768
3596
  });
2769
3597
  parentWatchers.push(parentWatcher);
2770
3598
  fileWatchers[watchPath] = childWatcher;
@@ -2777,22 +3605,23 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
2777
3605
  )
2778
3606
  );
2779
3607
  if (process.platform === "darwin") {
2780
- rescanTimers.push(
2781
- setInterval(() => {
2782
- if (!ready || rescanInProgress) return;
2783
- rescanInProgress = true;
2784
- rescanDirectoryForDelta(
2785
- watchPath,
2786
- config,
2787
- extensions,
2788
- onEventFunc,
2789
- onFinishFunc,
2790
- trackSymlink
2791
- ).finally(() => {
2792
- rescanInProgress = false;
2793
- });
2794
- }, RESCAN_INTERVAL_MS).unref()
2795
- );
3608
+ rescanTimer = setInterval(async () => {
3609
+ if (!ready || rescanInProgress || parentUnlinkFired) return;
3610
+ if (await tryFireParentUnlink()) return;
3611
+ rescanInProgress = true;
3612
+ rescanDirectoryForDelta(
3613
+ watchPath,
3614
+ config,
3615
+ extensions,
3616
+ onEventFunc,
3617
+ onFinishFunc,
3618
+ trackSymlink
3619
+ ).finally(() => {
3620
+ rescanInProgress = false;
3621
+ });
3622
+ }, RESCAN_INTERVAL_MS);
3623
+ rescanTimer.unref();
3624
+ rescanTimers.push(rescanTimer);
2796
3625
  }
2797
3626
  }
2798
3627
  readyPromises.push(
@@ -2867,11 +3696,11 @@ async function rescanDirectoryForDelta(watchPath, config, extensions, onEventFun
2867
3696
  const trackedToRecheck = [];
2868
3697
  for (const entry of entries) {
2869
3698
  if (entry.isDirectory()) {
2870
- presentDirs.add(path8.join(entry.parentPath, entry.name));
3699
+ presentDirs.add(path9.join(entry.parentPath, entry.name));
2871
3700
  continue;
2872
3701
  }
2873
3702
  if (!entry.isFile() && !entry.isSymbolicLink()) continue;
2874
- const entryPath = path8.join(entry.parentPath, entry.name);
3703
+ const entryPath = path9.join(entry.parentPath, entry.name);
2875
3704
  presentDirs.add(entry.parentPath);
2876
3705
  if (!extensions.some((ext) => entryPath.endsWith(`.${ext}`))) continue;
2877
3706
  presentPaths.add(entryPath);
@@ -2894,13 +3723,13 @@ async function rescanDirectoryForDelta(watchPath, config, extensions, onEventFun
2894
3723
  }
2895
3724
  })
2896
3725
  );
2897
- const watchPrefix = watchPath + path8.sep;
3726
+ const watchPrefix = watchPath + path9.sep;
2898
3727
  const firedDirPrefixes = [];
2899
3728
  for (const trackedPath of Object.keys(config.fsTree)) {
2900
3729
  if (!trackedPath.startsWith(watchPrefix) || presentPaths.has(trackedPath)) continue;
2901
- if (firedDirPrefixes.some((p) => trackedPath.startsWith(p + path8.sep))) continue;
2902
- const parts = trackedPath.slice(watchPrefix.length).split(path8.sep);
2903
- const goneDirPath = parts.slice(0, -1).map((_, i) => watchPrefix + parts.slice(0, i + 1).join(path8.sep)).find((p) => !presentDirs.has(p)) ?? null;
3730
+ if (firedDirPrefixes.some((p) => trackedPath.startsWith(p + path9.sep))) continue;
3731
+ const parts = trackedPath.slice(watchPrefix.length).split(path9.sep);
3732
+ const goneDirPath = parts.slice(0, -1).map((_, i) => watchPrefix + parts.slice(0, i + 1).join(path9.sep)).find((p) => !presentDirs.has(p)) ?? null;
2904
3733
  if (goneDirPath !== null) {
2905
3734
  firedDirPrefixes.push(goneDirPath);
2906
3735
  handleWatchEvent(config, extensions, "unlinkDir", goneDirPath, onEventFunc, onFinishFunc);
@@ -3032,48 +3861,90 @@ var init_keyboard_events = __esm({
3032
3861
  });
3033
3862
 
3034
3863
  // lib/setup/write-output-static-files.ts
3035
- import fs11 from "node:fs/promises";
3036
- import path9 from "node:path";
3864
+ import fs12 from "node:fs/promises";
3865
+ import path10 from "node:path";
3037
3866
  async function writeOutputStaticFiles({ projectRoot, output }, cachedContent) {
3038
3867
  const staticHTMLPromises = Object.keys(cachedContent.staticHTMLs).map(async (staticHTMLKey) => {
3039
- const htmlRelativePath = path9.relative(projectRoot, staticHTMLKey);
3040
- const outDir = path9.resolve(projectRoot, output);
3041
- await ensureFolderExists(path9.join(outDir, htmlRelativePath));
3042
- await fs11.writeFile(
3043
- path9.join(outDir, htmlRelativePath),
3868
+ const htmlRelativePath = path10.relative(projectRoot, staticHTMLKey);
3869
+ const outDir = path10.resolve(projectRoot, output);
3870
+ await ensureFolderExists(path10.join(outDir, htmlRelativePath));
3871
+ await fs12.writeFile(
3872
+ path10.join(outDir, htmlRelativePath),
3044
3873
  cachedContent.staticHTMLs[staticHTMLKey]
3045
3874
  );
3046
3875
  });
3047
3876
  const assetPromises = Array.from(cachedContent.assets).map(async (assetAbsolutePath) => {
3048
- const assetRelativePath = path9.relative(projectRoot, assetAbsolutePath);
3049
- const outDir = path9.resolve(projectRoot, output);
3050
- await ensureFolderExists(path9.join(outDir, assetRelativePath));
3051
- await fs11.copyFile(assetAbsolutePath, path9.join(outDir, assetRelativePath));
3877
+ const assetRelativePath = path10.relative(projectRoot, assetAbsolutePath);
3878
+ const outDir = path10.resolve(projectRoot, output);
3879
+ await ensureFolderExists(path10.join(outDir, assetRelativePath));
3880
+ await fs12.copyFile(assetAbsolutePath, path10.join(outDir, assetRelativePath));
3052
3881
  });
3053
3882
  await Promise.all(staticHTMLPromises.concat(assetPromises));
3054
3883
  }
3055
3884
  async function ensureFolderExists(assetPath) {
3056
- await fs11.mkdir(path9.dirname(assetPath), { recursive: true });
3885
+ await fs12.mkdir(path10.dirname(assetPath), { recursive: true });
3057
3886
  }
3058
3887
  var init_write_output_static_files = __esm({
3059
3888
  "lib/setup/write-output-static-files.ts"() {
3060
3889
  }
3061
3890
  });
3062
3891
 
3892
+ // lib/utils/daemon-hint.ts
3893
+ import fs13 from "node:fs/promises";
3894
+ import os3 from "node:os";
3895
+ import path11 from "node:path";
3896
+ function shouldShowDaemonHint(ctx) {
3897
+ const env = ctx.env ?? process.env;
3898
+ if (ctx.watch) return false;
3899
+ if (ctx.daemonMode) return false;
3900
+ if (env.CI) return false;
3901
+ if (env.QUNITX_DAEMON) return false;
3902
+ if (env.QUNITX_NO_DAEMON) return false;
3903
+ if (env.QUNITX_HINT_SHOWN) return false;
3904
+ if (ctx.durationMs < FAST_RUN_THRESHOLD_MS) return false;
3905
+ if (ctx.isTTY === false) return false;
3906
+ if (ctx.isTTY === void 0 && !process.stderr.isTTY) return false;
3907
+ return true;
3908
+ }
3909
+ async function maybePrintDaemonHint(ctx, opts = {}) {
3910
+ if (!shouldShowDaemonHint(ctx)) return;
3911
+ const sentinel = opts.sentinelPath ?? DEFAULT_SENTINEL;
3912
+ try {
3913
+ await fs13.access(sentinel);
3914
+ return;
3915
+ } catch {
3916
+ }
3917
+ (opts.write ?? ((t) => process.stderr.write(t)))(HINT_TEXT);
3918
+ try {
3919
+ await fs13.mkdir(path11.dirname(sentinel), { recursive: true });
3920
+ await fs13.writeFile(sentinel, (/* @__PURE__ */ new Date()).toISOString());
3921
+ } catch {
3922
+ }
3923
+ }
3924
+ var FAST_RUN_THRESHOLD_MS, HINT_TEXT, DEFAULT_SENTINEL;
3925
+ var init_daemon_hint = __esm({
3926
+ "lib/utils/daemon-hint.ts"() {
3927
+ FAST_RUN_THRESHOLD_MS = 500;
3928
+ HINT_TEXT = "\n\x1B[34m\u2139\x1B[39m Tip: export QUNITX_DAEMON=1 for ~2\xD7 faster repeated runs (qunitx daemon --help)\n";
3929
+ DEFAULT_SENTINEL = path11.join(os3.homedir(), ".cache", "qunitx", "hint-shown");
3930
+ }
3931
+ });
3932
+
3063
3933
  // lib/commands/run.ts
3064
3934
  var run_exports = {};
3065
3935
  __export(run_exports, {
3936
+ buildCachedContent: () => buildCachedContent,
3066
3937
  computeFileTimes: () => computeFileTimes,
3067
3938
  default: () => run,
3068
3939
  readTimingCache: () => readTimingCache,
3069
3940
  run: () => run
3070
3941
  });
3071
- import fs12 from "node:fs/promises";
3942
+ import fs14 from "node:fs/promises";
3072
3943
  import { join as join3, normalize } from "node:path";
3073
3944
  import { createRequire as createRequire2 } from "node:module";
3074
3945
  import { availableParallelism } from "node:os";
3075
3946
  async function run(config) {
3076
- const browserPromise = config.watch ? null : launchBrowser(config);
3947
+ const browserPromise = config._daemonBrowser ? Promise.resolve(config._daemonBrowser) : config.watch ? null : launchBrowser(config);
3077
3948
  const [cachedContent, timings] = await Promise.all([
3078
3949
  buildCachedContent(config, config.htmlPaths),
3079
3950
  config.watch ? Promise.resolve(null) : readTimingCache(config.projectRoot)
@@ -3184,7 +4055,7 @@ async function run(config) {
3184
4055
  })() : null;
3185
4056
  process.stdout.write("TAP version 13\n");
3186
4057
  process.stdout.write(
3187
- `# Running ${allFiles.length} test file${allFiles.length === 1 ? "" : "s"} across ${groupCount} group${groupCount === 1 ? "" : "s"}
4058
+ `# Running ${allFiles.length} test file${allFiles.length === 1 ? "" : "s"} across ${groupCount} group${groupCount === 1 ? "" : "s"}${config._daemonMode ? " (daemon)" : ""}
3188
4059
  `
3189
4060
  );
3190
4061
  const [browser] = await Promise.all([
@@ -3242,7 +4113,7 @@ async function run(config) {
3242
4113
  try {
3243
4114
  await runTestsInBrowser(groupConfig, groupCachedContents[i], connections);
3244
4115
  } finally {
3245
- await flushConsoleHandlers(groupConfig._pendingConsoleHandlers);
4116
+ await flushConsoleHandlers(groupConfig._pendingConsoleHandlers, connections.page);
3246
4117
  await closeWithGrace([
3247
4118
  sharedServer ? void 0 : connections.server?.close(),
3248
4119
  connections.page?.close()
@@ -3279,6 +4150,17 @@ async function run(config) {
3279
4150
  if (config.after) {
3280
4151
  await runUserModule(`${process.cwd()}/${config.after}`, config.COUNTER, "after");
3281
4152
  }
4153
+ if (config._daemonMode) {
4154
+ clearInterval(keepAlive);
4155
+ await closeWithGrace([
4156
+ sharedServer?.close().catch(
4157
+ (err) => config.debug && process.stderr.write(`# [qunitx] server.close: ${err.message}
4158
+ `)
4159
+ )
4160
+ ]);
4161
+ throw new DaemonRunError(exitCode);
4162
+ }
4163
+ await maybePrintDaemonHint({ durationMs: process.uptime() * 1e3 });
3282
4164
  const exitTimer = setTimeout(() => process.exit(exitCode), STDOUT_FLUSH_GRACE_MS);
3283
4165
  exitTimer.unref();
3284
4166
  process.stdout.write("\n", async () => {
@@ -3301,7 +4183,7 @@ async function run(config) {
3301
4183
  }
3302
4184
  async function buildCachedContent(config, htmlPaths) {
3303
4185
  const htmlBuffers = await Promise.all(
3304
- config.htmlPaths.map((htmlPath) => fs12.readFile(htmlPath).catch(() => null))
4186
+ config.htmlPaths.map((htmlPath) => fs14.readFile(htmlPath).catch(() => null))
3305
4187
  );
3306
4188
  const cachedContent = htmlPaths.reduce(
3307
4189
  (result, _htmlPath, index) => {
@@ -3356,7 +4238,7 @@ async function addCachedContentMainHTML(projectRoot, cachedContent) {
3356
4238
  }
3357
4239
  async function readTimingCache(projectRoot) {
3358
4240
  try {
3359
- const parsed = JSON.parse(await fs12.readFile(`${projectRoot}/tmp/test-timings.json`, "utf8"));
4241
+ const parsed = JSON.parse(await fs14.readFile(`${projectRoot}/tmp/test-timings.json`, "utf8"));
3360
4242
  return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : {};
3361
4243
  } catch {
3362
4244
  return {};
@@ -3375,7 +4257,7 @@ function computeFileTimes(groups, weights, wallTimes) {
3375
4257
  return result;
3376
4258
  }
3377
4259
  async function persistTimings(fileTimes, projectRoot) {
3378
- await fs12.writeFile(
4260
+ await fs14.writeFile(
3379
4261
  `${projectRoot}/tmp/test-timings.json`,
3380
4262
  JSON.stringify(Object.fromEntries(fileTimes), null, 2)
3381
4263
  );
@@ -3390,7 +4272,7 @@ ${lines.join("\n")}
3390
4272
  async function splitIntoGroups(files, groupCount, timings) {
3391
4273
  const sizes = await Promise.all(
3392
4274
  files.map(
3393
- (f) => timings[f] > 0 ? Promise.resolve(0) : fs12.stat(f).then((s) => s.size).catch(() => 0)
4275
+ (f) => timings[f] > 0 ? Promise.resolve(0) : fs14.stat(f).then((s) => s.size).catch(() => 0)
3394
4276
  )
3395
4277
  );
3396
4278
  const knownRates = files.map((f, i) => ({ ms: timings[f], size: sizes[i] })).filter(({ ms, size }) => ms > 0 && size > 0);
@@ -3450,539 +4332,485 @@ var init_run = __esm({
3450
4332
  init_read_template();
3451
4333
  init_html();
3452
4334
  init_close_with_grace();
3453
- WATCH_NAV_TIMEOUT_MS = 5e3;
3454
- STDOUT_FLUSH_GRACE_MS = 5e3;
3455
- KEEP_ALIVE_INTERVAL_MS = 1e4;
3456
- EXIT_CODE_SIGTERM = 128 + 15;
3457
- }
3458
- });
3459
-
3460
- // cli.ts
3461
- init_chrome_prelaunch();
3462
- import process4 from "node:process";
3463
-
3464
- // lib/commands/help.ts
3465
- init_color();
3466
-
3467
- // package.json
3468
- var package_default = {
3469
- name: "qunitx-cli",
3470
- type: "module",
3471
- version: "0.22.3",
3472
- description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
3473
- author: "Izel Nakri",
3474
- license: "MIT",
3475
- keywords: [
3476
- "test runner",
3477
- "testing",
3478
- "browser",
3479
- "ci",
3480
- "qunit",
3481
- "qunitx"
3482
- ],
3483
- files: [
3484
- "bin/",
3485
- "dist/",
3486
- "templates/"
3487
- ],
3488
- scripts: {
3489
- build: "node scripts/build-cli.js",
3490
- bin: "chmod +x cli.ts && ./cli.ts",
3491
- prepublishOnly: "npm run build",
3492
- format: 'prettier --check "lib/**/*.ts" "test/**/*.ts" "scripts/**/*.js" "bin/**/*.js" "*.ts" "package.json" ".github/**/*.yml"',
3493
- "format:fix": 'prettier --write "lib/**/*.ts" "test/**/*.ts" "scripts/**/*.js" "bin/**/*.js" "*.ts" "package.json" ".github/**/*.yml"',
3494
- lint: "deno lint lib/ bin/ cli.ts",
3495
- "lint:docs": "node scripts/lint-docs.js",
3496
- docs: `deno doc --html --name="qunitx-cli" --output=docs/lib 'lib/**/*.ts' README.md`,
3497
- "changelog:unreleased": "git-cliff --unreleased --strip all",
3498
- "changelog:preview": "git-cliff",
3499
- "changelog:update": "git-cliff --output CHANGELOG.md",
3500
- postinstall: "PLAYWRIGHT_SKIP_DOWNLOAD=true deno install --allow-scripts=npm:playwright-core || true",
3501
- test: "node test/runner.ts",
3502
- "test:debug": "QUNITX_DEBUG=1 node test/runner.ts",
3503
- dev: "node test/runner.ts --watch",
3504
- "test:browser": "node test/runner.ts test/flags/*-test.ts test/inputs/*-test.ts",
3505
- "test:release": "bash scripts/test-release.sh",
3506
- "test:sanity-first": "./cli.ts test/helpers/failing-tests.js test/helpers/failing-tests.ts",
3507
- "test:sanity-second": "./cli.ts test/helpers/passing-tests.js test/helpers/passing-tests.ts"
3508
- },
3509
- engines: {
3510
- node: ">=24.0.0",
3511
- deno: ">=2.7.0"
3512
- },
3513
- bin: {
3514
- qunitx: "bin/qunitx.js"
3515
- },
3516
- repository: {
3517
- type: "git",
3518
- url: "git+https://github.com/izelnakri/qunitx-cli.git"
3519
- },
3520
- dependencies: {
3521
- esbuild: "^0.28.0",
3522
- "playwright-core": "^1.59.1",
3523
- ws: "^8.20.0"
3524
- },
3525
- devDependencies: {
3526
- "js-yaml": "^4.1.1",
3527
- prettier: "^3.8.3",
3528
- qunitx: "^1.2.9",
3529
- react: "^19.2.5",
3530
- "react-dom": "^19.2.5",
3531
- typescript: "^6.0.3",
3532
- vue: "^3.5.33"
3533
- },
3534
- volta: {
3535
- node: "24.14.0"
3536
- },
3537
- prettier: {
3538
- printWidth: 100,
3539
- singleQuote: true,
3540
- arrowParens: "always"
3541
- },
3542
- optionalDependencies: {
3543
- "qunitx-cli-linux-x64": "*"
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
- }
4335
+ init_daemon_hint();
4336
+ WATCH_NAV_TIMEOUT_MS = 5e3;
4337
+ STDOUT_FLUSH_GRACE_MS = 5e3;
4338
+ KEEP_ALIVE_INTERVAL_MS = 1e4;
4339
+ EXIT_CODE_SIGTERM = 128 + 15;
4340
+ }
4341
+ });
4342
+
4343
+ // lib/commands/daemon/server.ts
4344
+ var server_exports = {};
4345
+ __export(server_exports, {
4346
+ runDaemonServer: () => runDaemonServer
4347
+ });
4348
+ import net2 from "node:net";
4349
+ import fs15 from "node:fs";
4350
+ import { writeFile, unlink, stat as stat2, chmod } from "node:fs/promises";
4351
+ import path12 from "node:path";
4352
+ async function runDaemonServer() {
4353
+ const cwd = process.cwd();
4354
+ const socketPath = daemonSocketPath(cwd);
4355
+ const infoPath = daemonInfoPath(cwd);
4356
+ if (fs15.existsSync(infoPath) && await isLiveSocket(socketPath)) process.exit(0);
4357
+ await unlink(socketPath).catch(() => {
4358
+ });
4359
+ const logPath = process.env.QUNITX_DAEMON_LOG;
4360
+ if (logPath) {
4361
+ const log = fs15.createWriteStream(logPath, { flags: "a" });
4362
+ log.on("error", () => {
4363
+ });
4364
+ const forward = log.write.bind(log);
4365
+ process.stdout.write = forward;
4366
+ process.stderr.write = forward;
4367
+ }
4368
+ const argvSnapshot = process.argv;
4369
+ process.argv = [argvSnapshot[0], argvSnapshot[1] ?? "cli.ts"];
4370
+ let baseConfig;
4371
+ try {
4372
+ baseConfig = await setupConfig();
4373
+ } finally {
4374
+ process.argv = argvSnapshot;
4375
+ }
4376
+ baseConfig._daemonMode = true;
4377
+ baseConfig.watch = false;
4378
+ baseConfig.open = false;
4379
+ const [browser, pkgMtime] = await Promise.all([launchBrowser(baseConfig), readPkgMtime(cwd)]);
4380
+ const state = {
4381
+ browser,
4382
+ baseConfig,
4383
+ cwd,
4384
+ startedAt: Date.now(),
4385
+ pkgMtime,
4386
+ runQueue: Promise.resolve(),
4387
+ shuttingDown: false,
4388
+ pendingClients: /* @__PURE__ */ new Set(),
4389
+ socketServer: null,
4390
+ idleTimer: null,
4391
+ socketPath,
4392
+ infoPath,
4393
+ consecutiveCrashes: 0,
4394
+ listenSucceeded: false,
4395
+ esbuildCache: { _esbuildContext: null }
4396
+ };
4397
+ const shutdown = (reason) => shutdownDaemon2(state, reason);
4398
+ process.on("SIGTERM", () => void shutdown("SIGTERM"));
4399
+ process.on("SIGINT", () => void shutdown("SIGINT"));
4400
+ process.on("unhandledRejection", (err) => {
4401
+ process.stderr.write(`# [qunitx daemon] unhandledRejection: ${err}
4402
+ `);
4403
+ void shutdown("unhandledRejection");
4404
+ });
4405
+ state.socketServer = net2.createServer((socket) => handleConnection(socket, state));
4406
+ state.socketServer.on("error", (err) => {
4407
+ process.stderr.write(`# [qunitx daemon] server error: ${err.message}
4408
+ `);
4409
+ void shutdown("server error");
4410
+ });
4411
+ await listen(state.socketServer, socketPath);
4412
+ state.listenSucceeded = true;
4413
+ if (process.platform !== "win32") await chmod(socketPath, 384).catch(() => {
4414
+ });
4415
+ const info = {
4416
+ pid: process.pid,
4417
+ socketPath,
4418
+ cwd,
4419
+ nodeVersion: process.version,
4420
+ startedAt: state.startedAt
4421
+ };
4422
+ await writeFile(infoPath, JSON.stringify(info, null, 2));
4423
+ resetIdleTimer(state);
4424
+ process.stderr.write(`# [qunitx daemon] listening on ${socketPath} (pid ${process.pid})
4425
+ `);
4426
+ return new Promise(() => {
3795
4427
  });
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
4428
  }
3808
- function pathIsFile(path10) {
3809
- return path10.includes(".", path10.lastIndexOf("/") + 1);
4429
+ function listen(server, socketPath) {
4430
+ return new Promise((resolve, reject) => {
4431
+ const onError = (err) => reject(err);
4432
+ server.once("error", onError);
4433
+ server.listen(socketPath, () => {
4434
+ server.removeListener("error", onError);
4435
+ resolve();
4436
+ });
4437
+ });
3810
4438
  }
3811
- function isIncludedIn(paths, target) {
3812
- return paths.some((path10) => path10 !== target && matchesGlob(target.input, path10.globFormat));
4439
+ async function shutdownDaemon2(state, reason) {
4440
+ if (state.shuttingDown) return;
4441
+ state.shuttingDown = true;
4442
+ process.stderr.write(`# [qunitx daemon] shutting down: ${reason}
4443
+ `);
4444
+ if (state.idleTimer) clearTimeout(state.idleTimer);
4445
+ for (const sock of state.pendingClients) {
4446
+ writeChunk(sock, { type: "fatal", message: `daemon shutting down: ${reason}` });
4447
+ sock.end();
4448
+ }
4449
+ await new Promise((resolve) => state.socketServer.close(() => resolve()));
4450
+ await Promise.all([
4451
+ state.listenSucceeded ? unlink(state.socketPath).catch(() => {
4452
+ }) : null,
4453
+ state.listenSucceeded ? unlink(state.infoPath).catch(() => {
4454
+ }) : null,
4455
+ state.browser.close().catch(() => {
4456
+ }),
4457
+ state.esbuildCache._esbuildContext?.dispose().catch(() => {
4458
+ })
4459
+ ]);
4460
+ process.exit(0);
3813
4461
  }
3814
- function isGlob2(str) {
3815
- return GLOB_CHARS.test(str);
4462
+ function resetIdleTimer(state) {
4463
+ if (state.idleTimer) clearTimeout(state.idleTimer);
4464
+ state.idleTimer = setTimeout(() => void shutdownDaemon2(state, "idle timeout"), IDLE_TIMEOUT_MS);
4465
+ state.idleTimer.unref();
3816
4466
  }
3817
-
3818
- // lib/utils/parse-cli-flags.ts
3819
- import path4 from "node:path";
3820
- var FALLBACK_TIMEOUT_MS = 1e4;
3821
- function parseCliFlags(projectRoot) {
3822
- const providedFlags = process.argv.slice(2).reduce(
3823
- (result, arg) => {
3824
- if (arg.startsWith("--debug")) {
3825
- return Object.assign(result, { debug: parseBoolean(arg.split("=")[1]) });
3826
- } else if (arg.startsWith("--watch")) {
3827
- return Object.assign(result, { watch: parseBoolean(arg.split("=")[1]) });
3828
- } else if (arg === "-o" || arg.startsWith("-o=") || arg.startsWith("--open")) {
3829
- const value = arg.split("=")[1];
3830
- const open = value === void 0 || value === "true" ? true : value === "false" ? false : value;
3831
- return Object.assign(result, { open });
3832
- } else if (arg.startsWith("--failfast") || arg.startsWith("--failFast")) {
3833
- return Object.assign(result, { failFast: parseBoolean(arg.split("=")[1]) });
3834
- } else if (arg.startsWith("--timeout")) {
3835
- return Object.assign(result, { timeout: Number(arg.split("=")[1]) || FALLBACK_TIMEOUT_MS });
3836
- } else if (arg.startsWith("--output")) {
3837
- return Object.assign(result, { output: arg.split("=")[1] });
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);
4467
+ function handleConnection(socket, state) {
4468
+ state.pendingClients.add(socket);
4469
+ socket.on("close", () => state.pendingClients.delete(socket));
4470
+ socket.on("error", () => {
4471
+ });
4472
+ attachLineParser(socket, (req) => void dispatch(req, socket, state));
4473
+ }
4474
+ async function dispatch(req, socket, state) {
4475
+ if (req.type === "ping") {
4476
+ writeChunk(socket, {
4477
+ type: "pong",
4478
+ pid: process.pid,
4479
+ nodeVersion: process.version,
4480
+ cwd: state.cwd,
4481
+ startedAt: state.startedAt
4482
+ });
4483
+ socket.end();
4484
+ } else if (req.type === "shutdown") {
4485
+ try {
4486
+ fs15.unlinkSync(state.infoPath);
4487
+ } catch {
3885
4488
  }
3886
- providedFlags.browser = envBrowser;
4489
+ writeChunk(socket, { type: "done", exitCode: 0 });
4490
+ socket.end();
4491
+ void shutdownDaemon2(state, "shutdown request");
4492
+ } else if (req.type === "run") {
4493
+ state.runQueue = state.runQueue.then(() => handleRun(req, socket, state));
4494
+ await state.runQueue;
3887
4495
  }
3888
- return { ...providedFlags, inputs: Array.from(providedFlags.inputs) };
3889
4496
  }
3890
- function parseBoolean(result, defaultValue = true) {
3891
- if (result === "true") {
4497
+ function writeChunk(socket, chunk) {
4498
+ if (socket.destroyed) return;
4499
+ try {
4500
+ socket.write(JSON.stringify(chunk) + "\n");
4501
+ } catch {
4502
+ }
4503
+ }
4504
+ function makeInterceptor(socket, type) {
4505
+ return ((chunk, ...args) => {
4506
+ if (!socket.destroyed) {
4507
+ const str = typeof chunk === "string" ? chunk : chunk.toString("utf8");
4508
+ writeChunk(socket, { type, data: str });
4509
+ }
4510
+ const cb = args[args.length - 1];
4511
+ if (typeof cb === "function") queueMicrotask(cb);
3892
4512
  return true;
3893
- } else if (result === "false") {
3894
- return false;
4513
+ });
4514
+ }
4515
+ async function handleRun(req, socket, state) {
4516
+ if (state.shuttingDown) {
4517
+ writeChunk(socket, { type: "fatal", message: "daemon shutting down" });
4518
+ return void socket.end();
4519
+ } else if (req.cwd !== state.cwd) {
4520
+ writeChunk(socket, {
4521
+ type: "fatal",
4522
+ message: `cwd mismatch: daemon=${state.cwd} client=${req.cwd}`
4523
+ });
4524
+ return void socket.end();
4525
+ } else if (req.nodeVersion !== process.version) {
4526
+ writeChunk(socket, {
4527
+ type: "fatal",
4528
+ message: `node version mismatch: daemon=${process.version} client=${req.nodeVersion}`
4529
+ });
4530
+ socket.end();
4531
+ return void shutdownDaemon2(state, "node version mismatch");
3895
4532
  }
3896
- return defaultValue;
4533
+ const currentMtime = await readPkgMtime(state.cwd);
4534
+ if (currentMtime !== state.pkgMtime) {
4535
+ writeChunk(socket, { type: "fatal", message: "package.json changed; restarting daemon" });
4536
+ socket.end();
4537
+ return void shutdownDaemon2(state, "package.json changed");
4538
+ }
4539
+ if (state.idleTimer) clearTimeout(state.idleTimer);
4540
+ if (!state.browser.isConnected()) {
4541
+ await recoverBrowser(state);
4542
+ if (state.shuttingDown) {
4543
+ writeChunk(socket, { type: "fatal", message: "browser recovery failed" });
4544
+ return void socket.end();
4545
+ }
4546
+ }
4547
+ const origStdoutWrite = process.stdout.write.bind(process.stdout);
4548
+ const origStderrWrite = process.stderr.write.bind(process.stderr);
4549
+ process.stdout.write = makeInterceptor(socket, "stdout");
4550
+ process.stderr.write = makeInterceptor(socket, "stderr");
4551
+ let exitCode = 0;
4552
+ try {
4553
+ exitCode = await runOnce(req.argv, req.env, state);
4554
+ } catch (err) {
4555
+ process.stderr.write = origStderrWrite;
4556
+ origStderrWrite(`# [qunitx daemon] run error: ${err.stack || err}
4557
+ `);
4558
+ if (!socket.destroyed)
4559
+ writeChunk(socket, { type: "fatal", message: err.message || String(err) });
4560
+ exitCode = 1;
4561
+ } finally {
4562
+ process.stdout.write = origStdoutWrite;
4563
+ process.stderr.write = origStderrWrite;
4564
+ }
4565
+ if (state.browser.isConnected()) state.consecutiveCrashes = 0;
4566
+ else await recoverBrowser(state);
4567
+ if (!socket.destroyed) {
4568
+ writeChunk(socket, { type: "done", exitCode });
4569
+ socket.end();
4570
+ }
4571
+ resetIdleTimer(state);
3897
4572
  }
3898
- function parseModule(value) {
3899
- if (["false", "'false'", '"false"', ""].includes(value)) {
3900
- return false;
4573
+ async function recoverBrowser(state) {
4574
+ if (++state.consecutiveCrashes > MAX_CONSECUTIVE_CRASHES) {
4575
+ return void shutdownDaemon2(state, `${state.consecutiveCrashes} consecutive browser crashes`);
4576
+ }
4577
+ process.stderr.write(
4578
+ `# [qunitx daemon] browser crashed; relaunching (${state.consecutiveCrashes}/${MAX_CONSECUTIVE_CRASHES})
4579
+ `
4580
+ );
4581
+ state.browser.close().catch(() => {
4582
+ });
4583
+ try {
4584
+ state.browser = await launchBrowser(state.baseConfig, true);
4585
+ } catch (err) {
4586
+ void shutdownDaemon2(state, `browser relaunch failed: ${err.message || err}`);
3901
4587
  }
3902
- return value;
3903
4588
  }
3904
-
3905
- // lib/setup/config.ts
3906
- async function setupConfig() {
3907
- const projectRoot = await findProjectRoot();
3908
- const cliConfigFlags = parseCliFlags(projectRoot);
3909
- const projectPackageJSON = await readConfigFromPackageJSON(projectRoot);
3910
- const { plugins: rawPlugins, ...userQunitx } = projectPackageJSON.qunitx ?? {};
3911
- const pluginsPromise = resolvePlugins(rawPlugins, projectRoot);
3912
- const inputs2 = cliConfigFlags.inputs.concat(readInputsFromPackageJSON(projectPackageJSON));
3913
- const config = {
3914
- ...defaultProjectConfigValues,
3915
- htmlPaths: [],
3916
- ...userQunitx,
3917
- ...cliConfigFlags,
3918
- projectRoot,
3919
- inputs: inputs2,
3920
- testFileLookupPaths: setupTestFilePaths(inputs2),
3921
- lastFailedTestFiles: null,
3922
- lastRanTestFiles: null,
3923
- COUNTER: {
3924
- testCount: 0,
3925
- failCount: 0,
3926
- skipCount: 0,
3927
- todoCount: 0,
3928
- passCount: 0,
3929
- errorCount: 0
3930
- },
3931
- _testRunDone: null,
3932
- _resetTestTimeout: null,
3933
- _onWsOpen: null,
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;
4589
+ async function runOnce(argv, env, state) {
4590
+ const envSnapshot = { ...process.env };
4591
+ for (const [key, value] of Object.entries(env)) {
4592
+ if (value !== void 0) process.env[key] = value;
4593
+ }
4594
+ const argvSnapshot = process.argv;
4595
+ process.argv = ["node", argvSnapshot[1] ?? "cli.ts", ...argv];
4596
+ let config;
4597
+ try {
4598
+ config = await setupConfig();
4599
+ } finally {
4600
+ process.argv = argvSnapshot;
4601
+ }
4602
+ config._daemonMode = true;
4603
+ config._daemonBrowser = state.browser;
4604
+ config._daemonEsbuildCache = state.esbuildCache;
4605
+ config.watch = false;
4606
+ config.open = false;
4607
+ try {
4608
+ await run(config);
4609
+ return config.COUNTER.failCount > 0 ? 1 : 0;
4610
+ } catch (err) {
4611
+ if (err instanceof DaemonRunError) return err.exitCode;
4612
+ throw err;
4613
+ } finally {
4614
+ for (const key of Object.keys(process.env)) {
4615
+ if (!(key in envSnapshot)) delete process.env[key];
4616
+ }
4617
+ Object.assign(process.env, envSnapshot);
4618
+ }
3942
4619
  }
3943
- async function readConfigFromPackageJSON(projectRoot) {
3944
- const packageJSON = await fs7.readFile(`${projectRoot}/package.json`);
3945
- return JSON.parse(packageJSON.toString());
4620
+ async function isLiveSocket(socketPath) {
4621
+ const sock = await probeSocket(socketPath, LIVENESS_PROBE_TIMEOUT_MS);
4622
+ if (!sock) return false;
4623
+ sock.destroy();
4624
+ return true;
3946
4625
  }
3947
- function normalizeHTMLPaths(projectRoot, htmlPaths) {
3948
- return Array.from(new Set(htmlPaths.map((htmlPath) => `${projectRoot}/${htmlPath}`)));
4626
+ async function readPkgMtime(cwd) {
4627
+ try {
4628
+ return (await stat2(path12.join(cwd, "package.json"))).mtimeMs;
4629
+ } catch {
4630
+ return 0;
4631
+ }
3949
4632
  }
3950
- function readInputsFromPackageJSON(packageJSON) {
3951
- const qunitx = packageJSON.qunitx;
3952
- return qunitx && qunitx.inputs ? qunitx.inputs : [];
4633
+ var IDLE_TIMEOUT_MS, LIVENESS_PROBE_TIMEOUT_MS, MAX_CONSECUTIVE_CRASHES;
4634
+ var init_server = __esm({
4635
+ "lib/commands/daemon/server.ts"() {
4636
+ init_daemon_socket_path();
4637
+ init_socket_utils();
4638
+ init_config();
4639
+ init_browser();
4640
+ init_tests_in_browser();
4641
+ init_run();
4642
+ IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
4643
+ LIVENESS_PROBE_TIMEOUT_MS = 500;
4644
+ MAX_CONSECUTIVE_CRASHES = 2;
4645
+ }
4646
+ });
4647
+
4648
+ // lib/commands/daemon/index.ts
4649
+ var daemon_exports = {};
4650
+ __export(daemon_exports, {
4651
+ ensureDaemonRunning: () => ensureDaemonRunning,
4652
+ runDaemonCommand: () => runDaemonCommand
4653
+ });
4654
+ import { spawn as spawn3 } from "node:child_process";
4655
+ import fs16, { existsSync as existsSync3 } from "node:fs";
4656
+ import path13 from "node:path";
4657
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
4658
+ function runDaemonCommand() {
4659
+ const sub = process.argv[3];
4660
+ if (sub === "_serve") return runServeMode();
4661
+ if (sub === "start") return startDaemon();
4662
+ if (sub === "stop") return stopDaemon();
4663
+ if (sub === "status") return statusDaemon();
4664
+ const helpRequested = !sub || sub === "--help" || sub === "-h" || sub === "help";
4665
+ const out = helpRequested ? process.stdout : process.stderr;
4666
+ out.write(USAGE);
4667
+ return Promise.resolve(helpRequested ? 0 : 1);
4668
+ }
4669
+ async function runServeMode() {
4670
+ const { runDaemonServer: runDaemonServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
4671
+ await runDaemonServer2();
4672
+ return 0;
4673
+ }
4674
+ function waitForFile(filePath, timeoutMs) {
4675
+ if (existsSync3(filePath)) return Promise.resolve(true);
4676
+ return new Promise((resolve) => {
4677
+ const dir = path13.dirname(filePath);
4678
+ const fileName = path13.basename(filePath);
4679
+ const settle = (ok) => {
4680
+ clearTimeout(timer);
4681
+ watcher.close();
4682
+ resolve(ok);
4683
+ };
4684
+ const timer = setTimeout(() => settle(false), timeoutMs);
4685
+ const watcher = fs16.watch(dir, (_event, name) => {
4686
+ if (name === fileName && existsSync3(filePath)) settle(true);
4687
+ });
4688
+ watcher.on("error", () => settle(false));
4689
+ if (existsSync3(filePath)) settle(true);
4690
+ });
3953
4691
  }
3954
- function resolvePlugins(raw, projectRoot) {
3955
- if (raw == null) return Promise.resolve([]);
3956
- if (!Array.isArray(raw)) {
3957
- console.error(`# qunitx: package.json#qunitx.plugins must be an array`);
3958
- process.exit(1);
4692
+ async function spawnAndWaitForDaemon() {
4693
+ spawn3(process.execPath, [CLI_ENTRY, "daemon", "_serve"], {
4694
+ detached: true,
4695
+ stdio: "ignore",
4696
+ env: { ...process.env, QUNITX_DAEMON_CWD: process.cwd() }
4697
+ }).unref();
4698
+ if (!await waitForFile(daemonInfoPath(), SPAWN_TIMEOUT_MS)) return null;
4699
+ const pong = await pingDaemon();
4700
+ return pong?.type === "pong" ? { pid: pong.pid } : null;
4701
+ }
4702
+ async function ensureDaemonRunning() {
4703
+ if ((await pingDaemon())?.type === "pong") return true;
4704
+ return Boolean(await spawnAndWaitForDaemon());
4705
+ }
4706
+ async function startDaemon() {
4707
+ const existing = await pingDaemon();
4708
+ if (existing?.type === "pong") {
4709
+ process.stdout.write(`Daemon already running (pid ${existing.pid})
4710
+ `);
4711
+ return 0;
3959
4712
  }
3960
- const projectRequire = createRequire(`${projectRoot}/package.json`);
3961
- return Promise.all(
3962
- raw.map(async (entry) => {
3963
- const [spec, options] = Array.isArray(entry) ? entry : [entry];
3964
- const mod = await import(pathToFileURL(projectRequire.resolve(spec)).href);
3965
- const exported = mod.default ?? mod;
3966
- return typeof exported === "function" ? exported(options) : exported;
3967
- })
4713
+ const result = await spawnAndWaitForDaemon();
4714
+ if (result) {
4715
+ process.stdout.write(`Daemon started (pid ${result.pid})
4716
+ `);
4717
+ return 0;
4718
+ }
4719
+ process.stderr.write(`Daemon did not start within ${SPAWN_TIMEOUT_MS / 1e3}s
4720
+ `);
4721
+ return 1;
4722
+ }
4723
+ async function stopDaemon() {
4724
+ const stopped = await shutdownDaemon();
4725
+ process.stdout.write(stopped ? "Daemon stopped\n" : "No daemon was running\n");
4726
+ return 0;
4727
+ }
4728
+ async function statusDaemon() {
4729
+ const pong = await pingDaemon();
4730
+ if (pong?.type !== "pong") {
4731
+ process.stdout.write("No daemon running for this project\n");
4732
+ return 1;
4733
+ }
4734
+ const ageMin = Math.round((Date.now() - pong.startedAt) / 6e4);
4735
+ process.stdout.write(
4736
+ `Daemon running
4737
+ pid: ${pong.pid}
4738
+ cwd: ${pong.cwd}
4739
+ node: ${pong.nodeVersion}
4740
+ uptime: ${ageMin} min
4741
+ socket: ${daemonSocketPath(pong.cwd)}
4742
+ `
3968
4743
  );
4744
+ return 0;
3969
4745
  }
4746
+ var SPAWN_TIMEOUT_MS, highlight2, color2, USAGE, __filename, CLI_ENTRY;
4747
+ var init_daemon = __esm({
4748
+ "lib/commands/daemon/index.ts"() {
4749
+ init_color();
4750
+ init_daemon_socket_path();
4751
+ init_client();
4752
+ init_package();
4753
+ SPAWN_TIMEOUT_MS = 3e4;
4754
+ highlight2 = (text) => magenta().bold(text);
4755
+ color2 = (text) => blue(text);
4756
+ USAGE = `${highlight2(`[qunitx v${package_default.version}] Usage:`)} qunitx ${color2("daemon <subcommand>")}
4757
+
4758
+ ${highlight2("Subcommands:")}
4759
+ ${color2("$ qunitx daemon start")} # Spawn a persistent daemon for this project (~2\xD7 faster repeated runs)
4760
+ ${color2("$ qunitx daemon stop")} # Stop the running daemon
4761
+ ${color2("$ qunitx daemon status")} # Print pid, socket, and uptime
4762
+
4763
+ ${highlight2("Environment:")}
4764
+ ${color2("QUNITX_DAEMON=1")} : auto-spawn the daemon on the first qunitx run; reuse it on every run after (overrides the CI=1 bypass)
4765
+ ${color2("QUNITX_NO_DAEMON=1")} : never use the daemon for this run
4766
+
4767
+ ${highlight2("Tip:")} set ${color2("QUNITX_DAEMON=1")} to auto-spawn the daemon on the first qunitx run; ${color2("$ qunitx --help")} for top-level options.
4768
+ `;
4769
+ __filename = fileURLToPath2(import.meta.url);
4770
+ CLI_ENTRY = path13.resolve(path13.dirname(__filename), "..", "..", "..", "cli.ts");
4771
+ }
4772
+ });
3970
4773
 
3971
4774
  // cli.ts
4775
+ init_chrome_prelaunch();
4776
+ init_package();
4777
+ import process4 from "node:process";
3972
4778
  process4.title = "qunitx";
3973
4779
  (async () => {
3974
- if (!process4.argv[2]) {
3975
- return await displayHelpOutput();
3976
- } else if (["--version", "-v", "version"].includes(process4.argv[2])) {
4780
+ const cmd2 = process4.argv[2];
4781
+ if (!cmd2) {
4782
+ return await (await Promise.resolve().then(() => (init_help(), help_exports))).displayHelpOutput();
4783
+ } else if (["--version", "-v", "version"].includes(cmd2)) {
3977
4784
  return process4.stdout.write(package_default.version + "\n");
3978
- } else if (["help", "h", "p", "print"].includes(process4.argv[2])) {
3979
- return await displayHelpOutput();
3980
- } else if (["new", "n", "g", "generate"].includes(process4.argv[2])) {
3981
- return await generateTestFiles();
3982
- } else if (["init"].includes(process4.argv[2])) {
3983
- return await initializeProject();
3984
- }
3985
- const [config, { run: run2 }] = await Promise.all([setupConfig(), Promise.resolve().then(() => (init_run(), run_exports))]);
4785
+ } else if (["help", "h", "p", "print"].includes(cmd2)) {
4786
+ return await (await Promise.resolve().then(() => (init_help(), help_exports))).displayHelpOutput();
4787
+ } else if (["new", "n", "g", "generate"].includes(cmd2)) {
4788
+ return await (await Promise.resolve().then(() => (init_generate(), generate_exports))).generateTestFiles();
4789
+ } else if (cmd2 === "init") {
4790
+ return await (await Promise.resolve().then(() => (init_init(), init_exports))).initializeProject();
4791
+ } else if (cmd2 === "daemon") {
4792
+ const { runDaemonCommand: runDaemonCommand2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
4793
+ process4.exit(await runDaemonCommand2());
4794
+ }
4795
+ const { shouldUseDaemon: shouldUseDaemon2, shouldAutoSpawnDaemon: shouldAutoSpawnDaemon2, runViaDaemon: runViaDaemon2 } = await Promise.resolve().then(() => (init_client(), client_exports));
4796
+ let useDaemon = shouldUseDaemon2();
4797
+ if (!useDaemon && shouldAutoSpawnDaemon2()) {
4798
+ const { ensureDaemonRunning: ensureDaemonRunning2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
4799
+ useDaemon = await ensureDaemonRunning2();
4800
+ }
4801
+ if (useDaemon) {
4802
+ try {
4803
+ const exitCode = await runViaDaemon2(process4.argv.slice(2));
4804
+ process4.stdout.write("", () => process4.exit(exitCode));
4805
+ return;
4806
+ } catch {
4807
+ }
4808
+ }
4809
+ const [{ setupConfig: setupConfig2 }, { run: run2 }] = await Promise.all([
4810
+ Promise.resolve().then(() => (init_config(), config_exports)),
4811
+ Promise.resolve().then(() => (init_run(), run_exports))
4812
+ ]);
4813
+ const config = await setupConfig2();
3986
4814
  try {
3987
4815
  return await run2(config);
3988
4816
  } catch (error) {