qunitx-cli 0.17.6 → 0.17.8

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 +216 -131
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -53,6 +53,76 @@ var init_kill_process_group = __esm({
53
53
  }
54
54
  });
55
55
 
56
+ // lib/utils/cleanup-browser-dir.ts
57
+ import fs from "node:fs/promises";
58
+ async function cleanupBrowserDir(dirPath) {
59
+ if (process.platform !== "linux") {
60
+ await fs.rm(dirPath, { recursive: true, force: true }).catch(() => {
61
+ });
62
+ return;
63
+ }
64
+ const dirName = dirPath.split("/").pop();
65
+ const killedPids = /* @__PURE__ */ new Set();
66
+ const procEntries = await fs.readdir("/proc").catch(() => []);
67
+ await Promise.all(
68
+ procEntries.map(async (entry) => {
69
+ if (!/^\d+$/.test(entry)) return;
70
+ const pid = parseInt(entry);
71
+ try {
72
+ const [cwd, cmdline] = await Promise.all([
73
+ fs.readlink(`/proc/${entry}/cwd`).catch(() => ""),
74
+ fs.readFile(`/proc/${entry}/cmdline`, "utf8").catch(() => "")
75
+ ]);
76
+ if (!cwd.startsWith(dirPath) && !cmdline.includes(dirName)) return;
77
+ try {
78
+ process.kill(pid, "SIGKILL");
79
+ killedPids.add(pid);
80
+ } catch {
81
+ }
82
+ } catch {
83
+ }
84
+ })
85
+ );
86
+ while (killedPids.size > 0) {
87
+ await new Promise((resolve) => setTimeout(resolve, 20));
88
+ for (const pid of killedPids) {
89
+ try {
90
+ process.kill(pid, 0);
91
+ } catch {
92
+ killedPids.delete(pid);
93
+ }
94
+ }
95
+ }
96
+ const deadline = Date.now() + 1e3;
97
+ while (Date.now() < deadline) {
98
+ const removed = await fs.rm(dirPath, { recursive: true, force: true }).then(() => true).catch(() => false);
99
+ if (removed) break;
100
+ await new Promise((resolve) => setTimeout(resolve, 20));
101
+ }
102
+ if (await fs.access(dirPath).then(() => true).catch(() => false)) {
103
+ const diagEntries = await fs.readdir("/proc").catch(() => []);
104
+ await Promise.all(
105
+ diagEntries.map(async (entry) => {
106
+ if (!/^\d+$/.test(entry)) return;
107
+ try {
108
+ const cwd = await fs.readlink(`/proc/${entry}/cwd`).catch(() => "");
109
+ if (!cwd.startsWith(dirPath)) return;
110
+ const cmdline = await fs.readFile(`/proc/${entry}/cmdline`, "utf8").catch(() => "");
111
+ process.stderr.write(
112
+ `# [qunitx] cleanup failed: pid ${entry} still holds ${dirPath} as cwd (cmdline: ${cmdline.replace(/\0/g, " ").slice(0, 120)})
113
+ `
114
+ );
115
+ } catch {
116
+ }
117
+ })
118
+ );
119
+ }
120
+ }
121
+ var init_cleanup_browser_dir = __esm({
122
+ "lib/utils/cleanup-browser-dir.ts"() {
123
+ }
124
+ });
125
+
56
126
  // lib/utils/pre-launch-chrome.ts
57
127
  import { spawn } from "node:child_process";
58
128
  import { mkdtemp, rm } from "node:fs/promises";
@@ -123,11 +193,10 @@ async function preLaunchChrome(chromePath, args, headless = true) {
123
193
  } catch {
124
194
  break;
125
195
  }
126
- await new Promise((r) => setTimeout(r, 20));
196
+ await new Promise((resolve) => setTimeout(resolve, 20));
127
197
  }
128
198
  clearTimeout(warnTimer);
129
- await rm(userDataDir, { recursive: true, force: true }).catch(() => {
130
- });
199
+ await cleanupBrowserDir(userDataDir);
131
200
  });
132
201
  }
133
202
  }
@@ -135,6 +204,7 @@ var CDP_URL_REGEX;
135
204
  var init_pre_launch_chrome = __esm({
136
205
  "lib/utils/pre-launch-chrome.ts"() {
137
206
  init_kill_process_group();
207
+ init_cleanup_browser_dir();
138
208
  CDP_URL_REGEX = /DevTools listening on (ws:\/\/[^\s]+)/;
139
209
  }
140
210
  });
@@ -231,16 +301,16 @@ var init_perf_logger = __esm({
231
301
  }
232
302
  });
233
303
 
234
- // lib/utils/early-chrome.ts
235
- async function shutdownEarlyBrowser() {
304
+ // lib/utils/chrome-prelaunch.ts
305
+ async function shutdownPrelaunch() {
236
306
  if (!earlyChrome) return;
237
307
  const { shutdown } = earlyChrome;
238
308
  earlyChrome = null;
239
309
  await shutdown();
240
310
  }
241
- var NON_RUN_COMMANDS, isRunCommand, browserFromArgv, openFromArgv, watchFromArgv, openWatchMode, earlyChrome, earlyBrowserPromise;
242
- var init_early_chrome = __esm({
243
- "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"() {
244
314
  init_find_chrome();
245
315
  init_pre_launch_chrome();
246
316
  init_kill_process_group();
@@ -265,12 +335,12 @@ var init_early_chrome = __esm({
265
335
  killProcessGroup(earlyChrome.proc.pid);
266
336
  });
267
337
  }
268
- perfLog("early-chrome.js: module evaluated");
269
- earlyBrowserPromise = isRunCommand && browserFromArgv === "chromium" ? findChrome().then((chromePath) => {
270
- 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);
271
341
  return preLaunchChrome(chromePath, CHROMIUM_ARGS, !openWatchMode);
272
342
  }).then((info) => {
273
- perfLog("early-chrome.js: Chrome CDP ready", info?.cdpEndpoint ?? null);
343
+ perfLog("chrome-prelaunch.ts: Chrome CDP ready", info?.cdpEndpoint ?? null);
274
344
  if (info) earlyChrome = info;
275
345
  return info;
276
346
  }) : Promise.resolve(null);
@@ -316,10 +386,10 @@ var init_color = __esm({
316
386
  });
317
387
 
318
388
  // lib/utils/path-exists.ts
319
- import fs from "node:fs/promises";
389
+ import fs2 from "node:fs/promises";
320
390
  async function pathExists(path6) {
321
391
  try {
322
- await fs.access(path6);
392
+ await fs2.access(path6);
323
393
  return true;
324
394
  } catch {
325
395
  return false;
@@ -330,17 +400,17 @@ var init_path_exists = __esm({
330
400
  }
331
401
  });
332
402
 
333
- // lib/utils/read-boilerplate.ts
334
- import fs2 from "node:fs/promises";
403
+ // lib/utils/read-template.ts
404
+ import fs3 from "node:fs/promises";
335
405
  import { dirname, join as join2 } from "node:path";
336
406
  import { fileURLToPath } from "node:url";
337
- async function readBoilerplate(relativePath) {
407
+ async function readTemplate(relativePath) {
338
408
  const sea = await import("node:sea").catch(() => null);
339
409
  if (sea?.isSea()) return sea.getAsset(relativePath, "utf8");
340
410
  const __dirname = dirname(fileURLToPath(import.meta.url));
341
411
  for (const base of ["../templates", "../../templates"]) {
342
412
  try {
343
- return (await fs2.readFile(join2(__dirname, base, relativePath))).toString();
413
+ return (await fs3.readFile(join2(__dirname, base, relativePath))).toString();
344
414
  } catch {
345
415
  }
346
416
  }
@@ -348,8 +418,8 @@ async function readBoilerplate(relativePath) {
348
418
  `qunitx-cli: template "${relativePath}" not found \u2014 try reinstalling the package.`
349
419
  );
350
420
  }
351
- var init_read_boilerplate = __esm({
352
- "lib/utils/read-boilerplate.ts"() {
421
+ var init_read_template = __esm({
422
+ "lib/utils/read-template.ts"() {
353
423
  }
354
424
  });
355
425
 
@@ -368,19 +438,19 @@ var init_find_internal_assets_from_html = __esm({
368
438
  }
369
439
  });
370
440
 
371
- // lib/utils/html-content-marker.ts
372
- function findHTMLContentMarker(html) {
373
- 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;
374
444
  }
375
- function htmlHasDynamicContentMarker(html) {
376
- return !!findHTMLContentMarker(html) || HANDLEBARS_TOKEN_REGEX.test(html);
445
+ function isCustomTemplate(html) {
446
+ return !!findScriptPlaceholder(html) || HANDLEBARS_TOKEN_REGEX.test(html);
377
447
  }
378
- function replaceHTMLContentMarker(html, content) {
379
- const marker = findHTMLContentMarker(html);
380
- if (marker) {
381
- return html.replace(marker, content);
448
+ function injectScript(html, content) {
449
+ const placeholder = findScriptPlaceholder(html);
450
+ if (placeholder) {
451
+ return html.replace(placeholder, content);
382
452
  }
383
- if (htmlHasDynamicContentMarker(html)) {
453
+ if (isCustomTemplate(html)) {
384
454
  if (html.includes("</body>")) {
385
455
  return html.replace("</body>", `${content}</body>`);
386
456
  }
@@ -391,10 +461,10 @@ function replaceHTMLContentMarker(html, content) {
391
461
  }
392
462
  return html;
393
463
  }
394
- var HTML_CONTENT_MARKER, HANDLEBARS_TOKEN_REGEX;
395
- var init_html_content_marker = __esm({
396
- "lib/utils/html-content-marker.ts"() {
397
- 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}}";
398
468
  HANDLEBARS_TOKEN_REGEX = /{{\s*[^}]+\s*}}/;
399
469
  }
400
470
  });
@@ -557,8 +627,8 @@ async function bindServerToPort(server, config) {
557
627
  var EXPLICIT_PORT_RETRIES, EXPLICIT_PORT_RETRY_DELAY_MS;
558
628
  var init_bind_server_to_port = __esm({
559
629
  "lib/setup/bind-server-to-port.ts"() {
560
- EXPLICIT_PORT_RETRIES = 5;
561
- EXPLICIT_PORT_RETRY_DELAY_MS = 20;
630
+ EXPLICIT_PORT_RETRIES = 20;
631
+ EXPLICIT_PORT_RETRY_DELAY_MS = 50;
562
632
  }
563
633
  });
564
634
 
@@ -810,7 +880,7 @@ var init_http = __esm({
810
880
  });
811
881
 
812
882
  // lib/setup/web-server.ts
813
- import fs7 from "node:fs";
883
+ import fs8 from "node:fs";
814
884
  import path4 from "node:path";
815
885
  function setupWebServer(config, cachedContent) {
816
886
  const STATIC_FILES_PATH = path4.join(config.projectRoot, config.output);
@@ -954,7 +1024,7 @@ function setupWebServer(config, cachedContent) {
954
1024
  if (statusCode === 404) {
955
1025
  res.end();
956
1026
  } else {
957
- fs7.createReadStream(filePath).pipe(res);
1027
+ fs8.createReadStream(filePath).pipe(res);
958
1028
  }
959
1029
  console.log(`# [HTTPServer] GET ${url} ${statusCode} - ${/* @__PURE__ */ new Date() - requestStartedAt}ms`);
960
1030
  });
@@ -1119,22 +1189,19 @@ function testRuntimeToInject(port, config) {
1119
1189
  </script>`;
1120
1190
  }
1121
1191
  function escapeAndInjectTestsToHTML(html, testRuntimeCode, testBundleUrl) {
1122
- return replaceHTMLContentMarker(
1123
- html,
1124
- `${testRuntimeCode}
1125
- <script src="${testBundleUrl}" async></script>`
1126
- );
1192
+ return injectScript(html, `${testRuntimeCode}
1193
+ <script src="${testBundleUrl}" async></script>`);
1127
1194
  }
1128
1195
  var fsPromise;
1129
1196
  var init_web_server = __esm({
1130
1197
  "lib/setup/web-server.ts"() {
1131
1198
  init_find_internal_assets_from_html();
1132
- init_html_content_marker();
1199
+ init_html();
1133
1200
  init_display_test_result();
1134
1201
  init_color();
1135
1202
  init_path_exists();
1136
1203
  init_http();
1137
- fsPromise = fs7.promises;
1204
+ fsPromise = fs8.promises;
1138
1205
  }
1139
1206
  });
1140
1207
 
@@ -1143,18 +1210,18 @@ async function launchBrowser(config) {
1143
1210
  const browserName = config.browser || "chromium";
1144
1211
  if (browserName === "chromium") {
1145
1212
  const waitStart = Date.now();
1146
- const [playwrightCore2, earlyChrome2] = await Promise.all([
1213
+ const [playwrightCore2, prelaunch] = await Promise.all([
1147
1214
  playwrightCorePromise,
1148
- earlyBrowserPromise
1215
+ prelaunchPromise
1149
1216
  ]);
1150
1217
  perfLog(
1151
- `browser.js: playwright-core + earlyChrome resolved in ${Date.now() - waitStart}ms, earlyChrome:`,
1152
- earlyChrome2?.cdpEndpoint ?? null
1218
+ `browser.js: playwright-core + prelaunch resolved in ${Date.now() - waitStart}ms, prelaunch:`,
1219
+ prelaunch?.cdpEndpoint ?? null
1153
1220
  );
1154
- if (earlyChrome2) {
1221
+ if (prelaunch) {
1155
1222
  const connectStart = Date.now();
1156
1223
  const browser = await playwrightCore2.chromium.connectOverCDP({
1157
- endpointURL: earlyChrome2.cdpEndpoint
1224
+ endpointURL: prelaunch.cdpEndpoint
1158
1225
  });
1159
1226
  perfLog(`browser.js: connectOverCDP took ${Date.now() - connectStart}ms`);
1160
1227
  return browser;
@@ -1209,8 +1276,8 @@ async function setupBrowser(config, cachedContent, existingBrowser = null) {
1209
1276
  }
1210
1277
  });
1211
1278
  page.on("pageerror", (error) => {
1212
- console.log(error.toString());
1213
1279
  console.error(error.toString());
1280
+ config.COUNTER.failCount++;
1214
1281
  });
1215
1282
  return { server, browser, page };
1216
1283
  }
@@ -1221,7 +1288,7 @@ var init_browser = __esm({
1221
1288
  init_bind_server_to_port();
1222
1289
  init_find_chrome();
1223
1290
  init_chromium_args();
1224
- init_early_chrome();
1291
+ init_chrome_prelaunch();
1225
1292
  init_perf_logger();
1226
1293
  playwrightCorePromise = import("playwright-core");
1227
1294
  perfLog("browser.js: playwright-core import started");
@@ -1314,7 +1381,7 @@ var init_display_final_result = __esm({
1314
1381
  });
1315
1382
 
1316
1383
  // lib/commands/run/tests-in-browser.ts
1317
- import fs8 from "node:fs/promises";
1384
+ import fs9 from "node:fs/promises";
1318
1385
  import esbuild from "esbuild";
1319
1386
  async function buildTestBundle(config, cachedContent) {
1320
1387
  const { projectRoot, output } = config;
@@ -1324,37 +1391,35 @@ async function buildTestBundle(config, cachedContent) {
1324
1391
  return;
1325
1392
  }
1326
1393
  const outfile = `${projectRoot}/${output}/tests.js`;
1327
- await fs8.mkdir(`${projectRoot}/${output}`, { recursive: true });
1394
+ await fs9.mkdir(`${projectRoot}/${output}`, { recursive: true });
1328
1395
  const sourcemap = config.debug ? "inline" : config.watch ? "linked" : false;
1329
1396
  const needsDisk = true;
1397
+ const buildOptions = {
1398
+ stdin: {
1399
+ contents: allTestFilePaths.map((filePath) => `import "${filePath}";`).join(""),
1400
+ resolveDir: process.cwd()
1401
+ },
1402
+ bundle: true,
1403
+ logLevel: "error",
1404
+ outfile,
1405
+ keepNames: true,
1406
+ legalComments: "none",
1407
+ target: esbuildTarget(config.browser),
1408
+ sourcemap,
1409
+ // Signal the runtime that all test modules are registered. The runtime's maybeStart()
1410
+ // waits for both this event and the WebSocket 'open' event before calling QUnit.start().
1411
+ // Dispatching from the bundle (rather than from a script onload attr) is reliable across
1412
+ // all browsers and does not require changes to user test code.
1413
+ footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
1414
+ };
1330
1415
  const [allTestCode] = await Promise.all([
1331
- buildWithOverlayfsRetry(
1332
- {
1333
- stdin: {
1334
- contents: allTestFilePaths.map((filePath) => `import "${filePath}";`).join(""),
1335
- resolveDir: process.cwd()
1336
- },
1337
- bundle: true,
1338
- logLevel: "error",
1339
- outfile,
1340
- keepNames: true,
1341
- legalComments: "none",
1342
- target: esbuildTarget(config.browser),
1343
- sourcemap,
1344
- // Signal the runtime that all test modules are registered. The runtime's maybeStart()
1345
- // waits for both this event and the WebSocket 'open' event before calling QUnit.start().
1346
- // Dispatching from the bundle (rather than from a script onload attr) is reliable across
1347
- // all browsers and does not require changes to user test code.
1348
- footer: { js: 'window.dispatchEvent(new CustomEvent("qunitx:tests-ready"));' }
1349
- },
1350
- needsDisk
1351
- ),
1416
+ config.watch ? buildIncrementally(buildOptions, allTestFilePaths.join("\0"), cachedContent, needsDisk) : buildWithOverlayfsRetry(buildOptions, needsDisk),
1352
1417
  Promise.all(
1353
1418
  cachedContent.htmlPathsToRunTests.map(async (htmlPath) => {
1354
1419
  const targetPath = `${config.projectRoot}/${config.output}${htmlPath}`;
1355
1420
  if (htmlPath !== "/") {
1356
- await fs8.rm(targetPath, { force: true, recursive: true });
1357
- await fs8.mkdir(targetPath.split("/").slice(0, -1).join("/"), { recursive: true });
1421
+ await fs9.rm(targetPath, { force: true, recursive: true });
1422
+ await fs9.mkdir(targetPath.split("/").slice(0, -1).join("/"), { recursive: true });
1358
1423
  }
1359
1424
  })
1360
1425
  )
@@ -1370,8 +1435,10 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
1370
1435
  }
1371
1436
  config.lastRanTestFiles = targetTestFilesToFilter || allTestFilePaths;
1372
1437
  try {
1438
+ const preBuildPromise = cachedContent._preBuildPromise;
1439
+ cachedContent._preBuildPromise = null;
1373
1440
  if (!cachedContent.allTestCode) {
1374
- await buildTestBundle(config, cachedContent);
1441
+ await (preBuildPromise ?? buildTestBundle(config, cachedContent));
1375
1442
  }
1376
1443
  if (!cachedContent.allTestCode) {
1377
1444
  return connections;
@@ -1405,7 +1472,7 @@ async function runTestsInBrowser(config, cachedContent = {}, connections, target
1405
1472
  connections.server && connections.server.close(),
1406
1473
  connections.browser && connections.browser.close()
1407
1474
  ]);
1408
- await shutdownEarlyBrowser();
1475
+ await shutdownPrelaunch();
1409
1476
  return process.exit(config.COUNTER.failCount > 0 ? 1 : 0);
1410
1477
  }
1411
1478
  }
@@ -1441,16 +1508,10 @@ function buildFilteredTests(filteredTests, outputPath, config) {
1441
1508
  needsDisk
1442
1509
  );
1443
1510
  }
1444
- async function buildWithOverlayfsRetry(options, needsDisk) {
1511
+ async function runWithOverlayfsRetry(getContents, needsDisk) {
1445
1512
  const RETRY_DELAY_MS = 100;
1446
1513
  const MAX_RETRIES = 3;
1447
1514
  const EMPTY_BUNDLE_THRESHOLD = 500;
1448
- const buildOpts = { ...options, write: false };
1449
- const getContents = async () => {
1450
- const result2 = await esbuild.build(buildOpts);
1451
- const jsFile = result2.outputFiles.find((outputFile) => !outputFile.path.endsWith(".map"));
1452
- return { result: result2, js: Buffer.from(jsFile.contents) };
1453
- };
1454
1515
  let { result, js } = await getContents();
1455
1516
  for (let retry = 1; retry <= MAX_RETRIES; retry++) {
1456
1517
  if (js.length >= EMPTY_BUNDLE_THRESHOLD) break;
@@ -1467,11 +1528,34 @@ async function buildWithOverlayfsRetry(options, needsDisk) {
1467
1528
  }
1468
1529
  if (needsDisk) {
1469
1530
  await Promise.all(
1470
- result.outputFiles.map((outputFile) => fs8.writeFile(outputFile.path, outputFile.contents))
1531
+ result.outputFiles.map((outputFile) => fs9.writeFile(outputFile.path, outputFile.contents))
1471
1532
  );
1472
1533
  }
1473
1534
  return js;
1474
1535
  }
1536
+ function buildWithOverlayfsRetry(options, needsDisk) {
1537
+ const buildOpts = { ...options, write: false };
1538
+ return runWithOverlayfsRetry(async () => {
1539
+ const result = await esbuild.build(buildOpts);
1540
+ const jsFile = result.outputFiles.find((f) => !f.path.endsWith(".map"));
1541
+ return { result, js: Buffer.from(jsFile.contents) };
1542
+ }, needsDisk);
1543
+ }
1544
+ async function buildIncrementally(options, fileKey, cachedContent, needsDisk) {
1545
+ const buildOpts = { ...options, write: false };
1546
+ if (!cachedContent._esbuildContext || cachedContent._esbuildContextKey !== fileKey) {
1547
+ cachedContent._esbuildContext?.dispose().catch(() => {
1548
+ });
1549
+ cachedContent._esbuildContext = await esbuild.context(buildOpts);
1550
+ cachedContent._esbuildContextKey = fileKey;
1551
+ }
1552
+ const ctx = cachedContent._esbuildContext;
1553
+ return runWithOverlayfsRetry(async () => {
1554
+ const result = await ctx.rebuild();
1555
+ const jsFile = result.outputFiles.find((f) => !f.path.endsWith(".map"));
1556
+ return { result, js: Buffer.from(jsFile.contents) };
1557
+ }, needsDisk);
1558
+ }
1475
1559
  function esbuildTarget(browser) {
1476
1560
  if (browser === "firefox") return ["firefox115"];
1477
1561
  if (browser === "webkit") return ["safari16"];
@@ -1556,7 +1640,7 @@ async function failOnNonWatchMode(watchMode = false, connections = {}, groupMode
1556
1640
  connections.server && connections.server.close(),
1557
1641
  connections.browser && connections.browser.close()
1558
1642
  ]);
1559
- await shutdownEarlyBrowser();
1643
+ await shutdownPrelaunch();
1560
1644
  process.exit(1);
1561
1645
  }
1562
1646
  }
@@ -1564,7 +1648,7 @@ var BundleError;
1564
1648
  var init_tests_in_browser = __esm({
1565
1649
  "lib/commands/run/tests-in-browser.ts"() {
1566
1650
  init_color();
1567
- init_early_chrome();
1651
+ init_chrome_prelaunch();
1568
1652
  init_time_counter();
1569
1653
  init_run_user_module();
1570
1654
  init_display_final_result();
@@ -1579,7 +1663,7 @@ var init_tests_in_browser = __esm({
1579
1663
  });
1580
1664
 
1581
1665
  // lib/setup/file-watcher.ts
1582
- import fs9 from "node:fs";
1666
+ import fs10 from "node:fs";
1583
1667
  import { stat, lstat } from "node:fs/promises";
1584
1668
  import path5 from "node:path";
1585
1669
  function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFunc) {
@@ -1592,15 +1676,15 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
1592
1676
  if (symlinkPollers.has(filePath)) return;
1593
1677
  const handler = (curr) => {
1594
1678
  if (curr.nlink === 0) {
1595
- fs9.unwatchFile(filePath, handler);
1679
+ fs10.unwatchFile(filePath, handler);
1596
1680
  symlinkPollers.delete(filePath);
1597
1681
  if (filePath in config.fsTree) {
1598
1682
  handleWatchEvent(config, extensions, "unlink", filePath, onEventFunc, onFinishFunc);
1599
1683
  }
1600
1684
  }
1601
1685
  };
1602
- fs9.watchFile(filePath, { interval: 500, persistent: false }, handler);
1603
- symlinkPollers.set(filePath, () => fs9.unwatchFile(filePath, handler));
1686
+ fs10.watchFile(filePath, { interval: 500, persistent: false }, handler);
1687
+ symlinkPollers.set(filePath, () => fs10.unwatchFile(filePath, handler));
1604
1688
  }
1605
1689
  function untrackSymlink(filePath) {
1606
1690
  symlinkPollers.get(filePath)?.();
@@ -1609,7 +1693,7 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
1609
1693
  for (const watchPath of testFileLookupPaths) {
1610
1694
  let ready = false;
1611
1695
  const lastChangeMs = {};
1612
- const childWatcher = fs9.watch(watchPath, { recursive: true }, async (eventType, filename) => {
1696
+ const childWatcher = fs10.watch(watchPath, { recursive: true }, async (eventType, filename) => {
1613
1697
  if (!ready || !filename) return;
1614
1698
  const fullPath = path5.join(watchPath, filename);
1615
1699
  if (eventType === "change") {
@@ -1639,7 +1723,7 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
1639
1723
  const parentDir = path5.dirname(watchPath);
1640
1724
  const watchedBasename = path5.basename(watchPath);
1641
1725
  let parentUnlinkFired = false;
1642
- const parentWatcher = fs9.watch(parentDir, async (eventType, filename) => {
1726
+ const parentWatcher = fs10.watch(parentDir, async (eventType, filename) => {
1643
1727
  if (!ready || filename !== watchedBasename || eventType !== "rename") return;
1644
1728
  if (parentUnlinkFired) return;
1645
1729
  parentUnlinkFired = true;
@@ -1840,12 +1924,12 @@ var init_keyboard_events = __esm({
1840
1924
  });
1841
1925
 
1842
1926
  // lib/setup/write-output-static-files.ts
1843
- import fs10 from "node:fs/promises";
1927
+ import fs11 from "node:fs/promises";
1844
1928
  async function writeOutputStaticFiles({ projectRoot, output }, cachedContent) {
1845
1929
  const staticHTMLPromises = Object.keys(cachedContent.staticHTMLs).map(async (staticHTMLKey) => {
1846
1930
  const htmlRelativePath = staticHTMLKey.replace(`${projectRoot}/`, "");
1847
1931
  await ensureFolderExists(`${projectRoot}/${output}/${htmlRelativePath}`);
1848
- await fs10.writeFile(
1932
+ await fs11.writeFile(
1849
1933
  `${projectRoot}/${output}/${htmlRelativePath}`,
1850
1934
  cachedContent.staticHTMLs[staticHTMLKey]
1851
1935
  );
@@ -1853,12 +1937,12 @@ async function writeOutputStaticFiles({ projectRoot, output }, cachedContent) {
1853
1937
  const assetPromises = Array.from(cachedContent.assets).map(async (assetAbsolutePath) => {
1854
1938
  const assetRelativePath = assetAbsolutePath.replace(`${projectRoot}/`, "");
1855
1939
  await ensureFolderExists(`${projectRoot}/${output}/${assetRelativePath}`);
1856
- await fs10.copyFile(assetAbsolutePath, `${projectRoot}/${output}/${assetRelativePath}`);
1940
+ await fs11.copyFile(assetAbsolutePath, `${projectRoot}/${output}/${assetRelativePath}`);
1857
1941
  });
1858
1942
  await Promise.all(staticHTMLPromises.concat(assetPromises));
1859
1943
  }
1860
1944
  async function ensureFolderExists(assetPath) {
1861
- await fs10.mkdir(assetPath.split("/").slice(0, -1).join("/"), { recursive: true });
1945
+ await fs11.mkdir(assetPath.split("/").slice(0, -1).join("/"), { recursive: true });
1862
1946
  }
1863
1947
  var init_write_output_static_files = __esm({
1864
1948
  "lib/setup/write-output-static-files.ts"() {
@@ -1871,12 +1955,13 @@ __export(run_exports, {
1871
1955
  default: () => run,
1872
1956
  run: () => run
1873
1957
  });
1874
- import fs11 from "node:fs/promises";
1958
+ import fs12 from "node:fs/promises";
1875
1959
  import { normalize } from "node:path";
1876
1960
  import { availableParallelism } from "node:os";
1877
1961
  async function run(config) {
1878
1962
  const cachedContent = await buildCachedContent(config, config.htmlPaths);
1879
1963
  if (config.watch) {
1964
+ cachedContent._preBuildPromise = buildTestBundle(config, cachedContent);
1880
1965
  const [connections] = await Promise.all([
1881
1966
  setupBrowser(config, cachedContent),
1882
1967
  writeOutputStaticFiles(config, cachedContent)
@@ -2029,14 +2114,14 @@ async function run(config) {
2029
2114
  clearInterval(keepAlive);
2030
2115
  await browser.close().catch(() => {
2031
2116
  });
2032
- await shutdownEarlyBrowser();
2117
+ await shutdownPrelaunch();
2033
2118
  process.exit(exitCode);
2034
2119
  });
2035
2120
  }
2036
2121
  }
2037
2122
  async function buildCachedContent(config, htmlPaths) {
2038
2123
  const htmlBuffers = await Promise.all(
2039
- config.htmlPaths.map((htmlPath) => fs11.readFile(htmlPath).catch(() => null))
2124
+ config.htmlPaths.map((htmlPath) => fs12.readFile(htmlPath).catch(() => null))
2040
2125
  );
2041
2126
  const cachedContent = htmlPaths.reduce(
2042
2127
  (result, _htmlPath, index) => {
@@ -2044,7 +2129,7 @@ async function buildCachedContent(config, htmlPaths) {
2044
2129
  if (buffer === null) return result;
2045
2130
  const filePath = config.htmlPaths[index];
2046
2131
  const html = buffer.toString();
2047
- if (htmlHasDynamicContentMarker(html)) {
2132
+ if (isCustomTemplate(html)) {
2048
2133
  result.dynamicContentHTMLs[filePath] = html;
2049
2134
  result.htmlPathsToRunTests.push(filePath.replace(config.projectRoot, ""));
2050
2135
  } else {
@@ -2083,7 +2168,7 @@ async function addCachedContentMainHTML(projectRoot, cachedContent) {
2083
2168
  html: cachedContent.dynamicContentHTMLs[mainHTMLPath]
2084
2169
  };
2085
2170
  } else {
2086
- const html = await readBoilerplate("setup/tests.hbs");
2171
+ const html = await readTemplate("setup/tests.hbs");
2087
2172
  cachedContent.mainHTML = { filePath: `${projectRoot}/test/tests.html`, html };
2088
2173
  cachedContent.assets.add(`${projectRoot}/node_modules/qunitx/vendor/qunit.css`);
2089
2174
  }
@@ -2114,7 +2199,7 @@ function normalizeInternalAssetPathFromHTML(projectRoot, assetPath, htmlPath) {
2114
2199
  var init_run = __esm({
2115
2200
  "lib/commands/run.ts"() {
2116
2201
  init_browser();
2117
- init_early_chrome();
2202
+ init_chrome_prelaunch();
2118
2203
  init_open_output_in_browser();
2119
2204
  init_color();
2120
2205
  init_tests_in_browser();
@@ -2125,13 +2210,13 @@ var init_run = __esm({
2125
2210
  init_write_output_static_files();
2126
2211
  init_time_counter();
2127
2212
  init_display_final_result();
2128
- init_read_boilerplate();
2129
- init_html_content_marker();
2213
+ init_read_template();
2214
+ init_html();
2130
2215
  }
2131
2216
  });
2132
2217
 
2133
2218
  // cli.ts
2134
- init_early_chrome();
2219
+ init_chrome_prelaunch();
2135
2220
  import process4 from "node:process";
2136
2221
 
2137
2222
  // lib/commands/help.ts
@@ -2141,7 +2226,7 @@ init_color();
2141
2226
  var package_default = {
2142
2227
  name: "qunitx-cli",
2143
2228
  type: "module",
2144
- version: "0.17.6",
2229
+ version: "0.17.8",
2145
2230
  description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
2146
2231
  author: "Izel Nakri",
2147
2232
  license: "MIT",
@@ -2200,7 +2285,7 @@ var package_default = {
2200
2285
  express: "^5.2.1",
2201
2286
  "js-yaml": "^4.1.1",
2202
2287
  prettier: "^3.8.2",
2203
- qunitx: "^1.2.1",
2288
+ qunitx: "^1.2.7",
2204
2289
  typescript: "^6.0.2"
2205
2290
  },
2206
2291
  volta: {
@@ -2251,7 +2336,7 @@ ${color("$ qunitx new $testFileName")} # Creates a qunitx test file
2251
2336
  }
2252
2337
 
2253
2338
  // lib/commands/init.ts
2254
- import fs3 from "node:fs/promises";
2339
+ import fs4 from "node:fs/promises";
2255
2340
  import path2 from "node:path";
2256
2341
 
2257
2342
  // lib/utils/find-project-root.ts
@@ -2300,10 +2385,10 @@ var defaultProjectConfigValues = {
2300
2385
  };
2301
2386
 
2302
2387
  // lib/commands/init.ts
2303
- init_read_boilerplate();
2388
+ init_read_template();
2304
2389
  async function initializeProject() {
2305
2390
  const projectRoot = await findProjectRoot();
2306
- const oldPackageJSON = JSON.parse(await fs3.readFile(`${projectRoot}/package.json`));
2391
+ const oldPackageJSON = JSON.parse(await fs4.readFile(`${projectRoot}/package.json`));
2307
2392
  const existingQunitx = oldPackageJSON.qunitx || {};
2308
2393
  const cliHtmlPaths = process.argv.slice(2).filter((arg) => arg.endsWith(".html"));
2309
2394
  const config = Object.assign({}, defaultProjectConfigValues, existingQunitx, {
@@ -2316,7 +2401,7 @@ async function initializeProject() {
2316
2401
  ]);
2317
2402
  }
2318
2403
  async function writeTestsHTML(projectRoot, config, oldPackageJSON) {
2319
- const testHTMLTemplateBuffer = await readBoilerplate("setup/tests.hbs");
2404
+ const testHTMLTemplateBuffer = await readTemplate("setup/tests.hbs");
2320
2405
  return await Promise.all(
2321
2406
  config.htmlPaths.map(async (htmlPath) => {
2322
2407
  const targetPath = `${projectRoot}/${htmlPath}`;
@@ -2332,8 +2417,8 @@ async function writeTestsHTML(projectRoot, config, oldPackageJSON) {
2332
2417
  "{{applicationName}}",
2333
2418
  oldPackageJSON.name
2334
2419
  );
2335
- await fs3.mkdir(targetDirectory, { recursive: true });
2336
- await fs3.writeFile(targetPath, testHTMLTemplate);
2420
+ await fs4.mkdir(targetDirectory, { recursive: true });
2421
+ await fs4.writeFile(targetPath, testHTMLTemplate);
2337
2422
  console.log(`${targetPath} written`);
2338
2423
  }
2339
2424
  })
@@ -2341,22 +2426,22 @@ async function writeTestsHTML(projectRoot, config, oldPackageJSON) {
2341
2426
  }
2342
2427
  async function rewritePackageJSON(projectRoot, config, oldPackageJSON) {
2343
2428
  const newPackageJSON = Object.assign(oldPackageJSON, { qunitx: config });
2344
- await fs3.writeFile(`${projectRoot}/package.json`, JSON.stringify(newPackageJSON, null, 2));
2429
+ await fs4.writeFile(`${projectRoot}/package.json`, JSON.stringify(newPackageJSON, null, 2));
2345
2430
  }
2346
2431
  async function writeTSConfigIfNeeded(projectRoot) {
2347
2432
  const targetPath = `${projectRoot}/tsconfig.json`;
2348
2433
  if (!await pathExists(targetPath)) {
2349
- const tsConfigTemplate = await readBoilerplate("setup/tsconfig.json");
2350
- await fs3.writeFile(targetPath, tsConfigTemplate);
2434
+ const tsConfigTemplate = await readTemplate("setup/tsconfig.json");
2435
+ await fs4.writeFile(targetPath, tsConfigTemplate);
2351
2436
  console.log(`${targetPath} written`);
2352
2437
  }
2353
2438
  }
2354
2439
 
2355
2440
  // lib/commands/generate.ts
2356
2441
  init_color();
2357
- import fs4 from "node:fs/promises";
2442
+ import fs5 from "node:fs/promises";
2358
2443
  init_path_exists();
2359
- init_read_boilerplate();
2444
+ init_read_template();
2360
2445
 
2361
2446
  // lib/utils/convert-to-pascal-case.ts
2362
2447
  function convertToPascalCase(str) {
@@ -2378,25 +2463,25 @@ async function generateTestFiles() {
2378
2463
  console.log(`${path6} already exists!`);
2379
2464
  return;
2380
2465
  }
2381
- const testJSContent = await readBoilerplate("test.js");
2466
+ const testJSContent = await readTemplate("test.js");
2382
2467
  const targetFolderPaths = path6.split("/");
2383
2468
  targetFolderPaths.pop();
2384
- await fs4.mkdir(targetFolderPaths.join("/"), { recursive: true });
2385
- await fs4.writeFile(path6, testJSContent.replace("{{moduleName}}", moduleName));
2469
+ await fs5.mkdir(targetFolderPaths.join("/"), { recursive: true });
2470
+ await fs5.writeFile(path6, testJSContent.replace("{{moduleName}}", moduleName));
2386
2471
  console.log(green(`${path6} written`));
2387
2472
  }
2388
2473
 
2389
2474
  // lib/setup/config.ts
2390
- import fs6 from "node:fs/promises";
2475
+ import fs7 from "node:fs/promises";
2391
2476
 
2392
2477
  // lib/setup/fs-tree.ts
2393
- import fs5, { glob as fsGlob } from "node:fs/promises";
2478
+ import fs6, { glob as fsGlob } from "node:fs/promises";
2394
2479
  import path3 from "node:path";
2395
2480
  function isGlob(str) {
2396
2481
  return /[*?{[]/.test(str);
2397
2482
  }
2398
2483
  async function readDirRecursive(dir, filter) {
2399
- const entries = await fs5.readdir(dir, { recursive: true, withFileTypes: true });
2484
+ const entries = await fs6.readdir(dir, { recursive: true, withFileTypes: true });
2400
2485
  const candidates = entries.filter(
2401
2486
  (dirent) => (dirent.isFile() || dirent.isSymbolicLink()) && filter(dirent.name)
2402
2487
  );
@@ -2405,7 +2490,7 @@ async function readDirRecursive(dir, filter) {
2405
2490
  const fullPath = path3.join(dirent.parentPath, dirent.name);
2406
2491
  if (dirent.isFile()) return fullPath;
2407
2492
  try {
2408
- const statResult = await fs5.stat(fullPath);
2493
+ const statResult = await fs6.stat(fullPath);
2409
2494
  return statResult.isFile() ? fullPath : null;
2410
2495
  } catch {
2411
2496
  return null;
@@ -2427,7 +2512,7 @@ async function buildFSTree(fileAbsolutePaths, config = {}) {
2427
2512
  }
2428
2513
  }
2429
2514
  } else {
2430
- const entry = await fs5.stat(fileAbsolutePath);
2515
+ const entry = await fs6.stat(fileAbsolutePath);
2431
2516
  if (entry.isFile()) {
2432
2517
  fsTree[fileAbsolutePath] = null;
2433
2518
  } else if (entry.isDirectory()) {
@@ -2600,7 +2685,7 @@ async function setupConfig() {
2600
2685
  return config;
2601
2686
  }
2602
2687
  async function readConfigFromPackageJSON(projectRoot) {
2603
- const packageJSON = await fs6.readFile(`${projectRoot}/package.json`);
2688
+ const packageJSON = await fs7.readFile(`${projectRoot}/package.json`);
2604
2689
  return JSON.parse(packageJSON.toString());
2605
2690
  }
2606
2691
  function normalizeHTMLPaths(projectRoot, htmlPaths) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "qunitx-cli",
3
3
  "type": "module",
4
- "version": "0.17.6",
4
+ "version": "0.17.8",
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": {