@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("./js/v8_coverage.js").then(n => n.v8_coverage),
166
+ import("./v8_coverage/v8_coverage.js"),
167
167
  ]);
168
168
  const coverageLight = await filterV8Coverage(coverage, {
169
169
  rootDirectoryUrl,
@@ -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 = skipUntilMatch({
124
+ const skipResult = skipUntilMatchIterative({
132
125
  pattern: remainingPattern,
133
126
  string: remainingString,
134
127
  canSkipSlash: true,
135
128
  });
136
- groups.push(...skipResult.groups);
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 (!remainingString.includes("/")) {
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 = skipUntilMatch({
157
+ const skipResult = skipUntilMatchIterative({
162
158
  pattern: remainingPattern,
163
159
  string: remainingString,
164
160
  canSkipSlash: true,
165
161
  });
166
- groups.push(...skipResult.groups);
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 = skipUntilMatch({
191
+ const skipResult = skipUntilMatchIterative({
193
192
  pattern: remainingPattern,
194
193
  string: remainingString,
195
194
  canSkipSlash: false,
196
195
  });
197
- groups.push(skipResult.group, ...skipResult.groups);
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
- iterate();
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
- const skipUntilMatch = ({ pattern, string, canSkipSlash }) => {
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
- const tryToMatch = () => {
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
- return tryToMatch();
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
- const v8_coverage = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
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.4",
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.5",
45
- "@jsenv/exception": "1.1.5",
46
- "@jsenv/filesystem": "4.14.2",
47
- "@jsenv/github-check-run": "0.2.17",
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.10",
51
- "@jsenv/sourcemap": "1.3.2",
52
- "@jsenv/url-meta": "8.5.4",
53
- "@jsenv/urls": "2.6.3",
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",