qunitx-cli 0.22.2 → 0.23.0

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