qunitx-cli 0.21.1 → 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 +743 -652
  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));
@@ -2266,174 +2286,10 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
2266
2286
  }
2267
2287
  return connections;
2268
2288
  }
2269
- function buildFilteredTests(filteredTests, outputPath, config) {
2270
- const sourcemap = "inline";
2271
- const needsDisk = Boolean(config.open);
2272
- return buildWithOverlayfsRetry(
2273
- {
2274
- stdin: {
2275
- contents: filteredTests.map((filePath) => `import "${filePath.replace(/\\/g, "/")}";`).join(""),
2276
- resolveDir: process.cwd()
2277
- },
2278
- nodePaths: ANCESTOR_NODE_MODULES,
2279
- bundle: true,
2280
- logLevel: "silent",
2281
- outfile: outputPath,
2282
- legalComments: "none",
2283
- target: esbuildTarget(config.browser),
2284
- sourcemap,
2285
- footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
2286
- },
2287
- needsDisk
2288
- );
2289
- }
2290
- async function runWithOverlayfsRetry(getContents, needsDisk) {
2291
- let { result, js } = await getContents();
2292
- const initialSize = js.length;
2293
- for (let retry = 1; retry <= MAX_RETRIES; retry++) {
2294
- if (js.length >= EMPTY_BUNDLE_THRESHOLD) break;
2295
- await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
2296
- ({ result, js } = await getContents());
2297
- }
2298
- if (js.length < EMPTY_BUNDLE_THRESHOLD && js.length !== initialSize) {
2299
- console.log(
2300
- `# [buildWithOverlayfsRetry] bundle is ${js.length} bytes after ${MAX_RETRIES} retries \u2014 proceeding`
2301
- );
2302
- }
2303
- if (needsDisk) {
2304
- await Promise.all(
2305
- result.outputFiles.map((outputFile) => fs9.writeFile(outputFile.path, outputFile.contents))
2306
- );
2307
- }
2308
- return js;
2309
- }
2310
- function buildWithOverlayfsRetry(options, needsDisk) {
2311
- const buildOpts = { ...options, write: false };
2312
- return runWithOverlayfsRetry(async () => {
2313
- const result = await esbuild.build(buildOpts);
2314
- const jsFile = result.outputFiles.find((f) => !f.path.endsWith(".map"));
2315
- return { result, js: Buffer.from(jsFile.contents) };
2316
- }, needsDisk);
2317
- }
2318
- async function buildIncrementally(options, fileKey, cachedContent, needsDisk) {
2319
- const buildOpts = { ...options, write: false };
2320
- if (!cachedContent._esbuildContext || cachedContent._esbuildContextKey !== fileKey) {
2321
- cachedContent._esbuildContext?.dispose().catch(() => {
2322
- });
2323
- cachedContent._esbuildContext = await esbuild.context(buildOpts);
2324
- cachedContent._esbuildContextKey = fileKey;
2325
- }
2326
- const ctx = cachedContent._esbuildContext;
2327
- return runWithOverlayfsRetry(async () => {
2328
- const result = await ctx.rebuild();
2329
- const jsFile = result.outputFiles.find((f) => !f.path.endsWith(".map"));
2330
- return { result, js: Buffer.from(jsFile.contents) };
2331
- }, needsDisk);
2332
- }
2333
- function esbuildTarget(browser) {
2334
- if (browser === "firefox") return ["firefox115"];
2335
- if (browser === "webkit") return ["safari16"];
2336
- return ["chrome120"];
2337
- }
2338
- async function runTestInsideHTMLFile(filePath, { page, server, browser }, config) {
2339
- let QUNIT_RESULT;
2340
- let targetError;
2341
- let timeoutHandle;
2342
- let wsConnected = false;
2343
- try {
2344
- console.log("#", blue(`QUnitX running: http://localhost:${config.port}${filePath}`));
2345
- const navMs = config.timeout + NAV_GRACE_MS;
2346
- const startupMs = Math.max(config.timeout * STARTUP_TIMEOUT_FACTOR, navMs);
2347
- const testsJsMs = Math.max(config.timeout * TESTS_JS_TIMEOUT_FACTOR, navMs);
2348
- let resolveTestRace;
2349
- const testRaceResult = new Promise((resolve) => {
2350
- resolveTestRace = resolve;
2351
- });
2352
- config._testRunDone = resolveTestRace;
2353
- config._onWsOpen = () => {
2354
- wsConnected = true;
2355
- clearTimeout(timeoutHandle);
2356
- timeoutHandle = setTimeout(resolveTestRace, startupMs);
2357
- };
2358
- config._onTestsJsServed = () => {
2359
- clearTimeout(timeoutHandle);
2360
- timeoutHandle = setTimeout(resolveTestRace, testsJsMs);
2361
- };
2362
- config._resetTestTimeout = () => {
2363
- wsConnected = true;
2364
- clearTimeout(timeoutHandle);
2365
- timeoutHandle = setTimeout(resolveTestRace, config.timeout + TEST_STALL_BUFFER_MS);
2366
- };
2367
- const targetUrl = `http://localhost:${config.port}${filePath}`;
2368
- const navOptions = { timeout: navMs, waitUntil: "commit" };
2369
- if (page.url().split("?")[0] === targetUrl) {
2370
- await page.reload(navOptions);
2371
- } else {
2372
- await page.goto(targetUrl, navOptions);
2373
- }
2374
- clearTimeout(timeoutHandle);
2375
- timeoutHandle = setTimeout(resolveTestRace, startupMs);
2376
- await testRaceResult;
2377
- QUNIT_RESULT = config._lastQUnitResult ?? await page.evaluate(() => window.QUNIT_RESULT);
2378
- } catch (error) {
2379
- targetError = error;
2380
- console.log(error);
2381
- console.error(error);
2382
- } finally {
2383
- clearTimeout(timeoutHandle);
2384
- config._onWsOpen = null;
2385
- config._onTestsJsServed = null;
2386
- config._resetTestTimeout = null;
2387
- config._testRunDone = null;
2388
- config._lastQUnitResult = null;
2389
- }
2390
- if (!QUNIT_RESULT) {
2391
- if (targetError) console.log(targetError);
2392
- const wsReason = !wsConnected ? "WebSocket connection never received \u2014 Chrome may be CPU-starved or the page failed to load" : "WebSocket connected but no tests ran \u2014 QUnit may have failed to start";
2393
- console.log(`# TIMEOUT: ${wsReason}`);
2394
- console.log("BROWSER: runtime error thrown during executing tests");
2395
- console.error("BROWSER: runtime error thrown during executing tests");
2396
- await failOnNonWatchMode(
2397
- config.watch,
2398
- { server, browser },
2399
- config._groupMode,
2400
- config._pendingConsoleHandlers
2401
- );
2402
- } else if (QUNIT_RESULT.totalTests === 0) {
2403
- return;
2404
- } else if (QUNIT_RESULT.totalTests > QUNIT_RESULT.finishedTests) {
2405
- if (targetError) console.log(targetError);
2406
- console.log(
2407
- `# TIMEOUT: test stalled after ${QUNIT_RESULT.finishedTests}/${QUNIT_RESULT.totalTests} finished \u2014 last active: ${QUNIT_RESULT.currentTest}`
2408
- );
2409
- console.log(`BROWSER: TEST TIMED OUT: ${QUNIT_RESULT.currentTest}`);
2410
- console.error(`BROWSER: TEST TIMED OUT: ${QUNIT_RESULT.currentTest}`);
2411
- await failOnNonWatchMode(
2412
- config.watch,
2413
- { server, browser },
2414
- config._groupMode,
2415
- config._pendingConsoleHandlers
2416
- );
2417
- } else if (QUNIT_RESULT.failedTests > config.COUNTER.failCount) {
2418
- config.COUNTER.failCount = QUNIT_RESULT.failedTests;
2419
- }
2420
- }
2421
- async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode = false, pendingHandlers) {
2422
- if (!watchMode) {
2423
- if (groupMode) {
2424
- throw new Error("Browser test run failed");
2425
- }
2426
- await flushConsoleHandlers(pendingHandlers);
2427
- await Promise.all([
2428
- connections.server && connections.server.close(),
2429
- connections.browser && connections.browser.close()
2430
- ]);
2431
- await shutdownPrelaunch();
2432
- process.exit(1);
2433
- }
2434
- }
2435
2289
  async function flushConsoleHandlers(handlers, deadline = Date.now() + CONSOLE_FLUSH_TIMEOUT_MS) {
2436
- if (!handlers || handlers.size === 0 || Date.now() >= deadline) return;
2290
+ if (!handlers || Date.now() >= deadline) return;
2291
+ await new Promise((resolve) => setImmediate(resolve));
2292
+ if (handlers.size === 0) return;
2437
2293
  await Promise.allSettled([...handlers]);
2438
2294
  return flushConsoleHandlers(handlers, deadline);
2439
2295
  }
@@ -2536,24 +2392,196 @@ async function buildAllGroupBundles(groupConfigs, groupCachedContents) {
2536
2392
  })
2537
2393
  );
2538
2394
  } 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
- })
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
+ }
2411
+ }
2412
+ function buildFilteredTests(filteredTests, outputPath, config) {
2413
+ const sourcemap = "inline";
2414
+ const needsDisk = Boolean(config.open);
2415
+ return buildWithOverlayfsRetry(
2416
+ {
2417
+ stdin: {
2418
+ contents: filteredTests.map((filePath) => `import "${filePath.replace(/\\/g, "/")}";`).join(""),
2419
+ resolveDir: process.cwd()
2420
+ },
2421
+ nodePaths: ANCESTOR_NODE_MODULES,
2422
+ bundle: true,
2423
+ logLevel: "silent",
2424
+ outfile: outputPath,
2425
+ legalComments: "none",
2426
+ target: esbuildTarget(config.browser),
2427
+ sourcemap,
2428
+ footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
2429
+ },
2430
+ needsDisk
2431
+ );
2432
+ }
2433
+ async function runWithOverlayfsRetry(getContents, needsDisk) {
2434
+ let { result, js } = await getContents();
2435
+ const initialSize = js.length;
2436
+ for (let retry = 1; retry <= MAX_RETRIES; retry++) {
2437
+ if (js.length >= EMPTY_BUNDLE_THRESHOLD) break;
2438
+ await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
2439
+ ({ result, js } = await getContents());
2440
+ }
2441
+ if (js.length < EMPTY_BUNDLE_THRESHOLD && js.length !== initialSize) {
2442
+ console.log(
2443
+ `# [buildWithOverlayfsRetry] bundle is ${js.length} bytes after ${MAX_RETRIES} retries \u2014 proceeding`
2444
+ );
2445
+ }
2446
+ if (needsDisk) {
2447
+ await Promise.all(
2448
+ result.outputFiles.map((outputFile) => fs9.writeFile(outputFile.path, outputFile.contents))
2449
+ );
2450
+ }
2451
+ return js;
2452
+ }
2453
+ function buildWithOverlayfsRetry(options, needsDisk) {
2454
+ const buildOpts = { ...options, write: false };
2455
+ return runWithOverlayfsRetry(async () => {
2456
+ const result = await esbuild.build(buildOpts);
2457
+ const jsFile = result.outputFiles.find((f) => !f.path.endsWith(".map"));
2458
+ return { result, js: Buffer.from(jsFile.contents) };
2459
+ }, needsDisk);
2460
+ }
2461
+ async function buildIncrementally(options, fileKey, cachedContent, needsDisk) {
2462
+ const buildOpts = { ...options, write: false };
2463
+ if (!cachedContent._esbuildContext || cachedContent._esbuildContextKey !== fileKey) {
2464
+ cachedContent._esbuildContext?.dispose().catch(() => {
2465
+ });
2466
+ cachedContent._esbuildContext = await esbuild.context(buildOpts);
2467
+ cachedContent._esbuildContextKey = fileKey;
2468
+ }
2469
+ const ctx = cachedContent._esbuildContext;
2470
+ return runWithOverlayfsRetry(async () => {
2471
+ const result = await ctx.rebuild();
2472
+ const jsFile = result.outputFiles.find((f) => !f.path.endsWith(".map"));
2473
+ return { result, js: Buffer.from(jsFile.contents) };
2474
+ }, needsDisk);
2475
+ }
2476
+ function esbuildTarget(browser) {
2477
+ if (browser === "firefox") return ["firefox115"];
2478
+ if (browser === "webkit") return ["safari16"];
2479
+ return ["chrome120"];
2480
+ }
2481
+ async function runTestInsideHTMLFile(filePath, { page, server, browser }, config) {
2482
+ let QUNIT_RESULT;
2483
+ let targetError;
2484
+ let timeoutHandle;
2485
+ let wsConnected = false;
2486
+ try {
2487
+ console.log("#", blue(`QUnitX running: http://localhost:${config.port}${filePath}`));
2488
+ const navMs = Math.max(config.timeout + NAV_GRACE_MS, MIN_NAV_MS);
2489
+ const startupMs = Math.max(config.timeout * STARTUP_TIMEOUT_FACTOR, navMs);
2490
+ const testsJsMs = Math.max(config.timeout * TESTS_JS_TIMEOUT_FACTOR, navMs);
2491
+ let resolveTestRace;
2492
+ const testRaceResult = new Promise((resolve) => {
2493
+ resolveTestRace = resolve;
2494
+ });
2495
+ config._testRunDone = resolveTestRace;
2496
+ config._onWsOpen = () => {
2497
+ wsConnected = true;
2498
+ clearTimeout(timeoutHandle);
2499
+ timeoutHandle = setTimeout(resolveTestRace, startupMs);
2500
+ };
2501
+ config._onTestsJsServed = () => {
2502
+ clearTimeout(timeoutHandle);
2503
+ timeoutHandle = setTimeout(resolveTestRace, testsJsMs);
2504
+ };
2505
+ config._resetTestTimeout = () => {
2506
+ wsConnected = true;
2507
+ clearTimeout(timeoutHandle);
2508
+ timeoutHandle = setTimeout(resolveTestRace, config.timeout + TEST_STALL_BUFFER_MS);
2509
+ };
2510
+ const targetUrl = `http://localhost:${config.port}${filePath}`;
2511
+ const navOptions = { timeout: navMs, waitUntil: "commit" };
2512
+ if (page.url().split("?")[0] === targetUrl) {
2513
+ await page.reload(navOptions);
2514
+ } else {
2515
+ await page.goto(targetUrl, navOptions);
2516
+ }
2517
+ clearTimeout(timeoutHandle);
2518
+ timeoutHandle = setTimeout(resolveTestRace, startupMs);
2519
+ await testRaceResult;
2520
+ QUNIT_RESULT = config._lastQUnitResult ?? await page.evaluate(() => window.QUNIT_RESULT);
2521
+ } catch (error) {
2522
+ targetError = error;
2523
+ console.log(error);
2524
+ console.error(error);
2525
+ } finally {
2526
+ clearTimeout(timeoutHandle);
2527
+ config._onWsOpen = null;
2528
+ config._onTestsJsServed = null;
2529
+ config._resetTestTimeout = null;
2530
+ config._testRunDone = null;
2531
+ config._lastQUnitResult = null;
2532
+ }
2533
+ if (!QUNIT_RESULT) {
2534
+ if (targetError) console.log(targetError);
2535
+ const wsReason = !wsConnected ? "WebSocket connection never received \u2014 Chrome may be CPU-starved or the page failed to load" : "WebSocket connected but no tests ran \u2014 QUnit may have failed to start";
2536
+ console.log(`# TIMEOUT: ${wsReason}`);
2537
+ console.log("BROWSER: runtime error thrown during executing tests");
2538
+ console.error("BROWSER: runtime error thrown during executing tests");
2539
+ await failOnNonWatchMode(
2540
+ config.watch,
2541
+ { server, browser },
2542
+ config._groupMode,
2543
+ config._pendingConsoleHandlers
2552
2544
  );
2553
- throw error;
2545
+ } else if (QUNIT_RESULT.totalTests === 0) {
2546
+ return;
2547
+ } else if (QUNIT_RESULT.totalTests > QUNIT_RESULT.finishedTests) {
2548
+ if (targetError) console.log(targetError);
2549
+ console.log(
2550
+ `# TIMEOUT: test stalled after ${QUNIT_RESULT.finishedTests}/${QUNIT_RESULT.totalTests} finished \u2014 last active: ${QUNIT_RESULT.currentTest}`
2551
+ );
2552
+ console.log(`BROWSER: TEST TIMED OUT: ${QUNIT_RESULT.currentTest}`);
2553
+ console.error(`BROWSER: TEST TIMED OUT: ${QUNIT_RESULT.currentTest}`);
2554
+ await failOnNonWatchMode(
2555
+ config.watch,
2556
+ { server, browser },
2557
+ config._groupMode,
2558
+ config._pendingConsoleHandlers
2559
+ );
2560
+ } else if (QUNIT_RESULT.failedTests > config.COUNTER.failCount) {
2561
+ config.COUNTER.failCount = QUNIT_RESULT.failedTests;
2562
+ }
2563
+ }
2564
+ async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode = false, pendingHandlers) {
2565
+ if (!watchMode) {
2566
+ if (groupMode) {
2567
+ throw new Error("Browser test run failed");
2568
+ }
2569
+ await flushConsoleHandlers(pendingHandlers);
2570
+ await Promise.all([
2571
+ connections.server && connections.server.close(),
2572
+ connections.browser && connections.browser.close()
2573
+ ]);
2574
+ await shutdownPrelaunch();
2575
+ process.exit(1);
2554
2576
  }
2555
2577
  }
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;
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;
2583
+ }
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,22 +2599,31 @@ 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
 
2589
2618
  // lib/setup/file-watcher.ts
2590
2619
  import fs10 from "node:fs";
2591
- import { stat, lstat } from "node:fs/promises";
2620
+ import { readdir, stat, lstat } from "node:fs/promises";
2592
2621
  import path8 from "node:path";
2593
2622
  function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFunc) {
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,10 +2650,27 @@ 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
- if (!ready || !filename) return;
2657
+ if (!ready) return;
2658
+ if (!filename) {
2659
+ if (process.platform === "darwin" && !rescanInProgress) {
2660
+ rescanInProgress = true;
2661
+ rescanDirectoryForDelta(
2662
+ watchPath,
2663
+ config,
2664
+ extensions,
2665
+ onEventFunc,
2666
+ onFinishFunc,
2667
+ trackSymlink
2668
+ ).finally(() => {
2669
+ rescanInProgress = false;
2670
+ });
2671
+ }
2672
+ return;
2673
+ }
2627
2674
  const fullPath = filename === path8.basename(watchPath) ? watchPath : path8.join(watchPath, filename);
2628
2675
  if (eventType === "change") {
2629
2676
  const now = Date.now();
@@ -2680,6 +2727,24 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
2680
2727
  })
2681
2728
  )
2682
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
+ }
2683
2748
  }
2684
2749
  readyPromises.push(
2685
2750
  (async () => {
@@ -2699,28 +2764,13 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
2699
2764
  killFileWatchers() {
2700
2765
  Object.keys(fileWatchers).forEach((key) => fileWatchers[key].close());
2701
2766
  parentWatchers.forEach((pw) => pw.close());
2767
+ rescanTimers.forEach((t) => clearInterval(t));
2702
2768
  symlinkPollers.forEach((cancel) => cancel());
2703
2769
  symlinkPollers.clear();
2704
2770
  return fileWatchers;
2705
2771
  }
2706
2772
  };
2707
2773
  }
2708
- async function classifyRenameEvent(fullPath, fsTree) {
2709
- for (const delay of [0, OVERLAYFS_RENAME_RETRY_MS]) {
2710
- if (delay) await new Promise((resolve) => setTimeout(resolve, delay));
2711
- try {
2712
- const statResult = await stat(fullPath);
2713
- if (statResult.isDirectory()) return "addDir";
2714
- return fsTree && fullPath in fsTree ? "change" : "add";
2715
- } catch {
2716
- }
2717
- }
2718
- if (!fsTree) return null;
2719
- if (fullPath in fsTree) return "unlink";
2720
- return Object.keys(fsTree).some(
2721
- (trackedPath) => trackedPath.startsWith(fullPath + "/") || trackedPath.startsWith(fullPath + "\\")
2722
- ) ? "unlinkDir" : null;
2723
- }
2724
2774
  function handleWatchEvent(config, extensions, event, filePath, onEventFunc, onFinishFunc) {
2725
2775
  if (event !== "unlinkDir" && !extensions.some((ext) => filePath.endsWith(`.${ext}`)))
2726
2776
  return Promise.resolve();
@@ -2759,6 +2809,44 @@ function handleWatchEvent(config, extensions, event, filePath, onEventFunc, onFi
2759
2809
  }
2760
2810
  });
2761
2811
  }
2812
+ async function rescanDirectoryForDelta(watchPath, config, extensions, onEventFunc, onFinishFunc, trackSymlinkFn) {
2813
+ try {
2814
+ const entries = await readdir(watchPath, { withFileTypes: true, recursive: true });
2815
+ const presentPaths = /* @__PURE__ */ new Set();
2816
+ const presentDirs = /* @__PURE__ */ new Set();
2817
+ presentDirs.add(watchPath);
2818
+ for (const entry of entries) {
2819
+ if (entry.isDirectory()) {
2820
+ presentDirs.add(path8.join(entry.parentPath, entry.name));
2821
+ continue;
2822
+ }
2823
+ if (!entry.isFile() && !entry.isSymbolicLink()) continue;
2824
+ const entryPath = path8.join(entry.parentPath, entry.name);
2825
+ presentDirs.add(entry.parentPath);
2826
+ if (!extensions.some((ext) => entryPath.endsWith(`.${ext}`))) continue;
2827
+ presentPaths.add(entryPath);
2828
+ if (!(entryPath in config.fsTree)) {
2829
+ if (entry.isSymbolicLink()) trackSymlinkFn?.(entryPath);
2830
+ handleWatchEvent(config, extensions, "add", entryPath, onEventFunc, onFinishFunc);
2831
+ }
2832
+ }
2833
+ const watchPrefix = watchPath + path8.sep;
2834
+ const firedDirPrefixes = [];
2835
+ for (const trackedPath of Object.keys(config.fsTree)) {
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 {
2844
+ handleWatchEvent(config, extensions, "unlink", trackedPath, onEventFunc, onFinishFunc);
2845
+ }
2846
+ }
2847
+ } catch {
2848
+ }
2849
+ }
2762
2850
  function mutateFSTree(fsTree, event, filePath) {
2763
2851
  if (event === "add") {
2764
2852
  fsTree[filePath] = null;
@@ -2771,18 +2859,35 @@ function mutateFSTree(fsTree, event, filePath) {
2771
2859
  }
2772
2860
  }
2773
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
+ }
2774
2878
  function colorEvent(event) {
2775
2879
  if (event === "change") return yellow("CHANGED:");
2776
2880
  if (event === "add" || event === "addDir") return green("ADDED:");
2777
2881
  return red("REMOVED:");
2778
2882
  }
2779
- 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;
2780
2884
  var init_file_watcher = __esm({
2781
2885
  "lib/setup/file-watcher.ts"() {
2782
2886
  init_color();
2783
2887
  CHANGE_DEDUPE_MS = 10;
2784
2888
  SYMLINK_POLL_INTERVAL_MS = 500;
2785
2889
  OVERLAYFS_RENAME_RETRY_MS = 50;
2890
+ RESCAN_INTERVAL_MS = 1e3;
2786
2891
  }
2787
2892
  });
2788
2893
 
@@ -3269,7 +3374,7 @@ var init_run = __esm({
3269
3374
  "lib/commands/run.ts"() {
3270
3375
  init_browser();
3271
3376
  init_chrome_prelaunch();
3272
- init_http();
3377
+ init_web();
3273
3378
  init_bind_server_to_port();
3274
3379
  init_web_server();
3275
3380
  init_open_output_in_browser();
@@ -3303,7 +3408,7 @@ init_color();
3303
3408
  var package_default = {
3304
3409
  name: "qunitx-cli",
3305
3410
  type: "module",
3306
- version: "0.21.1",
3411
+ version: "0.21.3",
3307
3412
  description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
3308
3413
  author: "Izel Nakri",
3309
3414
  license: "MIT",
@@ -3530,12 +3635,6 @@ function convertToPascalCase(str) {
3530
3635
  }
3531
3636
 
3532
3637
  // lib/commands/generate.ts
3533
- function pathToModuleName(filePath) {
3534
- const withoutExt = filePath.replace(/\.(js|ts)$/, "");
3535
- const segments = withoutExt.split("/");
3536
- const targetNames = segments[0] === "test" || segments[0] === "tests" ? segments.slice(1) : segments;
3537
- return targetNames.map(convertToPascalCase).join(" | ");
3538
- }
3539
3638
  async function generateTestFiles() {
3540
3639
  const projectRoot = await findProjectRoot();
3541
3640
  const moduleName = pathToModuleName(process.argv[3]);
@@ -3551,6 +3650,12 @@ async function generateTestFiles() {
3551
3650
  await fs5.writeFile(path10, testJSContent.replace("{{moduleName}}", moduleName));
3552
3651
  console.log(green(`${path10} written`));
3553
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
+ }
3554
3659
 
3555
3660
  // lib/setup/config.ts
3556
3661
  import fs7 from "node:fs/promises";
@@ -3558,28 +3663,6 @@ import fs7 from "node:fs/promises";
3558
3663
  // lib/setup/fs-tree.ts
3559
3664
  import fs6, { glob as fsGlob } from "node:fs/promises";
3560
3665
  import path3 from "node:path";
3561
- function isGlob(str) {
3562
- return /[*?{[]/.test(str);
3563
- }
3564
- async function readDirRecursive(dir, filter) {
3565
- const entries = await fs6.readdir(dir, { recursive: true, withFileTypes: true });
3566
- const candidates = entries.filter(
3567
- (dirent) => (dirent.isFile() || dirent.isSymbolicLink()) && filter(dirent.name)
3568
- );
3569
- const resolvedPaths = await Promise.all(
3570
- candidates.map(async (dirent) => {
3571
- const fullPath = path3.join(dirent.parentPath, dirent.name);
3572
- if (dirent.isFile()) return fullPath;
3573
- try {
3574
- const statResult = await fs6.stat(fullPath);
3575
- return statResult.isFile() ? fullPath : null;
3576
- } catch {
3577
- return null;
3578
- }
3579
- })
3580
- );
3581
- return resolvedPaths.filter((resolvedPath) => resolvedPath !== null);
3582
- }
3583
3666
  async function buildFSTree(fileAbsolutePaths, config = {}) {
3584
3667
  const targetExtensions = config.extensions || ["js", "ts"];
3585
3668
  const fsTree = {};
@@ -3613,57 +3696,65 @@ async function buildFSTree(fileAbsolutePaths, config = {}) {
3613
3696
  );
3614
3697
  return fsTree;
3615
3698
  }
3616
-
3617
- // lib/setup/test-file-paths.ts
3618
- import { matchesGlob } from "node:path";
3619
- function isGlob2(str) {
3699
+ function isGlob(str) {
3620
3700
  return /[*?{[]/.test(str);
3621
3701
  }
3622
- function setupTestFilePaths(_projectRoot, inputs2) {
3623
- const [folders, filesWithGlob, filesWithoutGlob] = inputs2.reduce(
3624
- (result2, input) => {
3625
- const glob = isGlob2(input);
3626
- if (!pathIsFile(input)) {
3627
- result2[0].push({ input, isFile: false, isGlob: glob });
3628
- } else {
3629
- 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;
3630
3716
  }
3631
- return result2;
3632
- },
3633
- [[], [], []]
3717
+ })
3634
3718
  );
3635
- const result = folders.reduce((folderResult, folder) => {
3636
- if (!pathIsIncludedInPaths(folders, folder)) {
3637
- folderResult.push(folder);
3638
- }
3639
- return folderResult;
3640
- }, []);
3641
- filesWithGlob.forEach((file) => {
3642
- if (!pathIsIncludedInPaths(result, file) && !pathIsIncludedInPaths(filesWithGlob, file)) {
3643
- 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 });
3644
3736
  }
3645
3737
  });
3646
- filesWithoutGlob.forEach((file) => {
3647
- if (!pathIsIncludedInPaths(result, file)) {
3648
- 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);
3649
3745
  }
3650
- });
3651
- return result.map((metaItem) => metaItem.input);
3746
+ return acc;
3747
+ }, []);
3748
+ return dedupedFolders.concat(dedupedGlobFiles, dedupedPlainFiles).map((meta) => meta.input);
3652
3749
  }
3653
3750
  function pathIsFile(path10) {
3654
- const inputs2 = path10.split("/");
3655
- return inputs2[inputs2.length - 1].includes(".");
3751
+ return path10.includes(".", path10.lastIndexOf("/") + 1);
3656
3752
  }
3657
- function pathIsIncludedInPaths(paths, targetPath) {
3658
- return paths.some((path10) => {
3659
- if (path10 === targetPath) {
3660
- return false;
3661
- }
3662
- return matchesGlob(targetPath.input, buildGlobFormat(path10));
3663
- });
3753
+ function isIncludedIn(paths, target) {
3754
+ return paths.some((path10) => path10 !== target && matchesGlob(target.input, path10.globFormat));
3664
3755
  }
3665
- function buildGlobFormat(path10) {
3666
- return path10.isFile ? path10.input : `${path10.input}/**`;
3756
+ function isGlob2(str) {
3757
+ return GLOB_CHARS.test(str);
3667
3758
  }
3668
3759
 
3669
3760
  // lib/utils/parse-cli-flags.ts
@@ -3766,7 +3857,7 @@ async function setupConfig() {
3766
3857
  ...cliConfigFlags,
3767
3858
  projectRoot,
3768
3859
  inputs: inputs2,
3769
- testFileLookupPaths: setupTestFilePaths(projectRoot, inputs2),
3860
+ testFileLookupPaths: setupTestFilePaths(inputs2),
3770
3861
  lastFailedTestFiles: null,
3771
3862
  lastRanTestFiles: null,
3772
3863
  COUNTER: {