@jsenv/test 3.6.4 → 3.6.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.
|
@@ -163,7 +163,7 @@ const executeUsingDynamicImport = async ({
|
|
|
163
163
|
finalizeCoverage = async () => {
|
|
164
164
|
const [coverage, { filterV8Coverage }] = await Promise.all([
|
|
165
165
|
stopJsCoverage(),
|
|
166
|
-
import("./
|
|
166
|
+
import("./v8_coverage/v8_coverage.js"),
|
|
167
167
|
]);
|
|
168
168
|
const coverageLight = await filterV8Coverage(coverage, {
|
|
169
169
|
rootDirectoryUrl,
|
package/dist/jsenv_test.js
CHANGED
|
@@ -4,7 +4,6 @@ import os, { cpus, totalmem, release, availableParallelism, freemem } from "node
|
|
|
4
4
|
import tty from "node:tty";
|
|
5
5
|
import stringWidth from "string-width";
|
|
6
6
|
import { applyNodeEsmResolution } from "@jsenv/node-esm-resolution";
|
|
7
|
-
import { URL_META, filterV8Coverage } from "./js/v8_coverage.js";
|
|
8
7
|
import { readdir, chmod, stat, lstat, chmodSync, statSync, lstatSync, promises, readFile as readFile$1, readdirSync, openSync, closeSync, unlinkSync, rmdirSync, mkdirSync, readFileSync, writeFileSync as writeFileSync$1, unlink, rmdir, existsSync } from "node:fs";
|
|
9
8
|
import { takeCoverage } from "node:v8";
|
|
10
9
|
import stripAnsi from "strip-ansi";
|
|
@@ -2154,6 +2153,605 @@ const findAncestorDirectoryUrl = (url, callback) => {
|
|
|
2154
2153
|
return null;
|
|
2155
2154
|
};
|
|
2156
2155
|
|
|
2156
|
+
/*
|
|
2157
|
+
* Link to things doing pattern matching:
|
|
2158
|
+
* https://git-scm.com/docs/gitignore
|
|
2159
|
+
* https://github.com/kaelzhang/node-ignore
|
|
2160
|
+
*/
|
|
2161
|
+
|
|
2162
|
+
/** @module jsenv_url_meta **/
|
|
2163
|
+
/**
|
|
2164
|
+
* An object representing the result of applying a pattern to an url
|
|
2165
|
+
* @typedef {Object} MatchResult
|
|
2166
|
+
* @property {boolean} matched Indicates if url matched pattern
|
|
2167
|
+
* @property {number} patternIndex Index where pattern stopped matching url, otherwise pattern.length
|
|
2168
|
+
* @property {number} urlIndex Index where url stopped matching pattern, otherwise url.length
|
|
2169
|
+
* @property {Array} matchGroups Array of strings captured during pattern matching
|
|
2170
|
+
*/
|
|
2171
|
+
|
|
2172
|
+
/**
|
|
2173
|
+
* Apply a pattern to an url
|
|
2174
|
+
* @param {Object} applyPatternMatchingParams
|
|
2175
|
+
* @param {string} applyPatternMatchingParams.pattern "*", "**" and trailing slash have special meaning
|
|
2176
|
+
* @param {string} applyPatternMatchingParams.url a string representing an url
|
|
2177
|
+
* @return {MatchResult}
|
|
2178
|
+
*/
|
|
2179
|
+
const applyPattern = ({ url, pattern }) => {
|
|
2180
|
+
const { matched, patternIndex, index, groups } = applyMatching(pattern, url);
|
|
2181
|
+
const matchGroups = [];
|
|
2182
|
+
let groupIndex = 0;
|
|
2183
|
+
for (const group of groups) {
|
|
2184
|
+
if (group.name) {
|
|
2185
|
+
matchGroups[group.name] = group.string;
|
|
2186
|
+
} else {
|
|
2187
|
+
matchGroups[groupIndex] = group.string;
|
|
2188
|
+
groupIndex++;
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
return {
|
|
2192
|
+
matched,
|
|
2193
|
+
patternIndex,
|
|
2194
|
+
urlIndex: index,
|
|
2195
|
+
matchGroups,
|
|
2196
|
+
};
|
|
2197
|
+
};
|
|
2198
|
+
|
|
2199
|
+
/**
|
|
2200
|
+
* Core pattern matching function that processes patterns against strings
|
|
2201
|
+
* @param {string} pattern - The pattern with special syntax (*,**,/) to match against
|
|
2202
|
+
* @param {string} string - The string to test against the pattern
|
|
2203
|
+
* @returns {Object} Result containing match status and capture groups
|
|
2204
|
+
* @private
|
|
2205
|
+
*/
|
|
2206
|
+
const applyMatching = (pattern, string) => {
|
|
2207
|
+
const groups = [];
|
|
2208
|
+
let patternIndex = 0;
|
|
2209
|
+
let index = 0;
|
|
2210
|
+
let remainingPattern = pattern;
|
|
2211
|
+
let remainingString = string;
|
|
2212
|
+
let restoreIndexes = true;
|
|
2213
|
+
|
|
2214
|
+
const consumePattern = (count) => {
|
|
2215
|
+
const subpattern = remainingPattern.slice(0, count);
|
|
2216
|
+
remainingPattern = remainingPattern.slice(count);
|
|
2217
|
+
patternIndex += count;
|
|
2218
|
+
return subpattern;
|
|
2219
|
+
};
|
|
2220
|
+
const consumeString = (count) => {
|
|
2221
|
+
const substring = remainingString.slice(0, count);
|
|
2222
|
+
remainingString = remainingString.slice(count);
|
|
2223
|
+
index += count;
|
|
2224
|
+
return substring;
|
|
2225
|
+
};
|
|
2226
|
+
const consumeRemainingString = () => {
|
|
2227
|
+
return consumeString(remainingString.length);
|
|
2228
|
+
};
|
|
2229
|
+
|
|
2230
|
+
const matchOne = () => {
|
|
2231
|
+
// pattern consumed
|
|
2232
|
+
if (remainingPattern === "") {
|
|
2233
|
+
if (remainingString === "") {
|
|
2234
|
+
return true; // string fully matched pattern
|
|
2235
|
+
}
|
|
2236
|
+
if (remainingString[0] === "?") {
|
|
2237
|
+
// match search params
|
|
2238
|
+
consumeRemainingString();
|
|
2239
|
+
return true;
|
|
2240
|
+
}
|
|
2241
|
+
return false; // fails because string longer than expect
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
// -- from this point pattern is not consumed --
|
|
2245
|
+
// string consumed, pattern not consumed
|
|
2246
|
+
if (remainingString === "") {
|
|
2247
|
+
if (remainingPattern === "**") {
|
|
2248
|
+
// trailing "**" is optional
|
|
2249
|
+
consumePattern(2);
|
|
2250
|
+
return true;
|
|
2251
|
+
}
|
|
2252
|
+
if (remainingPattern === "*") {
|
|
2253
|
+
groups.push({ string: "" });
|
|
2254
|
+
}
|
|
2255
|
+
return false; // fail because string shorter than expect
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
// -- from this point pattern and string are not consumed --
|
|
2259
|
+
// fast path trailing slash
|
|
2260
|
+
if (remainingPattern === "/") {
|
|
2261
|
+
if (remainingString[0] === "/") {
|
|
2262
|
+
// trailing slash match remaining
|
|
2263
|
+
consumePattern(1);
|
|
2264
|
+
groups.push({ string: consumeRemainingString() });
|
|
2265
|
+
return true;
|
|
2266
|
+
}
|
|
2267
|
+
return false;
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
// fast path trailing '**'
|
|
2271
|
+
if (remainingPattern === "**") {
|
|
2272
|
+
consumePattern(2);
|
|
2273
|
+
consumeRemainingString();
|
|
2274
|
+
return true;
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
if (remainingPattern.slice(0, 4) === "/**/") {
|
|
2278
|
+
consumePattern(3); // consumes "/**/"
|
|
2279
|
+
const skipResult = skipUntilMatchIterative({
|
|
2280
|
+
pattern: remainingPattern,
|
|
2281
|
+
string: remainingString,
|
|
2282
|
+
canSkipSlash: true,
|
|
2283
|
+
});
|
|
2284
|
+
for (let i = 0; i < skipResult.groups.length; i++) {
|
|
2285
|
+
groups.push(skipResult.groups[i]);
|
|
2286
|
+
}
|
|
2287
|
+
consumePattern(skipResult.patternIndex);
|
|
2288
|
+
consumeRemainingString();
|
|
2289
|
+
restoreIndexes = false;
|
|
2290
|
+
return skipResult.matched;
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
// pattern leading **
|
|
2294
|
+
if (remainingPattern.slice(0, 2) === "**") {
|
|
2295
|
+
consumePattern(2); // consumes "**"
|
|
2296
|
+
let skipAllowed = true;
|
|
2297
|
+
if (remainingPattern[0] === "/") {
|
|
2298
|
+
consumePattern(1); // consumes "/"
|
|
2299
|
+
// when remainingPattern was preceeded by "**/"
|
|
2300
|
+
// and remainingString have no "/"
|
|
2301
|
+
// then skip is not allowed, a regular match will be performed
|
|
2302
|
+
if (remainingString.includes("/")) {
|
|
2303
|
+
skipAllowed = false;
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
// pattern ending with "**" or "**/" match remaining string
|
|
2307
|
+
if (remainingPattern === "") {
|
|
2308
|
+
consumeRemainingString();
|
|
2309
|
+
return true;
|
|
2310
|
+
}
|
|
2311
|
+
if (skipAllowed) {
|
|
2312
|
+
const skipResult = skipUntilMatchIterative({
|
|
2313
|
+
pattern: remainingPattern,
|
|
2314
|
+
string: remainingString,
|
|
2315
|
+
canSkipSlash: true,
|
|
2316
|
+
});
|
|
2317
|
+
for (let i = 0; i < skipResult.groups.length; i++) {
|
|
2318
|
+
groups.push(skipResult.groups[i]);
|
|
2319
|
+
}
|
|
2320
|
+
consumePattern(skipResult.patternIndex);
|
|
2321
|
+
consumeRemainingString();
|
|
2322
|
+
restoreIndexes = false;
|
|
2323
|
+
return skipResult.matched;
|
|
2324
|
+
}
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
if (remainingPattern[0] === "*") {
|
|
2328
|
+
consumePattern(1); // consumes "*"
|
|
2329
|
+
if (remainingPattern === "") {
|
|
2330
|
+
// matches everything except "/"
|
|
2331
|
+
const slashIndex = remainingString.indexOf("/");
|
|
2332
|
+
if (slashIndex === -1) {
|
|
2333
|
+
groups.push({ string: consumeRemainingString() });
|
|
2334
|
+
return true;
|
|
2335
|
+
}
|
|
2336
|
+
groups.push({ string: consumeString(slashIndex) });
|
|
2337
|
+
return false;
|
|
2338
|
+
}
|
|
2339
|
+
// the next char must not the one expect by remainingPattern[0]
|
|
2340
|
+
// because * is greedy and expect to skip at least one char
|
|
2341
|
+
if (remainingPattern[0] === remainingString[0]) {
|
|
2342
|
+
groups.push({ string: "" });
|
|
2343
|
+
patternIndex = patternIndex - 1;
|
|
2344
|
+
return false;
|
|
2345
|
+
}
|
|
2346
|
+
const skipResult = skipUntilMatchIterative({
|
|
2347
|
+
pattern: remainingPattern,
|
|
2348
|
+
string: remainingString,
|
|
2349
|
+
canSkipSlash: false,
|
|
2350
|
+
});
|
|
2351
|
+
groups.push(skipResult.group);
|
|
2352
|
+
for (let i = 0; i < skipResult.groups.length; i++) {
|
|
2353
|
+
groups.push(skipResult.groups[i]);
|
|
2354
|
+
}
|
|
2355
|
+
consumePattern(skipResult.patternIndex);
|
|
2356
|
+
consumeString(skipResult.index);
|
|
2357
|
+
restoreIndexes = false;
|
|
2358
|
+
return skipResult.matched;
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
if (remainingPattern[0] !== remainingString[0]) {
|
|
2362
|
+
return false;
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
return undefined;
|
|
2366
|
+
};
|
|
2367
|
+
|
|
2368
|
+
// Replace recursive iterate() with iterative approach
|
|
2369
|
+
let matched;
|
|
2370
|
+
let continueIteration = true;
|
|
2371
|
+
|
|
2372
|
+
while (continueIteration) {
|
|
2373
|
+
const patternIndexBefore = patternIndex;
|
|
2374
|
+
const indexBefore = index;
|
|
2375
|
+
|
|
2376
|
+
matched = matchOne();
|
|
2377
|
+
|
|
2378
|
+
if (matched === undefined) {
|
|
2379
|
+
consumePattern(1);
|
|
2380
|
+
consumeString(1);
|
|
2381
|
+
// Continue the loop instead of recursion
|
|
2382
|
+
continue;
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
if (matched === false && restoreIndexes) {
|
|
2386
|
+
patternIndex = patternIndexBefore;
|
|
2387
|
+
index = indexBefore;
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
// End the loop
|
|
2391
|
+
continueIteration = false;
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
return {
|
|
2395
|
+
matched,
|
|
2396
|
+
patternIndex,
|
|
2397
|
+
index,
|
|
2398
|
+
groups,
|
|
2399
|
+
};
|
|
2400
|
+
};
|
|
2401
|
+
|
|
2402
|
+
/**
|
|
2403
|
+
* Iterative version of skipUntilMatch that avoids recursion
|
|
2404
|
+
* @param {Object} params
|
|
2405
|
+
* @param {string} params.pattern - The pattern to match
|
|
2406
|
+
* @param {string} params.string - The string to test against
|
|
2407
|
+
* @param {boolean} params.canSkipSlash - Whether slash characters can be skipped
|
|
2408
|
+
* @returns {Object} Result of the matching attempt
|
|
2409
|
+
*/
|
|
2410
|
+
const skipUntilMatchIterative = ({ pattern, string, canSkipSlash }) => {
|
|
2411
|
+
let index = 0;
|
|
2412
|
+
let remainingString = string;
|
|
2413
|
+
let longestAttemptRange = null;
|
|
2414
|
+
let isLastAttempt = false;
|
|
2415
|
+
|
|
2416
|
+
const failure = () => {
|
|
2417
|
+
return {
|
|
2418
|
+
matched: false,
|
|
2419
|
+
patternIndex: longestAttemptRange.patternIndex,
|
|
2420
|
+
index: longestAttemptRange.index + longestAttemptRange.length,
|
|
2421
|
+
groups: longestAttemptRange.groups,
|
|
2422
|
+
group: {
|
|
2423
|
+
string: string.slice(0, longestAttemptRange.index),
|
|
2424
|
+
},
|
|
2425
|
+
};
|
|
2426
|
+
};
|
|
2427
|
+
|
|
2428
|
+
// Loop until a match is found or all attempts fail
|
|
2429
|
+
while (true) {
|
|
2430
|
+
const matchAttempt = applyMatching(pattern, remainingString);
|
|
2431
|
+
|
|
2432
|
+
if (matchAttempt.matched) {
|
|
2433
|
+
return {
|
|
2434
|
+
matched: true,
|
|
2435
|
+
patternIndex: matchAttempt.patternIndex,
|
|
2436
|
+
index: index + matchAttempt.index,
|
|
2437
|
+
groups: matchAttempt.groups,
|
|
2438
|
+
group: {
|
|
2439
|
+
string:
|
|
2440
|
+
remainingString === ""
|
|
2441
|
+
? string
|
|
2442
|
+
: string.slice(0, -remainingString.length),
|
|
2443
|
+
},
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
const attemptIndex = matchAttempt.index;
|
|
2448
|
+
const attemptRange = {
|
|
2449
|
+
patternIndex: matchAttempt.patternIndex,
|
|
2450
|
+
index,
|
|
2451
|
+
length: attemptIndex,
|
|
2452
|
+
groups: matchAttempt.groups,
|
|
2453
|
+
};
|
|
2454
|
+
|
|
2455
|
+
if (
|
|
2456
|
+
!longestAttemptRange ||
|
|
2457
|
+
longestAttemptRange.length < attemptRange.length
|
|
2458
|
+
) {
|
|
2459
|
+
longestAttemptRange = attemptRange;
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
if (isLastAttempt) {
|
|
2463
|
+
return failure();
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
const nextIndex = attemptIndex + 1;
|
|
2467
|
+
if (nextIndex >= remainingString.length) {
|
|
2468
|
+
return failure();
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
if (remainingString[0] === "/") {
|
|
2472
|
+
if (!canSkipSlash) {
|
|
2473
|
+
return failure();
|
|
2474
|
+
}
|
|
2475
|
+
// when it's the last slash, the next attempt is the last
|
|
2476
|
+
if (remainingString.indexOf("/", 1) === -1) {
|
|
2477
|
+
isLastAttempt = true;
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
// search against the next unattempted string
|
|
2482
|
+
index += nextIndex;
|
|
2483
|
+
remainingString = remainingString.slice(nextIndex);
|
|
2484
|
+
}
|
|
2485
|
+
};
|
|
2486
|
+
|
|
2487
|
+
const applyPatternMatching = ({ url, pattern }) => {
|
|
2488
|
+
assertUrlLike(pattern, "pattern");
|
|
2489
|
+
if (url && typeof url.href === "string") url = url.href;
|
|
2490
|
+
assertUrlLike(url, "url");
|
|
2491
|
+
return applyPattern({ url, pattern });
|
|
2492
|
+
};
|
|
2493
|
+
|
|
2494
|
+
const resolveAssociations = (associations, baseUrl) => {
|
|
2495
|
+
if (baseUrl && typeof baseUrl.href === "string") baseUrl = baseUrl.href;
|
|
2496
|
+
assertUrlLike(baseUrl, "baseUrl");
|
|
2497
|
+
|
|
2498
|
+
const associationsResolved = {};
|
|
2499
|
+
for (const key of Object.keys(associations)) {
|
|
2500
|
+
const value = associations[key];
|
|
2501
|
+
if (typeof value === "object" && value !== null) {
|
|
2502
|
+
const valueMapResolved = {};
|
|
2503
|
+
for (const pattern of Object.keys(value)) {
|
|
2504
|
+
const valueAssociated = value[pattern];
|
|
2505
|
+
let patternResolved;
|
|
2506
|
+
try {
|
|
2507
|
+
patternResolved = String(new URL(pattern, baseUrl));
|
|
2508
|
+
} catch {
|
|
2509
|
+
// it's not really an url, no need to perform url resolution nor encoding
|
|
2510
|
+
patternResolved = pattern;
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
valueMapResolved[patternResolved] = valueAssociated;
|
|
2514
|
+
}
|
|
2515
|
+
associationsResolved[key] = valueMapResolved;
|
|
2516
|
+
} else {
|
|
2517
|
+
associationsResolved[key] = value;
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
return associationsResolved;
|
|
2521
|
+
};
|
|
2522
|
+
|
|
2523
|
+
const asFlatAssociations = (associations) => {
|
|
2524
|
+
if (!isPlainObject(associations)) {
|
|
2525
|
+
throw new TypeError(
|
|
2526
|
+
`associations must be a plain object, got ${associations}`,
|
|
2527
|
+
);
|
|
2528
|
+
}
|
|
2529
|
+
const flatAssociations = {};
|
|
2530
|
+
for (const associationName of Object.keys(associations)) {
|
|
2531
|
+
const associationValue = associations[associationName];
|
|
2532
|
+
if (!isPlainObject(associationValue)) {
|
|
2533
|
+
continue;
|
|
2534
|
+
}
|
|
2535
|
+
for (const pattern of Object.keys(associationValue)) {
|
|
2536
|
+
const patternValue = associationValue[pattern];
|
|
2537
|
+
const previousValue = flatAssociations[pattern];
|
|
2538
|
+
if (isPlainObject(previousValue)) {
|
|
2539
|
+
flatAssociations[pattern] = {
|
|
2540
|
+
...previousValue,
|
|
2541
|
+
[associationName]: patternValue,
|
|
2542
|
+
};
|
|
2543
|
+
} else {
|
|
2544
|
+
flatAssociations[pattern] = {
|
|
2545
|
+
[associationName]: patternValue,
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
return flatAssociations;
|
|
2551
|
+
};
|
|
2552
|
+
|
|
2553
|
+
const applyAssociations = ({ url, associations }) => {
|
|
2554
|
+
if (url && typeof url.href === "string") url = url.href;
|
|
2555
|
+
assertUrlLike(url);
|
|
2556
|
+
const flatAssociations = asFlatAssociations(associations);
|
|
2557
|
+
let associatedValue = {};
|
|
2558
|
+
for (const pattern of Object.keys(flatAssociations)) {
|
|
2559
|
+
const { matched } = applyPatternMatching({
|
|
2560
|
+
pattern,
|
|
2561
|
+
url,
|
|
2562
|
+
});
|
|
2563
|
+
if (matched) {
|
|
2564
|
+
const value = flatAssociations[pattern];
|
|
2565
|
+
associatedValue = deepAssign(associatedValue, value);
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
return associatedValue;
|
|
2569
|
+
};
|
|
2570
|
+
|
|
2571
|
+
const deepAssign = (firstValue, secondValue) => {
|
|
2572
|
+
if (!isPlainObject(firstValue)) {
|
|
2573
|
+
if (isPlainObject(secondValue)) {
|
|
2574
|
+
return deepAssign({}, secondValue);
|
|
2575
|
+
}
|
|
2576
|
+
return secondValue;
|
|
2577
|
+
}
|
|
2578
|
+
if (!isPlainObject(secondValue)) {
|
|
2579
|
+
return secondValue;
|
|
2580
|
+
}
|
|
2581
|
+
for (const key of Object.keys(secondValue)) {
|
|
2582
|
+
const leftPopertyValue = firstValue[key];
|
|
2583
|
+
const rightPropertyValue = secondValue[key];
|
|
2584
|
+
firstValue[key] = deepAssign(leftPopertyValue, rightPropertyValue);
|
|
2585
|
+
}
|
|
2586
|
+
return firstValue;
|
|
2587
|
+
};
|
|
2588
|
+
|
|
2589
|
+
const urlChildMayMatch = ({ url, associations, predicate }) => {
|
|
2590
|
+
if (url && typeof url.href === "string") url = url.href;
|
|
2591
|
+
assertUrlLike(url, "url");
|
|
2592
|
+
// the function was meants to be used on url ending with '/'
|
|
2593
|
+
if (!url.endsWith("/")) {
|
|
2594
|
+
throw new Error(`url should end with /, got ${url}`);
|
|
2595
|
+
}
|
|
2596
|
+
if (typeof predicate !== "function") {
|
|
2597
|
+
throw new TypeError(`predicate must be a function, got ${predicate}`);
|
|
2598
|
+
}
|
|
2599
|
+
const flatAssociations = asFlatAssociations(associations);
|
|
2600
|
+
// for full match we must create an object to allow pattern to override previous ones
|
|
2601
|
+
let fullMatchMeta = {};
|
|
2602
|
+
let someFullMatch = false;
|
|
2603
|
+
// for partial match, any meta satisfying predicate will be valid because
|
|
2604
|
+
// we don't know for sure if pattern will still match for a file inside pathname
|
|
2605
|
+
const partialMatchMetaArray = [];
|
|
2606
|
+
for (const pattern of Object.keys(flatAssociations)) {
|
|
2607
|
+
const value = flatAssociations[pattern];
|
|
2608
|
+
const matchResult = applyPatternMatching({
|
|
2609
|
+
pattern,
|
|
2610
|
+
url,
|
|
2611
|
+
});
|
|
2612
|
+
if (matchResult.matched) {
|
|
2613
|
+
someFullMatch = true;
|
|
2614
|
+
if (isPlainObject(fullMatchMeta) && isPlainObject(value)) {
|
|
2615
|
+
fullMatchMeta = {
|
|
2616
|
+
...fullMatchMeta,
|
|
2617
|
+
...value,
|
|
2618
|
+
};
|
|
2619
|
+
} else {
|
|
2620
|
+
fullMatchMeta = value;
|
|
2621
|
+
}
|
|
2622
|
+
} else if (someFullMatch === false && matchResult.urlIndex >= url.length) {
|
|
2623
|
+
partialMatchMetaArray.push(value);
|
|
2624
|
+
}
|
|
2625
|
+
}
|
|
2626
|
+
if (someFullMatch) {
|
|
2627
|
+
return Boolean(predicate(fullMatchMeta));
|
|
2628
|
+
}
|
|
2629
|
+
return partialMatchMetaArray.some((partialMatchMeta) =>
|
|
2630
|
+
predicate(partialMatchMeta),
|
|
2631
|
+
);
|
|
2632
|
+
};
|
|
2633
|
+
|
|
2634
|
+
const applyAliases = ({ url, aliases }) => {
|
|
2635
|
+
let aliasFullMatchResult;
|
|
2636
|
+
const aliasMatchingKey = Object.keys(aliases).find((key) => {
|
|
2637
|
+
const aliasMatchResult = applyPatternMatching({
|
|
2638
|
+
pattern: key,
|
|
2639
|
+
url,
|
|
2640
|
+
});
|
|
2641
|
+
if (aliasMatchResult.matched) {
|
|
2642
|
+
aliasFullMatchResult = aliasMatchResult;
|
|
2643
|
+
return true;
|
|
2644
|
+
}
|
|
2645
|
+
return false;
|
|
2646
|
+
});
|
|
2647
|
+
if (!aliasMatchingKey) {
|
|
2648
|
+
return url;
|
|
2649
|
+
}
|
|
2650
|
+
const { matchGroups } = aliasFullMatchResult;
|
|
2651
|
+
const alias = aliases[aliasMatchingKey];
|
|
2652
|
+
const parts = alias.split("*");
|
|
2653
|
+
let newUrl = "";
|
|
2654
|
+
let index = 0;
|
|
2655
|
+
for (const part of parts) {
|
|
2656
|
+
newUrl += `${part}`;
|
|
2657
|
+
if (index < parts.length - 1) {
|
|
2658
|
+
newUrl += matchGroups[index];
|
|
2659
|
+
}
|
|
2660
|
+
index++;
|
|
2661
|
+
}
|
|
2662
|
+
return newUrl;
|
|
2663
|
+
};
|
|
2664
|
+
|
|
2665
|
+
const matches = (url, patterns) => {
|
|
2666
|
+
return Boolean(
|
|
2667
|
+
applyAssociations({
|
|
2668
|
+
url,
|
|
2669
|
+
associations: {
|
|
2670
|
+
yes: patterns,
|
|
2671
|
+
},
|
|
2672
|
+
}).yes,
|
|
2673
|
+
);
|
|
2674
|
+
};
|
|
2675
|
+
|
|
2676
|
+
// const assertSpecifierMetaMap = (value, checkComposition = true) => {
|
|
2677
|
+
// if (!isPlainObject(value)) {
|
|
2678
|
+
// throw new TypeError(
|
|
2679
|
+
// `specifierMetaMap must be a plain object, got ${value}`,
|
|
2680
|
+
// );
|
|
2681
|
+
// }
|
|
2682
|
+
// if (checkComposition) {
|
|
2683
|
+
// const plainObject = value;
|
|
2684
|
+
// Object.keys(plainObject).forEach((key) => {
|
|
2685
|
+
// assertUrlLike(key, "specifierMetaMap key");
|
|
2686
|
+
// const value = plainObject[key];
|
|
2687
|
+
// if (value !== null && !isPlainObject(value)) {
|
|
2688
|
+
// throw new TypeError(
|
|
2689
|
+
// `specifierMetaMap value must be a plain object or null, got ${value} under key ${key}`,
|
|
2690
|
+
// );
|
|
2691
|
+
// }
|
|
2692
|
+
// });
|
|
2693
|
+
// }
|
|
2694
|
+
// };
|
|
2695
|
+
const assertUrlLike = (value, name = "url") => {
|
|
2696
|
+
if (typeof value !== "string") {
|
|
2697
|
+
throw new TypeError(`${name} must be a url string, got ${value}`);
|
|
2698
|
+
}
|
|
2699
|
+
if (isWindowsPathnameSpecifier(value)) {
|
|
2700
|
+
throw new TypeError(
|
|
2701
|
+
`${name} must be a url but looks like a windows pathname, got ${value}`,
|
|
2702
|
+
);
|
|
2703
|
+
}
|
|
2704
|
+
if (!hasScheme(value)) {
|
|
2705
|
+
throw new TypeError(
|
|
2706
|
+
`${name} must be a url and no scheme found, got ${value}`,
|
|
2707
|
+
);
|
|
2708
|
+
}
|
|
2709
|
+
};
|
|
2710
|
+
const isPlainObject = (value) => {
|
|
2711
|
+
if (value === null) {
|
|
2712
|
+
return false;
|
|
2713
|
+
}
|
|
2714
|
+
if (typeof value === "object") {
|
|
2715
|
+
if (Array.isArray(value)) {
|
|
2716
|
+
return false;
|
|
2717
|
+
}
|
|
2718
|
+
return true;
|
|
2719
|
+
}
|
|
2720
|
+
return false;
|
|
2721
|
+
};
|
|
2722
|
+
const isWindowsPathnameSpecifier = (specifier) => {
|
|
2723
|
+
const firstChar = specifier[0];
|
|
2724
|
+
if (!/[a-zA-Z]/.test(firstChar)) return false;
|
|
2725
|
+
const secondChar = specifier[1];
|
|
2726
|
+
if (secondChar !== ":") return false;
|
|
2727
|
+
const thirdChar = specifier[2];
|
|
2728
|
+
return thirdChar === "/" || thirdChar === "\\";
|
|
2729
|
+
};
|
|
2730
|
+
const hasScheme = (specifier) => /^[a-zA-Z]+:/.test(specifier);
|
|
2731
|
+
|
|
2732
|
+
const createFilter = (patterns, url, map = (v) => v) => {
|
|
2733
|
+
const associations = resolveAssociations(
|
|
2734
|
+
{
|
|
2735
|
+
yes: patterns,
|
|
2736
|
+
},
|
|
2737
|
+
url,
|
|
2738
|
+
);
|
|
2739
|
+
return (url) => {
|
|
2740
|
+
const meta = applyAssociations({ url, associations });
|
|
2741
|
+
return Boolean(map(meta.yes));
|
|
2742
|
+
};
|
|
2743
|
+
};
|
|
2744
|
+
|
|
2745
|
+
const URL_META = {
|
|
2746
|
+
resolveAssociations,
|
|
2747
|
+
applyAssociations,
|
|
2748
|
+
applyAliases,
|
|
2749
|
+
applyPatternMatching,
|
|
2750
|
+
urlChildMayMatch,
|
|
2751
|
+
matches,
|
|
2752
|
+
createFilter,
|
|
2753
|
+
};
|
|
2754
|
+
|
|
2157
2755
|
const readDirectory = async (url, { emfileMaxWait = 1000 } = {}) => {
|
|
2158
2756
|
const directoryUrl = assertAndNormalizeDirectoryUrl(url);
|
|
2159
2757
|
const directoryUrlObject = new URL(directoryUrl);
|
|
@@ -3450,6 +4048,31 @@ const composeV8AndIstanbul = (
|
|
|
3450
4048
|
return fileByFileCoverage;
|
|
3451
4049
|
};
|
|
3452
4050
|
|
|
4051
|
+
const filterV8Coverage = async (
|
|
4052
|
+
v8Coverage,
|
|
4053
|
+
{ rootDirectoryUrl, coverageInclude },
|
|
4054
|
+
) => {
|
|
4055
|
+
const associations = URL_META.resolveAssociations(
|
|
4056
|
+
{ cover: coverageInclude },
|
|
4057
|
+
rootDirectoryUrl,
|
|
4058
|
+
);
|
|
4059
|
+
const urlShouldBeCovered = (url) => {
|
|
4060
|
+
const { cover } = URL_META.applyAssociations({
|
|
4061
|
+
url: new URL(url, rootDirectoryUrl).href,
|
|
4062
|
+
associations,
|
|
4063
|
+
});
|
|
4064
|
+
return cover;
|
|
4065
|
+
};
|
|
4066
|
+
|
|
4067
|
+
const v8CoverageFiltered = {
|
|
4068
|
+
...v8Coverage,
|
|
4069
|
+
result: v8Coverage.result.filter((fileReport) =>
|
|
4070
|
+
urlShouldBeCovered(fileReport.url),
|
|
4071
|
+
),
|
|
4072
|
+
};
|
|
4073
|
+
return v8CoverageFiltered;
|
|
4074
|
+
};
|
|
4075
|
+
|
|
3453
4076
|
const composeTwoV8Coverages = (firstV8Coverage, secondV8Coverage) => {
|
|
3454
4077
|
if (secondV8Coverage.result.length === 0) {
|
|
3455
4078
|
return firstV8Coverage;
|
|
@@ -41,6 +41,13 @@ const applyPattern = ({ url, pattern }) => {
|
|
|
41
41
|
};
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Core pattern matching function that processes patterns against strings
|
|
46
|
+
* @param {string} pattern - The pattern with special syntax (*,**,/) to match against
|
|
47
|
+
* @param {string} string - The string to test against the pattern
|
|
48
|
+
* @returns {Object} Result containing match status and capture groups
|
|
49
|
+
* @private
|
|
50
|
+
*/
|
|
44
51
|
const applyMatching = (pattern, string) => {
|
|
45
52
|
const groups = [];
|
|
46
53
|
let patternIndex = 0;
|
|
@@ -65,22 +72,6 @@ const applyMatching = (pattern, string) => {
|
|
|
65
72
|
return consumeString(remainingString.length);
|
|
66
73
|
};
|
|
67
74
|
|
|
68
|
-
let matched;
|
|
69
|
-
const iterate = () => {
|
|
70
|
-
const patternIndexBefore = patternIndex;
|
|
71
|
-
const indexBefore = index;
|
|
72
|
-
matched = matchOne();
|
|
73
|
-
if (matched === undefined) {
|
|
74
|
-
consumePattern(1);
|
|
75
|
-
consumeString(1);
|
|
76
|
-
iterate();
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
if (matched === false && restoreIndexes) {
|
|
80
|
-
patternIndex = patternIndexBefore;
|
|
81
|
-
index = indexBefore;
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
75
|
const matchOne = () => {
|
|
85
76
|
// pattern consumed
|
|
86
77
|
if (remainingPattern === "") {
|
|
@@ -90,12 +81,11 @@ const applyMatching = (pattern, string) => {
|
|
|
90
81
|
if (remainingString[0] === "?") {
|
|
91
82
|
// match search params
|
|
92
83
|
consumeRemainingString();
|
|
93
|
-
|
|
94
84
|
return true;
|
|
95
85
|
}
|
|
96
|
-
// if remainingString
|
|
97
86
|
return false; // fails because string longer than expect
|
|
98
87
|
}
|
|
88
|
+
|
|
99
89
|
// -- from this point pattern is not consumed --
|
|
100
90
|
// string consumed, pattern not consumed
|
|
101
91
|
if (remainingString === "") {
|
|
@@ -109,6 +99,7 @@ const applyMatching = (pattern, string) => {
|
|
|
109
99
|
}
|
|
110
100
|
return false; // fail because string shorter than expect
|
|
111
101
|
}
|
|
102
|
+
|
|
112
103
|
// -- from this point pattern and string are not consumed --
|
|
113
104
|
// fast path trailing slash
|
|
114
105
|
if (remainingPattern === "/") {
|
|
@@ -120,25 +111,30 @@ const applyMatching = (pattern, string) => {
|
|
|
120
111
|
}
|
|
121
112
|
return false;
|
|
122
113
|
}
|
|
114
|
+
|
|
123
115
|
// fast path trailing '**'
|
|
124
116
|
if (remainingPattern === "**") {
|
|
125
117
|
consumePattern(2);
|
|
126
118
|
consumeRemainingString();
|
|
127
119
|
return true;
|
|
128
120
|
}
|
|
121
|
+
|
|
129
122
|
if (remainingPattern.slice(0, 4) === "/**/") {
|
|
130
123
|
consumePattern(3); // consumes "/**/"
|
|
131
|
-
const skipResult =
|
|
124
|
+
const skipResult = skipUntilMatchIterative({
|
|
132
125
|
pattern: remainingPattern,
|
|
133
126
|
string: remainingString,
|
|
134
127
|
canSkipSlash: true,
|
|
135
128
|
});
|
|
136
|
-
|
|
129
|
+
for (let i = 0; i < skipResult.groups.length; i++) {
|
|
130
|
+
groups.push(skipResult.groups[i]);
|
|
131
|
+
}
|
|
137
132
|
consumePattern(skipResult.patternIndex);
|
|
138
133
|
consumeRemainingString();
|
|
139
134
|
restoreIndexes = false;
|
|
140
135
|
return skipResult.matched;
|
|
141
136
|
}
|
|
137
|
+
|
|
142
138
|
// pattern leading **
|
|
143
139
|
if (remainingPattern.slice(0, 2) === "**") {
|
|
144
140
|
consumePattern(2); // consumes "**"
|
|
@@ -148,7 +144,7 @@ const applyMatching = (pattern, string) => {
|
|
|
148
144
|
// when remainingPattern was preceeded by "**/"
|
|
149
145
|
// and remainingString have no "/"
|
|
150
146
|
// then skip is not allowed, a regular match will be performed
|
|
151
|
-
if (
|
|
147
|
+
if (remainingString.includes("/")) {
|
|
152
148
|
skipAllowed = false;
|
|
153
149
|
}
|
|
154
150
|
}
|
|
@@ -158,18 +154,21 @@ const applyMatching = (pattern, string) => {
|
|
|
158
154
|
return true;
|
|
159
155
|
}
|
|
160
156
|
if (skipAllowed) {
|
|
161
|
-
const skipResult =
|
|
157
|
+
const skipResult = skipUntilMatchIterative({
|
|
162
158
|
pattern: remainingPattern,
|
|
163
159
|
string: remainingString,
|
|
164
160
|
canSkipSlash: true,
|
|
165
161
|
});
|
|
166
|
-
|
|
162
|
+
for (let i = 0; i < skipResult.groups.length; i++) {
|
|
163
|
+
groups.push(skipResult.groups[i]);
|
|
164
|
+
}
|
|
167
165
|
consumePattern(skipResult.patternIndex);
|
|
168
166
|
consumeRemainingString();
|
|
169
167
|
restoreIndexes = false;
|
|
170
168
|
return skipResult.matched;
|
|
171
169
|
}
|
|
172
170
|
}
|
|
171
|
+
|
|
173
172
|
if (remainingPattern[0] === "*") {
|
|
174
173
|
consumePattern(1); // consumes "*"
|
|
175
174
|
if (remainingPattern === "") {
|
|
@@ -189,23 +188,53 @@ const applyMatching = (pattern, string) => {
|
|
|
189
188
|
patternIndex = patternIndex - 1;
|
|
190
189
|
return false;
|
|
191
190
|
}
|
|
192
|
-
const skipResult =
|
|
191
|
+
const skipResult = skipUntilMatchIterative({
|
|
193
192
|
pattern: remainingPattern,
|
|
194
193
|
string: remainingString,
|
|
195
194
|
canSkipSlash: false,
|
|
196
195
|
});
|
|
197
|
-
groups.push(skipResult.group
|
|
196
|
+
groups.push(skipResult.group);
|
|
197
|
+
for (let i = 0; i < skipResult.groups.length; i++) {
|
|
198
|
+
groups.push(skipResult.groups[i]);
|
|
199
|
+
}
|
|
198
200
|
consumePattern(skipResult.patternIndex);
|
|
199
201
|
consumeString(skipResult.index);
|
|
200
202
|
restoreIndexes = false;
|
|
201
203
|
return skipResult.matched;
|
|
202
204
|
}
|
|
205
|
+
|
|
203
206
|
if (remainingPattern[0] !== remainingString[0]) {
|
|
204
207
|
return false;
|
|
205
208
|
}
|
|
209
|
+
|
|
206
210
|
return undefined;
|
|
207
211
|
};
|
|
208
|
-
|
|
212
|
+
|
|
213
|
+
// Replace recursive iterate() with iterative approach
|
|
214
|
+
let matched;
|
|
215
|
+
let continueIteration = true;
|
|
216
|
+
|
|
217
|
+
while (continueIteration) {
|
|
218
|
+
const patternIndexBefore = patternIndex;
|
|
219
|
+
const indexBefore = index;
|
|
220
|
+
|
|
221
|
+
matched = matchOne();
|
|
222
|
+
|
|
223
|
+
if (matched === undefined) {
|
|
224
|
+
consumePattern(1);
|
|
225
|
+
consumeString(1);
|
|
226
|
+
// Continue the loop instead of recursion
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (matched === false && restoreIndexes) {
|
|
231
|
+
patternIndex = patternIndexBefore;
|
|
232
|
+
index = indexBefore;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// End the loop
|
|
236
|
+
continueIteration = false;
|
|
237
|
+
}
|
|
209
238
|
|
|
210
239
|
return {
|
|
211
240
|
matched,
|
|
@@ -215,7 +244,15 @@ const applyMatching = (pattern, string) => {
|
|
|
215
244
|
};
|
|
216
245
|
};
|
|
217
246
|
|
|
218
|
-
|
|
247
|
+
/**
|
|
248
|
+
* Iterative version of skipUntilMatch that avoids recursion
|
|
249
|
+
* @param {Object} params
|
|
250
|
+
* @param {string} params.pattern - The pattern to match
|
|
251
|
+
* @param {string} params.string - The string to test against
|
|
252
|
+
* @param {boolean} params.canSkipSlash - Whether slash characters can be skipped
|
|
253
|
+
* @returns {Object} Result of the matching attempt
|
|
254
|
+
*/
|
|
255
|
+
const skipUntilMatchIterative = ({ pattern, string, canSkipSlash }) => {
|
|
219
256
|
let index = 0;
|
|
220
257
|
let remainingString = string;
|
|
221
258
|
let longestAttemptRange = null;
|
|
@@ -233,8 +270,10 @@ const skipUntilMatch = ({ pattern, string, canSkipSlash }) => {
|
|
|
233
270
|
};
|
|
234
271
|
};
|
|
235
272
|
|
|
236
|
-
|
|
273
|
+
// Loop until a match is found or all attempts fail
|
|
274
|
+
while (true) {
|
|
237
275
|
const matchAttempt = applyMatching(pattern, remainingString);
|
|
276
|
+
|
|
238
277
|
if (matchAttempt.matched) {
|
|
239
278
|
return {
|
|
240
279
|
matched: true,
|
|
@@ -249,6 +288,7 @@ const skipUntilMatch = ({ pattern, string, canSkipSlash }) => {
|
|
|
249
288
|
},
|
|
250
289
|
};
|
|
251
290
|
}
|
|
291
|
+
|
|
252
292
|
const attemptIndex = matchAttempt.index;
|
|
253
293
|
const attemptRange = {
|
|
254
294
|
patternIndex: matchAttempt.patternIndex,
|
|
@@ -256,19 +296,23 @@ const skipUntilMatch = ({ pattern, string, canSkipSlash }) => {
|
|
|
256
296
|
length: attemptIndex,
|
|
257
297
|
groups: matchAttempt.groups,
|
|
258
298
|
};
|
|
299
|
+
|
|
259
300
|
if (
|
|
260
301
|
!longestAttemptRange ||
|
|
261
302
|
longestAttemptRange.length < attemptRange.length
|
|
262
303
|
) {
|
|
263
304
|
longestAttemptRange = attemptRange;
|
|
264
305
|
}
|
|
306
|
+
|
|
265
307
|
if (isLastAttempt) {
|
|
266
308
|
return failure();
|
|
267
309
|
}
|
|
310
|
+
|
|
268
311
|
const nextIndex = attemptIndex + 1;
|
|
269
312
|
if (nextIndex >= remainingString.length) {
|
|
270
313
|
return failure();
|
|
271
314
|
}
|
|
315
|
+
|
|
272
316
|
if (remainingString[0] === "/") {
|
|
273
317
|
if (!canSkipSlash) {
|
|
274
318
|
return failure();
|
|
@@ -278,12 +322,11 @@ const skipUntilMatch = ({ pattern, string, canSkipSlash }) => {
|
|
|
278
322
|
isLastAttempt = true;
|
|
279
323
|
}
|
|
280
324
|
}
|
|
325
|
+
|
|
281
326
|
// search against the next unattempted string
|
|
282
327
|
index += nextIndex;
|
|
283
328
|
remainingString = remainingString.slice(nextIndex);
|
|
284
|
-
|
|
285
|
-
};
|
|
286
|
-
return tryToMatch();
|
|
329
|
+
}
|
|
287
330
|
};
|
|
288
331
|
|
|
289
332
|
const applyPatternMatching = ({ url, pattern }) => {
|
|
@@ -579,9 +622,4 @@ const filterV8Coverage = async (
|
|
|
579
622
|
return v8CoverageFiltered;
|
|
580
623
|
};
|
|
581
624
|
|
|
582
|
-
|
|
583
|
-
__proto__: null,
|
|
584
|
-
filterV8Coverage
|
|
585
|
-
}, Symbol.toStringTag, { value: 'Module' }));
|
|
586
|
-
|
|
587
|
-
export { URL_META, filterV8Coverage, v8_coverage };
|
|
625
|
+
export { filterV8Coverage };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jsenv/test",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.6",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -41,16 +41,16 @@
|
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@c88/v8-coverage": "0.1.1",
|
|
43
43
|
"@jsenv/abort": "4.3.1",
|
|
44
|
-
"@jsenv/ast": "6.6.
|
|
45
|
-
"@jsenv/exception": "1.1.
|
|
46
|
-
"@jsenv/filesystem": "4.14.
|
|
47
|
-
"@jsenv/github-check-run": "0.2.
|
|
44
|
+
"@jsenv/ast": "6.6.6",
|
|
45
|
+
"@jsenv/exception": "1.1.6",
|
|
46
|
+
"@jsenv/filesystem": "4.14.4",
|
|
47
|
+
"@jsenv/github-check-run": "0.2.19",
|
|
48
48
|
"@jsenv/node-esm-resolution": "1.1.0",
|
|
49
49
|
"@jsenv/humanize": "1.3.1",
|
|
50
|
-
"@jsenv/plugin-supervisor": "1.6.
|
|
51
|
-
"@jsenv/sourcemap": "1.3.
|
|
52
|
-
"@jsenv/url-meta": "8.5.
|
|
53
|
-
"@jsenv/urls": "2.
|
|
50
|
+
"@jsenv/plugin-supervisor": "1.6.11",
|
|
51
|
+
"@jsenv/sourcemap": "1.3.3",
|
|
52
|
+
"@jsenv/url-meta": "8.5.5",
|
|
53
|
+
"@jsenv/urls": "2.7.0",
|
|
54
54
|
"@jsenv/utils": "2.2.1",
|
|
55
55
|
"he": "1.2.0",
|
|
56
56
|
"istanbul-lib-coverage": "3.2.2",
|