qunitx-cli 0.21.2 → 0.21.3

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 (2) hide show
  1. package/dist/cli.js +651 -595
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -12,6 +12,9 @@ var __export = (target, all) => {
12
12
  // lib/utils/find-chrome.ts
13
13
  import { accessSync, constants } from "node:fs";
14
14
  import { join } from "node:path";
15
+ function findChrome() {
16
+ return Promise.resolve(findChromeSync());
17
+ }
15
18
  function findChromeSync() {
16
19
  if (process.env.CHROME_BIN) return process.env.CHROME_BIN;
17
20
  for (const dir of PATH_DIRS) {
@@ -26,9 +29,6 @@ function findChromeSync() {
26
29
  }
27
30
  return null;
28
31
  }
29
- function findChrome() {
30
- return Promise.resolve(findChromeSync());
31
- }
32
32
  var CANDIDATES, PATH_DIRS;
33
33
  var init_find_chrome = __esm({
34
34
  "lib/utils/find-chrome.ts"() {
@@ -56,35 +56,6 @@ var init_kill_process_group = __esm({
56
56
 
57
57
  // lib/utils/cleanup-browser-dir.ts
58
58
  import fs from "node:fs/promises";
59
- async function processReferencesDir(entry, dirPath, dirName) {
60
- try {
61
- const [cwd, cmdline] = await Promise.all([
62
- fs.readlink(`/proc/${entry}/cwd`).catch(() => ""),
63
- fs.readFile(`/proc/${entry}/cmdline`, "utf8").catch(() => "")
64
- ]);
65
- if (cwd.startsWith(dirPath) || cmdline.includes(dirName)) return true;
66
- const fds = await fs.readdir(`/proc/${entry}/fd`).catch(() => []);
67
- const fdTargets = await Promise.all(
68
- fds.map((fd) => fs.readlink(`/proc/${entry}/fd/${fd}`).catch(() => ""))
69
- );
70
- return fdTargets.some((target) => target.startsWith(dirPath));
71
- } catch {
72
- return false;
73
- }
74
- }
75
- async function killAllReferencingProcesses(dirPath, dirName) {
76
- const procEntries = await fs.readdir("/proc").catch(() => []);
77
- await Promise.all(
78
- procEntries.map(async (entry) => {
79
- if (!/^\d+$/.test(entry)) return;
80
- if (!await processReferencesDir(entry, dirPath, dirName)) return;
81
- try {
82
- process.kill(parseInt(entry), "SIGKILL");
83
- } catch {
84
- }
85
- })
86
- );
87
- }
88
59
  async function cleanupBrowserDir(dirPath) {
89
60
  if (process.platform !== "linux") {
90
61
  await fs.rm(dirPath, { recursive: true, force: true }).catch(() => {
@@ -123,6 +94,35 @@ async function cleanupBrowserDir(dirPath) {
123
94
  })
124
95
  );
125
96
  }
97
+ async function processReferencesDir(entry, dirPath, dirName) {
98
+ try {
99
+ const [cwd, cmdline] = await Promise.all([
100
+ fs.readlink(`/proc/${entry}/cwd`).catch(() => ""),
101
+ fs.readFile(`/proc/${entry}/cmdline`, "utf8").catch(() => "")
102
+ ]);
103
+ if (cwd.startsWith(dirPath) || cmdline.includes(dirName)) return true;
104
+ const fds = await fs.readdir(`/proc/${entry}/fd`).catch(() => []);
105
+ const fdTargets = await Promise.all(
106
+ fds.map((fd) => fs.readlink(`/proc/${entry}/fd/${fd}`).catch(() => ""))
107
+ );
108
+ return fdTargets.some((target) => target.startsWith(dirPath));
109
+ } catch {
110
+ return false;
111
+ }
112
+ }
113
+ async function killAllReferencingProcesses(dirPath, dirName) {
114
+ const procEntries = await fs.readdir("/proc").catch(() => []);
115
+ await Promise.all(
116
+ procEntries.map(async (entry) => {
117
+ if (!/^\d+$/.test(entry)) return;
118
+ if (!await processReferencesDir(entry, dirPath, dirName)) return;
119
+ try {
120
+ process.kill(parseInt(entry), "SIGKILL");
121
+ } catch {
122
+ }
123
+ })
124
+ );
125
+ }
126
126
  var CLEANUP_DEADLINE_MS, CLEANUP_RETRY_MS;
127
127
  var init_cleanup_browser_dir = __esm({
128
128
  "lib/utils/cleanup-browser-dir.ts"() {
@@ -447,6 +447,18 @@ var init_html = __esm({
447
447
  });
448
448
 
449
449
  // lib/tap/dump-yaml.ts
450
+ function dumpYaml({
451
+ name,
452
+ actual,
453
+ expected,
454
+ message,
455
+ stack,
456
+ source,
457
+ at
458
+ }) {
459
+ return `name: ${dumpString(name, "")}
460
+ ` + 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) : "");
461
+ }
450
462
  function needsQuoting(str) {
451
463
  return NEEDS_QUOTING.test(str);
452
464
  }
@@ -484,18 +496,6 @@ function yamlLine(key, value) {
484
496
  ` : `${key}: ${serialized}
485
497
  `;
486
498
  }
487
- function dumpYaml({
488
- name,
489
- actual,
490
- expected,
491
- message,
492
- stack,
493
- source,
494
- at
495
- }) {
496
- return `name: ${dumpString(name, "")}
497
- ` + 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) : "");
498
- }
499
499
  var NEEDS_QUOTING;
500
500
  var init_dump_yaml = __esm({
501
501
  "lib/tap/dump-yaml.ts"() {
@@ -518,43 +518,35 @@ var init_indent_string = __esm({
518
518
  });
519
519
 
520
520
  // lib/utils/source-map-decoder.ts
521
- function readVLQ(s, pos) {
522
- let accumulated = 0;
523
- let shift = 0;
524
- let digit;
525
- do {
526
- digit = BASE64_LOOKUP[s.charCodeAt(pos++)];
527
- accumulated |= (digit & 31) << shift;
528
- shift += 5;
529
- } while (digit & 32);
530
- return [accumulated & 1 ? -(accumulated >>> 1) : accumulated >>> 1, pos];
531
- }
532
521
  function decodeMappings(mappings) {
533
- let sourceIndex = 0, sourceLine = 0, sourceCol = 0;
534
- return mappings.split(";").map((lineStr) => {
535
- const segments = [];
536
- let generatedCol = 0;
537
- let pos = 0;
538
- while (pos < lineStr.length) {
539
- if (lineStr[pos] === ",") {
540
- pos++;
541
- continue;
522
+ const result = [];
523
+ const cursor = { position: 0 };
524
+ const mappingsLength = mappings.length;
525
+ let segments = [];
526
+ let generatedCol = 0, sourceIndex = 0, sourceLine = 0, sourceCol = 0;
527
+ for (; ; ) {
528
+ if (cursor.position >= mappingsLength) break;
529
+ const charCode = mappings.charCodeAt(cursor.position);
530
+ if (charCode !== COMMA && charCode !== SEMICOLON) {
531
+ generatedCol += readVlqAt(mappings, cursor);
532
+ if (atFieldStart(mappings, cursor.position)) {
533
+ sourceIndex += readVlqAt(mappings, cursor);
534
+ sourceLine += readVlqAt(mappings, cursor);
535
+ sourceCol += readVlqAt(mappings, cursor);
536
+ segments.push({ generatedCol, sourceIndex, sourceLine, sourceCol });
537
+ if (atFieldStart(mappings, cursor.position)) readVlqAt(mappings, cursor);
542
538
  }
543
- let delta;
544
- [delta, pos] = readVLQ(lineStr, pos);
545
- generatedCol += delta;
546
- if (pos >= lineStr.length || lineStr[pos] === ",") continue;
547
- [delta, pos] = readVLQ(lineStr, pos);
548
- sourceIndex += delta;
549
- [delta, pos] = readVLQ(lineStr, pos);
550
- sourceLine += delta;
551
- [delta, pos] = readVLQ(lineStr, pos);
552
- sourceCol += delta;
553
- if (pos < lineStr.length && lineStr[pos] !== ",") [, pos] = readVLQ(lineStr, pos);
554
- segments.push({ generatedCol, sourceIndex, sourceLine, sourceCol });
555
- }
556
- return segments;
557
- });
539
+ } else if (charCode === COMMA) {
540
+ cursor.position++;
541
+ } else {
542
+ result.push(segments);
543
+ segments = [];
544
+ generatedCol = 0;
545
+ cursor.position++;
546
+ }
547
+ }
548
+ result.push(segments);
549
+ return result;
558
550
  }
559
551
  function parseSourceMap(json, outDir) {
560
552
  const map = JSON.parse(json);
@@ -566,151 +558,185 @@ function parseSourceMap(json, outDir) {
566
558
  sourcesContent: map.sourcesContent ?? []
567
559
  };
568
560
  }
569
- function base64DecodeUtf8(b64) {
570
- const binary = atob(b64);
571
- return UTF8.decode(Uint8Array.from(binary, (c) => c.charCodeAt(0)));
572
- }
573
561
  function extractInlineSourceMap(bundle, outDir) {
574
562
  if (!bundle) return null;
575
- const text = typeof bundle === "string" ? bundle : UTF8.decode(bundle);
576
- const match = text.match(
577
- /\/\/# sourceMappingURL=data:application\/json;base64,([A-Za-z0-9+/=]+)/
578
- );
579
- if (!match) return null;
563
+ const base64Payload = readMarkerPayload(bundle);
564
+ if (!base64Payload) return null;
580
565
  try {
581
- return parseSourceMap(base64DecodeUtf8(match[1]), outDir);
566
+ return parseSourceMap(decodeBase64Utf8(base64Payload), outDir);
582
567
  } catch {
583
568
  return null;
584
569
  }
585
570
  }
586
- function normalizePosix(p) {
587
- const abs = p.startsWith("/");
588
- const parts = p.split("/");
589
- const out = [];
590
- for (const part of parts) {
591
- if (part === "..") out.pop();
592
- else if (part !== "" && part !== ".") out.push(part);
593
- }
594
- return (abs ? "/" : "") + out.join("/");
595
- }
596
- function posixResolve(base, relative) {
597
- if (relative.startsWith("/")) return normalizePosix(relative);
598
- return normalizePosix(base + "/" + relative);
599
- }
600
- function toAbsolutePath(raw, outDir, sourceRoot) {
601
- if (raw.startsWith("file://")) return raw.slice(7);
602
- if (raw.startsWith("/")) return raw;
603
- const base = sourceRoot ? normalizePosix(outDir + "/" + sourceRoot) : outDir;
604
- return posixResolve(base, raw);
605
- }
606
571
  function lookupPosition(decoder, generatedLine, generatedCol) {
607
572
  const segments = decoder.segmentsByLine[generatedLine - 1];
608
573
  if (!segments?.length) return null;
609
- const col0 = generatedCol - 1;
610
- let lo = 0, hi = segments.length - 1, best = -1;
611
- while (lo <= hi) {
612
- const mid = lo + hi >>> 1;
613
- if (segments[mid].generatedCol <= col0) {
614
- best = mid;
615
- lo = mid + 1;
616
- } else {
617
- hi = mid - 1;
618
- }
619
- }
620
- if (best === -1) return null;
621
- const { sourceIndex, sourceLine, sourceCol } = segments[best];
622
- const rawSource = decoder.sources[sourceIndex];
574
+ const targetCol = generatedCol - 1;
575
+ const segment = segments.findLast((s) => s.generatedCol <= targetCol);
576
+ if (!segment) return null;
577
+ const rawSource = decoder.sources[segment.sourceIndex];
623
578
  if (!rawSource) return null;
624
- const content = decoder.sourcesContent[sourceIndex];
625
- const sourceText = content ? content.split("\n", sourceLine + 1)[sourceLine]?.trim() || null : null;
626
579
  return {
627
580
  absolutePath: toAbsolutePath(rawSource, decoder.outDir, decoder.sourceRoot),
628
- line: sourceLine + 1,
629
- // 0-based 1-based
630
- col: sourceCol + 1,
631
- // 0-based → 1-based
632
- sourceText
581
+ line: segment.sourceLine + 1,
582
+ col: segment.sourceCol + 1,
583
+ sourceText: extractSourceLine(decoder.sourcesContent[segment.sourceIndex], segment.sourceLine)
633
584
  };
634
585
  }
635
- function parseFrameLocation(s) {
636
- const colSep = s.lastIndexOf(":");
637
- if (colSep < 0) return null;
638
- const colStr = s.slice(colSep + 1);
639
- if (!/^\d+$/.test(colStr)) return null;
640
- const lineSep = s.lastIndexOf(":", colSep - 1);
641
- if (lineSep < 0) return null;
642
- const lineStr = s.slice(lineSep + 1, colSep);
643
- if (!/^\d+$/.test(lineStr)) return null;
644
- return { url: s.slice(0, lineSep), line: +lineStr, col: +colStr };
586
+ function parseFrameLocation(text) {
587
+ const match = FRAME_LOCATION_RE.exec(text);
588
+ return match ? { url: match[1], line: +match[2], col: +match[3] } : null;
645
589
  }
646
590
  function isBundleUrl(url) {
647
- const normalized = url.startsWith("async ") ? url.slice(6) : url;
648
- return /^https?:\/\//.test(normalized) && /\/(tests|filtered-tests)\.js$/.test(normalized);
591
+ return BUNDLE_URL_RE.test(url);
592
+ }
593
+ function resolveFrame(frame, decoder, projectRoot) {
594
+ const hit = FRAME_FORMATS.map((format) => ({ format, match: frame.match(format.pattern) })).find(
595
+ ({ match }) => match !== null
596
+ );
597
+ if (!hit?.match) return null;
598
+ const original = tryResolve(hit.match[hit.format.urlGroup], decoder, projectRoot);
599
+ return original && {
600
+ resolved: hit.format.format(hit.match, original.display),
601
+ userPath: original.userPath,
602
+ sourceText: original.sourceText
603
+ };
604
+ }
605
+ function resolveStack(stack, decoder, projectRoot) {
606
+ const decoratedFrames = stack.split("\n").map((frame) => {
607
+ const resolution = resolveFrame(frame, decoder, projectRoot);
608
+ return {
609
+ line: resolution?.resolved ?? frame,
610
+ userPath: resolution?.userPath ?? null,
611
+ sourceText: resolution?.sourceText ?? null
612
+ };
613
+ });
614
+ const firstUser = decoratedFrames.find((entry) => entry.userPath !== null);
615
+ return {
616
+ resolvedStack: decoratedFrames.map((entry) => entry.line).join("\n"),
617
+ firstUserFrame: firstUser?.userPath ?? null,
618
+ firstUserSourceText: firstUser?.sourceText ?? null
619
+ };
620
+ }
621
+ function readVlqAt(text, cursor) {
622
+ let value = 0, bitShift = 0, position = cursor.position;
623
+ for (; ; ) {
624
+ const digit = BASE64_LOOKUP[text.charCodeAt(position++)];
625
+ value |= (digit & VLQ_DATA_MASK) << bitShift;
626
+ if (!(digit & VLQ_CONTINUATION)) break;
627
+ bitShift += 5;
628
+ }
629
+ cursor.position = position;
630
+ return value & 1 ? -(value >>> 1) : value >>> 1;
631
+ }
632
+ function atFieldStart(text, position) {
633
+ if (position >= text.length) return false;
634
+ const charCode = text.charCodeAt(position);
635
+ return charCode !== COMMA && charCode !== SEMICOLON;
636
+ }
637
+ function decodeBase64Utf8(base64) {
638
+ if (Buffer2) return Buffer2.from(base64, "base64").toString("utf8");
639
+ return UTF8_DECODER.decode(Uint8Array.from(atob(base64), (char) => char.charCodeAt(0)));
640
+ }
641
+ function readMarkerPayload(bundle) {
642
+ if (typeof bundle === "string") return sliceMarkerFromString(bundle);
643
+ if (Buffer2) return sliceMarkerFromBuffer(asBuffer(bundle, Buffer2));
644
+ return sliceMarkerFromString(UTF8_DECODER.decode(bundle));
645
+ }
646
+ function asBuffer(view, BufferCtor) {
647
+ return BufferCtor.isBuffer(view) ? view : BufferCtor.from(view.buffer, view.byteOffset, view.byteLength);
648
+ }
649
+ function sliceMarkerFromString(text) {
650
+ const markerStart = text.lastIndexOf(SOURCE_MAP_MARKER);
651
+ if (markerStart < 0) return null;
652
+ const payloadStart = markerStart + SOURCE_MAP_MARKER.length;
653
+ const payloadEnd = text.indexOf("\n", payloadStart);
654
+ return (payloadEnd < 0 ? text.slice(payloadStart) : text.slice(payloadStart, payloadEnd)).trim();
655
+ }
656
+ function sliceMarkerFromBuffer(buffer) {
657
+ const markerBytes = SOURCE_MAP_MARKER_BYTES;
658
+ const markerStart = buffer.lastIndexOf(markerBytes);
659
+ if (markerStart < 0) return null;
660
+ const payloadStart = markerStart + markerBytes.length;
661
+ const newlineIndex = buffer.indexOf(NEWLINE, payloadStart);
662
+ const payloadEnd = newlineIndex < 0 ? buffer.length : newlineIndex;
663
+ return buffer.toString("latin1", payloadStart, payloadEnd).trim();
664
+ }
665
+ function extractSourceLine(content, lineIndex) {
666
+ if (!content || lineIndex < 0) return null;
667
+ const line = content.split("\n", lineIndex + 1)[lineIndex];
668
+ return line?.trim() || null;
669
+ }
670
+ function normalizePosix(path10) {
671
+ const parts = path10.split("/").reduce((acc, part) => {
672
+ if (part === "..") acc.pop();
673
+ else if (part && part !== ".") acc.push(part);
674
+ return acc;
675
+ }, []);
676
+ return (path10.startsWith("/") ? "/" : "") + parts.join("/");
677
+ }
678
+ function toAbsolutePath(rawSource, outDir, sourceRoot) {
679
+ if (rawSource.startsWith("file://")) return rawSource.slice(7);
680
+ if (rawSource.startsWith("/")) return rawSource;
681
+ const base = sourceRoot ? normalizePosix(`${outDir}/${sourceRoot}`) : outDir;
682
+ return normalizePosix(`${base}/${rawSource}`);
649
683
  }
650
- function isNodeModulesPath(absolutePath) {
651
- return absolutePath.includes("/node_modules/") || absolutePath.includes("\\node_modules\\");
684
+ function isNodeModulesPath(path10) {
685
+ return path10.includes("/node_modules/") || path10.includes("\\node_modules\\");
652
686
  }
653
687
  function makeDisplayPath(absolutePath, projectRoot) {
654
688
  const prefix = projectRoot + "/";
655
689
  return absolutePath.startsWith(prefix) ? absolutePath.slice(prefix.length) : absolutePath;
656
690
  }
657
691
  function tryResolve(urlLineCol, decoder, projectRoot) {
658
- const loc = parseFrameLocation(urlLineCol);
659
- if (!loc || !isBundleUrl(loc.url)) return null;
660
- const orig = lookupPosition(decoder, loc.line, loc.col);
661
- if (!orig) return null;
662
- const display = `${makeDisplayPath(orig.absolutePath, projectRoot)}:${orig.line}:${orig.col}`;
692
+ const location = parseFrameLocation(urlLineCol);
693
+ if (!location || !isBundleUrl(location.url)) return null;
694
+ const original = lookupPosition(decoder, location.line, location.col);
695
+ if (!original) return null;
696
+ const display = `${makeDisplayPath(original.absolutePath, projectRoot)}:${original.line}:${original.col}`;
663
697
  return {
664
698
  display,
665
- userPath: isNodeModulesPath(orig.absolutePath) ? null : display,
666
- sourceText: orig.sourceText
699
+ userPath: isNodeModulesPath(original.absolutePath) ? null : display,
700
+ sourceText: original.sourceText
667
701
  };
668
702
  }
669
- function resolveFrame(frame, decoder, projectRoot) {
670
- const chromeName = frame.match(/^(\s*at\s+)(.*?)\s+\(([^)]+)\)\s*$/);
671
- if (chromeName) {
672
- const r = tryResolve(chromeName[3], decoder, projectRoot);
673
- return r ? {
674
- resolved: `${chromeName[1]}${chromeName[2]} (${r.display})`,
675
- userPath: r.userPath,
676
- sourceText: r.sourceText
677
- } : null;
678
- }
679
- const chromeAnon = frame.match(/^(\s*at\s+(?:async\s+)?)(.+)/);
680
- if (chromeAnon) {
681
- const r = tryResolve(chromeAnon[2], decoder, projectRoot);
682
- return r ? { resolved: `${chromeAnon[1]}${r.display}`, userPath: r.userPath, sourceText: r.sourceText } : null;
683
- }
684
- const gecko = frame.match(/^([^@]*)@(.+)$/);
685
- if (gecko) {
686
- const r = tryResolve(gecko[2], decoder, projectRoot);
687
- return r ? { resolved: `${gecko[1]}@${r.display}`, userPath: r.userPath, sourceText: r.sourceText } : null;
688
- }
689
- return null;
690
- }
691
- function resolveStack(stack, decoder, projectRoot) {
692
- let firstUserFrame = null;
693
- let firstUserSourceText = null;
694
- const resolvedLines = stack.split("\n").map((frame) => {
695
- const result = resolveFrame(frame, decoder, projectRoot);
696
- if (!result) return frame;
697
- if (!firstUserFrame && result.userPath) {
698
- firstUserFrame = result.userPath;
699
- firstUserSourceText = result.sourceText;
700
- }
701
- return result.resolved;
702
- });
703
- return { resolvedStack: resolvedLines.join("\n"), firstUserFrame, firstUserSourceText };
704
- }
705
- var BASE64, BASE64_LOOKUP, UTF8;
703
+ var BASE64_ALPHABET, BASE64_LOOKUP, COMMA, SEMICOLON, NEWLINE, VLQ_CONTINUATION, VLQ_DATA_MASK, SOURCE_MAP_MARKER, FRAME_LOCATION_RE, BUNDLE_URL_RE, FRAME_FORMATS, Buffer2, SOURCE_MAP_MARKER_BYTES, UTF8_DECODER;
706
704
  var init_source_map_decoder = __esm({
707
705
  "lib/utils/source-map-decoder.ts"() {
708
- BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
706
+ BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
709
707
  BASE64_LOOKUP = new Uint8Array(128);
710
- [...BASE64].forEach((ch, i) => {
711
- BASE64_LOOKUP[ch.charCodeAt(0)] = i;
712
- });
713
- UTF8 = new TextDecoder();
708
+ [...BASE64_ALPHABET].forEach((char, index) => BASE64_LOOKUP[char.charCodeAt(0)] = index);
709
+ COMMA = 44;
710
+ SEMICOLON = 59;
711
+ NEWLINE = 10;
712
+ VLQ_CONTINUATION = 32;
713
+ VLQ_DATA_MASK = 31;
714
+ SOURCE_MAP_MARKER = "//# sourceMappingURL=data:application/json;base64,";
715
+ FRAME_LOCATION_RE = /^(.+):(\d+):(\d+)$/;
716
+ BUNDLE_URL_RE = /^(?:async )?https?:\/\/.*\/(?:tests|filtered-tests)\.js$/;
717
+ FRAME_FORMATS = [
718
+ // Chrome named: " at FUNC (URL:LINE:COL)"
719
+ {
720
+ pattern: /^(\s*at\s+)(.*?)\s+\(([^)]+)\)\s*$/,
721
+ urlGroup: 3,
722
+ format: (match, displayPath) => `${match[1]}${match[2]} (${displayPath})`
723
+ },
724
+ // Chrome anonymous (incl. async): " at [async] URL:LINE:COL"
725
+ {
726
+ pattern: /^(\s*at\s+(?:async\s+)?)(.+)/,
727
+ urlGroup: 2,
728
+ format: (match, displayPath) => `${match[1]}${displayPath}`
729
+ },
730
+ // Firefox / WebKit: "FUNC@URL:LINE:COL"
731
+ {
732
+ pattern: /^([^@]*)@(.+)$/,
733
+ urlGroup: 2,
734
+ format: (match, displayPath) => `${match[1]}@${displayPath}`
735
+ }
736
+ ];
737
+ Buffer2 = globalThis.Buffer;
738
+ SOURCE_MAP_MARKER_BYTES = Buffer2 ? Buffer2.from(SOURCE_MAP_MARKER, "utf8") : null;
739
+ UTF8_DECODER = new TextDecoder();
714
740
  }
715
741
  });
716
742
 
@@ -836,12 +862,12 @@ var init_bind_server_to_port = __esm({
836
862
  }
837
863
  });
838
864
 
839
- // lib/servers/http.ts
865
+ // lib/servers/web.ts
840
866
  import http from "node:http";
841
867
  import WebSocket, { WebSocketServer } from "ws";
842
868
  var MIME_TYPES, HTTPServer;
843
- var init_http = __esm({
844
- "lib/servers/http.ts"() {
869
+ var init_web = __esm({
870
+ "lib/servers/web.ts"() {
845
871
  init_bind_server_to_port();
846
872
  MIME_TYPES = {
847
873
  html: "text/html; charset=UTF-8",
@@ -1298,166 +1324,18 @@ function setupWebServer(config, cachedContent) {
1298
1324
  });
1299
1325
  return server;
1300
1326
  }
1301
- function replaceAssetPaths(html, htmlPath, projectRoot) {
1302
- const assetPaths = findInternalAssetsFromHTML(html);
1303
- const htmlDirectory = htmlPath.split("/").slice(0, -1).join("/");
1304
- return assetPaths.reduce((result, assetPath) => {
1305
- const normalizedFullAbsolutePath = path5.normalize(`${htmlDirectory}/${assetPath}`);
1306
- return result.replace(assetPath, normalizedFullAbsolutePath.replace(projectRoot, "."));
1307
- }, html);
1308
- }
1309
- function testRuntimeToInject(config, groupId) {
1310
- const groupIdPart = groupId !== void 0 ? `, groupId: ${groupId}` : "";
1311
- return `<script>
1312
- (function() {
1313
- // setupQUnit runs exactly once, after both the WebSocket is open and tests.js has loaded.
1314
- // Promise.all is naturally idempotent \u2014 resolving a Promise a second time is a no-op,
1315
- // so WebKit firing WS error after open (causing a retry that re-opens) cannot double-start.
1316
- let resolveWsReady = () => {};
1317
- const wsReadyPromise = window.location.protocol === 'file:'
1318
- ? Promise.resolve()
1319
- : new Promise(resolve => { resolveWsReady = resolve; });
1320
-
1321
- // { once: true } auto-removes the listener after the first fire.
1322
- const testsReadyPromise = new Promise(resolve => {
1323
- window.addEventListener('qunitx:tests-ready', resolve, { once: true });
1324
- });
1325
-
1326
- Promise.all([wsReadyPromise, testsReadyPromise]).then(setupQUnit);
1327
-
1328
- // For static files (file:// protocol) there is no WebSocket server; wsReadyPromise
1329
- // is already resolved above, so setupQUnit fires as soon as tests load.
1330
- if (window.location.protocol === 'file:') return;
1331
-
1332
- let wsRetryCount = 0;
1333
- const WS_MAX_RETRIES = Math.ceil(${config.timeout} / ${WS_RETRY_INTERVAL_MS}); // retry for the full test timeout window
1334
-
1335
- function setupWebSocket() {
1336
- try {
1337
- window.socket = new WebSocket(\`ws://localhost:\${location.port}\`);
1338
- } catch (error) {
1339
- console.log(error);
1340
- retryOrFail();
1341
- return;
1342
- }
1343
-
1344
- window.socket.addEventListener('open', function() {
1345
- resolveWsReady();
1346
- // Notify Node.js that the WS socket is open. This fires immediately (< 1 s) because
1347
- // this runtime script is tiny \u2014 tests.js background compilation hasn't finished yet.
1348
- // Node.js uses this to distinguish "WS never connected" from "WS connected but bundle slow".
1349
- if (navigator.webdriver) {
1350
- window.socket.send(JSON.stringify({ event: 'wsOpen'${groupIdPart} }));
1351
- }
1352
- });
1353
- window.socket.addEventListener('error', function() {
1354
- retryOrFail();
1355
- });
1356
- window.socket.addEventListener('message', function(messageEvent) {
1357
- if (!navigator.webdriver && messageEvent.data === 'refresh') {
1358
- window.location.reload(true);
1359
- } else if (navigator.webdriver && messageEvent.data === 'abort') {
1360
- window.abortQUnit = true;
1361
- window.QUnit.config.queue.length = 0;
1362
- window.socket.send(JSON.stringify({ event: 'abort' }));
1363
- }
1364
- });
1365
- }
1366
-
1367
- function retryOrFail() {
1368
- wsRetryCount++;
1369
- if (wsRetryCount > WS_MAX_RETRIES) {
1370
- console.log('WebSocket connection failed after ' + WS_MAX_RETRIES + ' retries');
1371
- return;
1372
- }
1373
- window.setTimeout(setupWebSocket, ${WS_RETRY_INTERVAL_MS});
1374
- }
1375
-
1376
- setupWebSocket();
1377
- })();
1378
-
1379
- function getCircularReplacer() {
1380
- const ancestors = [];
1381
- return function (key, value) {
1382
- if (typeof value !== "object" || value === null) {
1383
- return value;
1384
- }
1385
- while (ancestors.length > 0 && ancestors.at(-1) !== this) {
1386
- ancestors.pop();
1387
- }
1388
- if (ancestors.includes(value)) {
1389
- return "[Circular]";
1390
- }
1391
- ancestors.push(value);
1392
- return value;
1393
- };
1394
- }
1395
-
1396
- function setupQUnit() {
1397
- window.QUNIT_RESULT = { totalTests: 0, finishedTests: 0, failedTests: 0, currentTest: '' };
1398
-
1399
- if (!window.QUnit) {
1400
- console.log('QUnit not found after WebSocket connected');
1401
- if (navigator.webdriver) {
1402
- // Signal the Playwright runner that the run is complete with 0 tests rather than
1403
- // waiting for the inactivity timeout. The runner treats totalTests === 0 as a
1404
- // "no tests registered" warning (not a failure), so this gives a fast, clean result.
1405
- window.QUNIT_RESULT = { totalTests: 0, finishedTests: 0, failedTests: 0, currentTest: null };
1406
- window.socket.send(JSON.stringify({ event: 'done', details: { passed: 0, failed: 0, runtime: 0 } }));
1407
- }
1408
- return;
1409
- }
1410
-
1411
- window.QUnit.begin(() => { // NOTE: might be useful in future for hanged module tracking
1412
- if (navigator.webdriver) {
1413
- window.socket.send(JSON.stringify({ event: 'connection' }));
1414
- }
1415
- });
1416
- window.QUnit.on('testStart', (details) => {
1417
- window.QUNIT_RESULT.totalTests++;
1418
- window.QUNIT_RESULT.currentTest = details.fullName.join(' | ');
1419
- });
1420
- window.QUnit.on('testEnd', (details) => { // NOTE: https://github.com/qunitjs/qunit/blob/master/src/html-reporter/diff.js
1421
- window.QUNIT_RESULT.finishedTests++;
1422
- if (details.status === 'failed') window.QUNIT_RESULT.failedTests++;
1423
- window.QUNIT_RESULT.currentTest = null;
1424
- if (navigator.webdriver) {
1425
- const isFailed = details.status === 'failed';
1426
- const payload = isFailed ? details : { status: details.status, fullName: details.fullName, runtime: details.runtime };
1427
- window.socket.send(JSON.stringify({ event: 'testEnd', details: payload, abort: window.abortQUnit }, isFailed ? getCircularReplacer() : undefined));
1428
-
1429
- if (${config.failFast} && details.status === 'failed') {
1430
- window.QUnit.config.queue.length = 0;
1431
- }
1432
- }
1433
- });
1434
- window.QUnit.done((details) => {
1435
- if (navigator.webdriver) {
1436
- window.socket.send(JSON.stringify({ event: 'done', details: details, qunitResult: window.QUNIT_RESULT, abort: window.abortQUnit }, getCircularReplacer()));
1437
- }
1438
- });
1439
-
1440
- window.QUnit.config.testTimeout = ${config.timeout};
1441
- window.QUnit.start();
1442
- }
1443
- </script>`;
1444
- }
1445
- function escapeAndInjectTestsToHTML(html, testRuntimeCode, testBundleUrl) {
1446
- return injectScript(html, `${testRuntimeCode}
1447
- <script src="${testBundleUrl}" async></script>`);
1448
- }
1449
- function buildNoTestsHTML(files) {
1450
- const escaped = files.map((f) => f.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")).join("\n");
1451
- return `<!DOCTYPE html>
1452
- <html lang="en">
1453
- <head>
1454
- <meta charset="utf-8">
1455
- <meta name="viewport" content="width=device-width">
1456
- <title>No Tests Registered \u2014 qunitx</title>
1457
- <style>
1458
- * { box-sizing: border-box; margin: 0; padding: 0; }
1459
- #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-tests, #qunit-tests li {
1460
- font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
1327
+ function buildNoTestsHTML(files) {
1328
+ const escaped = files.map((f) => f.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")).join("\n");
1329
+ return `<!DOCTYPE html>
1330
+ <html lang="en">
1331
+ <head>
1332
+ <meta charset="utf-8">
1333
+ <meta name="viewport" content="width=device-width">
1334
+ <title>No Tests Registered \u2014 qunitx</title>
1335
+ <style>
1336
+ * { box-sizing: border-box; margin: 0; padding: 0; }
1337
+ #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-tests, #qunit-tests li {
1338
+ font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
1461
1339
  }
1462
1340
  #qunit-header {
1463
1341
  padding: 0.5em 0 0.5em 1em;
@@ -1700,16 +1578,6 @@ function registerGroupRoutes(server, groupConfig, groupCachedContent, groupId) {
1700
1578
  res.end(groupCachedContent.allTestCode);
1701
1579
  });
1702
1580
  }
1703
- function debugGroupHeader(config) {
1704
- const files = Object.keys(config.fsTree);
1705
- const rel = files.map((f) => f.replace(`${config.projectRoot}/`, ""));
1706
- const shown = rel.slice(0, 2);
1707
- const rest = rel.length - shown.length;
1708
- process.stdout.write(
1709
- `# ${blue(`\u2500\u2500 ${shown.join(" ")}${rest > 0 ? ` +${rest} more` : ""} \u2500\u2500`)}
1710
- `
1711
- );
1712
- }
1713
1581
  function setupGroupWSHandler(server, groupConfigs) {
1714
1582
  const socketToGroupId = /* @__PURE__ */ new WeakMap();
1715
1583
  server.wss.on("connection", function connection(socket) {
@@ -1790,6 +1658,164 @@ function registerSharedStaticHandler(server, groupConfigs) {
1790
1658
  });
1791
1659
  });
1792
1660
  }
1661
+ function replaceAssetPaths(html, htmlPath, projectRoot) {
1662
+ const assetPaths = findInternalAssetsFromHTML(html);
1663
+ const htmlDirectory = htmlPath.split("/").slice(0, -1).join("/");
1664
+ return assetPaths.reduce((result, assetPath) => {
1665
+ const normalizedFullAbsolutePath = path5.normalize(`${htmlDirectory}/${assetPath}`);
1666
+ return result.replace(assetPath, normalizedFullAbsolutePath.replace(projectRoot, "."));
1667
+ }, html);
1668
+ }
1669
+ function testRuntimeToInject(config, groupId) {
1670
+ const groupIdPart = groupId !== void 0 ? `, groupId: ${groupId}` : "";
1671
+ return `<script>
1672
+ (function() {
1673
+ // setupQUnit runs exactly once, after both the WebSocket is open and tests.js has loaded.
1674
+ // Promise.all is naturally idempotent \u2014 resolving a Promise a second time is a no-op,
1675
+ // so WebKit firing WS error after open (causing a retry that re-opens) cannot double-start.
1676
+ let resolveWsReady = () => {};
1677
+ const wsReadyPromise = window.location.protocol === 'file:'
1678
+ ? Promise.resolve()
1679
+ : new Promise(resolve => { resolveWsReady = resolve; });
1680
+
1681
+ // { once: true } auto-removes the listener after the first fire.
1682
+ const testsReadyPromise = new Promise(resolve => {
1683
+ window.addEventListener('qunitx:tests-ready', resolve, { once: true });
1684
+ });
1685
+
1686
+ Promise.all([wsReadyPromise, testsReadyPromise]).then(setupQUnit);
1687
+
1688
+ // For static files (file:// protocol) there is no WebSocket server; wsReadyPromise
1689
+ // is already resolved above, so setupQUnit fires as soon as tests load.
1690
+ if (window.location.protocol === 'file:') return;
1691
+
1692
+ let wsRetryCount = 0;
1693
+ const WS_MAX_RETRIES = Math.ceil(${config.timeout} / ${WS_RETRY_INTERVAL_MS}); // retry for the full test timeout window
1694
+
1695
+ function setupWebSocket() {
1696
+ try {
1697
+ window.socket = new WebSocket(\`ws://localhost:\${location.port}\`);
1698
+ } catch (error) {
1699
+ console.log(error);
1700
+ retryOrFail();
1701
+ return;
1702
+ }
1703
+
1704
+ window.socket.addEventListener('open', function() {
1705
+ resolveWsReady();
1706
+ // Notify Node.js that the WS socket is open. This fires immediately (< 1 s) because
1707
+ // this runtime script is tiny \u2014 tests.js background compilation hasn't finished yet.
1708
+ // Node.js uses this to distinguish "WS never connected" from "WS connected but bundle slow".
1709
+ if (navigator.webdriver) {
1710
+ window.socket.send(JSON.stringify({ event: 'wsOpen'${groupIdPart} }));
1711
+ }
1712
+ });
1713
+ window.socket.addEventListener('error', function() {
1714
+ retryOrFail();
1715
+ });
1716
+ window.socket.addEventListener('message', function(messageEvent) {
1717
+ if (!navigator.webdriver && messageEvent.data === 'refresh') {
1718
+ window.location.reload(true);
1719
+ } else if (navigator.webdriver && messageEvent.data === 'abort') {
1720
+ window.abortQUnit = true;
1721
+ window.QUnit.config.queue.length = 0;
1722
+ window.socket.send(JSON.stringify({ event: 'abort' }));
1723
+ }
1724
+ });
1725
+ }
1726
+
1727
+ function retryOrFail() {
1728
+ wsRetryCount++;
1729
+ if (wsRetryCount > WS_MAX_RETRIES) {
1730
+ console.log('WebSocket connection failed after ' + WS_MAX_RETRIES + ' retries');
1731
+ return;
1732
+ }
1733
+ window.setTimeout(setupWebSocket, ${WS_RETRY_INTERVAL_MS});
1734
+ }
1735
+
1736
+ setupWebSocket();
1737
+ })();
1738
+
1739
+ function getCircularReplacer() {
1740
+ const ancestors = [];
1741
+ return function (key, value) {
1742
+ if (typeof value !== "object" || value === null) {
1743
+ return value;
1744
+ }
1745
+ while (ancestors.length > 0 && ancestors.at(-1) !== this) {
1746
+ ancestors.pop();
1747
+ }
1748
+ if (ancestors.includes(value)) {
1749
+ return "[Circular]";
1750
+ }
1751
+ ancestors.push(value);
1752
+ return value;
1753
+ };
1754
+ }
1755
+
1756
+ function setupQUnit() {
1757
+ window.QUNIT_RESULT = { totalTests: 0, finishedTests: 0, failedTests: 0, currentTest: '' };
1758
+
1759
+ if (!window.QUnit) {
1760
+ console.log('QUnit not found after WebSocket connected');
1761
+ if (navigator.webdriver) {
1762
+ // Signal the Playwright runner that the run is complete with 0 tests rather than
1763
+ // waiting for the inactivity timeout. The runner treats totalTests === 0 as a
1764
+ // "no tests registered" warning (not a failure), so this gives a fast, clean result.
1765
+ window.QUNIT_RESULT = { totalTests: 0, finishedTests: 0, failedTests: 0, currentTest: null };
1766
+ window.socket.send(JSON.stringify({ event: 'done', details: { passed: 0, failed: 0, runtime: 0 } }));
1767
+ }
1768
+ return;
1769
+ }
1770
+
1771
+ window.QUnit.begin(() => { // NOTE: might be useful in future for hanged module tracking
1772
+ if (navigator.webdriver) {
1773
+ window.socket.send(JSON.stringify({ event: 'connection' }));
1774
+ }
1775
+ });
1776
+ window.QUnit.on('testStart', (details) => {
1777
+ window.QUNIT_RESULT.totalTests++;
1778
+ window.QUNIT_RESULT.currentTest = details.fullName.join(' | ');
1779
+ });
1780
+ window.QUnit.on('testEnd', (details) => { // NOTE: https://github.com/qunitjs/qunit/blob/master/src/html-reporter/diff.js
1781
+ window.QUNIT_RESULT.finishedTests++;
1782
+ if (details.status === 'failed') window.QUNIT_RESULT.failedTests++;
1783
+ window.QUNIT_RESULT.currentTest = null;
1784
+ if (navigator.webdriver) {
1785
+ const isFailed = details.status === 'failed';
1786
+ const payload = isFailed ? details : { status: details.status, fullName: details.fullName, runtime: details.runtime };
1787
+ window.socket.send(JSON.stringify({ event: 'testEnd', details: payload, abort: window.abortQUnit }, isFailed ? getCircularReplacer() : undefined));
1788
+
1789
+ if (${config.failFast} && details.status === 'failed') {
1790
+ window.QUnit.config.queue.length = 0;
1791
+ }
1792
+ }
1793
+ });
1794
+ window.QUnit.done((details) => {
1795
+ if (navigator.webdriver) {
1796
+ window.socket.send(JSON.stringify({ event: 'done', details: details, qunitResult: window.QUNIT_RESULT, abort: window.abortQUnit }, getCircularReplacer()));
1797
+ }
1798
+ });
1799
+
1800
+ window.QUnit.config.testTimeout = ${config.timeout};
1801
+ window.QUnit.start();
1802
+ }
1803
+ </script>`;
1804
+ }
1805
+ function escapeAndInjectTestsToHTML(html, testRuntimeCode, testBundleUrl) {
1806
+ return injectScript(html, `${testRuntimeCode}
1807
+ <script src="${testBundleUrl}" async></script>`);
1808
+ }
1809
+ function debugGroupHeader(config) {
1810
+ const files = Object.keys(config.fsTree);
1811
+ const rel = files.map((f) => f.replace(`${config.projectRoot}/`, ""));
1812
+ const shown = rel.slice(0, 2);
1813
+ const rest = rel.length - shown.length;
1814
+ process.stdout.write(
1815
+ `# ${blue(`\u2500\u2500 ${shown.join(" ")}${rest > 0 ? ` +${rest} more` : ""} \u2500\u2500`)}
1816
+ `
1817
+ );
1818
+ }
1793
1819
  var fsPromise, HTML_HEADERS, WATCH_WS_RECONNECT_INTERVAL_MS, WATCH_WS_RECONNECT_MAX_RETRIES, WS_RETRY_INTERVAL_MS, NOT_FOUND_HTML;
1794
1820
  var init_web_server = __esm({
1795
1821
  "lib/setup/web-server.ts"() {
@@ -1797,7 +1823,7 @@ var init_web_server = __esm({
1797
1823
  init_html();
1798
1824
  init_display_test_result();
1799
1825
  init_color();
1800
- init_http();
1826
+ init_web();
1801
1827
  fsPromise = fs8.promises;
1802
1828
  HTML_HEADERS = { "Content-Type": "text/html", "Cache-Control": "no-store" };
1803
1829
  WATCH_WS_RECONNECT_INTERVAL_MS = 1e3;
@@ -2058,12 +2084,6 @@ var init_display_final_result = __esm({
2058
2084
  import fs9 from "node:fs/promises";
2059
2085
  import path7 from "node:path";
2060
2086
  import esbuild from "esbuild";
2061
- function toEsbuildImportPath(filePath) {
2062
- const rel = path7.relative(process.cwd(), filePath);
2063
- const normalized = rel.replace(/\\/g, "/");
2064
- if (path7.isAbsolute(rel)) return filePath.replace(/\\/g, "/");
2065
- return normalized.startsWith(".") ? normalized : "./" + normalized;
2066
- }
2067
2087
  function deriveBuildErrorType(error) {
2068
2088
  const msgs = error?.errors ?? [];
2069
2089
  const text = msgs[0]?.text ?? (error instanceof Error ? error.message : String(error));
@@ -2264,7 +2284,130 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
2264
2284
  throw exception;
2265
2285
  }
2266
2286
  }
2267
- return connections;
2287
+ return connections;
2288
+ }
2289
+ async function flushConsoleHandlers(handlers, deadline = Date.now() + CONSOLE_FLUSH_TIMEOUT_MS) {
2290
+ if (!handlers || Date.now() >= deadline) return;
2291
+ await new Promise((resolve) => setImmediate(resolve));
2292
+ if (handlers.size === 0) return;
2293
+ await Promise.allSettled([...handlers]);
2294
+ return flushConsoleHandlers(handlers, deadline);
2295
+ }
2296
+ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
2297
+ groupCachedContents.forEach((cachedContent) => {
2298
+ cachedContent._buildError = null;
2299
+ cachedContent._noTestsWarning = null;
2300
+ });
2301
+ const { projectRoot, debug, browser } = groupConfigs[0];
2302
+ const activeGroups = groupConfigs.reduce(
2303
+ (acc, groupConfig, groupIndex) => {
2304
+ const files = Object.keys(groupConfig.fsTree);
2305
+ if (files.length > 0)
2306
+ acc.push({
2307
+ groupIndex,
2308
+ config: groupConfig,
2309
+ cachedContent: groupCachedContents[groupIndex],
2310
+ files
2311
+ });
2312
+ return acc;
2313
+ },
2314
+ []
2315
+ );
2316
+ if (activeGroups.length === 0)
2317
+ return console.log(
2318
+ "# [buildAllGroupBundles] all groups empty \u2014 skipping build (no test files found)"
2319
+ );
2320
+ await Promise.all(
2321
+ activeGroups.map(
2322
+ (group) => fs9.mkdir(path7.resolve(group.config.projectRoot, group.config.output), { recursive: true })
2323
+ )
2324
+ );
2325
+ const sourcemap = "inline";
2326
+ const groupEntryPlugin = {
2327
+ name: "group-entry-loader",
2328
+ setup(build) {
2329
+ build.onResolve({ filter: /^group-entry-\d+$/ }, (args) => ({
2330
+ path: args.path,
2331
+ namespace: "group-entry"
2332
+ }));
2333
+ build.onLoad({ filter: /.*/, namespace: "group-entry" }, (args) => {
2334
+ const slotIndex = parseInt(args.path.replace("group-entry-", ""));
2335
+ return {
2336
+ contents: activeGroups[slotIndex].files.map((filePath) => `import "${toEsbuildImportPath(filePath)}";`).join(""),
2337
+ resolveDir: process.cwd()
2338
+ };
2339
+ });
2340
+ }
2341
+ };
2342
+ const esbuildOutdir = path7.join(projectRoot, "tmp");
2343
+ const buildOptions = {
2344
+ entryPoints: activeGroups.map((_, slotIndex) => ({
2345
+ in: `group-entry-${slotIndex}`,
2346
+ out: `group-${slotIndex}`
2347
+ })),
2348
+ plugins: [groupEntryPlugin],
2349
+ nodePaths: ANCESTOR_NODE_MODULES,
2350
+ bundle: true,
2351
+ logLevel: "silent",
2352
+ // outdir only labels the paths in outputFiles[].path — nothing is written to disk
2353
+ // (write:false). Use projectRoot/tmp as a stable sentinel; mkdir is not required.
2354
+ outdir: esbuildOutdir,
2355
+ keepNames: true,
2356
+ legalComments: "none",
2357
+ target: esbuildTarget(browser),
2358
+ sourcemap,
2359
+ write: false,
2360
+ footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
2361
+ };
2362
+ const hasSmallOutput = (result) => (result.outputFiles ?? []).some(
2363
+ (outputFile) => GROUP_OUTPUT_REGEX.test(outputFile.path) && !outputFile.path.endsWith(".map") && outputFile.contents.length < EMPTY_BUNDLE_THRESHOLD
2364
+ );
2365
+ const buildWithRetry = async (retriesLeft) => {
2366
+ const result = await esbuild.build(buildOptions);
2367
+ if (!hasSmallOutput(result) || retriesLeft === 0) return result;
2368
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
2369
+ return buildWithRetry(retriesLeft - 1);
2370
+ };
2371
+ try {
2372
+ const result = await buildWithRetry(MAX_RETRIES);
2373
+ await Promise.all(
2374
+ (result.outputFiles ?? []).map((outputFile) => {
2375
+ const match = GROUP_OUTPUT_REGEX.exec(outputFile.path);
2376
+ if (!match) return Promise.resolve();
2377
+ const slotIndex = parseInt(match[1]);
2378
+ const isMap = Boolean(match[2]);
2379
+ const { config, cachedContent } = activeGroups[slotIndex];
2380
+ const destPath = path7.join(
2381
+ path7.resolve(config.projectRoot, config.output),
2382
+ "tests.js" + (isMap ? ".map" : "")
2383
+ );
2384
+ if (!isMap) {
2385
+ cachedContent.allTestCode = Buffer.from(outputFile.contents);
2386
+ config._sourceMapDecoder = extractInlineSourceMap(
2387
+ cachedContent.allTestCode,
2388
+ esbuildOutdir
2389
+ );
2390
+ }
2391
+ return fs9.writeFile(destPath, outputFile.contents);
2392
+ })
2393
+ );
2394
+ } catch (error) {
2395
+ const buildError = { type: deriveBuildErrorType(error), formatted: formatBuildErrors(error) };
2396
+ const errorHtml = buildErrorHTML(buildError);
2397
+ await Promise.all(
2398
+ activeGroups.map((group) => {
2399
+ group.cachedContent._buildError = buildError;
2400
+ return fs9.writeFile(
2401
+ path7.join(path7.resolve(group.config.projectRoot, group.config.output), "index.html"),
2402
+ errorHtml
2403
+ ).catch(
2404
+ (err) => debug && process.stderr.write(`# [qunitx] writeFile index.html: ${err.message}
2405
+ `)
2406
+ );
2407
+ })
2408
+ );
2409
+ throw error;
2410
+ }
2268
2411
  }
2269
2412
  function buildFilteredTests(filteredTests, outputPath, config) {
2270
2413
  const sourcemap = "inline";
@@ -2342,7 +2485,7 @@ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config
2342
2485
  let wsConnected = false;
2343
2486
  try {
2344
2487
  console.log("#", blue(`QUnitX running: http://localhost:${config.port}${filePath}`));
2345
- const navMs = config.timeout + NAV_GRACE_MS;
2488
+ const navMs = Math.max(config.timeout + NAV_GRACE_MS, MIN_NAV_MS);
2346
2489
  const startupMs = Math.max(config.timeout * STARTUP_TIMEOUT_FACTOR, navMs);
2347
2490
  const testsJsMs = Math.max(config.timeout * TESTS_JS_TIMEOUT_FACTOR, navMs);
2348
2491
  let resolveTestRace;
@@ -2432,128 +2575,13 @@ async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode
2432
2575
  process.exit(1);
2433
2576
  }
2434
2577
  }
2435
- async function flushConsoleHandlers(handlers, deadline = Date.now() + CONSOLE_FLUSH_TIMEOUT_MS) {
2436
- if (!handlers || handlers.size === 0 || Date.now() >= deadline) return;
2437
- await Promise.allSettled([...handlers]);
2438
- return flushConsoleHandlers(handlers, deadline);
2439
- }
2440
- async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
2441
- groupCachedContents.forEach((cachedContent) => {
2442
- cachedContent._buildError = null;
2443
- cachedContent._noTestsWarning = null;
2444
- });
2445
- const { projectRoot, debug, browser } = groupConfigs[0];
2446
- const activeGroups = groupConfigs.reduce(
2447
- (acc, groupConfig, groupIndex) => {
2448
- const files = Object.keys(groupConfig.fsTree);
2449
- if (files.length > 0)
2450
- acc.push({
2451
- groupIndex,
2452
- config: groupConfig,
2453
- cachedContent: groupCachedContents[groupIndex],
2454
- files
2455
- });
2456
- return acc;
2457
- },
2458
- []
2459
- );
2460
- if (activeGroups.length === 0)
2461
- return console.log(
2462
- "# [buildAllGroupBundles] all groups empty \u2014 skipping build (no test files found)"
2463
- );
2464
- await Promise.all(
2465
- activeGroups.map(
2466
- (group) => fs9.mkdir(path7.resolve(group.config.projectRoot, group.config.output), { recursive: true })
2467
- )
2468
- );
2469
- const sourcemap = "inline";
2470
- const groupEntryPlugin = {
2471
- name: "group-entry-loader",
2472
- setup(build) {
2473
- build.onResolve({ filter: /^group-entry-\d+$/ }, (args) => ({
2474
- path: args.path,
2475
- namespace: "group-entry"
2476
- }));
2477
- build.onLoad({ filter: /.*/, namespace: "group-entry" }, (args) => {
2478
- const slotIndex = parseInt(args.path.replace("group-entry-", ""));
2479
- return {
2480
- contents: activeGroups[slotIndex].files.map((filePath) => `import "${toEsbuildImportPath(filePath)}";`).join(""),
2481
- resolveDir: process.cwd()
2482
- };
2483
- });
2484
- }
2485
- };
2486
- const esbuildOutdir = path7.join(projectRoot, "tmp");
2487
- const buildOptions = {
2488
- entryPoints: activeGroups.map((_, slotIndex) => ({
2489
- in: `group-entry-${slotIndex}`,
2490
- out: `group-${slotIndex}`
2491
- })),
2492
- plugins: [groupEntryPlugin],
2493
- nodePaths: ANCESTOR_NODE_MODULES,
2494
- bundle: true,
2495
- logLevel: "silent",
2496
- // outdir only labels the paths in outputFiles[].path — nothing is written to disk
2497
- // (write:false). Use projectRoot/tmp as a stable sentinel; mkdir is not required.
2498
- outdir: esbuildOutdir,
2499
- keepNames: true,
2500
- legalComments: "none",
2501
- target: esbuildTarget(browser),
2502
- sourcemap,
2503
- write: false,
2504
- footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
2505
- };
2506
- const hasSmallOutput = (result) => (result.outputFiles ?? []).some(
2507
- (outputFile) => GROUP_OUTPUT_REGEX.test(outputFile.path) && !outputFile.path.endsWith(".map") && outputFile.contents.length < EMPTY_BUNDLE_THRESHOLD
2508
- );
2509
- const buildWithRetry = async (retriesLeft) => {
2510
- const result = await esbuild.build(buildOptions);
2511
- if (!hasSmallOutput(result) || retriesLeft === 0) return result;
2512
- await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
2513
- return buildWithRetry(retriesLeft - 1);
2514
- };
2515
- try {
2516
- const result = await buildWithRetry(MAX_RETRIES);
2517
- await Promise.all(
2518
- (result.outputFiles ?? []).map((outputFile) => {
2519
- const match = GROUP_OUTPUT_REGEX.exec(outputFile.path);
2520
- if (!match) return Promise.resolve();
2521
- const slotIndex = parseInt(match[1]);
2522
- const isMap = Boolean(match[2]);
2523
- const { config, cachedContent } = activeGroups[slotIndex];
2524
- const destPath = path7.join(
2525
- path7.resolve(config.projectRoot, config.output),
2526
- "tests.js" + (isMap ? ".map" : "")
2527
- );
2528
- if (!isMap) {
2529
- cachedContent.allTestCode = Buffer.from(outputFile.contents);
2530
- config._sourceMapDecoder = extractInlineSourceMap(
2531
- cachedContent.allTestCode,
2532
- esbuildOutdir
2533
- );
2534
- }
2535
- return fs9.writeFile(destPath, outputFile.contents);
2536
- })
2537
- );
2538
- } catch (error) {
2539
- const buildError = { type: deriveBuildErrorType(error), formatted: formatBuildErrors(error) };
2540
- const errorHtml = buildErrorHTML(buildError);
2541
- await Promise.all(
2542
- activeGroups.map((group) => {
2543
- group.cachedContent._buildError = buildError;
2544
- return fs9.writeFile(
2545
- path7.join(path7.resolve(group.config.projectRoot, group.config.output), "index.html"),
2546
- errorHtml
2547
- ).catch(
2548
- (err) => debug && process.stderr.write(`# [qunitx] writeFile index.html: ${err.message}
2549
- `)
2550
- );
2551
- })
2552
- );
2553
- throw error;
2554
- }
2578
+ function toEsbuildImportPath(filePath) {
2579
+ const rel = path7.relative(process.cwd(), filePath);
2580
+ const normalized = rel.replace(/\\/g, "/");
2581
+ if (path7.isAbsolute(rel)) return filePath.replace(/\\/g, "/");
2582
+ return normalized.startsWith(".") ? normalized : "./" + normalized;
2555
2583
  }
2556
- var BundleError, ancestorNodeModules, ANCESTOR_NODE_MODULES, RETRY_DELAY_MS, MAX_RETRIES, EMPTY_BUNDLE_THRESHOLD, NAV_GRACE_MS, STARTUP_TIMEOUT_FACTOR, TESTS_JS_TIMEOUT_FACTOR, CONSOLE_FLUSH_TIMEOUT_MS, TEST_STALL_BUFFER_MS, GROUP_OUTPUT_REGEX;
2584
+ 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;
2557
2585
  var init_tests_in_browser = __esm({
2558
2586
  "lib/commands/run/tests-in-browser.ts"() {
2559
2587
  init_color();
@@ -2563,13 +2591,6 @@ var init_tests_in_browser = __esm({
2563
2591
  init_display_final_result();
2564
2592
  init_web_server();
2565
2593
  init_source_map_decoder();
2566
- BundleError = class extends Error {
2567
- constructor(message) {
2568
- super(message);
2569
- this.name = "BundleError";
2570
- this.message = `esbuild Bundle Error: ${message}`.split("\n").join("\n# ");
2571
- }
2572
- };
2573
2594
  ancestorNodeModules = (dir) => dir.split(path7.sep).map(
2574
2595
  (_, i, parts) => path7.join(parts.slice(0, parts.length - i).join(path7.sep) || path7.sep, "node_modules")
2575
2596
  );
@@ -2578,11 +2599,19 @@ var init_tests_in_browser = __esm({
2578
2599
  MAX_RETRIES = 3;
2579
2600
  EMPTY_BUNDLE_THRESHOLD = 500;
2580
2601
  NAV_GRACE_MS = 1e4;
2602
+ MIN_NAV_MS = 3e4;
2581
2603
  STARTUP_TIMEOUT_FACTOR = 3;
2582
2604
  TESTS_JS_TIMEOUT_FACTOR = 4;
2583
2605
  CONSOLE_FLUSH_TIMEOUT_MS = 2e3;
2584
2606
  TEST_STALL_BUFFER_MS = 5e3;
2585
2607
  GROUP_OUTPUT_REGEX = /group-(\d+)\.js(\.map)?$/;
2608
+ BundleError = class extends Error {
2609
+ constructor(message) {
2610
+ super(message);
2611
+ this.name = "BundleError";
2612
+ this.message = `esbuild Bundle Error: ${message}`.split("\n").join("\n# ");
2613
+ }
2614
+ };
2586
2615
  }
2587
2616
  });
2588
2617
 
@@ -2594,6 +2623,7 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
2594
2623
  const extensions = config.extensions || ["js", "ts"];
2595
2624
  const readyPromises = [];
2596
2625
  const parentWatchers = [];
2626
+ const rescanTimers = [];
2597
2627
  const fileWatchers = {};
2598
2628
  const symlinkPollers = /* @__PURE__ */ new Map();
2599
2629
  function trackSymlink(filePath) {
@@ -2620,20 +2650,24 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
2620
2650
  }
2621
2651
  for (const watchPath of testFileLookupPaths) {
2622
2652
  let ready = false;
2653
+ let rescanInProgress = false;
2623
2654
  const lastEventMs = {};
2624
2655
  const seenMtimeMs = {};
2625
2656
  const childWatcher = fs10.watch(watchPath, { recursive: true }, async (eventType, filename) => {
2626
2657
  if (!ready) return;
2627
2658
  if (!filename) {
2628
- if (process.platform === "darwin") {
2629
- await rescanDirectoryForDelta(
2659
+ if (process.platform === "darwin" && !rescanInProgress) {
2660
+ rescanInProgress = true;
2661
+ rescanDirectoryForDelta(
2630
2662
  watchPath,
2631
2663
  config,
2632
2664
  extensions,
2633
2665
  onEventFunc,
2634
2666
  onFinishFunc,
2635
2667
  trackSymlink
2636
- );
2668
+ ).finally(() => {
2669
+ rescanInProgress = false;
2670
+ });
2637
2671
  }
2638
2672
  return;
2639
2673
  }
@@ -2693,6 +2727,24 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
2693
2727
  })
2694
2728
  )
2695
2729
  );
2730
+ if (process.platform === "darwin") {
2731
+ rescanTimers.push(
2732
+ setInterval(() => {
2733
+ if (!ready || rescanInProgress) return;
2734
+ rescanInProgress = true;
2735
+ rescanDirectoryForDelta(
2736
+ watchPath,
2737
+ config,
2738
+ extensions,
2739
+ onEventFunc,
2740
+ onFinishFunc,
2741
+ trackSymlink
2742
+ ).finally(() => {
2743
+ rescanInProgress = false;
2744
+ });
2745
+ }, RESCAN_INTERVAL_MS).unref()
2746
+ );
2747
+ }
2696
2748
  }
2697
2749
  readyPromises.push(
2698
2750
  (async () => {
@@ -2712,28 +2764,13 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
2712
2764
  killFileWatchers() {
2713
2765
  Object.keys(fileWatchers).forEach((key) => fileWatchers[key].close());
2714
2766
  parentWatchers.forEach((pw) => pw.close());
2767
+ rescanTimers.forEach((t) => clearInterval(t));
2715
2768
  symlinkPollers.forEach((cancel) => cancel());
2716
2769
  symlinkPollers.clear();
2717
2770
  return fileWatchers;
2718
2771
  }
2719
2772
  };
2720
2773
  }
2721
- async function classifyRenameEvent(fullPath, fsTree) {
2722
- for (const delay of [0, OVERLAYFS_RENAME_RETRY_MS]) {
2723
- if (delay) await new Promise((resolve) => setTimeout(resolve, delay));
2724
- try {
2725
- const statResult = await stat(fullPath);
2726
- if (statResult.isDirectory()) return "addDir";
2727
- return fsTree && fullPath in fsTree ? "change" : "add";
2728
- } catch {
2729
- }
2730
- }
2731
- if (!fsTree) return null;
2732
- if (fullPath in fsTree) return "unlink";
2733
- return Object.keys(fsTree).some(
2734
- (trackedPath) => trackedPath.startsWith(fullPath + "/") || trackedPath.startsWith(fullPath + "\\")
2735
- ) ? "unlinkDir" : null;
2736
- }
2737
2774
  function handleWatchEvent(config, extensions, event, filePath, onEventFunc, onFinishFunc) {
2738
2775
  if (event !== "unlinkDir" && !extensions.some((ext) => filePath.endsWith(`.${ext}`)))
2739
2776
  return Promise.resolve();
@@ -2776,9 +2813,16 @@ async function rescanDirectoryForDelta(watchPath, config, extensions, onEventFun
2776
2813
  try {
2777
2814
  const entries = await readdir(watchPath, { withFileTypes: true, recursive: true });
2778
2815
  const presentPaths = /* @__PURE__ */ new Set();
2816
+ const presentDirs = /* @__PURE__ */ new Set();
2817
+ presentDirs.add(watchPath);
2779
2818
  for (const entry of entries) {
2819
+ if (entry.isDirectory()) {
2820
+ presentDirs.add(path8.join(entry.parentPath, entry.name));
2821
+ continue;
2822
+ }
2780
2823
  if (!entry.isFile() && !entry.isSymbolicLink()) continue;
2781
2824
  const entryPath = path8.join(entry.parentPath, entry.name);
2825
+ presentDirs.add(entry.parentPath);
2782
2826
  if (!extensions.some((ext) => entryPath.endsWith(`.${ext}`))) continue;
2783
2827
  presentPaths.add(entryPath);
2784
2828
  if (!(entryPath in config.fsTree)) {
@@ -2787,9 +2831,18 @@ async function rescanDirectoryForDelta(watchPath, config, extensions, onEventFun
2787
2831
  }
2788
2832
  }
2789
2833
  const watchPrefix = watchPath + path8.sep;
2834
+ const firedDirPrefixes = [];
2790
2835
  for (const trackedPath of Object.keys(config.fsTree)) {
2791
- if (trackedPath.startsWith(watchPrefix) && !presentPaths.has(trackedPath))
2836
+ if (!trackedPath.startsWith(watchPrefix) || presentPaths.has(trackedPath)) continue;
2837
+ if (firedDirPrefixes.some((p) => trackedPath.startsWith(p + path8.sep))) continue;
2838
+ const parts = trackedPath.slice(watchPrefix.length).split(path8.sep);
2839
+ const goneDirPath = parts.slice(0, -1).map((_, i) => watchPrefix + parts.slice(0, i + 1).join(path8.sep)).find((p) => !presentDirs.has(p)) ?? null;
2840
+ if (goneDirPath !== null) {
2841
+ firedDirPrefixes.push(goneDirPath);
2842
+ handleWatchEvent(config, extensions, "unlinkDir", goneDirPath, onEventFunc, onFinishFunc);
2843
+ } else {
2792
2844
  handleWatchEvent(config, extensions, "unlink", trackedPath, onEventFunc, onFinishFunc);
2845
+ }
2793
2846
  }
2794
2847
  } catch {
2795
2848
  }
@@ -2806,18 +2859,35 @@ function mutateFSTree(fsTree, event, filePath) {
2806
2859
  }
2807
2860
  }
2808
2861
  }
2862
+ async function classifyRenameEvent(fullPath, fsTree) {
2863
+ for (const delay of [0, OVERLAYFS_RENAME_RETRY_MS]) {
2864
+ if (delay) await new Promise((resolve) => setTimeout(resolve, delay));
2865
+ try {
2866
+ const statResult = await stat(fullPath);
2867
+ if (statResult.isDirectory()) return "addDir";
2868
+ return fsTree && fullPath in fsTree ? "change" : "add";
2869
+ } catch {
2870
+ }
2871
+ }
2872
+ if (!fsTree) return null;
2873
+ if (fullPath in fsTree) return "unlink";
2874
+ return Object.keys(fsTree).some(
2875
+ (trackedPath) => trackedPath.startsWith(fullPath + "/") || trackedPath.startsWith(fullPath + "\\")
2876
+ ) ? "unlinkDir" : null;
2877
+ }
2809
2878
  function colorEvent(event) {
2810
2879
  if (event === "change") return yellow("CHANGED:");
2811
2880
  if (event === "add" || event === "addDir") return green("ADDED:");
2812
2881
  return red("REMOVED:");
2813
2882
  }
2814
- var CHANGE_DEDUPE_MS, SYMLINK_POLL_INTERVAL_MS, OVERLAYFS_RENAME_RETRY_MS;
2883
+ var CHANGE_DEDUPE_MS, SYMLINK_POLL_INTERVAL_MS, OVERLAYFS_RENAME_RETRY_MS, RESCAN_INTERVAL_MS;
2815
2884
  var init_file_watcher = __esm({
2816
2885
  "lib/setup/file-watcher.ts"() {
2817
2886
  init_color();
2818
2887
  CHANGE_DEDUPE_MS = 10;
2819
2888
  SYMLINK_POLL_INTERVAL_MS = 500;
2820
2889
  OVERLAYFS_RENAME_RETRY_MS = 50;
2890
+ RESCAN_INTERVAL_MS = 1e3;
2821
2891
  }
2822
2892
  });
2823
2893
 
@@ -3304,7 +3374,7 @@ var init_run = __esm({
3304
3374
  "lib/commands/run.ts"() {
3305
3375
  init_browser();
3306
3376
  init_chrome_prelaunch();
3307
- init_http();
3377
+ init_web();
3308
3378
  init_bind_server_to_port();
3309
3379
  init_web_server();
3310
3380
  init_open_output_in_browser();
@@ -3338,7 +3408,7 @@ init_color();
3338
3408
  var package_default = {
3339
3409
  name: "qunitx-cli",
3340
3410
  type: "module",
3341
- version: "0.21.2",
3411
+ version: "0.21.3",
3342
3412
  description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
3343
3413
  author: "Izel Nakri",
3344
3414
  license: "MIT",
@@ -3565,12 +3635,6 @@ function convertToPascalCase(str) {
3565
3635
  }
3566
3636
 
3567
3637
  // lib/commands/generate.ts
3568
- function pathToModuleName(filePath) {
3569
- const withoutExt = filePath.replace(/\.(js|ts)$/, "");
3570
- const segments = withoutExt.split("/");
3571
- const targetNames = segments[0] === "test" || segments[0] === "tests" ? segments.slice(1) : segments;
3572
- return targetNames.map(convertToPascalCase).join(" | ");
3573
- }
3574
3638
  async function generateTestFiles() {
3575
3639
  const projectRoot = await findProjectRoot();
3576
3640
  const moduleName = pathToModuleName(process.argv[3]);
@@ -3586,6 +3650,12 @@ async function generateTestFiles() {
3586
3650
  await fs5.writeFile(path10, testJSContent.replace("{{moduleName}}", moduleName));
3587
3651
  console.log(green(`${path10} written`));
3588
3652
  }
3653
+ function pathToModuleName(filePath) {
3654
+ const withoutExt = filePath.replace(/\.(js|ts)$/, "");
3655
+ const segments = withoutExt.split("/");
3656
+ const targetNames = segments[0] === "test" || segments[0] === "tests" ? segments.slice(1) : segments;
3657
+ return targetNames.map(convertToPascalCase).join(" | ");
3658
+ }
3589
3659
 
3590
3660
  // lib/setup/config.ts
3591
3661
  import fs7 from "node:fs/promises";
@@ -3593,28 +3663,6 @@ import fs7 from "node:fs/promises";
3593
3663
  // lib/setup/fs-tree.ts
3594
3664
  import fs6, { glob as fsGlob } from "node:fs/promises";
3595
3665
  import path3 from "node:path";
3596
- function isGlob(str) {
3597
- return /[*?{[]/.test(str);
3598
- }
3599
- async function readDirRecursive(dir, filter) {
3600
- const entries = await fs6.readdir(dir, { recursive: true, withFileTypes: true });
3601
- const candidates = entries.filter(
3602
- (dirent) => (dirent.isFile() || dirent.isSymbolicLink()) && filter(dirent.name)
3603
- );
3604
- const resolvedPaths = await Promise.all(
3605
- candidates.map(async (dirent) => {
3606
- const fullPath = path3.join(dirent.parentPath, dirent.name);
3607
- if (dirent.isFile()) return fullPath;
3608
- try {
3609
- const statResult = await fs6.stat(fullPath);
3610
- return statResult.isFile() ? fullPath : null;
3611
- } catch {
3612
- return null;
3613
- }
3614
- })
3615
- );
3616
- return resolvedPaths.filter((resolvedPath) => resolvedPath !== null);
3617
- }
3618
3666
  async function buildFSTree(fileAbsolutePaths, config = {}) {
3619
3667
  const targetExtensions = config.extensions || ["js", "ts"];
3620
3668
  const fsTree = {};
@@ -3648,57 +3696,65 @@ async function buildFSTree(fileAbsolutePaths, config = {}) {
3648
3696
  );
3649
3697
  return fsTree;
3650
3698
  }
3651
-
3652
- // lib/setup/test-file-paths.ts
3653
- import { matchesGlob } from "node:path";
3654
- function isGlob2(str) {
3699
+ function isGlob(str) {
3655
3700
  return /[*?{[]/.test(str);
3656
3701
  }
3657
- function setupTestFilePaths(_projectRoot, inputs2) {
3658
- const [folders, filesWithGlob, filesWithoutGlob] = inputs2.reduce(
3659
- (result2, input) => {
3660
- const glob = isGlob2(input);
3661
- if (!pathIsFile(input)) {
3662
- result2[0].push({ input, isFile: false, isGlob: glob });
3663
- } else {
3664
- result2[glob ? 1 : 2].push({ input, isFile: true, isGlob: glob });
3702
+ async function readDirRecursive(dir, filter) {
3703
+ const entries = await fs6.readdir(dir, { recursive: true, withFileTypes: true });
3704
+ const candidates = entries.filter(
3705
+ (dirent) => (dirent.isFile() || dirent.isSymbolicLink()) && filter(dirent.name)
3706
+ );
3707
+ const resolvedPaths = await Promise.all(
3708
+ candidates.map(async (dirent) => {
3709
+ const fullPath = path3.join(dirent.parentPath, dirent.name);
3710
+ if (dirent.isFile()) return fullPath;
3711
+ try {
3712
+ const statResult = await fs6.stat(fullPath);
3713
+ return statResult.isFile() ? fullPath : null;
3714
+ } catch {
3715
+ return null;
3665
3716
  }
3666
- return result2;
3667
- },
3668
- [[], [], []]
3717
+ })
3669
3718
  );
3670
- const result = folders.reduce((folderResult, folder) => {
3671
- if (!pathIsIncludedInPaths(folders, folder)) {
3672
- folderResult.push(folder);
3673
- }
3674
- return folderResult;
3675
- }, []);
3676
- filesWithGlob.forEach((file) => {
3677
- if (!pathIsIncludedInPaths(result, file) && !pathIsIncludedInPaths(filesWithGlob, file)) {
3678
- result.push(file);
3719
+ return resolvedPaths.filter((resolvedPath) => resolvedPath !== null);
3720
+ }
3721
+
3722
+ // lib/setup/test-file-paths.ts
3723
+ import { matchesGlob } from "node:path";
3724
+ var GLOB_CHARS = /[*?{[]/;
3725
+ function setupTestFilePaths(inputs2) {
3726
+ const folders = [];
3727
+ const filesWithGlob = [];
3728
+ const filesWithoutGlob = [];
3729
+ inputs2.forEach((input) => {
3730
+ if (!pathIsFile(input)) {
3731
+ folders.push({ input, globFormat: `${input}/**` });
3732
+ } else if (isGlob2(input)) {
3733
+ filesWithGlob.push({ input, globFormat: input });
3734
+ } else {
3735
+ filesWithoutGlob.push({ input, globFormat: input });
3679
3736
  }
3680
3737
  });
3681
- filesWithoutGlob.forEach((file) => {
3682
- if (!pathIsIncludedInPaths(result, file)) {
3683
- result.push(file);
3738
+ const dedupedFolders = folders.filter((folder) => !isIncludedIn(folders, folder));
3739
+ const dedupedGlobFiles = filesWithGlob.filter(
3740
+ (file) => !isIncludedIn(dedupedFolders, file) && !isIncludedIn(filesWithGlob, file)
3741
+ );
3742
+ const dedupedPlainFiles = filesWithoutGlob.reduce((acc, file) => {
3743
+ if (!isIncludedIn(dedupedFolders, file) && !isIncludedIn(dedupedGlobFiles, file) && !isIncludedIn(acc, file)) {
3744
+ acc.push(file);
3684
3745
  }
3685
- });
3686
- return result.map((metaItem) => metaItem.input);
3746
+ return acc;
3747
+ }, []);
3748
+ return dedupedFolders.concat(dedupedGlobFiles, dedupedPlainFiles).map((meta) => meta.input);
3687
3749
  }
3688
3750
  function pathIsFile(path10) {
3689
- const inputs2 = path10.split("/");
3690
- return inputs2[inputs2.length - 1].includes(".");
3751
+ return path10.includes(".", path10.lastIndexOf("/") + 1);
3691
3752
  }
3692
- function pathIsIncludedInPaths(paths, targetPath) {
3693
- return paths.some((path10) => {
3694
- if (path10 === targetPath) {
3695
- return false;
3696
- }
3697
- return matchesGlob(targetPath.input, buildGlobFormat(path10));
3698
- });
3753
+ function isIncludedIn(paths, target) {
3754
+ return paths.some((path10) => path10 !== target && matchesGlob(target.input, path10.globFormat));
3699
3755
  }
3700
- function buildGlobFormat(path10) {
3701
- return path10.isFile ? path10.input : `${path10.input}/**`;
3756
+ function isGlob2(str) {
3757
+ return GLOB_CHARS.test(str);
3702
3758
  }
3703
3759
 
3704
3760
  // lib/utils/parse-cli-flags.ts
@@ -3801,7 +3857,7 @@ async function setupConfig() {
3801
3857
  ...cliConfigFlags,
3802
3858
  projectRoot,
3803
3859
  inputs: inputs2,
3804
- testFileLookupPaths: setupTestFilePaths(projectRoot, inputs2),
3860
+ testFileLookupPaths: setupTestFilePaths(inputs2),
3805
3861
  lastFailedTestFiles: null,
3806
3862
  lastRanTestFiles: null,
3807
3863
  COUNTER: {