qunitx-cli 0.23.0 → 0.23.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 (3) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +63 -23
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -115,7 +115,7 @@ What it does: cold-start cost dominates a single `qunitx` run — launching Chro
115
115
 
116
116
  A single one-off run won't get faster from spinning the daemon up; it's an opt-in optimization aimed at two situations:
117
117
 
118
- - **Local TDD loops.** Export `QUNITX_DAEMON=1` in your shell profile (or run `qunitx daemon start` once at the top of your session) and forget about it — subsequent runs reuse the daemon automatically until you `daemon stop` or 30 idle minutes pass.
118
+ - **Local TDD loops.** Export `QUNITX_DAEMON=1` in your shell profile (or run `qunitx daemon start` once at the top of your session) and forget about it — subsequent runs reuse the daemon automatically until you `daemon stop` or 30 idle minutes pass. Override the idle window with `QUNITX_DAEMON_IDLE_TIMEOUT` (`1h`, `45s`, `500ms`; bare numbers are minutes), or set it to `false` to disable auto-shutdown entirely.
119
119
  - **Monorepo CI** where each package shells out to `qunitx` separately. Set `QUNITX_DAEMON=1` for the job and a single daemon is auto-spawned and reused across every package's invocation.
120
120
 
121
121
  ```sh
package/dist/cli.js CHANGED
@@ -374,7 +374,7 @@ var init_package = __esm({
374
374
  package_default = {
375
375
  name: "qunitx-cli",
376
376
  type: "module",
377
- version: "0.23.0",
377
+ version: "0.23.2",
378
378
  description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
379
379
  author: "Izel Nakri",
380
380
  license: "MIT",
@@ -748,6 +748,31 @@ var init_init = __esm({
748
748
  }
749
749
  });
750
750
 
751
+ // lib/utils/parse-daemon-idle-timeout.ts
752
+ function parseDaemonIdleTimeout(value) {
753
+ if (!value) return { ms: DEFAULT_DAEMON_IDLE_TIMEOUT_MS, warning: null };
754
+ if (/^\s*false\s*$/i.test(value)) return { ms: Infinity, warning: null };
755
+ const match = /^\s*(\d+(?:\.\d+)?)\s*(ms|s|m|h)?\s*$/i.exec(value);
756
+ if (!match) return invalid(value);
757
+ const n = Number(match[1]);
758
+ if (!(n > 0)) return invalid(value);
759
+ const unit = (match[2] ?? "m").toLowerCase();
760
+ return { ms: Math.round(n * UNIT_TO_MS[unit]), warning: null };
761
+ }
762
+ function invalid(value) {
763
+ return {
764
+ ms: DEFAULT_DAEMON_IDLE_TIMEOUT_MS,
765
+ warning: `\u26A0 qunitx: QUNITX_DAEMON_IDLE_TIMEOUT=${JSON.stringify(value)} is not a valid duration; using 30-minute default. Accepted: bare number = minutes, "30m", "1h", "45s", "500ms", "false" (no auto-shutdown).`
766
+ };
767
+ }
768
+ var UNIT_TO_MS, DEFAULT_DAEMON_IDLE_TIMEOUT_MS;
769
+ var init_parse_daemon_idle_timeout = __esm({
770
+ "lib/utils/parse-daemon-idle-timeout.ts"() {
771
+ UNIT_TO_MS = { ms: 1, s: 1e3, m: 6e4, h: 36e5 };
772
+ DEFAULT_DAEMON_IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
773
+ }
774
+ });
775
+
751
776
  // lib/utils/close-with-grace.ts
752
777
  function closeWithGrace(closes, graceMs = CLEANUP_GRACE_MS) {
753
778
  return new Promise((resolve) => {
@@ -3573,19 +3598,26 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
3573
3598
  const parentDir = path9.dirname(watchPath);
3574
3599
  const watchedBasename = path9.basename(watchPath);
3575
3600
  let parentUnlinkFired = false;
3576
- const parentWatcher = fs11.watch(parentDir, async (eventType, filename) => {
3577
- if (!ready || filename !== watchedBasename || eventType !== "rename") return;
3578
- if (parentUnlinkFired) return;
3601
+ let rescanTimer = null;
3602
+ const tryFireParentUnlink = async () => {
3603
+ if (parentUnlinkFired) return false;
3579
3604
  parentUnlinkFired = true;
3580
3605
  try {
3581
3606
  await stat(watchPath);
3582
3607
  parentUnlinkFired = false;
3608
+ return false;
3583
3609
  } catch {
3584
3610
  handleWatchEvent(config, extensions, "unlinkDir", watchPath, onEventFunc, onFinishFunc);
3585
3611
  childWatcher.close();
3586
3612
  parentWatcher.close();
3613
+ if (rescanTimer) clearInterval(rescanTimer);
3587
3614
  delete fileWatchers[watchPath];
3615
+ return true;
3588
3616
  }
3617
+ };
3618
+ const parentWatcher = fs11.watch(parentDir, async (eventType, filename) => {
3619
+ if (!ready || filename !== watchedBasename || eventType !== "rename") return;
3620
+ await tryFireParentUnlink();
3589
3621
  });
3590
3622
  parentWatchers.push(parentWatcher);
3591
3623
  fileWatchers[watchPath] = childWatcher;
@@ -3598,22 +3630,23 @@ function setupFileWatchers(testFileLookupPaths, config, onEventFunc, onFinishFun
3598
3630
  )
3599
3631
  );
3600
3632
  if (process.platform === "darwin") {
3601
- rescanTimers.push(
3602
- setInterval(() => {
3603
- if (!ready || rescanInProgress) return;
3604
- rescanInProgress = true;
3605
- rescanDirectoryForDelta(
3606
- watchPath,
3607
- config,
3608
- extensions,
3609
- onEventFunc,
3610
- onFinishFunc,
3611
- trackSymlink
3612
- ).finally(() => {
3613
- rescanInProgress = false;
3614
- });
3615
- }, RESCAN_INTERVAL_MS).unref()
3616
- );
3633
+ rescanTimer = setInterval(async () => {
3634
+ if (!ready || rescanInProgress || parentUnlinkFired) return;
3635
+ if (await tryFireParentUnlink()) return;
3636
+ rescanInProgress = true;
3637
+ rescanDirectoryForDelta(
3638
+ watchPath,
3639
+ config,
3640
+ extensions,
3641
+ onEventFunc,
3642
+ onFinishFunc,
3643
+ trackSymlink
3644
+ ).finally(() => {
3645
+ rescanInProgress = false;
3646
+ });
3647
+ }, RESCAN_INTERVAL_MS);
3648
+ rescanTimer.unref();
3649
+ rescanTimers.push(rescanTimer);
3617
3650
  }
3618
3651
  }
3619
3652
  readyPromises.push(
@@ -4453,6 +4486,7 @@ async function shutdownDaemon2(state, reason) {
4453
4486
  }
4454
4487
  function resetIdleTimer(state) {
4455
4488
  if (state.idleTimer) clearTimeout(state.idleTimer);
4489
+ if (!Number.isFinite(IDLE_TIMEOUT_MS)) return;
4456
4490
  state.idleTimer = setTimeout(() => void shutdownDaemon2(state, "idle timeout"), IDLE_TIMEOUT_MS);
4457
4491
  state.idleTimer.unref();
4458
4492
  }
@@ -4626,12 +4660,13 @@ var IDLE_TIMEOUT_MS, LIVENESS_PROBE_TIMEOUT_MS, MAX_CONSECUTIVE_CRASHES;
4626
4660
  var init_server = __esm({
4627
4661
  "lib/commands/daemon/server.ts"() {
4628
4662
  init_daemon_socket_path();
4663
+ init_parse_daemon_idle_timeout();
4629
4664
  init_socket_utils();
4630
4665
  init_config();
4631
4666
  init_browser();
4632
4667
  init_tests_in_browser();
4633
4668
  init_run();
4634
- IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
4669
+ IDLE_TIMEOUT_MS = parseDaemonIdleTimeout(process.env.QUNITX_DAEMON_IDLE_TIMEOUT).ms;
4635
4670
  LIVENESS_PROBE_TIMEOUT_MS = 500;
4636
4671
  MAX_CONSECUTIVE_CRASHES = 2;
4637
4672
  }
@@ -4682,6 +4717,8 @@ function waitForFile(filePath, timeoutMs) {
4682
4717
  });
4683
4718
  }
4684
4719
  async function spawnAndWaitForDaemon() {
4720
+ const parsed = parseDaemonIdleTimeout(process.env.QUNITX_DAEMON_IDLE_TIMEOUT);
4721
+ if (parsed.warning) process.stderr.write(parsed.warning + "\n");
4685
4722
  spawn3(process.execPath, [CLI_ENTRY, "daemon", "_serve"], {
4686
4723
  detached: true,
4687
4724
  stdio: "ignore",
@@ -4740,6 +4777,7 @@ var init_daemon = __esm({
4740
4777
  "lib/commands/daemon/index.ts"() {
4741
4778
  init_color();
4742
4779
  init_daemon_socket_path();
4780
+ init_parse_daemon_idle_timeout();
4743
4781
  init_client();
4744
4782
  init_package();
4745
4783
  SPAWN_TIMEOUT_MS = 3e4;
@@ -4753,8 +4791,10 @@ ${color2("$ qunitx daemon stop")} # Stop the running daemon
4753
4791
  ${color2("$ qunitx daemon status")} # Print pid, socket, and uptime
4754
4792
 
4755
4793
  ${highlight2("Environment:")}
4756
- ${color2("QUNITX_DAEMON=1")} : auto-spawn the daemon on the first qunitx run; reuse it on every run after (overrides the CI=1 bypass)
4757
- ${color2("QUNITX_NO_DAEMON=1")} : never use the daemon for this run
4794
+ ${color2("QUNITX_DAEMON=1")} : auto-spawn the daemon on the first qunitx run; reuse it on every run after (overrides the CI=1 bypass)
4795
+ ${color2("QUNITX_NO_DAEMON=1")} : never use the daemon for this run
4796
+ ${color2("QUNITX_DAEMON_IDLE_TIMEOUT")} : idle window before self-shutdown (default 30m). Bare number = minutes; ${color2("ms")} / ${color2("s")} / ${color2("m")} / ${color2("h")} suffixes accepted (${color2("1h")}, ${color2("45s")}, ${color2("500ms")}). Set to ${color2("false")} to disable auto-shutdown. Read at daemon spawn; invalid values warn and fall back to the default.
4797
+ ${color2("QUNITX_DAEMON_LOG=<path>")} : redirect the daemon's stdout + stderr to a file (otherwise lost to the detached spawn)
4758
4798
 
4759
4799
  ${highlight2("Tip:")} set ${color2("QUNITX_DAEMON=1")} to auto-spawn the daemon on the first qunitx run; ${color2("$ qunitx --help")} for top-level options.
4760
4800
  `;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "qunitx-cli",
3
3
  "type": "module",
4
- "version": "0.23.0",
4
+ "version": "0.23.2",
5
5
  "description": "Browser runner for QUnitx: run your qunitx tests in google-chrome",
6
6
  "author": "Izel Nakri",
7
7
  "license": "MIT",