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.
- package/dist/cli.js +216 -131
- 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((
|
|
196
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
127
197
|
}
|
|
128
198
|
clearTimeout(warnTimer);
|
|
129
|
-
await
|
|
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/
|
|
235
|
-
async function
|
|
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,
|
|
242
|
-
var
|
|
243
|
-
"lib/utils/
|
|
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("
|
|
269
|
-
|
|
270
|
-
perfLog("
|
|
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("
|
|
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
|
|
389
|
+
import fs2 from "node:fs/promises";
|
|
320
390
|
async function pathExists(path6) {
|
|
321
391
|
try {
|
|
322
|
-
await
|
|
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-
|
|
334
|
-
import
|
|
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
|
|
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
|
|
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
|
|
352
|
-
"lib/utils/read-
|
|
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
|
|
372
|
-
function
|
|
373
|
-
return html.includes(
|
|
441
|
+
// lib/utils/html.ts
|
|
442
|
+
function findScriptPlaceholder(html) {
|
|
443
|
+
return html.includes(SCRIPT_PLACEHOLDER) ? SCRIPT_PLACEHOLDER : void 0;
|
|
374
444
|
}
|
|
375
|
-
function
|
|
376
|
-
return !!
|
|
445
|
+
function isCustomTemplate(html) {
|
|
446
|
+
return !!findScriptPlaceholder(html) || HANDLEBARS_TOKEN_REGEX.test(html);
|
|
377
447
|
}
|
|
378
|
-
function
|
|
379
|
-
const
|
|
380
|
-
if (
|
|
381
|
-
return html.replace(
|
|
448
|
+
function injectScript(html, content) {
|
|
449
|
+
const placeholder = findScriptPlaceholder(html);
|
|
450
|
+
if (placeholder) {
|
|
451
|
+
return html.replace(placeholder, content);
|
|
382
452
|
}
|
|
383
|
-
if (
|
|
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
|
|
395
|
-
var
|
|
396
|
-
"lib/utils/html
|
|
397
|
-
|
|
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 =
|
|
561
|
-
EXPLICIT_PORT_RETRY_DELAY_MS =
|
|
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
|
|
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
|
-
|
|
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
|
|
1123
|
-
|
|
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
|
-
|
|
1199
|
+
init_html();
|
|
1133
1200
|
init_display_test_result();
|
|
1134
1201
|
init_color();
|
|
1135
1202
|
init_path_exists();
|
|
1136
1203
|
init_http();
|
|
1137
|
-
fsPromise =
|
|
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,
|
|
1213
|
+
const [playwrightCore2, prelaunch] = await Promise.all([
|
|
1147
1214
|
playwrightCorePromise,
|
|
1148
|
-
|
|
1215
|
+
prelaunchPromise
|
|
1149
1216
|
]);
|
|
1150
1217
|
perfLog(
|
|
1151
|
-
`browser.js: playwright-core +
|
|
1152
|
-
|
|
1218
|
+
`browser.js: playwright-core + prelaunch resolved in ${Date.now() - waitStart}ms, prelaunch:`,
|
|
1219
|
+
prelaunch?.cdpEndpoint ?? null
|
|
1153
1220
|
);
|
|
1154
|
-
if (
|
|
1221
|
+
if (prelaunch) {
|
|
1155
1222
|
const connectStart = Date.now();
|
|
1156
1223
|
const browser = await playwrightCore2.chromium.connectOverCDP({
|
|
1157
|
-
endpointURL:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
1357
|
-
await
|
|
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
|
|
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
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1603
|
-
symlinkPollers.set(filePath, () =>
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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) =>
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2129
|
-
|
|
2213
|
+
init_read_template();
|
|
2214
|
+
init_html();
|
|
2130
2215
|
}
|
|
2131
2216
|
});
|
|
2132
2217
|
|
|
2133
2218
|
// cli.ts
|
|
2134
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
2388
|
+
init_read_template();
|
|
2304
2389
|
async function initializeProject() {
|
|
2305
2390
|
const projectRoot = await findProjectRoot();
|
|
2306
|
-
const oldPackageJSON = JSON.parse(await
|
|
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
|
|
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
|
|
2336
|
-
await
|
|
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
|
|
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
|
|
2350
|
-
await
|
|
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
|
|
2442
|
+
import fs5 from "node:fs/promises";
|
|
2358
2443
|
init_path_exists();
|
|
2359
|
-
|
|
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
|
|
2466
|
+
const testJSContent = await readTemplate("test.js");
|
|
2382
2467
|
const targetFolderPaths = path6.split("/");
|
|
2383
2468
|
targetFolderPaths.pop();
|
|
2384
|
-
await
|
|
2385
|
-
await
|
|
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
|
|
2475
|
+
import fs7 from "node:fs/promises";
|
|
2391
2476
|
|
|
2392
2477
|
// lib/setup/fs-tree.ts
|
|
2393
|
-
import
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
63
|
+
"qunitx": "^1.2.7",
|
|
64
64
|
"typescript": "^6.0.2"
|
|
65
65
|
},
|
|
66
66
|
"volta": {
|