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 +13 -4
- package/dist/cli.js +85 -31
- package/package.json +2 -2
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
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
143
|
+
var CHROMIUM_ARGS;
|
|
103
144
|
var init_chromium_args = __esm({
|
|
104
145
|
"lib/utils/chromium-args.ts"() {
|
|
105
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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
|
|
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
|
|
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({},
|
|
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
|
-
...
|
|
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, {
|
|
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.
|
|
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",
|