qunitx-cli 0.17.7 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +178 -133
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -301,16 +301,16 @@ var init_perf_logger = __esm({
301
301
  }
302
302
  });
303
303
 
304
- // lib/utils/early-chrome.ts
305
- async function shutdownEarlyBrowser() {
304
+ // lib/utils/chrome-prelaunch.ts
305
+ async function shutdownPrelaunch() {
306
306
  if (!earlyChrome) return;
307
307
  const { shutdown } = earlyChrome;
308
308
  earlyChrome = null;
309
309
  await shutdown();
310
310
  }
311
- var NON_RUN_COMMANDS, isRunCommand, browserFromArgv, openFromArgv, watchFromArgv, openWatchMode, earlyChrome, earlyBrowserPromise;
312
- var init_early_chrome = __esm({
313
- "lib/utils/early-chrome.ts"() {
311
+ var NON_RUN_COMMANDS, isRunCommand, browserFromArgv, openFromArgv, watchFromArgv, openWatchMode, earlyChrome, prelaunchPromise;
312
+ var init_chrome_prelaunch = __esm({
313
+ "lib/utils/chrome-prelaunch.ts"() {
314
314
  init_find_chrome();
315
315
  init_pre_launch_chrome();
316
316
  init_kill_process_group();
@@ -335,12 +335,12 @@ var init_early_chrome = __esm({
335
335
  killProcessGroup(earlyChrome.proc.pid);
336
336
  });
337
337
  }
338
- perfLog("early-chrome.js: module evaluated");
339
- earlyBrowserPromise = isRunCommand && browserFromArgv === "chromium" ? findChrome().then((chromePath) => {
340
- perfLog("early-chrome.js: findChrome resolved", chromePath);
338
+ perfLog("chrome-prelaunch.ts: module evaluated");
339
+ prelaunchPromise = isRunCommand && browserFromArgv === "chromium" ? findChrome().then((chromePath) => {
340
+ perfLog("chrome-prelaunch.ts: findChrome resolved", chromePath);
341
341
  return preLaunchChrome(chromePath, CHROMIUM_ARGS, !openWatchMode);
342
342
  }).then((info) => {
343
- perfLog("early-chrome.js: Chrome CDP ready", info?.cdpEndpoint ?? null);
343
+ perfLog("chrome-prelaunch.ts: Chrome CDP ready", info?.cdpEndpoint ?? null);
344
344
  if (info) earlyChrome = info;
345
345
  return info;
346
346
  }) : Promise.resolve(null);
@@ -400,11 +400,11 @@ var init_path_exists = __esm({
400
400
  }
401
401
  });
402
402
 
403
- // lib/utils/read-boilerplate.ts
403
+ // lib/utils/read-template.ts
404
404
  import fs3 from "node:fs/promises";
405
405
  import { dirname, join as join2 } from "node:path";
406
406
  import { fileURLToPath } from "node:url";
407
- async function readBoilerplate(relativePath) {
407
+ async function readTemplate(relativePath) {
408
408
  const sea = await import("node:sea").catch(() => null);
409
409
  if (sea?.isSea()) return sea.getAsset(relativePath, "utf8");
410
410
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -418,8 +418,8 @@ async function readBoilerplate(relativePath) {
418
418
  `qunitx-cli: template "${relativePath}" not found \u2014 try reinstalling the package.`
419
419
  );
420
420
  }
421
- var init_read_boilerplate = __esm({
422
- "lib/utils/read-boilerplate.ts"() {
421
+ var init_read_template = __esm({
422
+ "lib/utils/read-template.ts"() {
423
423
  }
424
424
  });
425
425
 
@@ -438,19 +438,19 @@ var init_find_internal_assets_from_html = __esm({
438
438
  }
439
439
  });
440
440
 
441
- // lib/utils/html-content-marker.ts
442
- function findHTMLContentMarker(html) {
443
- return html.includes(HTML_CONTENT_MARKER) ? HTML_CONTENT_MARKER : void 0;
441
+ // lib/utils/html.ts
442
+ function findScriptPlaceholder(html) {
443
+ return html.includes(SCRIPT_PLACEHOLDER) ? SCRIPT_PLACEHOLDER : void 0;
444
444
  }
445
- function htmlHasDynamicContentMarker(html) {
446
- return !!findHTMLContentMarker(html) || HANDLEBARS_TOKEN_REGEX.test(html);
445
+ function isCustomTemplate(html) {
446
+ return !!findScriptPlaceholder(html) || HANDLEBARS_TOKEN_REGEX.test(html);
447
447
  }
448
- function replaceHTMLContentMarker(html, content) {
449
- const marker = findHTMLContentMarker(html);
450
- if (marker) {
451
- return html.replace(marker, content);
448
+ function injectScript(html, content) {
449
+ const placeholder = findScriptPlaceholder(html);
450
+ if (placeholder) {
451
+ return html.replace(placeholder, content);
452
452
  }
453
- if (htmlHasDynamicContentMarker(html)) {
453
+ if (isCustomTemplate(html)) {
454
454
  if (html.includes("</body>")) {
455
455
  return html.replace("</body>", `${content}</body>`);
456
456
  }
@@ -461,10 +461,10 @@ function replaceHTMLContentMarker(html, content) {
461
461
  }
462
462
  return html;
463
463
  }
464
- var HTML_CONTENT_MARKER, HANDLEBARS_TOKEN_REGEX;
465
- var init_html_content_marker = __esm({
466
- "lib/utils/html-content-marker.ts"() {
467
- HTML_CONTENT_MARKER = "{{qunitxScript}}";
464
+ var SCRIPT_PLACEHOLDER, HANDLEBARS_TOKEN_REGEX;
465
+ var init_html = __esm({
466
+ "lib/utils/html.ts"() {
467
+ SCRIPT_PLACEHOLDER = "{{qunitxScript}}";
468
468
  HANDLEBARS_TOKEN_REGEX = /{{\s*[^}]+\s*}}/;
469
469
  }
470
470
  });
@@ -488,12 +488,18 @@ function dumpValue(value, indent) {
488
488
  if (Array.isArray(value)) {
489
489
  if (value.length === 0) return "[]";
490
490
  const next2 = `${indent} `;
491
- return "\n" + value.map((item) => `${next2}- ${dumpValue(item, next2)}`).join("\n");
491
+ return "\n" + value.map((item) => {
492
+ const v = dumpValue(item, next2);
493
+ return v[0] === "\n" ? `${next2}-${v}` : `${next2}- ${v}`;
494
+ }).join("\n");
492
495
  }
493
496
  const entries = Object.entries(value);
494
497
  if (entries.length === 0) return "{}";
495
498
  const next = `${indent} `;
496
- return "\n" + entries.map(([entryKey, entryValue]) => `${next}${entryKey}: ${dumpValue(entryValue, next)}`).join("\n");
499
+ return "\n" + entries.map(([entryKey, entryValue]) => {
500
+ const v = dumpValue(entryValue, next);
501
+ return v[0] === "\n" ? `${next}${entryKey}:${v}` : `${next}${entryKey}: ${v}`;
502
+ }).join("\n");
497
503
  }
498
504
  function yamlLine(key, value) {
499
505
  const serialized = dumpValue(value, "");
@@ -510,12 +516,12 @@ function dumpYaml({
510
516
  at
511
517
  }) {
512
518
  return `name: ${dumpString(name, "")}
513
- ` + yamlLine("actual", actual) + yamlLine("expected", expected) + yamlLine("message", message) + yamlLine("stack", stack) + yamlLine("at", at);
519
+ ` + yamlLine("actual", actual) + yamlLine("expected", expected) + (message !== null ? yamlLine("message", message) : "") + (stack !== null ? yamlLine("stack", stack) : "") + (at !== null ? yamlLine("at", at) : "");
514
520
  }
515
521
  var NEEDS_QUOTING;
516
522
  var init_dump_yaml = __esm({
517
523
  "lib/tap/dump-yaml.ts"() {
518
- NEEDS_QUOTING = /^$|^(null|true|false|~|yes|no|on|off|y|n)$|^[{[!|>'"#%@`]|^[-?:](\s|$)|^---|^[-+]?(\d|\.\d)|^\d{4}-\d{2}-\d{2}|: |#/i;
524
+ NEEDS_QUOTING = /^$|^\s|^(null|true|false|~|yes|no|on|off|y|n)$|^[{[!|>'"#%@`]|^[-?:](\s|$)|^---|^[-+]?(\d|\.\d)|^\d{4}-\d{2}-\d{2}|: |#/i;
519
525
  }
520
526
  });
521
527
 
@@ -538,46 +544,55 @@ function TAPDisplayTestResult(COUNTER, details) {
538
544
  COUNTER.testCount++;
539
545
  if (details.status === "skipped") {
540
546
  COUNTER.skipCount++;
541
- console.log(`ok ${COUNTER.testCount}`, details.fullName.join(" | "), "# skip");
547
+ process.stdout.write(`ok ${COUNTER.testCount} ${details.fullName.join(" | ")} # skip
548
+ `);
542
549
  } else if (details.status === "todo") {
543
- console.log(`not ok ${COUNTER.testCount}`, details.fullName.join(" | "), "# skip");
550
+ process.stdout.write(`not ok ${COUNTER.testCount} ${details.fullName.join(" | ")} # skip
551
+ `);
544
552
  } else if (details.status === "failed") {
545
553
  COUNTER.failCount++;
546
- console.log(
547
- `not ok ${COUNTER.testCount}`,
548
- details.fullName.join(" | "),
549
- `# (${details.runtime.toFixed(0)} ms)`
554
+ process.stdout.write(
555
+ `not ok ${COUNTER.testCount} ${details.fullName.join(" | ")} # (${details.runtime.toFixed(0)} ms)
556
+ `
550
557
  );
551
558
  details.assertions.forEach((assertion, index) => {
552
559
  if (!assertion.passed && assertion.todo === false) {
553
560
  COUNTER.errorCount = (COUNTER.errorCount ?? 0) + 1;
554
- const stack = assertion.stack?.match(/\(.+\)/g);
555
- console.log(" ---");
556
- console.log(
561
+ process.stdout.write(" ---\n");
562
+ process.stdout.write(
557
563
  indentString(
558
564
  dumpYaml({
559
565
  name: `Assertion #${index + 1}`,
560
- actual: assertion.actual ? JSON.parse(JSON.stringify(assertion.actual, getCircularReplacer())) : assertion.actual,
561
- expected: assertion.expected ? JSON.parse(JSON.stringify(assertion.expected, getCircularReplacer())) : assertion.expected,
566
+ actual: assertion.actual !== null && typeof assertion.actual === "object" ? JSON.parse(JSON.stringify(assertion.actual, getCircularReplacer())) : assertion.actual,
567
+ expected: assertion.expected !== null && typeof assertion.expected === "object" ? JSON.parse(JSON.stringify(assertion.expected, getCircularReplacer())) : assertion.expected,
562
568
  message: assertion.message || null,
563
- stack: assertion.stack || null,
564
- at: stack ? stack[0].replace("(file://", "").replace(")", "") : null
569
+ // Trim leading/trailing whitespace: Chrome stacks start with " at ..."
570
+ // (4 spaces per frame) which would otherwise render as "stack: at ..." in YAML.
571
+ stack: assertion.stack?.trim() || null,
572
+ at: extractStackAt(assertion.stack)
565
573
  }),
566
574
  4
567
575
  )
568
576
  );
569
- console.log(" ...");
577
+ process.stdout.write(" ...\n");
570
578
  }
571
579
  });
572
580
  } else if (details.status === "passed") {
573
581
  COUNTER.passCount++;
574
- console.log(
575
- `ok ${COUNTER.testCount}`,
576
- details.fullName.join(" | "),
577
- `# (${details.runtime.toFixed(0)} ms)`
582
+ process.stdout.write(
583
+ `ok ${COUNTER.testCount} ${details.fullName.join(" | ")} # (${details.runtime.toFixed(0)} ms)
584
+ `
578
585
  );
579
586
  }
580
587
  }
588
+ function extractStackAt(stack) {
589
+ if (!stack) return null;
590
+ const chromeMatch = stack.match(/\(([^)\n]+:[0-9]+:[0-9]+)\)/);
591
+ if (chromeMatch) return chromeMatch[1].replace("file://", "");
592
+ const geckoMatch = stack.match(/@([^\s\n@]+:[0-9]+:[0-9]+)/);
593
+ if (geckoMatch) return geckoMatch[1];
594
+ return null;
595
+ }
581
596
  function getCircularReplacer() {
582
597
  const ancestors = [];
583
598
  return function(_key, value) {
@@ -627,8 +642,8 @@ async function bindServerToPort(server, config) {
627
642
  var EXPLICIT_PORT_RETRIES, EXPLICIT_PORT_RETRY_DELAY_MS;
628
643
  var init_bind_server_to_port = __esm({
629
644
  "lib/setup/bind-server-to-port.ts"() {
630
- EXPLICIT_PORT_RETRIES = 5;
631
- EXPLICIT_PORT_RETRY_DELAY_MS = 20;
645
+ EXPLICIT_PORT_RETRIES = 20;
646
+ EXPLICIT_PORT_RETRY_DELAY_MS = 50;
632
647
  }
633
648
  });
634
649
 
@@ -898,7 +913,7 @@ function setupWebServer(config, cachedContent) {
898
913
  config._onWsOpen?.();
899
914
  } else if (event === "connection") {
900
915
  config._phase = "running";
901
- if (!config._groupMode) console.log("TAP version 13");
916
+ if (!config._groupMode) process.stdout.write("TAP version 13\n");
902
917
  if (config.debug && config._groupMode) {
903
918
  const allFiles = Object.keys(config.fsTree);
904
919
  const relFiles = allFiles.map(
@@ -907,7 +922,8 @@ function setupWebServer(config, cachedContent) {
907
922
  const shown = relFiles.slice(0, 2);
908
923
  const rest = relFiles.length - shown.length;
909
924
  const fileList = rest > 0 ? `${shown.join(" ")} +${rest} more` : shown.join(" ");
910
- console.log("#", blue(`\u2500\u2500 ${fileList} \u2500\u2500`));
925
+ process.stdout.write(`# ${blue(`\u2500\u2500 ${fileList} \u2500\u2500`)}
926
+ `);
911
927
  }
912
928
  config._resetTestTimeout?.();
913
929
  } else if (event === "testEnd" && !abort) {
@@ -915,8 +931,9 @@ function setupWebServer(config, cachedContent) {
915
931
  config.lastFailedTestFiles = config.lastRanTestFiles;
916
932
  }
917
933
  if (config.debug && details.runtime > config.timeout * 0.8) {
918
- console.log(
919
- `# SLOW (${details.runtime.toFixed(0)}ms / ${config.timeout}ms timeout): ${details.fullName.join(" | ")}`
934
+ process.stdout.write(
935
+ `# SLOW (${details.runtime.toFixed(0)}ms / ${config.timeout}ms timeout): ${details.fullName.join(" | ")}
936
+ `
920
937
  );
921
938
  }
922
939
  config._resetTestTimeout?.();
@@ -924,8 +941,9 @@ function setupWebServer(config, cachedContent) {
924
941
  } else if (event === "done") {
925
942
  config._phase = "done";
926
943
  if (config.debug && config._groupMode) {
927
- console.log(
928
- `# group done: ${details.passed} passed, ${details.failed} failed (${details.runtime}ms)`
944
+ process.stdout.write(
945
+ `# group done: ${details.passed} passed, ${details.failed} failed (${details.runtime}ms)
946
+ `
929
947
  );
930
948
  }
931
949
  if (typeof config._testRunDone === "function") {
@@ -937,8 +955,9 @@ function setupWebServer(config, cachedContent) {
937
955
  });
938
956
  server.get("/tests.js", (_req, res) => {
939
957
  const bytes = cachedContent.allTestCode?.length ?? null;
940
- console.log(
941
- `# [HTTPServer] GET /tests.js \u2192 ${bytes !== null ? `${bytes} bytes` : "NOT READY (allTestCode is null)"}`
958
+ process.stdout.write(
959
+ `# [HTTPServer] GET /tests.js \u2192 ${bytes !== null ? `${bytes} bytes` : "NOT READY (allTestCode is null)"}
960
+ `
942
961
  );
943
962
  if (bytes === null) {
944
963
  res.writeHead(503, { "Content-Type": "application/javascript", "Cache-Control": "no-store" });
@@ -953,8 +972,9 @@ function setupWebServer(config, cachedContent) {
953
972
  });
954
973
  server.get("/filtered-tests.js", (_req, res) => {
955
974
  const bytes = cachedContent.filteredTestCode?.length ?? null;
956
- console.log(
957
- `# [HTTPServer] GET /filtered-tests.js \u2192 ${bytes !== null ? `${bytes} bytes` : "NOT READY (filteredTestCode is null)"}`
975
+ process.stdout.write(
976
+ `# [HTTPServer] GET /filtered-tests.js \u2192 ${bytes !== null ? `${bytes} bytes` : "NOT READY (filteredTestCode is null)"}
977
+ `
958
978
  );
959
979
  if (bytes === null) {
960
980
  res.writeHead(503, { "Content-Type": "application/javascript", "Cache-Control": "no-store" });
@@ -1026,7 +1046,10 @@ function setupWebServer(config, cachedContent) {
1026
1046
  } else {
1027
1047
  fs8.createReadStream(filePath).pipe(res);
1028
1048
  }
1029
- console.log(`# [HTTPServer] GET ${url} ${statusCode} - ${/* @__PURE__ */ new Date() - requestStartedAt}ms`);
1049
+ process.stdout.write(
1050
+ `# [HTTPServer] GET ${url} ${statusCode} - ${/* @__PURE__ */ new Date() - requestStartedAt}ms
1051
+ `
1052
+ );
1030
1053
  });
1031
1054
  return server;
1032
1055
  }
@@ -1189,17 +1212,14 @@ function testRuntimeToInject(port, config) {
1189
1212
  </script>`;
1190
1213
  }
1191
1214
  function escapeAndInjectTestsToHTML(html, testRuntimeCode, testBundleUrl) {
1192
- return replaceHTMLContentMarker(
1193
- html,
1194
- `${testRuntimeCode}
1195
- <script src="${testBundleUrl}" async></script>`
1196
- );
1215
+ return injectScript(html, `${testRuntimeCode}
1216
+ <script src="${testBundleUrl}" async></script>`);
1197
1217
  }
1198
1218
  var fsPromise;
1199
1219
  var init_web_server = __esm({
1200
1220
  "lib/setup/web-server.ts"() {
1201
1221
  init_find_internal_assets_from_html();
1202
- init_html_content_marker();
1222
+ init_html();
1203
1223
  init_display_test_result();
1204
1224
  init_color();
1205
1225
  init_path_exists();
@@ -1213,18 +1233,18 @@ async function launchBrowser(config) {
1213
1233
  const browserName = config.browser || "chromium";
1214
1234
  if (browserName === "chromium") {
1215
1235
  const waitStart = Date.now();
1216
- const [playwrightCore2, earlyChrome2] = await Promise.all([
1236
+ const [playwrightCore2, prelaunch] = await Promise.all([
1217
1237
  playwrightCorePromise,
1218
- earlyBrowserPromise
1238
+ prelaunchPromise
1219
1239
  ]);
1220
1240
  perfLog(
1221
- `browser.js: playwright-core + earlyChrome resolved in ${Date.now() - waitStart}ms, earlyChrome:`,
1222
- earlyChrome2?.cdpEndpoint ?? null
1241
+ `browser.js: playwright-core + prelaunch resolved in ${Date.now() - waitStart}ms, prelaunch:`,
1242
+ prelaunch?.cdpEndpoint ?? null
1223
1243
  );
1224
- if (earlyChrome2) {
1244
+ if (prelaunch) {
1225
1245
  const connectStart = Date.now();
1226
1246
  const browser = await playwrightCore2.chromium.connectOverCDP({
1227
- endpointURL: earlyChrome2.cdpEndpoint
1247
+ endpointURL: prelaunch.cdpEndpoint
1228
1248
  });
1229
1249
  perfLog(`browser.js: connectOverCDP took ${Date.now() - connectStart}ms`);
1230
1250
  return browser;
@@ -1279,8 +1299,8 @@ async function setupBrowser(config, cachedContent, existingBrowser = null) {
1279
1299
  }
1280
1300
  });
1281
1301
  page.on("pageerror", (error) => {
1282
- console.log(error.toString());
1283
1302
  console.error(error.toString());
1303
+ config.COUNTER.failCount++;
1284
1304
  });
1285
1305
  return { server, browser, page };
1286
1306
  }
@@ -1291,7 +1311,7 @@ var init_browser = __esm({
1291
1311
  init_bind_server_to_port();
1292
1312
  init_find_chrome();
1293
1313
  init_chromium_args();
1294
- init_early_chrome();
1314
+ init_chrome_prelaunch();
1295
1315
  init_perf_logger();
1296
1316
  playwrightCorePromise = import("playwright-core");
1297
1317
  perfLog("browser.js: playwright-core import started");
@@ -1369,14 +1389,20 @@ var init_run_user_module = __esm({
1369
1389
 
1370
1390
  // lib/tap/display-final-result.ts
1371
1391
  function TAPDisplayFinalResult({ testCount, passCount, skipCount, failCount }, timeTaken) {
1372
- console.log("");
1373
- console.log(`1..${testCount}`);
1374
- console.log(`# tests ${testCount}`);
1375
- console.log(`# pass ${passCount}`);
1376
- console.log(`# skip ${skipCount}`);
1377
- console.log(`# fail ${failCount}`);
1378
- console.log(`# duration ${timeTaken}`);
1379
- console.log("");
1392
+ process.stdout.write("\n");
1393
+ process.stdout.write(`1..${testCount}
1394
+ `);
1395
+ process.stdout.write(`# tests ${testCount}
1396
+ `);
1397
+ process.stdout.write(`# pass ${passCount}
1398
+ `);
1399
+ process.stdout.write(`# skip ${skipCount}
1400
+ `);
1401
+ process.stdout.write(`# fail ${failCount}
1402
+ `);
1403
+ process.stdout.write(`# duration ${timeTaken}
1404
+ `);
1405
+ process.stdout.write("\n");
1380
1406
  }
1381
1407
  var init_display_final_result = __esm({
1382
1408
  "lib/tap/display-final-result.ts"() {
@@ -1397,28 +1423,26 @@ async function buildTestBundle(config, cachedContent) {
1397
1423
  await fs9.mkdir(`${projectRoot}/${output}`, { recursive: true });
1398
1424
  const sourcemap = config.debug ? "inline" : config.watch ? "linked" : false;
1399
1425
  const needsDisk = true;
1426
+ const buildOptions = {
1427
+ stdin: {
1428
+ contents: allTestFilePaths.map((filePath) => `import "${filePath}";`).join(""),
1429
+ resolveDir: process.cwd()
1430
+ },
1431
+ bundle: true,
1432
+ logLevel: "error",
1433
+ outfile,
1434
+ keepNames: true,
1435
+ legalComments: "none",
1436
+ target: esbuildTarget(config.browser),
1437
+ sourcemap,
1438
+ // Signal the runtime that all test modules are registered. The runtime's maybeStart()
1439
+ // waits for both this event and the WebSocket 'open' event before calling QUnit.start().
1440
+ // Dispatching from the bundle (rather than from a script onload attr) is reliable across
1441
+ // all browsers and does not require changes to user test code.
1442
+ footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
1443
+ };
1400
1444
  const [allTestCode] = await Promise.all([
1401
- buildWithOverlayfsRetry(
1402
- {
1403
- stdin: {
1404
- contents: allTestFilePaths.map((filePath) => `import "${filePath}";`).join(""),
1405
- resolveDir: process.cwd()
1406
- },
1407
- bundle: true,
1408
- logLevel: "error",
1409
- outfile,
1410
- keepNames: true,
1411
- legalComments: "none",
1412
- target: esbuildTarget(config.browser),
1413
- sourcemap,
1414
- // Signal the runtime that all test modules are registered. The runtime's maybeStart()
1415
- // waits for both this event and the WebSocket 'open' event before calling QUnit.start().
1416
- // Dispatching from the bundle (rather than from a script onload attr) is reliable across
1417
- // all browsers and does not require changes to user test code.
1418
- footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
1419
- },
1420
- needsDisk
1421
- ),
1445
+ config.watch ? buildIncrementally(buildOptions, allTestFilePaths.join("\0"), cachedContent, needsDisk) : buildWithOverlayfsRetry(buildOptions, needsDisk),
1422
1446
  Promise.all(
1423
1447
  cachedContent.htmlPathsToRunTests.map(async (htmlPath) => {
1424
1448
  const targetPath = `${config.projectRoot}/${config.output}${htmlPath}`;
@@ -1440,8 +1464,10 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
1440
1464
  }
1441
1465
  config.lastRanTestFiles = targetTestFilesToFilter || allTestFilePaths;
1442
1466
  try {
1467
+ const preBuildPromise = cachedContent._preBuildPromise;
1468
+ cachedContent._preBuildPromise = null;
1443
1469
  if (!cachedContent.allTestCode) {
1444
- await buildTestBundle(config, cachedContent);
1470
+ await (preBuildPromise ?? buildTestBundle(config, cachedContent));
1445
1471
  }
1446
1472
  if (!cachedContent.allTestCode) {
1447
1473
  return connections;
@@ -1475,7 +1501,7 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
1475
1501
  connections.server && connections.server.close(),
1476
1502
  connections.browser && connections.browser.close()
1477
1503
  ]);
1478
- await shutdownEarlyBrowser();
1504
+ await shutdownPrelaunch();
1479
1505
  return process.exit(config.COUNTER.failCount > 0 ? 1 : 0);
1480
1506
  }
1481
1507
  }
@@ -1511,16 +1537,10 @@ function buildFilteredTests(filteredTests, outputPath, config) {
1511
1537
  needsDisk
1512
1538
  );
1513
1539
  }
1514
- async function buildWithOverlayfsRetry(options, needsDisk) {
1540
+ async function runWithOverlayfsRetry(getContents, needsDisk) {
1515
1541
  const RETRY_DELAY_MS = 100;
1516
1542
  const MAX_RETRIES = 3;
1517
1543
  const EMPTY_BUNDLE_THRESHOLD = 500;
1518
- const buildOpts = { ...options, write: false };
1519
- const getContents = async () => {
1520
- const result2 = await esbuild.build(buildOpts);
1521
- const jsFile = result2.outputFiles.find((outputFile) => !outputFile.path.endsWith(".map"));
1522
- return { result: result2, js: Buffer.from(jsFile.contents) };
1523
- };
1524
1544
  let { result, js } = await getContents();
1525
1545
  for (let retry = 1; retry <= MAX_RETRIES; retry++) {
1526
1546
  if (js.length >= EMPTY_BUNDLE_THRESHOLD) break;
@@ -1542,6 +1562,29 @@ async function buildWithOverlayfsRetry(options, needsDisk) {
1542
1562
  }
1543
1563
  return js;
1544
1564
  }
1565
+ function buildWithOverlayfsRetry(options, needsDisk) {
1566
+ const buildOpts = { ...options, write: false };
1567
+ return runWithOverlayfsRetry(async () => {
1568
+ const result = await esbuild.build(buildOpts);
1569
+ const jsFile = result.outputFiles.find((f) => !f.path.endsWith(".map"));
1570
+ return { result, js: Buffer.from(jsFile.contents) };
1571
+ }, needsDisk);
1572
+ }
1573
+ async function buildIncrementally(options, fileKey, cachedContent, needsDisk) {
1574
+ const buildOpts = { ...options, write: false };
1575
+ if (!cachedContent._esbuildContext || cachedContent._esbuildContextKey !== fileKey) {
1576
+ cachedContent._esbuildContext?.dispose().catch(() => {
1577
+ });
1578
+ cachedContent._esbuildContext = await esbuild.context(buildOpts);
1579
+ cachedContent._esbuildContextKey = fileKey;
1580
+ }
1581
+ const ctx = cachedContent._esbuildContext;
1582
+ return runWithOverlayfsRetry(async () => {
1583
+ const result = await ctx.rebuild();
1584
+ const jsFile = result.outputFiles.find((f) => !f.path.endsWith(".map"));
1585
+ return { result, js: Buffer.from(jsFile.contents) };
1586
+ }, needsDisk);
1587
+ }
1545
1588
  function esbuildTarget(browser) {
1546
1589
  if (browser === "firefox") return ["firefox115"];
1547
1590
  if (browser === "webkit") return ["safari16"];
@@ -1626,7 +1669,7 @@ async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode
1626
1669
  connections.server && connections.server.close(),
1627
1670
  connections.browser && connections.browser.close()
1628
1671
  ]);
1629
- await shutdownEarlyBrowser();
1672
+ await shutdownPrelaunch();
1630
1673
  process.exit(1);
1631
1674
  }
1632
1675
  }
@@ -1634,7 +1677,7 @@ var BundleError;
1634
1677
  var init_tests_in_browser = __esm({
1635
1678
  "lib/commands/run/tests-in-browser.ts"() {
1636
1679
  init_color();
1637
- init_early_chrome();
1680
+ init_chrome_prelaunch();
1638
1681
  init_time_counter();
1639
1682
  init_run_user_module();
1640
1683
  init_display_final_result();
@@ -1947,6 +1990,7 @@ import { availableParallelism } from "node:os";
1947
1990
  async function run(config) {
1948
1991
  const cachedContent = await buildCachedContent(config, config.htmlPaths);
1949
1992
  if (config.watch) {
1993
+ cachedContent._preBuildPromise = buildTestBundle(config, cachedContent);
1950
1994
  const [connections] = await Promise.all([
1951
1995
  setupBrowser(config, cachedContent),
1952
1996
  writeOutputStaticFiles(config, cachedContent)
@@ -2011,9 +2055,10 @@ async function run(config) {
2011
2055
  _phase: "bundling"
2012
2056
  }));
2013
2057
  const groupCachedContents = groups.map(() => ({ ...cachedContent }));
2014
- console.log("TAP version 13");
2015
- console.log(
2016
- `# Running ${allFiles.length} test file${allFiles.length === 1 ? "" : "s"} across ${groupCount} group${groupCount === 1 ? "" : "s"}`
2058
+ process.stdout.write("TAP version 13\n");
2059
+ process.stdout.write(
2060
+ `# Running ${allFiles.length} test file${allFiles.length === 1 ? "" : "s"} across ${groupCount} group${groupCount === 1 ? "" : "s"}
2061
+ `
2017
2062
  );
2018
2063
  const [browser] = await Promise.all([
2019
2064
  launchBrowser(config),
@@ -2099,7 +2144,7 @@ async function run(config) {
2099
2144
  clearInterval(keepAlive);
2100
2145
  await browser.close().catch(() => {
2101
2146
  });
2102
- await shutdownEarlyBrowser();
2147
+ await shutdownPrelaunch();
2103
2148
  process.exit(exitCode);
2104
2149
  });
2105
2150
  }
@@ -2114,7 +2159,7 @@ async function buildCachedContent(config, htmlPaths) {
2114
2159
  if (buffer === null) return result;
2115
2160
  const filePath = config.htmlPaths[index];
2116
2161
  const html = buffer.toString();
2117
- if (htmlHasDynamicContentMarker(html)) {
2162
+ if (isCustomTemplate(html)) {
2118
2163
  result.dynamicContentHTMLs[filePath] = html;
2119
2164
  result.htmlPathsToRunTests.push(filePath.replace(config.projectRoot, ""));
2120
2165
  } else {
@@ -2153,7 +2198,7 @@ async function addCachedContentMainHTML(projectRoot, cachedContent) {
2153
2198
  html: cachedContent.dynamicContentHTMLs[mainHTMLPath]
2154
2199
  };
2155
2200
  } else {
2156
- const html = await readBoilerplate("setup/tests.hbs");
2201
+ const html = await readTemplate("setup/tests.hbs");
2157
2202
  cachedContent.mainHTML = { filePath: `${projectRoot}/test/tests.html`, html };
2158
2203
  cachedContent.assets.add(`${projectRoot}/node_modules/qunitx/vendor/qunit.css`);
2159
2204
  }
@@ -2184,7 +2229,7 @@ function normalizeInternalAssetPathFromHTML(projectRoot, assetPath, htmlPath) {
2184
2229
  var init_run = __esm({
2185
2230
  "lib/commands/run.ts"() {
2186
2231
  init_browser();
2187
- init_early_chrome();
2232
+ init_chrome_prelaunch();
2188
2233
  init_open_output_in_browser();
2189
2234
  init_color();
2190
2235
  init_tests_in_browser();
@@ -2195,13 +2240,13 @@ var init_run = __esm({
2195
2240
  init_write_output_static_files();
2196
2241
  init_time_counter();
2197
2242
  init_display_final_result();
2198
- init_read_boilerplate();
2199
- init_html_content_marker();
2243
+ init_read_template();
2244
+ init_html();
2200
2245
  }
2201
2246
  });
2202
2247
 
2203
2248
  // cli.ts
2204
- init_early_chrome();
2249
+ init_chrome_prelaunch();
2205
2250
  import process4 from "node:process";
2206
2251
 
2207
2252
  // lib/commands/help.ts
@@ -2211,7 +2256,7 @@ init_color();
2211
2256
  var package_default = {
2212
2257
  name: "qunitx-cli",
2213
2258
  type: "module",
2214
- version: "0.17.7",
2259
+ version: "0.18.0",
2215
2260
  description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
2216
2261
  author: "Izel Nakri",
2217
2262
  license: "MIT",
@@ -2270,7 +2315,7 @@ var package_default = {
2270
2315
  express: "^5.2.1",
2271
2316
  "js-yaml": "^4.1.1",
2272
2317
  prettier: "^3.8.2",
2273
- qunitx: "^1.2.1",
2318
+ qunitx: "^1.2.7",
2274
2319
  typescript: "^6.0.2"
2275
2320
  },
2276
2321
  volta: {
@@ -2370,7 +2415,7 @@ var defaultProjectConfigValues = {
2370
2415
  };
2371
2416
 
2372
2417
  // lib/commands/init.ts
2373
- init_read_boilerplate();
2418
+ init_read_template();
2374
2419
  async function initializeProject() {
2375
2420
  const projectRoot = await findProjectRoot();
2376
2421
  const oldPackageJSON = JSON.parse(await fs4.readFile(`${projectRoot}/package.json`));
@@ -2386,7 +2431,7 @@ async function initializeProject() {
2386
2431
  ]);
2387
2432
  }
2388
2433
  async function writeTestsHTML(projectRoot, config, oldPackageJSON) {
2389
- const testHTMLTemplateBuffer = await readBoilerplate("setup/tests.hbs");
2434
+ const testHTMLTemplateBuffer = await readTemplate("setup/tests.hbs");
2390
2435
  return await Promise.all(
2391
2436
  config.htmlPaths.map(async (htmlPath) => {
2392
2437
  const targetPath = `${projectRoot}/${htmlPath}`;
@@ -2416,7 +2461,7 @@ async function rewritePackageJSON(projectRoot, config, oldPackageJSON) {
2416
2461
  async function writeTSConfigIfNeeded(projectRoot) {
2417
2462
  const targetPath = `${projectRoot}/tsconfig.json`;
2418
2463
  if (!await pathExists(targetPath)) {
2419
- const tsConfigTemplate = await readBoilerplate("setup/tsconfig.json");
2464
+ const tsConfigTemplate = await readTemplate("setup/tsconfig.json");
2420
2465
  await fs4.writeFile(targetPath, tsConfigTemplate);
2421
2466
  console.log(`${targetPath} written`);
2422
2467
  }
@@ -2426,7 +2471,7 @@ async function writeTSConfigIfNeeded(projectRoot) {
2426
2471
  init_color();
2427
2472
  import fs5 from "node:fs/promises";
2428
2473
  init_path_exists();
2429
- init_read_boilerplate();
2474
+ init_read_template();
2430
2475
 
2431
2476
  // lib/utils/convert-to-pascal-case.ts
2432
2477
  function convertToPascalCase(str) {
@@ -2448,7 +2493,7 @@ async function generateTestFiles() {
2448
2493
  console.log(`${path6} already exists!`);
2449
2494
  return;
2450
2495
  }
2451
- const testJSContent = await readBoilerplate("test.js");
2496
+ const testJSContent = await readTemplate("test.js");
2452
2497
  const targetFolderPaths = path6.split("/");
2453
2498
  targetFolderPaths.pop();
2454
2499
  await fs5.mkdir(targetFolderPaths.join("/"), { recursive: true });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "qunitx-cli",
3
3
  "type": "module",
4
- "version": "0.17.7",
4
+ "version": "0.18.0",
5
5
  "description": "Browser runner for QUnitx: run your qunitx tests in google-chrome",
6
6
  "author": "Izel Nakri",
7
7
  "license": "MIT",
@@ -60,7 +60,7 @@
60
60
  "express": "^5.2.1",
61
61
  "js-yaml": "^4.1.1",
62
62
  "prettier": "^3.8.2",
63
- "qunitx": "^1.2.1",
63
+ "qunitx": "^1.2.7",
64
64
  "typescript": "^6.0.2"
65
65
  },
66
66
  "volta": {