qunitx-cli 0.19.1 → 0.19.2

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 +182 -106
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -385,21 +385,6 @@ var init_color = __esm({
385
385
  }
386
386
  });
387
387
 
388
- // lib/utils/path-exists.ts
389
- import fs2 from "node:fs/promises";
390
- async function pathExists(path7) {
391
- try {
392
- await fs2.access(path7);
393
- return true;
394
- } catch {
395
- return false;
396
- }
397
- }
398
- var init_path_exists = __esm({
399
- "lib/utils/path-exists.ts"() {
400
- }
401
- });
402
-
403
388
  // lib/utils/read-template.ts
404
389
  import fs3 from "node:fs/promises";
405
390
  import { dirname, join as join2 } from "node:path";
@@ -906,6 +891,7 @@ function setupWebServer(config, cachedContent) {
906
891
  cachedContent.mainHTML.filePath,
907
892
  config.projectRoot
908
893
  );
894
+ const runtimeScript = testRuntimeToInject(config);
909
895
  server.wss.on("connection", function connection(socket) {
910
896
  socket.on("message", function message(data) {
911
897
  const { event, details, qunitResult, abort } = JSON.parse(data);
@@ -957,7 +943,7 @@ function setupWebServer(config, cachedContent) {
957
943
  });
958
944
  server.get("/tests.js", (_req, res) => {
959
945
  const bytes = cachedContent.allTestCode?.length ?? null;
960
- process.stdout.write(
946
+ config.debug && process.stdout.write(
961
947
  `# [HTTPServer] GET /tests.js \u2192 ${bytes !== null ? `${bytes} bytes` : "NOT READY (allTestCode is null)"}
962
948
  `
963
949
  );
@@ -978,7 +964,7 @@ function setupWebServer(config, cachedContent) {
978
964
  });
979
965
  server.get("/filtered-tests.js", (_req, res) => {
980
966
  const bytes = cachedContent.filteredTestCode?.length ?? null;
981
- process.stdout.write(
967
+ config.debug && process.stdout.write(
982
968
  `# [HTTPServer] GET /filtered-tests.js \u2192 ${bytes !== null ? `${bytes} bytes` : "NOT READY (filteredTestCode is null)"}
983
969
  `
984
970
  );
@@ -1017,7 +1003,7 @@ function setupWebServer(config, cachedContent) {
1017
1003
  }
1018
1004
  const htmlContent = escapeAndInjectTestsToHTML(
1019
1005
  mainHTMLWithReplacedAssets,
1020
- testRuntimeToInject(config.port, config),
1006
+ runtimeScript,
1021
1007
  "./tests.js"
1022
1008
  );
1023
1009
  res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
@@ -1048,7 +1034,7 @@ function setupWebServer(config, cachedContent) {
1048
1034
  }
1049
1035
  const htmlContent = escapeAndInjectTestsToHTML(
1050
1036
  mainHTMLWithReplacedAssets,
1051
- testRuntimeToInject(config.port, config),
1037
+ runtimeScript,
1052
1038
  "./filtered-tests.js"
1053
1039
  );
1054
1040
  res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
@@ -1064,7 +1050,7 @@ function setupWebServer(config, cachedContent) {
1064
1050
  if (possibleDynamicHTML) {
1065
1051
  const htmlContent = escapeAndInjectTestsToHTML(
1066
1052
  possibleDynamicHTML,
1067
- testRuntimeToInject(config.port, config),
1053
+ runtimeScript,
1068
1054
  "/tests.js"
1069
1055
  );
1070
1056
  res.writeHead(200, { "Content-Type": "text/html", "Cache-Control": "no-store" });
@@ -1076,21 +1062,26 @@ function setupWebServer(config, cachedContent) {
1076
1062
  );
1077
1063
  }
1078
1064
  const url = req.url;
1079
- const requestStartedAt = /* @__PURE__ */ new Date();
1065
+ const requestStartedAt = Date.now();
1080
1066
  const filePath = (url.endsWith("/") ? [STATIC_FILES_PATH, url, "index.html"] : [STATIC_FILES_PATH, url]).join("");
1081
- const statusCode = await pathExists(filePath) ? 200 : 404;
1082
- res.writeHead(statusCode, {
1083
- "Content-Type": req.headers.accept?.includes("text/html") ? MIME_TYPES.html : MIME_TYPES[path4.extname(filePath).substring(1).toLowerCase()] || MIME_TYPES.html
1067
+ const contentType = req.headers.accept?.includes("text/html") ? MIME_TYPES.html : MIME_TYPES[path4.extname(filePath).substring(1).toLowerCase()] || MIME_TYPES.html;
1068
+ const stream = fs8.createReadStream(filePath);
1069
+ stream.on("open", () => {
1070
+ res.writeHead(200, { "Content-Type": contentType });
1071
+ stream.pipe(res);
1072
+ config.debug && process.stdout.write(
1073
+ `# [HTTPServer] GET ${url} 200 - ${Date.now() - requestStartedAt}ms
1074
+ `
1075
+ );
1084
1076
  });
1085
- if (statusCode === 404) {
1086
- res.end();
1087
- } else {
1088
- fs8.createReadStream(filePath).pipe(res);
1089
- }
1090
- process.stdout.write(
1091
- `# [HTTPServer] GET ${url} ${statusCode} - ${/* @__PURE__ */ new Date() - requestStartedAt}ms
1077
+ stream.on("error", () => {
1078
+ res.writeHead(404, { "Content-Type": contentType });
1079
+ res.end(contentType === MIME_TYPES.html ? NOT_FOUND_HTML : void 0);
1080
+ config.debug && process.stdout.write(
1081
+ `# [HTTPServer] GET ${url} 404 - ${Date.now() - requestStartedAt}ms
1092
1082
  `
1093
- );
1083
+ );
1084
+ });
1094
1085
  });
1095
1086
  return server;
1096
1087
  }
@@ -1102,7 +1093,7 @@ function replaceAssetPaths(html, htmlPath, projectRoot) {
1102
1093
  return result.replace(assetPath, normalizedFullAbsolutePath.replace(projectRoot, "."));
1103
1094
  }, html);
1104
1095
  }
1105
- function testRuntimeToInject(port, config) {
1096
+ function testRuntimeToInject(config) {
1106
1097
  return `<script>
1107
1098
  window.testTimeout = 0;
1108
1099
  setInterval(() => {
@@ -1138,7 +1129,7 @@ function testRuntimeToInject(port, config) {
1138
1129
 
1139
1130
  function setupWebSocket() {
1140
1131
  try {
1141
- window.socket = new WebSocket('ws://localhost:${port}');
1132
+ window.socket = new WebSocket(\`ws://localhost:\${location.port}\`);
1142
1133
  } catch (error) {
1143
1134
  console.log(error);
1144
1135
  retryOrFail();
@@ -1150,7 +1141,7 @@ function testRuntimeToInject(port, config) {
1150
1141
  // Notify Node.js that the WS socket is open. This fires immediately (< 1 s) because
1151
1142
  // this runtime script is tiny \u2014 tests.js background compilation hasn't finished yet.
1152
1143
  // Node.js uses this to distinguish "WS never connected" from "WS connected but bundle slow".
1153
- if (window.IS_PLAYWRIGHT) {
1144
+ if (navigator.webdriver) {
1154
1145
  window.socket.send(JSON.stringify({ event: 'wsOpen' }));
1155
1146
  }
1156
1147
  maybeStart();
@@ -1159,9 +1150,9 @@ function testRuntimeToInject(port, config) {
1159
1150
  retryOrFail();
1160
1151
  });
1161
1152
  window.socket.addEventListener('message', function(messageEvent) {
1162
- if (!window.IS_PLAYWRIGHT && messageEvent.data === 'refresh') {
1153
+ if (!navigator.webdriver && messageEvent.data === 'refresh') {
1163
1154
  window.location.reload(true);
1164
- } else if (window.IS_PLAYWRIGHT && messageEvent.data === 'abort') {
1155
+ } else if (navigator.webdriver && messageEvent.data === 'abort') {
1165
1156
  window.abortQUnit = true;
1166
1157
  window.QUnit.config.queue.length = 0;
1167
1158
  window.socket.send(JSON.stringify({ event: 'abort' }));
@@ -1204,7 +1195,7 @@ function testRuntimeToInject(port, config) {
1204
1195
 
1205
1196
  if (!window.QUnit) {
1206
1197
  console.log('QUnit not found after WebSocket connected');
1207
- if (window.IS_PLAYWRIGHT) {
1198
+ if (navigator.webdriver) {
1208
1199
  // Signal the Playwright runner that the run is complete with 0 tests rather than
1209
1200
  // waiting for the inactivity timeout. The runner treats totalTests === 0 as a
1210
1201
  // "no tests registered" warning (not a failure), so this gives a fast, clean result.
@@ -1217,7 +1208,7 @@ function testRuntimeToInject(port, config) {
1217
1208
  }
1218
1209
 
1219
1210
  window.QUnit.begin(() => { // NOTE: might be useful in future for hanged module tracking
1220
- if (window.IS_PLAYWRIGHT) {
1211
+ if (navigator.webdriver) {
1221
1212
  window.socket.send(JSON.stringify({ event: 'connection' }));
1222
1213
  }
1223
1214
  });
@@ -1230,8 +1221,10 @@ function testRuntimeToInject(port, config) {
1230
1221
  window.QUNIT_RESULT.finishedTests++;
1231
1222
  if (details.status === 'failed') window.QUNIT_RESULT.failedTests++;
1232
1223
  window.QUNIT_RESULT.currentTest = null;
1233
- if (window.IS_PLAYWRIGHT) {
1234
- window.socket.send(JSON.stringify({ event: 'testEnd', details: details, abort: window.abortQUnit }, getCircularReplacer()));
1224
+ if (navigator.webdriver) {
1225
+ const isFailed = details.status === 'failed';
1226
+ const payload = isFailed ? details : { status: details.status, fullName: details.fullName, runtime: details.runtime };
1227
+ window.socket.send(JSON.stringify({ event: 'testEnd', details: payload, abort: window.abortQUnit }, isFailed ? getCircularReplacer() : undefined));
1235
1228
 
1236
1229
  if (${config.failFast} && details.status === 'failed') {
1237
1230
  window.QUnit.config.queue.length = 0;
@@ -1239,7 +1232,7 @@ function testRuntimeToInject(port, config) {
1239
1232
  }
1240
1233
  });
1241
1234
  window.QUnit.done((details) => {
1242
- if (window.IS_PLAYWRIGHT) {
1235
+ if (navigator.webdriver) {
1243
1236
  window.socket.send(JSON.stringify({ event: 'done', details: details, qunitResult: window.QUNIT_RESULT, abort: window.abortQUnit }, getCircularReplacer()));
1244
1237
  // Do NOT set testTimeout here. The WS 'done' event (testsDone promise) is the
1245
1238
  // canonical completion signal for Playwright runs. waitForFunction is reserved
@@ -1330,7 +1323,7 @@ function buildNoTestsHTML(files) {
1330
1323
  </head>
1331
1324
  <body>
1332
1325
  <div id="qunit">
1333
- <h1 id="qunit-header">qunitx</h1>
1326
+ <h1 id="qunit-header"><a href="/" style="color:inherit;text-decoration:none">qunitx</a></h1>
1334
1327
  <h2 id="qunit-banner"></h2>
1335
1328
  <div id="qunit-userAgent">Warning: No Tests Registered</div>
1336
1329
  <ol id="qunit-tests">
@@ -1350,7 +1343,7 @@ function buildNoTestsHTML(files) {
1350
1343
  (function () {
1351
1344
  var retries = 0;
1352
1345
  function connect() {
1353
- var ws = new WebSocket('ws://' + location.hostname + ':' + location.port);
1346
+ var ws = new WebSocket(\`ws://\${location.hostname}:\${location.port}\`);
1354
1347
  ws.addEventListener('message', function (e) { if (e.data === 'refresh') location.reload(true); });
1355
1348
  ws.addEventListener('close', function () { if (retries++ < 120) setTimeout(connect, 1000); });
1356
1349
  ws.addEventListener('error', function () { ws.close(); });
@@ -1432,7 +1425,7 @@ function buildErrorHTML(buildError) {
1432
1425
  </head>
1433
1426
  <body>
1434
1427
  <div id="qunit">
1435
- <h1 id="qunit-header">qunitx</h1>
1428
+ <h1 id="qunit-header"><a href="/" style="color:inherit;text-decoration:none">qunitx</a></h1>
1436
1429
  <h2 id="qunit-banner"></h2>
1437
1430
  <div id="qunit-userAgent">Build Error: ${buildError.type}</div>
1438
1431
  <ol id="qunit-tests">
@@ -1452,7 +1445,7 @@ function buildErrorHTML(buildError) {
1452
1445
  (function () {
1453
1446
  var retries = 0;
1454
1447
  function connect() {
1455
- var ws = new WebSocket('ws://' + location.hostname + ':' + location.port);
1448
+ var ws = new WebSocket(\`ws://\${location.hostname}:\${location.port}\`);
1456
1449
  ws.addEventListener('message', function (e) { if (e.data === 'refresh') location.reload(true); });
1457
1450
  ws.addEventListener('close', function () { if (retries++ < 120) setTimeout(connect, 1000); });
1458
1451
  ws.addEventListener('error', function () { ws.close(); });
@@ -1464,16 +1457,42 @@ function buildErrorHTML(buildError) {
1464
1457
  </body>
1465
1458
  </html>`;
1466
1459
  }
1467
- var fsPromise;
1460
+ var fsPromise, NOT_FOUND_HTML;
1468
1461
  var init_web_server = __esm({
1469
1462
  "lib/setup/web-server.ts"() {
1470
1463
  init_find_internal_assets_from_html();
1471
1464
  init_html();
1472
1465
  init_display_test_result();
1473
1466
  init_color();
1474
- init_path_exists();
1475
1467
  init_http();
1476
1468
  fsPromise = fs8.promises;
1469
+ NOT_FOUND_HTML = `<!DOCTYPE html>
1470
+ <html lang="en">
1471
+ <head>
1472
+ <meta charset="utf-8">
1473
+ <meta name="viewport" content="width=device-width">
1474
+ <title>404 Not Found \u2014 qunitx</title>
1475
+ <style>
1476
+ *{box-sizing:border-box;margin:0;padding:0}
1477
+ body{font-family:"Helvetica Neue Light","HelveticaNeue-Light","Helvetica Neue",Calibri,Helvetica,Arial,sans-serif}
1478
+ #qunit-header{padding:.5em 0 .5em 1em;color:#C2CCD1;background-color:#0D3349;font-size:1.5em;line-height:1em;font-weight:400;border-radius:5px 5px 0 0}
1479
+ #qunit-banner{height:5px;background-color:#EE5757}
1480
+ #qunit-userAgent{padding:.5em 1em;color:#fff;background-color:#2B81AF;text-shadow:rgba(0,0,0,.5) 2px 2px 1px;font-size:small}
1481
+ #qunit-tests{list-style:none;font-size:smaller}
1482
+ #qunit-tests li{display:list-item;padding:.4em 1em;color:#000;background-color:#EE5757;border-radius:0 0 5px 5px}
1483
+ </style>
1484
+ </head>
1485
+ <body>
1486
+ <div id="qunit">
1487
+ <h1 id="qunit-header"><a href="/" style="color:inherit;text-decoration:none">qunitx</a></h1>
1488
+ <h2 id="qunit-banner"></h2>
1489
+ <div id="qunit-userAgent">404 Not Found</div>
1490
+ <ol id="qunit-tests">
1491
+ <li id="qunit-testresult"><script>document.getElementById('qunit-testresult').prepend(location.pathname)</script> was not found on this server.</li>
1492
+ </ol>
1493
+ </div>
1494
+ </body>
1495
+ </html>`;
1477
1496
  }
1478
1497
  });
1479
1498
 
@@ -1535,9 +1554,22 @@ async function setupBrowser(config, cachedContent, existingBrowser = null) {
1535
1554
  const getPage = isHeadedWatchMode ? () => browser.contexts()[0]?.pages()[0] ?? browser.newPage() : () => browser.newPage();
1536
1555
  const [page] = await Promise.all([getPage(), bindServerToPort(server, config)]);
1537
1556
  perfLog(`browser.js: newPage + bindServerToPort took ${Date.now() - pageStart}ms`);
1538
- await page.addInitScript(() => {
1539
- window.IS_PLAYWRIGHT = true;
1540
- });
1557
+ if (config.browser === "firefox") {
1558
+ await page.addInitScript(() => {
1559
+ const preSerialize = (arg) => {
1560
+ if (arg === null || typeof arg !== "object") return arg;
1561
+ try {
1562
+ return JSON.stringify(arg, (_key, v) => v instanceof Date ? v.toISOString() : v);
1563
+ } catch {
1564
+ return String(arg);
1565
+ }
1566
+ };
1567
+ ["log", "warn", "error", "info", "debug"].forEach((method) => {
1568
+ const orig = console[method].bind(console);
1569
+ console[method] = (...args) => orig(...args.map(preSerialize));
1570
+ });
1571
+ });
1572
+ }
1541
1573
  config._pendingConsoleHandlers = /* @__PURE__ */ new Set();
1542
1574
  page.on("console", (msg) => {
1543
1575
  const type = msg.type();
@@ -1545,11 +1577,7 @@ async function setupBrowser(config, cachedContent, existingBrowser = null) {
1545
1577
  if (!alwaysShow && !config.debug) return;
1546
1578
  const handler = (async () => {
1547
1579
  try {
1548
- const values = await Promise.all(
1549
- msg.args().map(
1550
- (arg) => arg.jsonValue().catch(() => arg.evaluate((v) => JSON.stringify(v)).then(JSON.parse))
1551
- )
1552
- );
1580
+ const values = await Promise.all(msg.args().map((arg) => arg.jsonValue()));
1553
1581
  console.log(...values);
1554
1582
  } catch {
1555
1583
  console.log(msg.text());
@@ -2346,7 +2374,9 @@ var init_write_output_static_files = __esm({
2346
2374
  // lib/commands/run.ts
2347
2375
  var run_exports = {};
2348
2376
  __export(run_exports, {
2377
+ computeFileTimes: () => computeFileTimes,
2349
2378
  default: () => run,
2379
+ readTimingCache: () => readTimingCache,
2350
2380
  run: () => run
2351
2381
  });
2352
2382
  import fs12 from "node:fs/promises";
@@ -2422,7 +2452,8 @@ async function run(config) {
2422
2452
  } else {
2423
2453
  const allFiles = Object.keys(config.fsTree);
2424
2454
  const groupCount = Math.min(allFiles.length, availableParallelism());
2425
- const groups = await splitIntoGroups(allFiles, groupCount);
2455
+ const timings = await readTimingCache(config.projectRoot);
2456
+ const { groups, weights } = await splitIntoGroups(allFiles, groupCount, timings);
2426
2457
  config.COUNTER = {
2427
2458
  testCount: 0,
2428
2459
  failCount: 0,
@@ -2461,6 +2492,7 @@ async function run(config) {
2461
2492
  void openOutputInBrowser(config);
2462
2493
  }
2463
2494
  const TIME_COUNTER = timeCounter();
2495
+ const wallTimes = /* @__PURE__ */ new Map();
2464
2496
  const GROUP_TIMEOUT_MS = 3 * 60 * 1e3;
2465
2497
  const keepAlive = setInterval(() => {
2466
2498
  }, 1e3);
@@ -2480,35 +2512,36 @@ async function run(config) {
2480
2512
  }, GROUP_TIMEOUT_MS);
2481
2513
  timeoutId.unref();
2482
2514
  });
2483
- return Promise.race([
2484
- (async () => {
2485
- groupConfig._phase = "connecting";
2486
- const connections = await setupBrowser(groupConfig, groupCachedContents[i], browser);
2487
- groupConfig.expressApp = connections.server;
2488
- if (config.before) {
2489
- await runUserModule(`${process.cwd()}/${config.before}`, groupConfig, "before");
2490
- }
2491
- try {
2492
- await runTestsInBrowser(groupConfig, groupCachedContents[i], connections);
2493
- } finally {
2494
- await flushConsoleHandlers(groupConfig._pendingConsoleHandlers);
2495
- await Promise.all([
2496
- connections.server && connections.server.close(),
2497
- connections.page && // Unref'd: the keepAlive interval above holds the event loop open, so this
2498
- // timer still fires if page.close() hangs, without preventing process exit later.
2499
- Promise.race([
2500
- connections.page.close(),
2501
- new Promise((resolve) => {
2502
- const pageCloseTimeoutId = setTimeout(resolve, 1e4);
2503
- pageCloseTimeoutId.unref();
2504
- })
2505
- ]).catch(() => {
2515
+ const startMs = Date.now();
2516
+ const work = (async () => {
2517
+ groupConfig._phase = "connecting";
2518
+ const connections = await setupBrowser(groupConfig, groupCachedContents[i], browser);
2519
+ groupConfig.expressApp = connections.server;
2520
+ if (config.before) {
2521
+ await runUserModule(`${process.cwd()}/${config.before}`, groupConfig, "before");
2522
+ }
2523
+ try {
2524
+ await runTestsInBrowser(groupConfig, groupCachedContents[i], connections);
2525
+ } finally {
2526
+ await flushConsoleHandlers(groupConfig._pendingConsoleHandlers);
2527
+ await Promise.all([
2528
+ connections.server && connections.server.close(),
2529
+ connections.page && // Unref'd: the keepAlive interval above holds the event loop open, so this
2530
+ // timer still fires if page.close() hangs, without preventing process exit later.
2531
+ Promise.race([
2532
+ connections.page.close(),
2533
+ new Promise((resolve) => {
2534
+ const pageCloseTimeoutId = setTimeout(resolve, 1e4);
2535
+ pageCloseTimeoutId.unref();
2506
2536
  })
2507
- ]);
2508
- }
2509
- })(),
2510
- groupTimeout
2511
- ]);
2537
+ ]).catch(() => {
2538
+ })
2539
+ ]);
2540
+ }
2541
+ })();
2542
+ const record = () => wallTimes.set(i, Date.now() - startMs);
2543
+ work.then(record, record);
2544
+ return Promise.race([work, groupTimeout]);
2512
2545
  })
2513
2546
  );
2514
2547
  const exitCode = groupResults.reduce(
@@ -2527,6 +2560,10 @@ async function run(config) {
2527
2560
  );
2528
2561
  }
2529
2562
  TAPDisplayFinalResult(config.COUNTER, TIME_COUNTER.stop());
2563
+ const fileTimes = computeFileTimes(groups, weights, wallTimes);
2564
+ persistTimings(fileTimes, config.projectRoot).catch(() => {
2565
+ });
2566
+ printFileTimings(fileTimes, config.projectRoot);
2530
2567
  if (config.after) {
2531
2568
  await runUserModule(`${process.cwd()}/${config.after}`, config.COUNTER, "after");
2532
2569
  }
@@ -2597,24 +2634,57 @@ async function addCachedContentMainHTML(projectRoot, cachedContent) {
2597
2634
  }
2598
2635
  return cachedContent;
2599
2636
  }
2600
- async function splitIntoGroups(files, groupCount) {
2601
- const withSizes = await Promise.all(
2637
+ async function readTimingCache(projectRoot) {
2638
+ try {
2639
+ const parsed = JSON.parse(await fs12.readFile(`${projectRoot}/tmp/test-timings.json`, "utf8"));
2640
+ return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : {};
2641
+ } catch {
2642
+ return {};
2643
+ }
2644
+ }
2645
+ function computeFileTimes(groups, weights, wallTimes) {
2646
+ const result = /* @__PURE__ */ new Map();
2647
+ groups.forEach((group, i) => {
2648
+ const wallMs = wallTimes.get(i);
2649
+ if (wallMs === void 0) return;
2650
+ const total = group.reduce((sum, f) => sum + (weights.get(f) ?? 0), 0);
2651
+ group.forEach(
2652
+ (f) => result.set(f, total > 0 ? wallMs * ((weights.get(f) ?? 0) / total) : wallMs / group.length)
2653
+ );
2654
+ });
2655
+ return result;
2656
+ }
2657
+ async function persistTimings(fileTimes, projectRoot) {
2658
+ await fs12.writeFile(
2659
+ `${projectRoot}/tmp/test-timings.json`,
2660
+ JSON.stringify(Object.fromEntries(fileTimes), null, 2)
2661
+ );
2662
+ }
2663
+ function printFileTimings(fileTimes, projectRoot) {
2664
+ if (fileTimes.size === 0) return;
2665
+ const lines = [...fileTimes.entries()].sort(([, a], [, b]) => b - a).map(([f, ms]) => `# ${ms.toFixed(0)}ms ${f.replace(`${projectRoot}/`, "")}`);
2666
+ process.stdout.write(`# File execution times:
2667
+ ${lines.join("\n")}
2668
+ `);
2669
+ }
2670
+ async function splitIntoGroups(files, groupCount, timings) {
2671
+ const sizes = await Promise.all(
2602
2672
  files.map(
2603
- (f) => fs12.stat(f).then(({ size }) => ({ f, size })).catch(() => ({ f, size: 0 }))
2673
+ (f) => fs12.stat(f).then((s) => s.size).catch(() => 0)
2604
2674
  )
2605
2675
  );
2606
- return withSizes.sort((a, b) => b.size - a.size).reduce(
2607
- (groups, { f, size }) => {
2608
- const { idx } = groups.reduce(
2609
- (min, { total }, i) => total < min.total ? { idx: i, total } : min,
2610
- { idx: 0, total: groups[0].total }
2611
- );
2612
- groups[idx].files.push(f);
2613
- groups[idx].total += size;
2614
- return groups;
2615
- },
2616
- Array.from({ length: groupCount }, () => ({ files: [], total: 0 }))
2617
- ).flatMap((g) => g.files.length > 0 ? [g.files] : []);
2676
+ const knownRates = files.map((f, i) => ({ ms: timings[f], size: sizes[i] })).filter(({ ms, size }) => ms > 0 && size > 0);
2677
+ const msPerByte = knownRates.length > 0 ? knownRates.reduce((sum, { ms, size }) => sum + ms / size, 0) / knownRates.length : 1;
2678
+ const weights = new Map(
2679
+ files.map((f, i) => [f, timings[f] > 0 ? timings[f] : sizes[i] * msPerByte])
2680
+ );
2681
+ const buckets = Array.from({ length: groupCount }, () => ({ files: [], total: 0 }));
2682
+ [...files].sort((a, b) => (weights.get(b) ?? 0) - (weights.get(a) ?? 0)).forEach((f) => {
2683
+ const min = buckets.reduce((m, _, i) => buckets[i].total < buckets[m].total ? i : m, 0);
2684
+ buckets[min].files.push(f);
2685
+ buckets[min].total += weights.get(f) ?? 0;
2686
+ });
2687
+ return { groups: buckets.filter((b) => b.files.length > 0).map((b) => b.files), weights };
2618
2688
  }
2619
2689
  function logWatcherAndKeyboardShortcutInfo(config, _server) {
2620
2690
  const prefix = "Watching files...";
@@ -2663,7 +2733,7 @@ init_color();
2663
2733
  var package_default = {
2664
2734
  name: "qunitx-cli",
2665
2735
  type: "module",
2666
- version: "0.19.1",
2736
+ version: "0.19.2",
2667
2737
  description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
2668
2738
  author: "Izel Nakri",
2669
2739
  license: "MIT",
@@ -2779,8 +2849,18 @@ import path2 from "node:path";
2779
2849
  // lib/utils/find-project-root.ts
2780
2850
  import process2 from "node:process";
2781
2851
 
2852
+ // lib/utils/path-exists.ts
2853
+ import fs2 from "node:fs/promises";
2854
+ async function pathExists(path7) {
2855
+ try {
2856
+ await fs2.access(path7);
2857
+ return true;
2858
+ } catch {
2859
+ return false;
2860
+ }
2861
+ }
2862
+
2782
2863
  // lib/utils/search-in-parent-directories.ts
2783
- init_path_exists();
2784
2864
  async function searchInParentDirectories(directory, targetEntry) {
2785
2865
  const resolvedDirectory = directory === "." ? process.cwd() : directory;
2786
2866
  if (await pathExists(`${resolvedDirectory}/${targetEntry}`)) {
@@ -2808,9 +2888,6 @@ async function findProjectRoot() {
2808
2888
  }
2809
2889
  }
2810
2890
 
2811
- // lib/commands/init.ts
2812
- init_path_exists();
2813
-
2814
2891
  // lib/setup/default-project-config-values.ts
2815
2892
  var defaultProjectConfigValues = {
2816
2893
  output: "tmp",
@@ -2877,7 +2954,6 @@ async function writeTSConfigIfNeeded(projectRoot) {
2877
2954
  // lib/commands/generate.ts
2878
2955
  init_color();
2879
2956
  import fs5 from "node:fs/promises";
2880
- init_path_exists();
2881
2957
  init_read_template();
2882
2958
 
2883
2959
  // lib/utils/convert-to-pascal-case.ts
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "qunitx-cli",
3
3
  "type": "module",
4
- "version": "0.19.1",
4
+ "version": "0.19.2",
5
5
  "description": "Browser runner for QUnitx: run your qunitx tests in google-chrome",
6
6
  "author": "Izel Nakri",
7
7
  "license": "MIT",