monocart-coverage-reports 2.13.2 → 3.1.0

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.
Files changed (69) hide show
  1. package/README.md +314 -116
  2. package/README.zh-Hans.md +315 -117
  3. package/lib/assets.js +6 -6
  4. package/lib/cli.js +191 -95
  5. package/lib/client/cdp-client.js +74 -43
  6. package/lib/client/coverage-client.js +24 -8
  7. package/lib/client/ws-session.js +115 -37
  8. package/lib/converter/ast-visitor.js +5 -7
  9. package/lib/converter/ast.js +29 -8
  10. package/lib/converter/collect-source-maps.js +42 -21
  11. package/lib/converter/converter.js +124 -129
  12. package/lib/converter/css-coverage.js +76 -0
  13. package/lib/converter/fake-source-coverage.js +64 -0
  14. package/lib/converter/find-original-range.js +214 -6
  15. package/lib/converter/flatten-source-maps.js +2 -4
  16. package/lib/converter/ignore.js +2 -4
  17. package/lib/converter/info-branch.js +5 -3
  18. package/lib/converter/info-function.js +5 -3
  19. package/lib/converter/info-statement.js +5 -3
  20. package/lib/converter/range-index.js +78 -0
  21. package/lib/converter/source-range-merge.js +103 -0
  22. package/lib/converter/untested.js +30 -60
  23. package/lib/default/options.js +13 -1
  24. package/lib/generate.js +44 -26
  25. package/lib/index.d.ts +52 -4
  26. package/lib/index.js +35 -41
  27. package/lib/istanbul/istanbul-summary.js +2 -2
  28. package/lib/istanbul/istanbul.js +35 -13
  29. package/lib/packages/monocart-coverage-assets.js +4 -3
  30. package/lib/packages/monocart-coverage-converter.js +27 -0
  31. package/lib/packages/monocart-coverage-vendor.js +2 -41
  32. package/lib/packages/monocart-coverage-vendor.js.LICENSE +3 -221
  33. package/lib/platform/concurrency.js +47 -43
  34. package/lib/platform/share.js +8 -1
  35. package/lib/register/hooks.js +19 -13
  36. package/lib/register/register.js +5 -3
  37. package/lib/reports/codacy.js +3 -5
  38. package/lib/reports/codecov.js +3 -5
  39. package/lib/reports/console-details.js +5 -5
  40. package/lib/reports/console-summary.js +4 -4
  41. package/lib/reports/custom.js +8 -24
  42. package/lib/reports/markdown-details.js +4 -6
  43. package/lib/reports/markdown-summary.js +4 -6
  44. package/lib/reports/raw.js +10 -12
  45. package/lib/reports/v8-json.js +3 -5
  46. package/lib/reports/v8.js +3 -5
  47. package/lib/utils/dedupe.js +1 -1
  48. package/lib/utils/gc.js +30 -14
  49. package/lib/utils/markdown.js +2 -2
  50. package/lib/utils/merge/merge.js +2 -4
  51. package/lib/utils/merge/range-tree.js +1 -1
  52. package/lib/utils/request.js +85 -73
  53. package/lib/utils/snapshot.js +4 -4
  54. package/lib/utils/source-path.js +26 -11
  55. package/lib/utils/util.js +67 -42
  56. package/lib/v8/v8-summary.js +2 -4
  57. package/lib/v8/v8.js +11 -11
  58. package/migrate-to-3.md +175 -0
  59. package/package.json +54 -42
  60. package/skills/monocart-coverage-reports/SKILL.md +106 -0
  61. package/skills/monocart-coverage-reports/references/best-practices.md +218 -0
  62. package/skills/monocart-coverage-reports/references/cli.md +108 -0
  63. package/skills/monocart-coverage-reports/references/collection.md +154 -0
  64. package/skills/monocart-coverage-reports/references/filtering-and-sourcemaps.md +170 -0
  65. package/skills/monocart-coverage-reports/references/reports-and-ci.md +219 -0
  66. package/skills/monocart-coverage-reports/references/repository-map.md +59 -0
  67. package/lib/index.mjs +0 -9
  68. package/lib/register/hooks.mjs +0 -3
  69. package/lib/register/register.mjs +0 -3
@@ -1,5 +1,5 @@
1
- const { diffSequence } = require('../packages/monocart-coverage-vendor.js');
2
- const Util = require('../utils/util.js');
1
+ import { diffSequence } from '../packages/monocart-coverage-converter.js';
2
+ import { Util } from '../utils/util.js';
3
3
  const EC = Util.EC;
4
4
 
5
5
  const findMapping = (list, offset) => {
@@ -813,6 +813,44 @@ const checkSourceFileIndexes = (startIndexes, endIndexes) => {
813
813
 
814
814
  };
815
815
 
816
+ const findMappingStartIndex = (mappings, offset) => {
817
+ let low = 0;
818
+ let high = mappings.length;
819
+ while (low < high) {
820
+ const mid = Math.floor((low + high) / 2);
821
+ if (mappings[mid].generatedOffset < offset) {
822
+ low = mid + 1;
823
+ } else {
824
+ high = mid;
825
+ }
826
+ }
827
+ return low;
828
+ };
829
+
830
+ const isFilteredRange = (start, end, mappingInfo, state) => {
831
+ const filtered = state.filteredSourceIndexes;
832
+ if (state.js || !filtered?.size) {
833
+ return false;
834
+ }
835
+ // A mapping after the exclusive end can belong to the next rule. Only
836
+ // inspect the active start segment and mappings inside this range.
837
+ const [startMapping] = [].concat(mappingInfo.startMappings);
838
+ if (!filtered.has(startMapping.sourceIndex)) {
839
+ return false;
840
+ }
841
+
842
+ const mappings = state.decodedMappings;
843
+ const low = findMappingStartIndex(mappings, start);
844
+ // Excluded endpoints alone are not enough: a retained source may have
845
+ // mappings in the middle of a rule that combines multiple source files.
846
+ for (let i = low; i < mappings.length && mappings[i].generatedOffset < end; i++) {
847
+ if (!filtered.has(mappings[i].sourceIndex)) {
848
+ return false;
849
+ }
850
+ }
851
+ return true;
852
+ };
853
+
816
854
  const getOriginalState = (start, end, state, originalMap) => {
817
855
 
818
856
  const mappingInfo = getMappingInfo(start, end, state);
@@ -829,6 +867,7 @@ const getOriginalState = (start, end, state, originalMap) => {
829
867
  if (!results) {
830
868
  return {
831
869
  error: true,
870
+ filtered: isFilteredRange(start, end, mappingInfo, state),
832
871
  errors: [`invalid source indexes: ${EC.yellow(`${startIndexes} ~ ${endIndexes}`)}`]
833
872
  };
834
873
  }
@@ -845,6 +884,7 @@ const getOriginalState = (start, end, state, originalMap) => {
845
884
  if (!originalState) {
846
885
  return {
847
886
  error: true,
887
+ filtered: state.filteredSourceIndexes?.has(sourceIndex) && isFilteredRange(start, end, mappingInfo, state),
848
888
  errors: [`not found original file: ${EC.yellow(sourceIndex)}`]
849
889
  };
850
890
  }
@@ -904,7 +944,167 @@ const getMappingInfo = (start, end, state) => {
904
944
  };
905
945
 
906
946
 
907
- const findOriginalRange = (start, end, state, originalMap, options = {}) => {
947
+ // CSS uses the shared mapping lookup, but owns its endpoint repair and validation.
948
+ const getCssOriginalEndPosition = (cache, state) => {
949
+ const { end, endMappings } = cache;
950
+ if (!Array.isArray(endMappings)) {
951
+ // Synthetic line ends are already exclusive, unlike real token mappings.
952
+ if (endMappings.end) {
953
+ return {
954
+ originalEnd: endMappings.originalOffset
955
+ };
956
+ }
957
+ return getOriginalEndPosition(cache, state);
958
+ }
959
+
960
+ const exclusiveMappings = findMapping(state.decodedMappings, end);
961
+ if (Array.isArray(exclusiveMappings) && !cache.crossEnd) {
962
+ const [mapping] = exclusiveMappings;
963
+ if (mapping.end && mapping.generatedOffset === end - 1) {
964
+ return {
965
+ originalEnd: mapping.originalOffset
966
+ };
967
+ }
968
+ }
969
+ return getOriginalEndPosition(cache, state);
970
+ };
971
+
972
+ const isValidCssOriginalRange = (start, end, length) => Number.isInteger(start) && Number.isInteger(end)
973
+ && start >= 0 && start < end && end <= length;
974
+
975
+ const getCssRangeMappingError = (start, end, state, cache) => {
976
+ // Matching endpoints do not guarantee a contiguous rule: a Sass mixin
977
+ // body can map to another file or an earlier location in the same file.
978
+ const mappings = state.decodedMappings;
979
+ const low = findMappingStartIndex(mappings, start);
980
+ for (let i = low; i < mappings.length && mappings[i].generatedOffset < end; i++) {
981
+ const mapping = mappings[i];
982
+ if (mapping.sourceIndex !== cache.sourceIndex) {
983
+ return {
984
+ error: true,
985
+ errors: ['cross-source CSS range']
986
+ };
987
+ }
988
+ // Synthetic line ends describe whole lines, not actual mapped tokens.
989
+ // Do not validate against the inferred end: indentation-based sources
990
+ // can map a generated closing brace back to the selector's line end.
991
+ if (!mapping.end && mapping.originalOffset < cache.originalStart) {
992
+ return {
993
+ error: true,
994
+ errors: ['mapped token before original CSS range']
995
+ };
996
+ }
997
+ }
998
+ };
999
+
1000
+ const mapCssOriginalRange = (start, end, state, originalMap, options) => {
1001
+ const cache = getOriginalState(start, end, state, originalMap);
1002
+ if (cache.error) {
1003
+ return cache;
1004
+ }
1005
+ const { originalStart, originalName } = getOriginalStartPosition(cache, state, options);
1006
+ cache.originalStart = originalStart;
1007
+ const mappingError = getCssRangeMappingError(start, end, state, cache);
1008
+ if (mappingError) {
1009
+ return mappingError;
1010
+ }
1011
+ const endResult = getCssOriginalEndPosition(cache, state);
1012
+ if (endResult.error) {
1013
+ return endResult;
1014
+ }
1015
+ const { originalEnd } = endResult;
1016
+ const { originalState } = cache;
1017
+ if (!isValidCssOriginalRange(originalStart, originalEnd, originalState.source.length)) {
1018
+ return {
1019
+ error: true,
1020
+ errors: [`invalid original range: ${EC.yellow(`${originalStart} ~ ${originalEnd}`)}`]
1021
+ };
1022
+ }
1023
+ if (originalState.locator.lineParser.commentParser.isComment(originalStart, originalEnd)) {
1024
+ return {
1025
+ error: true,
1026
+ errors: ['the range in a original comment']
1027
+ };
1028
+ }
1029
+ return {
1030
+ start: originalStart,
1031
+ end: originalEnd,
1032
+ name: originalName,
1033
+ originalState
1034
+ };
1035
+ };
1036
+
1037
+ const skipCssTrivia = (originalState, offset, direction) => {
1038
+ const { source, locator } = originalState;
1039
+ const { commentParser } = locator.lineParser;
1040
+ while (offset >= 0 && offset < source.length) {
1041
+ if ((/\s/).test(source[offset])) {
1042
+ offset += direction;
1043
+ continue;
1044
+ }
1045
+ if (commentParser.isComment(offset, offset + 1)) {
1046
+ const comment = commentParser.findComment(offset);
1047
+ offset = direction > 0 ? comment.end : comment.start - 1;
1048
+ continue;
1049
+ }
1050
+ break;
1051
+ }
1052
+ return offset;
1053
+ };
1054
+
1055
+ const fixCssOriginalRange = (result, state, end) => {
1056
+ if (result.error || result.filtered || state.source[end - 1] !== '}') {
1057
+ return result;
1058
+ }
1059
+ const { originalState } = result;
1060
+ const { source } = originalState;
1061
+ // A synthetic endpoint can include whitespace or comments after a brace
1062
+ // already mapped. Do not mistake the next enclosing brace for this rule's.
1063
+ const previousToken = skipCssTrivia(originalState, result.end - 1, -1);
1064
+ if (source[previousToken] === '}') {
1065
+ return result;
1066
+ }
1067
+ // Sass can omit both closing-brace mappings and intervening line comments.
1068
+ // Extend only when the next non-comment, non-whitespace token is a brace.
1069
+ const nextToken = skipCssTrivia(originalState, result.end, 1);
1070
+ if (source[nextToken] === '}') {
1071
+ return {
1072
+ ... result, end: nextToken + 1
1073
+ };
1074
+ }
1075
+ return result;
1076
+ };
1077
+
1078
+ const findCssOriginalRange = (start, end, state, originalMap, options) => {
1079
+ const { sourcePath, rangeCache } = state;
1080
+ const key = `${start}_${end}_${Boolean(options.fixOriginalRange)}`;
1081
+ if (rangeCache.has(key)) {
1082
+ return rangeCache.get(key);
1083
+ }
1084
+
1085
+ let result = mapCssOriginalRange(start, end, state, originalMap, options);
1086
+ if (options.fixOriginalRange) {
1087
+ // A closing brace may map to a mixin caller in another source. Retry the
1088
+ // body when necessary, then repair using the full generated rule end.
1089
+ if (result.error && !result.filtered && state.source[end - 1] === '}') {
1090
+ result = mapCssOriginalRange(start, end - 1, state, originalMap, options);
1091
+ }
1092
+ result = fixCssOriginalRange(result, state, end);
1093
+ }
1094
+ if (result.error) {
1095
+ result = {
1096
+ ... result, start, end, sourcePath
1097
+ };
1098
+ }
1099
+
1100
+ // Cache the final CSS range, error or deliberate exclusion.
1101
+ rangeCache.set(key, result);
1102
+ return result;
1103
+ };
1104
+
1105
+ // ========================================================================================================
1106
+
1107
+ const findJsOriginalRange = (start, end, state, originalMap, options = {}) => {
908
1108
 
909
1109
  const { sourcePath, rangeCache } = state;
910
1110
 
@@ -913,9 +1113,10 @@ const findOriginalRange = (start, end, state, originalMap, options = {}) => {
913
1113
  return rangeCache.get(key);
914
1114
  }
915
1115
 
916
- const createMappingError = (errors) => {
1116
+ const createMappingError = (errors, filtered) => {
917
1117
  const res = {
918
1118
  error: true,
1119
+ filtered,
919
1120
  start,
920
1121
  end,
921
1122
  sourcePath,
@@ -930,7 +1131,7 @@ const findOriginalRange = (start, end, state, originalMap, options = {}) => {
930
1131
 
931
1132
  const cache = getOriginalState(start, end, state, originalMap);
932
1133
  if (cache.error) {
933
- return createMappingError(cache.errors);
1134
+ return createMappingError(cache.errors, cache.filtered);
934
1135
  }
935
1136
 
936
1137
  const originalStartResult = getOriginalStartPosition(cache, state, options);
@@ -977,4 +1178,11 @@ const findOriginalRange = (start, end, state, originalMap, options = {}) => {
977
1178
 
978
1179
  };
979
1180
 
980
- module.exports = findOriginalRange;
1181
+ const findOriginalRange = (start, end, state, originalMap, options = {}) => {
1182
+ if (state.js) {
1183
+ return findJsOriginalRange(start, end, state, originalMap, options);
1184
+ }
1185
+ return findCssOriginalRange(start, end, state, originalMap, options);
1186
+ };
1187
+
1188
+ export { findOriginalRange };
@@ -1,4 +1,4 @@
1
- const { decode } = require('../packages/monocart-coverage-vendor.js');
1
+ import { decode } from '@jridgewell/sourcemap-codec';
2
2
 
3
3
  const COLUMN = 0;
4
4
  const SOURCES_INDEX = 1;
@@ -181,6 +181,4 @@ const flattenSourceMaps = function(indexedMap, mapUrl) {
181
181
  return indexedMap;
182
182
  };
183
183
 
184
- module.exports = {
185
- flattenSourceMaps
186
- };
184
+ export { flattenSourceMaps };
@@ -1,4 +1,4 @@
1
- const Util = require('../utils/util.js');
1
+ import { Util } from '../utils/util.js';
2
2
 
3
3
  const getNextN = (content, currentLine, item, locator) => {
4
4
 
@@ -220,6 +220,4 @@ const getIgnoredRanges = (locator, options) => {
220
220
  };
221
221
 
222
222
 
223
- module.exports = {
224
- getIgnoredRanges
225
- };
223
+ export { getIgnoredRanges };
@@ -1,5 +1,5 @@
1
- const Util = require('../utils/util.js');
2
- module.exports = class InfoBranch {
1
+ import { Util } from '../utils/util.js';
2
+ class InfoBranch {
3
3
  constructor(data, original) {
4
4
  const {
5
5
  start, end, locations, type
@@ -90,4 +90,6 @@ module.exports = class InfoBranch {
90
90
  };
91
91
  }
92
92
 
93
- };
93
+ }
94
+
95
+ export { InfoBranch };
@@ -1,5 +1,5 @@
1
- const Util = require('../utils/util.js');
2
- module.exports = class InfoFunction {
1
+ import { Util } from '../utils/util.js';
2
+ class InfoFunction {
3
3
  constructor(data, original) {
4
4
 
5
5
  const {
@@ -63,4 +63,6 @@ module.exports = class InfoFunction {
63
63
  line: line
64
64
  };
65
65
  }
66
- };
66
+ }
67
+
68
+ export { InfoFunction };
@@ -1,5 +1,5 @@
1
- const Util = require('../utils/util.js');
2
- module.exports = class InfoStatement {
1
+ import { Util } from '../utils/util.js';
2
+ class InfoStatement {
3
3
  constructor(data, original) {
4
4
  const {
5
5
  start, end, count
@@ -40,4 +40,6 @@ module.exports = class InfoStatement {
40
40
 
41
41
  return loc;
42
42
  }
43
- };
43
+ }
44
+
45
+ export { InfoStatement };
@@ -0,0 +1,78 @@
1
+ // AST block arrays are immutable during a visit. Weak keys release the index
2
+ // with the array, and rebuilding coverage info creates fresh arrays/indices.
3
+ const indices = new WeakMap();
4
+ const linearSearchLimit = 16;
5
+
6
+ const createIndex = (ranges) => {
7
+ // V8 ranges are in start-offset order. Preserve last-in-input semantics for
8
+ // custom, unsorted coverage rather than sorting it and changing the result.
9
+ for (let i = 1; i < ranges.length; i++) {
10
+ if (ranges[i].startOffset < ranges[i - 1].startOffset) {
11
+ return null;
12
+ }
13
+ }
14
+
15
+ let size = 1;
16
+ while (size < ranges.length) {
17
+ size *= 2;
18
+ }
19
+ const maxEnds = new Float64Array(size * 2);
20
+ maxEnds.fill(-Infinity);
21
+ for (let i = 0; i < ranges.length; i++) {
22
+ maxEnds[size + i] = ranges[i].endOffset;
23
+ }
24
+ for (let i = size - 1; i > 0; i--) {
25
+ maxEnds[i] = Math.max(maxEnds[i * 2], maxEnds[i * 2 + 1]);
26
+ }
27
+ return {
28
+ size, maxEnds
29
+ };
30
+ };
31
+
32
+ const findRightmost = (maxEnds, node, left, right, limit, end) => {
33
+ if (left >= limit || maxEnds[node] < end) {
34
+ return -1;
35
+ }
36
+ if (right - left === 1) {
37
+ return left;
38
+ }
39
+ const mid = (left + right) / 2;
40
+ const found = findRightmost(maxEnds, node * 2 + 1, mid, right, limit, end);
41
+ if (found !== -1) {
42
+ return found;
43
+ }
44
+ return findRightmost(maxEnds, node * 2, left, mid, limit, end);
45
+ };
46
+
47
+ // Return the last enclosing range, including equal-start ranges. For sorted
48
+ // arrays the O(n) index is built once; subsequent queries take O(log n).
49
+ const findContainingRange = (ranges, start, end) => {
50
+ if (ranges.length <= linearSearchLimit) {
51
+ return ranges.findLast((range) => range.startOffset <= start && range.endOffset >= end);
52
+ }
53
+ if (!indices.has(ranges)) {
54
+ indices.set(ranges, createIndex(ranges));
55
+ }
56
+ const index = indices.get(ranges);
57
+ if (!index) {
58
+ return ranges.findLast((range) => range.startOffset <= start && range.endOffset >= end);
59
+ }
60
+
61
+ // Only the prefix with startOffset <= start can contain the query.
62
+ let low = 0;
63
+ let high = ranges.length;
64
+ while (low < high) {
65
+ const mid = Math.floor((low + high) / 2);
66
+ if (ranges[mid].startOffset <= start) {
67
+ low = mid + 1;
68
+ } else {
69
+ high = mid;
70
+ }
71
+ }
72
+ const found = findRightmost(index.maxEnds, 1, 0, index.size, low, end);
73
+ if (found !== -1) {
74
+ return ranges[found];
75
+ }
76
+ };
77
+
78
+ export { findContainingRange };
@@ -0,0 +1,103 @@
1
+ // Each group belongs to one source-file merge and is immutable while queried.
2
+ // Weak keys keep indices from retaining coverage after that merge finishes.
3
+ const rangeIndices = new WeakMap();
4
+ const mappingIndices = new WeakMap();
5
+ const mappingScanLimit = 32;
6
+
7
+ const createRangeIndex = (groupMap) => {
8
+ // Copy before sorting: custom group order must not be changed.
9
+ const ranges = Array.from(groupMap.values()).sort((a, b) => a.start - b.start);
10
+ const starts = new Float64Array(ranges.length);
11
+ const maxEnds = new Float64Array(ranges.length);
12
+ let maxEnd = -Infinity;
13
+ for (let i = 0; i < ranges.length; i++) {
14
+ starts[i] = ranges[i].start;
15
+ maxEnd = Math.max(maxEnd, ranges[i].end);
16
+ maxEnds[i] = maxEnd;
17
+ }
18
+ return {
19
+ starts, maxEnds
20
+ };
21
+ };
22
+
23
+ const containsRange = (group, start, end) => {
24
+ let index = rangeIndices.get(group);
25
+ if (!index) {
26
+ index = createRangeIndex(group.groupMap);
27
+ rangeIndices.set(group, index);
28
+ }
29
+ const { starts, maxEnds } = index;
30
+ let low = 0;
31
+ let high = starts.length;
32
+ // The prefix ending at low contains every possible enclosing range.
33
+ while (low < high) {
34
+ const mid = Math.floor((low + high) / 2);
35
+ if (starts[mid] <= start) {
36
+ low = mid + 1;
37
+ } else {
38
+ high = mid;
39
+ }
40
+ }
41
+ return low > 0 && maxEnds[low - 1] >= end;
42
+ };
43
+
44
+ const scanMappingPrefix = (mappings, start, end) => {
45
+ const limit = Math.min(mappingScanLimit, mappings.length);
46
+ for (let i = 0; i < limit; i++) {
47
+ const offset = mappings[i].originalOffset;
48
+ if (offset >= start && offset <= end) {
49
+ return true;
50
+ }
51
+ }
52
+ // A short list was exhausted; otherwise the answer is still unknown.
53
+ return mappings.length <= limit ? false : null;
54
+ };
55
+
56
+ const hasMapping = (group, start, end) => {
57
+ let offsets = mappingIndices.get(group);
58
+ if (!offsets) {
59
+ const mappings = group.state.decodedMappings;
60
+ // Early hits (or exhausted short lists) need no full copy/sort. Probe
61
+ // only a bounded prefix so many misses cannot become quadratic scans.
62
+ const match = scanMappingPrefix(mappings, start, end);
63
+ if (match !== null) {
64
+ return match;
65
+ }
66
+ // Sort a separate numeric index, not the shared decoded mappings. The
67
+ // upstream order is by line/column; malformed columns can produce
68
+ // non-monotonic offsets, so do not assume offset order here.
69
+ offsets = Float64Array.from(mappings, (item) => item.originalOffset).sort();
70
+ mappingIndices.set(group, offsets);
71
+ }
72
+ let low = 0;
73
+ let high = offsets.length;
74
+ while (low < high) {
75
+ const mid = Math.floor((low + high) / 2);
76
+ if (offsets[mid] < start) {
77
+ low = mid + 1;
78
+ } else {
79
+ high = mid;
80
+ }
81
+ }
82
+ // Preserve the old inclusive endpoints, including a mapping at range.end.
83
+ return low < offsets.length && offsets[low] <= end;
84
+ };
85
+
86
+ const isUncoveredRange = (range, key, uncoveredGroups) => {
87
+ for (const group of uncoveredGroups) {
88
+ if (group.groupMap.has(key)) {
89
+ continue;
90
+ }
91
+ if (containsRange(group, range.start, range.end)) {
92
+ continue;
93
+ }
94
+ if (hasMapping(group, range.start, range.end)) {
95
+ // Another group maps this range without marking it uncovered.
96
+ // No later group can change that decision.
97
+ return false;
98
+ }
99
+ }
100
+ return true;
101
+ };
102
+
103
+ export { isUncoveredRange };
@@ -1,10 +1,12 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const Util = require('../utils/util.js');
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { Util } from '../utils/util.js';
4
4
 
5
- const { pathToFileURL, fileURLToPath } = require('url');
5
+ import { pathToFileURL, fileURLToPath } from 'node:url';
6
6
 
7
- const { normalizeSourcePath, getSourcePathReplacer } = require('../utils/source-path.js');
7
+ import {
8
+ normalizeSourcePath, getSourcePathReplacer, getSourceType, cssTypes
9
+ } from '../utils/source-path.js';
8
10
 
9
11
  // ========================================================================================================
10
12
 
@@ -20,41 +22,7 @@ const resolveAllDirList = (input) => {
20
22
  return dirList;
21
23
  };
22
24
 
23
- const resolveAllFilter = (input) => {
24
- // for function handler
25
- if (typeof input === 'function') {
26
- return input;
27
- }
28
-
29
- // for single minimatch pattern
30
- if (input && typeof input === 'string') {
31
- // string to multiple patterns "{...}"
32
- const obj = Util.strToObj(input);
33
- if (obj) {
34
- input = obj;
35
- } else {
36
- return (filePath) => {
37
- return Util.betterMinimatch(filePath, input);
38
- };
39
- }
40
- }
41
-
42
- // for patterns
43
- if (input && typeof input === 'object') {
44
- const patterns = Object.keys(input);
45
- return (filePath) => {
46
- for (const pattern of patterns) {
47
- if (Util.betterMinimatch(filePath, pattern)) {
48
- return input[pattern];
49
- }
50
- }
51
- // false if not matched
52
- };
53
- }
54
-
55
- // default
56
- return () => true;
57
- };
25
+ const resolveAllFilter = (input) => Util.createFilterHandler(input, (filePath) => filePath);
58
26
 
59
27
  const resolveAllOptions = (input) => {
60
28
  if (!input) {
@@ -101,11 +69,7 @@ const resolveFileType = (fileType, filePath) => {
101
69
  if (fileType === 'js' || fileType === 'css') {
102
70
  return fileType;
103
71
  }
104
- const extname = path.extname(filePath);
105
- if (['.css', '.scss', '.sass', '.less'].includes(extname)) {
106
- return 'css';
107
- }
108
- return 'js';
72
+ return cssTypes.has(getSourceType(filePath)) ? 'css' : 'js';
109
73
  };
110
74
 
111
75
  const saveUntestedFileSource = async (entryFile, options) => {
@@ -204,9 +168,22 @@ const getUntestedCoverageData = async (entryList, options, coverageType) => {
204
168
  return emptyCoverageList;
205
169
  };
206
170
 
171
+ const resolveUntestedPath = (entry, baseDir, sourcePathReplacer) => {
172
+ const sourcePath = normalizeSourcePath(entry.url, baseDir);
173
+ if (sourcePathReplacer) {
174
+ const replacement = sourcePathReplacer(sourcePath, entry);
175
+ if (typeof replacement === 'string' && replacement) {
176
+ return replacement;
177
+ }
178
+ }
179
+ return sourcePath;
180
+ };
181
+
207
182
  const getEmptyCoverages = async (testedMap, options, coverageType, fileList, fileTransformer) => {
208
183
  const sourceFilter = Util.getSourceFilter(options);
209
184
  const sourcePathReplacer = getSourcePathReplacer(options);
185
+ // User callbacks may inspect or mutate entry.source; object replacements cannot.
186
+ const needsSourceForPath = typeof options.sourcePath === 'function';
210
187
  const baseDir = options.baseDir;
211
188
 
212
189
  // console.log(fileList);
@@ -217,24 +194,16 @@ const getEmptyCoverages = async (testedMap, options, coverageType, fileList, fil
217
194
  const { fileType, filePath } = item;
218
195
  const type = resolveFileType(fileType, filePath);
219
196
  const url = pathToFileURL(filePath).toString();
220
- const source = Util.readFileSync(filePath);
221
197
  const entry = {
222
198
  empty: true,
223
199
  type,
224
- url,
225
- source
200
+ url
226
201
  };
227
-
228
- // normalize sourcePath here
229
- let sourcePath = normalizeSourcePath(url, baseDir);
230
- if (sourcePathReplacer) {
231
- const newSourcePath = sourcePathReplacer(sourcePath, entry);
232
- if (typeof newSourcePath === 'string' && newSourcePath) {
233
- sourcePath = newSourcePath;
234
- }
202
+ if (needsSourceForPath) {
203
+ entry.source = Util.readFileSync(filePath);
235
204
  }
236
205
 
237
- // console.log(sourcePath);
206
+ const sourcePath = resolveUntestedPath(entry, baseDir, sourcePathReplacer);
238
207
 
239
208
  if (testedMap.has(sourcePath)) {
240
209
  continue;
@@ -244,6 +213,9 @@ const getEmptyCoverages = async (testedMap, options, coverageType, fileList, fil
244
213
  continue;
245
214
  }
246
215
 
216
+ if (!needsSourceForPath) {
217
+ entry.source = Util.readFileSync(filePath);
218
+ }
247
219
  entry.sourcePath = sourcePath;
248
220
 
249
221
  await fileTransformer(entry, coverageType);
@@ -302,6 +274,4 @@ const getUntestedList = (testedMap, options, coverageType = 'v8') => {
302
274
  };
303
275
 
304
276
 
305
- module.exports = {
306
- getUntestedList
307
- };
277
+ export { getUntestedList };