qunitx-cli 0.17.1 → 0.17.6

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,45 @@ 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(async () => {
111
+ const pgid = proc.pid;
112
+ const warnTimer = setTimeout(
113
+ () => process.stderr.write(
114
+ `# [qunitx] warning: Chrome process group ${pgid} still alive 500ms after SIGKILL, waiting...
115
+ `
116
+ ),
117
+ 500
118
+ );
119
+ warnTimer.unref();
120
+ while (true) {
121
+ try {
122
+ process.kill(-pgid, 0);
123
+ } catch {
124
+ break;
125
+ }
126
+ await new Promise((r) => setTimeout(r, 20));
127
+ }
128
+ clearTimeout(warnTimer);
129
+ await rm(userDataDir, { recursive: true, force: true }).catch(() => {
130
+ });
131
+ });
92
132
  }
93
133
  }
94
134
  var CDP_URL_REGEX;
95
135
  var init_pre_launch_chrome = __esm({
96
136
  "lib/utils/pre-launch-chrome.ts"() {
137
+ init_kill_process_group();
97
138
  CDP_URL_REGEX = /DevTools listening on (ws:\/\/[^\s]+)/;
98
139
  }
99
140
  });
100
141
 
101
142
  // lib/utils/chromium-args.ts
102
- var chromium_args_default;
143
+ var CHROMIUM_ARGS;
103
144
  var init_chromium_args = __esm({
104
145
  "lib/utils/chromium-args.ts"() {
105
- chromium_args_default = [
146
+ CHROMIUM_ARGS = [
106
147
  // ── Sandbox / rendering ──────────────────────────────────────────────────────
107
148
  "--no-sandbox",
108
149
  // required in most CI/container environments
@@ -202,6 +243,7 @@ var init_early_chrome = __esm({
202
243
  "lib/utils/early-chrome.ts"() {
203
244
  init_find_chrome();
204
245
  init_pre_launch_chrome();
246
+ init_kill_process_group();
205
247
  init_chromium_args();
206
248
  init_perf_logger();
207
249
  NON_RUN_COMMANDS = /* @__PURE__ */ new Set(["help", "h", "p", "print", "new", "n", "g", "generate", "init"]);
@@ -220,16 +262,13 @@ var init_early_chrome = __esm({
220
262
  if (!openWatchMode) {
221
263
  process.on("exit", () => {
222
264
  if (!earlyChrome) return;
223
- try {
224
- earlyChrome.proc.kill("SIGKILL");
225
- } catch {
226
- }
265
+ killProcessGroup(earlyChrome.proc.pid);
227
266
  });
228
267
  }
229
268
  perfLog("early-chrome.js: module evaluated");
230
269
  earlyBrowserPromise = isRunCommand && browserFromArgv === "chromium" ? findChrome().then((chromePath) => {
231
270
  perfLog("early-chrome.js: findChrome resolved", chromePath);
232
- return preLaunchChrome(chromePath, chromium_args_default, !openWatchMode);
271
+ return preLaunchChrome(chromePath, CHROMIUM_ARGS, !openWatchMode);
233
272
  }).then((info) => {
234
273
  perfLog("early-chrome.js: Chrome CDP ready", info?.cdpEndpoint ?? null);
235
274
  if (info) earlyChrome = info;
@@ -611,8 +650,12 @@ var init_http = __esm({
611
650
  * @returns {Promise<void>}
612
651
  */
613
652
  close() {
653
+ this.wss.clients.forEach((client) => client.terminate());
654
+ const wssClose = new Promise((resolve) => this.wss.close(() => resolve()));
614
655
  this._server.closeAllConnections?.();
615
- return new Promise((resolve) => this._server.close(resolve));
656
+ const serverClose = new Promise((resolve) => this._server.close(resolve));
657
+ return Promise.all([wssClose, serverClose]).then(() => {
658
+ });
616
659
  }
617
660
  /** Registers a GET route handler. */
618
661
  get(path6, handler) {
@@ -1118,7 +1161,7 @@ async function launchBrowser(config) {
1118
1161
  }
1119
1162
  const executablePath = await findChrome();
1120
1163
  const launchOptions = {
1121
- args: chromium_args_default,
1164
+ args: CHROMIUM_ARGS,
1122
1165
  headless: true,
1123
1166
  // Disable Playwright's async SIGTERM/SIGHUP handlers. When the CLI is killed by an
1124
1167
  // external signal (e.g. exec() timeout in tests), those handlers start an async browser
@@ -1281,6 +1324,7 @@ async function buildTestBundle(config, cachedContent) {
1281
1324
  return;
1282
1325
  }
1283
1326
  const outfile = `${projectRoot}/${output}/tests.js`;
1327
+ await fs8.mkdir(`${projectRoot}/${output}`, { recursive: true });
1284
1328
  const sourcemap = config.debug ? "inline" : config.watch ? "linked" : false;
1285
1329
  const needsDisk = true;
1286
1330
  const [allTestCode] = await Promise.all([
@@ -1824,7 +1868,8 @@ var init_write_output_static_files = __esm({
1824
1868
  // lib/commands/run.ts
1825
1869
  var run_exports = {};
1826
1870
  __export(run_exports, {
1827
- default: () => run
1871
+ default: () => run,
1872
+ run: () => run
1828
1873
  });
1829
1874
  import fs11 from "node:fs/promises";
1830
1875
  import { normalize } from "node:path";
@@ -2096,7 +2141,7 @@ init_color();
2096
2141
  var package_default = {
2097
2142
  name: "qunitx-cli",
2098
2143
  type: "module",
2099
- version: "0.17.1",
2144
+ version: "0.17.6",
2100
2145
  description: "Browser runner for QUnitx: run your qunitx tests in google-chrome",
2101
2146
  author: "Izel Nakri",
2102
2147
  license: "MIT",
@@ -2119,7 +2164,7 @@ var package_default = {
2119
2164
  prepublishOnly: "npm run build",
2120
2165
  format: 'prettier --check "lib/**/*.ts" "test/**/*.ts" "scripts/**/*.js" "bin/**/*.js" "*.ts" "package.json" ".github/**/*.yml"',
2121
2166
  "format:fix": 'prettier --write "lib/**/*.ts" "test/**/*.ts" "scripts/**/*.js" "bin/**/*.js" "*.ts" "package.json" ".github/**/*.yml"',
2122
- lint: "deno lint lib/ cli.ts",
2167
+ lint: "deno lint lib/ bin/ cli.ts",
2123
2168
  "lint:docs": "node scripts/lint-docs.js",
2124
2169
  docs: `deno doc --html --name="qunitx-cli" --output=docs/lib 'lib/**/*.ts' README.md`,
2125
2170
  "changelog:unreleased": "git-cliff --unreleased --strip all",
@@ -2226,12 +2271,11 @@ async function searchInParentDirectories(directory, targetEntry) {
2226
2271
  targetEntry
2227
2272
  );
2228
2273
  }
2229
- var search_in_parent_directories_default = searchInParentDirectories;
2230
2274
 
2231
2275
  // lib/utils/find-project-root.ts
2232
2276
  async function findProjectRoot() {
2233
2277
  try {
2234
- const absolutePath = await search_in_parent_directories_default(".", "package.json");
2278
+ const absolutePath = await searchInParentDirectories(".", "package.json");
2235
2279
  if (!absolutePath.includes("package.json")) {
2236
2280
  throw new Error("package.json mising");
2237
2281
  }
@@ -2246,7 +2290,7 @@ async function findProjectRoot() {
2246
2290
  init_path_exists();
2247
2291
 
2248
2292
  // lib/setup/default-project-config-values.ts
2249
- var default_project_config_values_default = {
2293
+ var defaultProjectConfigValues = {
2250
2294
  output: "tmp",
2251
2295
  timeout: 2e4,
2252
2296
  failFast: false,
@@ -2262,7 +2306,7 @@ async function initializeProject() {
2262
2306
  const oldPackageJSON = JSON.parse(await fs3.readFile(`${projectRoot}/package.json`));
2263
2307
  const existingQunitx = oldPackageJSON.qunitx || {};
2264
2308
  const cliHtmlPaths = process.argv.slice(2).filter((arg) => arg.endsWith(".html"));
2265
- const config = Object.assign({}, default_project_config_values_default, existingQunitx, {
2309
+ const config = Object.assign({}, defaultProjectConfigValues, existingQunitx, {
2266
2310
  htmlPaths: cliHtmlPaths.length > 0 ? cliHtmlPaths : existingQunitx.htmlPaths || ["test/tests.html"]
2267
2311
  });
2268
2312
  await Promise.all([
@@ -2313,9 +2357,22 @@ init_color();
2313
2357
  import fs4 from "node:fs/promises";
2314
2358
  init_path_exists();
2315
2359
  init_read_boilerplate();
2360
+
2361
+ // lib/utils/convert-to-pascal-case.ts
2362
+ function convertToPascalCase(str) {
2363
+ return str.split(/[-_]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
2364
+ }
2365
+
2366
+ // lib/commands/generate.ts
2367
+ function pathToModuleName(filePath) {
2368
+ const withoutExt = filePath.replace(/\.(js|ts)$/, "");
2369
+ const segments = withoutExt.split("/");
2370
+ const targetNames = segments[0] === "test" || segments[0] === "tests" ? segments.slice(1) : segments;
2371
+ return targetNames.map(convertToPascalCase).join(" | ");
2372
+ }
2316
2373
  async function generateTestFiles() {
2317
2374
  const projectRoot = await findProjectRoot();
2318
- const moduleName = process.argv[3];
2375
+ const moduleName = pathToModuleName(process.argv[3]);
2319
2376
  const path6 = process.argv[3].endsWith(".js") || process.argv[3].endsWith(".ts") ? `${projectRoot}/${process.argv[3]}` : `${projectRoot}/${process.argv[3]}.js`;
2320
2377
  if (await pathExists(path6)) {
2321
2378
  console.log(`${path6} already exists!`);
@@ -2523,7 +2580,7 @@ async function setupConfig() {
2523
2580
  const projectPackageJSON = await readConfigFromPackageJSON(projectRoot);
2524
2581
  const inputs2 = cliConfigFlags.inputs.concat(readInputsFromPackageJSON(projectPackageJSON));
2525
2582
  const config = {
2526
- ...default_project_config_values_default,
2583
+ ...defaultProjectConfigValues,
2527
2584
  htmlPaths: [],
2528
2585
  ...projectPackageJSON.qunitx || {},
2529
2586
  ...cliConfigFlags,
@@ -2568,10 +2625,7 @@ process4.title = "qunitx";
2568
2625
  } else if (["init"].includes(process4.argv[2])) {
2569
2626
  return await initializeProject();
2570
2627
  }
2571
- const [config, { default: run2 }] = await Promise.all([
2572
- setupConfig(),
2573
- Promise.resolve().then(() => (init_run(), run_exports))
2574
- ]);
2628
+ const [config, { run: run2 }] = await Promise.all([setupConfig(), Promise.resolve().then(() => (init_run(), run_exports))]);
2575
2629
  try {
2576
2630
  return await run2(config);
2577
2631
  } 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.1",
4
+ "version": "0.17.6",
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",