qunitx-cli 0.17.0 → 0.17.5

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.
package/bin/qunitx.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // (qunitx-cli-linux-x64, qunitx-cli-darwin-arm64, etc.) when available.
5
5
  // Falls back to the bundled JS CLI (dist/cli.js) which requires Node.js + node_modules.
6
6
  import { spawn } from 'node:child_process';
7
- import { access, constants } from 'node:fs/promises';
7
+ import { access, constants, readFile } from 'node:fs/promises';
8
8
  import { createRequire } from 'node:module';
9
9
  import { fileURLToPath } from 'node:url';
10
10
  import { dirname, join } from 'node:path';
@@ -12,6 +12,10 @@ import { dirname, join } from 'node:path';
12
12
  const __dirname = dirname(fileURLToPath(import.meta.url));
13
13
  const require = createRequire(import.meta.url);
14
14
 
15
+ const currentVersion = JSON.parse(
16
+ await readFile(join(__dirname, '../package.json'), 'utf8'),
17
+ ).version;
18
+
15
19
  const platformMap = {
16
20
  'linux-x64': { seaPkg: 'qunitx-cli-linux-x64', esbuildPkg: '@esbuild/linux-x64', bin: 'qunitx' },
17
21
  'linux-arm64': {
@@ -41,7 +45,10 @@ const target = platformMap[`${process.platform}-${process.arch}`];
41
45
  async function trySeaBinary() {
42
46
  if (!target) return false;
43
47
  try {
44
- const pkgDir = dirname(require.resolve(`${target.seaPkg}/package.json`));
48
+ const pkgJsonPath = require.resolve(`${target.seaPkg}/package.json`);
49
+ const seaVersion = JSON.parse(await readFile(pkgJsonPath, 'utf8')).version;
50
+ if (seaVersion !== currentVersion) return false;
51
+ const pkgDir = dirname(pkgJsonPath);
45
52
  const binaryPath = join(pkgDir, 'bin', target.bin);
46
53
  await access(binaryPath, constants.X_OK);
47
54
 
@@ -52,10 +59,12 @@ async function trySeaBinary() {
52
59
  `${target.esbuildPkg}/bin/esbuild${process.platform === 'win32' ? '.exe' : ''}`,
53
60
  );
54
61
  env = { ...env, ESBUILD_BINARY_PATH: esbuildBin };
55
- } catch (_) {}
62
+ } catch (_e) {
63
+ // esbuild binary not found in optional package — env stays as-is
64
+ }
56
65
  }
57
66
 
58
- await new Promise((resolve, reject) => {
67
+ await new Promise((_resolve, reject) => {
59
68
  const child = spawn(binaryPath, process.argv.slice(2), { stdio: 'inherit', env });
60
69
  child.on('close', (code) => process.exit(code ?? 1));
61
70
  child.on('error', reject);
package/dist/cli.js CHANGED
@@ -37,6 +37,22 @@ var init_find_chrome = __esm({
37
37
  }
38
38
  });
39
39
 
40
+ // lib/utils/kill-process-group.ts
41
+ function killProcessGroup(pid) {
42
+ try {
43
+ if (process.platform === "win32") {
44
+ process.kill(pid, "SIGKILL");
45
+ } else {
46
+ process.kill(-pid, "SIGKILL");
47
+ }
48
+ } catch {
49
+ }
50
+ }
51
+ var init_kill_process_group = __esm({
52
+ "lib/utils/kill-process-group.ts"() {
53
+ }
54
+ });
55
+
40
56
  // lib/utils/pre-launch-chrome.ts
41
57
  import { spawn } from "node:child_process";
42
58
  import { mkdtemp, rm } from "node:fs/promises";
@@ -45,16 +61,20 @@ import path from "node:path";
45
61
  async function preLaunchChrome(chromePath, args, headless = true) {
46
62
  if (!chromePath) return null;
47
63
  const userDataDir = await mkdtemp(path.join(os.tmpdir(), "qunitx-chrome-"));
48
- const cleanup = () => rm(userDataDir, { recursive: true, force: true }).catch(() => {
49
- });
50
64
  const headlessArgs = headless ? ["--headless=new"] : [];
51
65
  const proc = spawn(
52
66
  chromePath,
53
67
  ["--remote-debugging-port=0", `--user-data-dir=${userDataDir}`, ...headlessArgs, ...args],
54
- { stdio: ["ignore", "ignore", "pipe"] }
68
+ // detached: true puts Chrome in its own process group (PGID = proc.pid).
69
+ // This lets shutdown() kill the entire group (main process + all renderer/GPU/utility
70
+ // children) with process.kill(-proc.pid, 'SIGKILL'), preventing orphaned Chrome
71
+ // subprocesses from accumulating inotify watches across test runs.
72
+ { stdio: ["ignore", "ignore", "pipe"], detached: true }
55
73
  );
74
+ let cdpConnected = false;
56
75
  proc.on("close", () => {
57
- cleanup();
76
+ if (!cdpConnected) rm(userDataDir, { recursive: true, force: true }).catch(() => {
77
+ });
58
78
  resolveWith(null);
59
79
  });
60
80
  proc.on("error", () => resolveWith(null));
@@ -66,6 +86,7 @@ async function preLaunchChrome(chromePath, args, headless = true) {
66
86
  buffer += chunk.toString();
67
87
  const match = buffer.match(CDP_URL_REGEX);
68
88
  if (!match) return;
89
+ cdpConnected = true;
69
90
  proc.unref();
70
91
  proc.stderr.unref();
71
92
  resolve({
@@ -84,25 +105,25 @@ async function preLaunchChrome(chromePath, args, headless = true) {
84
105
  }
85
106
  proc.once("close", resolve);
86
107
  });
87
- try {
88
- if (proc.exitCode === null) proc.kill("SIGKILL");
89
- } catch {
90
- }
91
- await closed.then(() => cleanup());
108
+ if (proc.exitCode === null) killProcessGroup(proc.pid);
109
+ await closed;
110
+ await rm(userDataDir, { recursive: true, force: true }).catch(() => {
111
+ });
92
112
  }
93
113
  }
94
114
  var CDP_URL_REGEX;
95
115
  var init_pre_launch_chrome = __esm({
96
116
  "lib/utils/pre-launch-chrome.ts"() {
117
+ init_kill_process_group();
97
118
  CDP_URL_REGEX = /DevTools listening on (ws:\/\/[^\s]+)/;
98
119
  }
99
120
  });
100
121
 
101
122
  // lib/utils/chromium-args.ts
102
- var chromium_args_default;
123
+ var CHROMIUM_ARGS;
103
124
  var init_chromium_args = __esm({
104
125
  "lib/utils/chromium-args.ts"() {
105
- chromium_args_default = [
126
+ CHROMIUM_ARGS = [
106
127
  // ── Sandbox / rendering ──────────────────────────────────────────────────────
107
128
  "--no-sandbox",
108
129
  // required in most CI/container environments
@@ -202,6 +223,7 @@ var init_early_chrome = __esm({
202
223
  "lib/utils/early-chrome.ts"() {
203
224
  init_find_chrome();
204
225
  init_pre_launch_chrome();
226
+ init_kill_process_group();
205
227
  init_chromium_args();
206
228
  init_perf_logger();
207
229
  NON_RUN_COMMANDS = /* @__PURE__ */ new Set(["help", "h", "p", "print", "new", "n", "g", "generate", "init"]);
@@ -220,16 +242,13 @@ var init_early_chrome = __esm({
220
242
  if (!openWatchMode) {
221
243
  process.on("exit", () => {
222
244
  if (!earlyChrome) return;
223
- try {
224
- earlyChrome.proc.kill("SIGKILL");
225
- } catch {
226
- }
245
+ killProcessGroup(earlyChrome.proc.pid);
227
246
  });
228
247
  }
229
248
  perfLog("early-chrome.js: module evaluated");
230
249
  earlyBrowserPromise = isRunCommand && browserFromArgv === "chromium" ? findChrome().then((chromePath) => {
231
250
  perfLog("early-chrome.js: findChrome resolved", chromePath);
232
- return preLaunchChrome(chromePath, chromium_args_default, !openWatchMode);
251
+ return preLaunchChrome(chromePath, CHROMIUM_ARGS, !openWatchMode);
233
252
  }).then((info) => {
234
253
  perfLog("early-chrome.js: Chrome CDP ready", info?.cdpEndpoint ?? null);
235
254
  if (info) earlyChrome = info;
@@ -611,8 +630,12 @@ var init_http = __esm({
611
630
  * @returns {Promise<void>}
612
631
  */
613
632
  close() {
633
+ this.wss.clients.forEach((client) => client.terminate());
634
+ const wssClose = new Promise((resolve) => this.wss.close(() => resolve()));
614
635
  this._server.closeAllConnections?.();
615
- return new Promise((resolve) => this._server.close(resolve));
636
+ const serverClose = new Promise((resolve) => this._server.close(resolve));
637
+ return Promise.all([wssClose, serverClose]).then(() => {
638
+ });
616
639
  }
617
640
  /** Registers a GET route handler. */
618
641
  get(path6, handler) {
@@ -1118,7 +1141,7 @@ async function launchBrowser(config) {
1118
1141
  }
1119
1142
  const executablePath = await findChrome();
1120
1143
  const launchOptions = {
1121
- args: chromium_args_default,
1144
+ args: CHROMIUM_ARGS,
1122
1145
  headless: true,
1123
1146
  // Disable Playwright's async SIGTERM/SIGHUP handlers. When the CLI is killed by an
1124
1147
  // external signal (e.g. exec() timeout in tests), those handlers start an async browser
@@ -1281,6 +1304,7 @@ async function buildTestBundle(config, cachedContent) {
1281
1304
  return;
1282
1305
  }
1283
1306
  const outfile = `${projectRoot}/${output}/tests.js`;
1307
+ await fs8.mkdir(`${projectRoot}/${output}`, { recursive: true });
1284
1308
  const sourcemap = config.debug ? "inline" : config.watch ? "linked" : false;
1285
1309
  const needsDisk = true;
1286
1310
  const [allTestCode] = await Promise.all([
@@ -1824,7 +1848,8 @@ var init_write_output_static_files = __esm({
1824
1848
  // lib/commands/run.ts
1825
1849
  var run_exports = {};
1826
1850
  __export(run_exports, {
1827
- default: () => run
1851
+ default: () => run,
1852
+ run: () => run
1828
1853
  });
1829
1854
  import fs11 from "node:fs/promises";
1830
1855
  import { normalize } from "node:path";
@@ -2096,7 +2121,7 @@ init_color();
2096
2121
  var package_default = {
2097
2122
  name: "qunitx-cli",
2098
2123
  type: "module",
2099
- version: "0.17.0",
2124
+ version: "0.17.5",
2100
2125
  description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
2101
2126
  author: "Izel Nakri",
2102
2127
  license: "MIT",
@@ -2119,7 +2144,7 @@ var package_default = {
2119
2144
  prepublishOnly: "npm run build",
2120
2145
  format: 'prettier --check "lib/**/*.ts" "test/**/*.ts" "scripts/**/*.js" "bin/**/*.js" "*.ts" "package.json" ".github/**/*.yml"',
2121
2146
  "format:fix": 'prettier --write "lib/**/*.ts" "test/**/*.ts" "scripts/**/*.js" "bin/**/*.js" "*.ts" "package.json" ".github/**/*.yml"',
2122
- lint: "deno lint lib/ cli.ts",
2147
+ lint: "deno lint lib/ bin/ cli.ts",
2123
2148
  "lint:docs": "node scripts/lint-docs.js",
2124
2149
  docs: `deno doc --html --name="qunitx-cli" --output=docs/lib 'lib/**/*.ts' README.md`,
2125
2150
  "changelog:unreleased": "git-cliff --unreleased --strip all",
@@ -2226,12 +2251,11 @@ async function searchInParentDirectories(directory, targetEntry) {
2226
2251
  targetEntry
2227
2252
  );
2228
2253
  }
2229
- var search_in_parent_directories_default = searchInParentDirectories;
2230
2254
 
2231
2255
  // lib/utils/find-project-root.ts
2232
2256
  async function findProjectRoot() {
2233
2257
  try {
2234
- const absolutePath = await search_in_parent_directories_default(".", "package.json");
2258
+ const absolutePath = await searchInParentDirectories(".", "package.json");
2235
2259
  if (!absolutePath.includes("package.json")) {
2236
2260
  throw new Error("package.json mising");
2237
2261
  }
@@ -2246,7 +2270,7 @@ async function findProjectRoot() {
2246
2270
  init_path_exists();
2247
2271
 
2248
2272
  // lib/setup/default-project-config-values.ts
2249
- var default_project_config_values_default = {
2273
+ var defaultProjectConfigValues = {
2250
2274
  output: "tmp",
2251
2275
  timeout: 2e4,
2252
2276
  failFast: false,
@@ -2262,7 +2286,7 @@ async function initializeProject() {
2262
2286
  const oldPackageJSON = JSON.parse(await fs3.readFile(`${projectRoot}/package.json`));
2263
2287
  const existingQunitx = oldPackageJSON.qunitx || {};
2264
2288
  const cliHtmlPaths = process.argv.slice(2).filter((arg) => arg.endsWith(".html"));
2265
- const config = Object.assign({}, default_project_config_values_default, existingQunitx, {
2289
+ const config = Object.assign({}, defaultProjectConfigValues, existingQunitx, {
2266
2290
  htmlPaths: cliHtmlPaths.length > 0 ? cliHtmlPaths : existingQunitx.htmlPaths || ["test/tests.html"]
2267
2291
  });
2268
2292
  await Promise.all([
@@ -2313,9 +2337,22 @@ init_color();
2313
2337
  import fs4 from "node:fs/promises";
2314
2338
  init_path_exists();
2315
2339
  init_read_boilerplate();
2340
+
2341
+ // lib/utils/convert-to-pascal-case.ts
2342
+ function convertToPascalCase(str) {
2343
+ return str.split(/[-_]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
2344
+ }
2345
+
2346
+ // lib/commands/generate.ts
2347
+ function pathToModuleName(filePath) {
2348
+ const withoutExt = filePath.replace(/\.(js|ts)$/, "");
2349
+ const segments = withoutExt.split("/");
2350
+ const targetNames = segments[0] === "test" || segments[0] === "tests" ? segments.slice(1) : segments;
2351
+ return targetNames.map(convertToPascalCase).join(" | ");
2352
+ }
2316
2353
  async function generateTestFiles() {
2317
2354
  const projectRoot = await findProjectRoot();
2318
- const moduleName = process.argv[3];
2355
+ const moduleName = pathToModuleName(process.argv[3]);
2319
2356
  const path6 = process.argv[3].endsWith(".js") || process.argv[3].endsWith(".ts") ? `${projectRoot}/${process.argv[3]}` : `${projectRoot}/${process.argv[3]}.js`;
2320
2357
  if (await pathExists(path6)) {
2321
2358
  console.log(`${path6} already exists!`);
@@ -2523,7 +2560,7 @@ async function setupConfig() {
2523
2560
  const projectPackageJSON = await readConfigFromPackageJSON(projectRoot);
2524
2561
  const inputs2 = cliConfigFlags.inputs.concat(readInputsFromPackageJSON(projectPackageJSON));
2525
2562
  const config = {
2526
- ...default_project_config_values_default,
2563
+ ...defaultProjectConfigValues,
2527
2564
  htmlPaths: [],
2528
2565
  ...projectPackageJSON.qunitx || {},
2529
2566
  ...cliConfigFlags,
@@ -2568,10 +2605,7 @@ process4.title = "qunitx";
2568
2605
  } else if (["init"].includes(process4.argv[2])) {
2569
2606
  return await initializeProject();
2570
2607
  }
2571
- const [config, { default: run2 }] = await Promise.all([
2572
- setupConfig(),
2573
- Promise.resolve().then(() => (init_run(), run_exports))
2574
- ]);
2608
+ const [config, { run: run2 }] = await Promise.all([setupConfig(), Promise.resolve().then(() => (init_run(), run_exports))]);
2575
2609
  try {
2576
2610
  return await run2(config);
2577
2611
  } catch (error) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "qunitx-cli",
3
3
  "type": "module",
4
- "version": "0.17.0",
4
+ "version": "0.17.5",
5
5
  "description": "Browser runner for QUnitx: run your qunitx tests in google-chrome",
6
6
  "author": "Izel Nakri",
7
7
  "license": "MIT",
@@ -24,7 +24,7 @@
24
24
  "prepublishOnly": "npm run build",
25
25
  "format": "prettier --check \"lib/**/*.ts\" \"test/**/*.ts\" \"scripts/**/*.js\" \"bin/**/*.js\" \"*.ts\" \"package.json\" \".github/**/*.yml\"",
26
26
  "format:fix": "prettier --write \"lib/**/*.ts\" \"test/**/*.ts\" \"scripts/**/*.js\" \"bin/**/*.js\" \"*.ts\" \"package.json\" \".github/**/*.yml\"",
27
- "lint": "deno lint lib/ cli.ts",
27
+ "lint": "deno lint lib/ bin/ cli.ts",
28
28
  "lint:docs": "node scripts/lint-docs.js",
29
29
  "docs": "deno doc --html --name=\"qunitx-cli\" --output=docs/lib 'lib/**/*.ts' README.md",
30
30
  "changelog:unreleased": "git-cliff --unreleased --strip all",