monocart-coverage-reports 3.0.0 → 3.2.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.
@@ -11,16 +11,18 @@ import { Util } from '../utils/util.js';
11
11
 
12
12
  import { findOriginalRange } from './find-original-range.js';
13
13
  import { isUncoveredRange } from './source-range-merge.js';
14
+ import { mergeCssBytes } from './css-coverage.js';
14
15
  import { handleUncoveredInCovered } from './fake-source-coverage.js';
15
16
  import { getJsAstInfo, getCssAstInfo } from './ast.js';
16
17
  import { getIgnoredRanges } from './ignore.js';
17
18
 
18
19
  import { getUntestedList } from './untested.js';
19
-
20
20
  import {
21
21
  sortRanges, dedupeCountRanges, mergeRangesWith
22
22
  } from '../utils/dedupe.js';
23
- import { getSourceType, initSourceMapSourcesPath } from '../utils/source-path.js';
23
+ import {
24
+ getSourceType, initSourceMapSourcesPath, cssTypes
25
+ } from '../utils/source-path.js';
24
26
 
25
27
  import { decode } from '@jridgewell/sourcemap-codec';
26
28
 
@@ -759,31 +761,25 @@ const handleOriginalEmptyBytesCoverage = (state, originalStateMap) => {
759
761
  }
760
762
 
761
763
  const { decodedMappings } = originalState;
762
- const len = decodedMappings.length;
763
- if (len < 2) {
764
- return;
765
- }
766
- // sort by original line/column
767
- const startMapping = decodedMappings[0];
768
- const endMapping = decodedMappings[len - 1];
769
- // console.log(startMapping, endMapping);
770
- const startOffset = startMapping.generatedOffset;
771
-
772
- if (!endMapping.generatedEndOffset) {
773
- // line last one
774
- const line = state.locator.getLine(endMapping.generatedLine + 1);
775
- // could be no line found
776
- if (line) {
777
- // last column
778
- endMapping.generatedEndOffset = line.end;
779
- } else {
780
- endMapping.generatedEndOffset = endMapping.generatedOffset;
764
+ // The generated range the file's own mappings start in. Leave out the
765
+ // added line-end mappings: the last one is at the end of its line, which
766
+ // in a minified bundle is the end of the whole script, not of the wrapper
767
+ // the file is in.
768
+ let startOffset = Infinity;
769
+ let endOffset = -Infinity;
770
+ decodedMappings.forEach((mapping) => {
771
+ if (mapping.end) {
772
+ return;
781
773
  }
782
- }
783
- const endOffset = endMapping.generatedEndOffset;
774
+ startOffset = Math.min(startOffset, mapping.generatedOffset);
775
+ endOffset = Math.max(endOffset, mapping.generatedOffset);
776
+ });
784
777
 
785
- // console.log('===========================================================', originalState.sourcePath);
786
- // console.log(originalState.source.length, endMapping);
778
+ // A single real mapping is enough; auxiliary line-end mappings are not
779
+ // guaranteed for every source. Do not infer coverage without mappings.
780
+ if (startOffset === Infinity) {
781
+ return;
782
+ }
787
783
 
788
784
  checkList.push({
789
785
  originalState,
@@ -817,7 +813,9 @@ const handleOriginalEmptyBytesCoverage = (state, originalStateMap) => {
817
813
  if (range.count > 0) {
818
814
  continue;
819
815
  }
820
- if (startOffset >= range.start && endOffset <= range.end) {
816
+ // endOffset is the last mapping's start, not an exclusive end.
817
+ // V8 ranges exclude range.end, so a mapping there is outside.
818
+ if (startOffset >= range.start && endOffset < range.end) {
821
819
  // console.log('------------', 'added');
822
820
  originalState.bytes.push({
823
821
  start: 0,
@@ -831,16 +829,71 @@ const handleOriginalEmptyBytesCoverage = (state, originalStateMap) => {
831
829
  });
832
830
  };
833
831
 
834
-
835
832
  const handleAllOriginalBytesCoverage = (state, originalStateMap) => {
836
833
 
837
- const { js, coverageList } = state;
834
+ const {
835
+ js, coverageList, type
836
+ } = state;
837
+
838
+ if (cssTypes.has(type)) {
839
+ // An intentional exclusion is not a broken source map, even when all
840
+ // sources were excluded and there is nothing left to map.
841
+ if (state.filteredSourceIndexes.size > 0 && state.filteredSourceIndexes.size === state.sourceMap.sources.length) {
842
+ return;
843
+ }
844
+ const positionMap = new Map();
845
+ const couldMapAllRanges = coverageList.every((range) => {
846
+ const {
847
+ start, end, originalState, error, filtered
848
+ } = findOriginalRange(range.start, range.end, state, originalStateMap, {
849
+ fixOriginalRange: true
850
+ });
851
+
852
+ if (filtered) {
853
+ return true;
854
+ }
855
+ if (error) {
856
+ return false;
857
+ }
858
+
859
+ // Offsets are local to each source, not unique across the bundle.
860
+ let positions = positionMap.get(originalState);
861
+ if (!positions) {
862
+ positions = new Set();
863
+ positionMap.set(originalState, positions);
864
+ }
865
+ const positionKey = `${start}-${end}`;
866
+ if (positions.has(positionKey)) {
867
+ return false;
868
+ }
869
+ positions.add(positionKey);
870
+ originalState.bytes.push({
871
+ start, end, count: range.count
872
+ });
873
+ return true;
874
+ });
875
+
876
+ // make sure every range mapped correctly,
877
+ // otherwise discard changes and fall back to dist file to show at least some meaningful coverage.
878
+ // Sass `@include` is a common case: one generated rule maps to both the
879
+ // call site and the mixin body, which cannot be one contiguous original
880
+ // range. See "Sass mixins and @include" in README.
881
+ if (!couldMapAllRanges) {
882
+ Util.logDebug(`Could not map all ranges, fallback to dist file for ${EC.yellow(state.sourcePath)}`);
883
+ state.v8Data.dedupe = false;
884
+ // The fallback is now a real report file, not a debug-only copy.
885
+ delete state.v8Data.debug;
886
+ state.originalList = [];
887
+ originalStateMap.clear();
888
+ generateCoverageForDist(state);
889
+ }
838
890
 
839
- // only for js, no sourcemap for css for now
840
- if (!js) {
841
891
  return;
842
892
  }
843
893
 
894
+ if (!js) {
895
+ return;
896
+ }
844
897
  // v8 coverage
845
898
  coverageList.forEach((block) => {
846
899
  block.ranges.forEach((range) => {
@@ -1046,7 +1099,6 @@ const getOriginalDecodedMappings = (originalDecodedMap, sourceIndex, locator) =>
1046
1099
  };
1047
1100
 
1048
1101
  // ========================================================================================================
1049
-
1050
1102
  const initOriginalList = (state, originalDecodedMap, options) => {
1051
1103
 
1052
1104
  // source filter
@@ -1054,6 +1106,9 @@ const initOriginalList = (state, originalDecodedMap, options) => {
1054
1106
 
1055
1107
  // create original content mappings
1056
1108
  const originalStateMap = new Map();
1109
+ if (!state.js) {
1110
+ state.filteredSourceIndexes = new Set();
1111
+ }
1057
1112
 
1058
1113
  const { sources, sourcesContent } = state.sourceMap;
1059
1114
 
@@ -1065,7 +1120,7 @@ const initOriginalList = (state, originalDecodedMap, options) => {
1065
1120
  // filter
1066
1121
  // do not change for sourceIndex
1067
1122
  if (!sourceFilter(sourcePath)) {
1068
- // console.log('-', sourcePath);
1123
+ state.filteredSourceIndexes?.add(sourceIndex);
1069
1124
  return;
1070
1125
  }
1071
1126
  // console.log(sourcePath);
@@ -1082,14 +1137,12 @@ const initOriginalList = (state, originalDecodedMap, options) => {
1082
1137
 
1083
1138
  const decodedMappings = getOriginalDecodedMappings(originalDecodedMap, sourceIndex, locator);
1084
1139
 
1085
- // unpacked file always is js
1086
-
1087
1140
  const type = getSourceType(sourcePath);
1088
1141
 
1089
1142
  const originalState = {
1090
1143
  original: true,
1091
- // original file is js
1092
- js: true,
1144
+ // Only known style types are CSS; preserve JS coverage for all other sources.
1145
+ js: !cssTypes.has(type),
1093
1146
  type,
1094
1147
  source: sourceContent,
1095
1148
  sourcePath,
@@ -1149,6 +1202,12 @@ const collectOriginalList = (state, originalStateMap) => {
1149
1202
  source
1150
1203
  };
1151
1204
 
1205
+ // Sourcemap gaps must not count as covered bytes for untested files.
1206
+ // Keep the marker so real coverage can replace this entry during merge.
1207
+ if (state.v8Data.empty) {
1208
+ sourceItem.empty = true;
1209
+ }
1210
+
1152
1211
  // save v8 data and add to originalList
1153
1212
  originalState.v8Data = sourceItem;
1154
1213
  state.originalList.push(originalState);
@@ -1695,8 +1754,14 @@ const generateV8DataList = (stateList, options, needsIstanbul) => {
1695
1754
  // exists item
1696
1755
  const prevState = stateMap.get(id);
1697
1756
  if (prevState) {
1698
- // ignore empty item, just override it
1699
- if (!prevState.v8Data.empty) {
1757
+ // Empty entries and debug copies must not replace real original
1758
+ // coverage, nor contribute a duplicate execution to its merge.
1759
+ if (!prevState.v8Data.empty && !prevState.v8Data.debug) {
1760
+ // An untested build may contain different mapped AST nodes
1761
+ // (for example ES5 helpers). It must not alter real metrics.
1762
+ if (v8Data.empty) {
1763
+ return;
1764
+ }
1700
1765
  if (mergeMap.has(id)) {
1701
1766
  mergeMap.get(id).push(originalState);
1702
1767
  } else {
@@ -1715,7 +1780,7 @@ const generateV8DataList = (stateList, options, needsIstanbul) => {
1715
1780
  if (state.js) {
1716
1781
  mergeV8Data(state, mergeMap.get(id));
1717
1782
  } else {
1718
- // should no css here, css can not be in sources
1783
+ state.bytes = mergeCssBytes(mergeMap.get(id).map((it) => it.bytes));
1719
1784
  }
1720
1785
  }
1721
1786
 
@@ -0,0 +1,76 @@
1
+ const getCssRangePoints = (groups) => {
2
+ const points = [];
3
+ groups.forEach((ranges) => {
4
+ ranges.forEach(({
5
+ start, end, count
6
+ }) => {
7
+ if (start >= end) {
8
+ return;
9
+ }
10
+ const covered = count > 0 ? 1 : 0;
11
+ const uncovered = count > 0 ? 0 : 1;
12
+ points.push({
13
+ offset: start, covered, uncovered
14
+ }, {
15
+ offset: end, covered: -covered, uncovered: -uncovered
16
+ });
17
+ });
18
+ });
19
+ return points.sort((a, b) => a.offset - b.offset);
20
+ };
21
+
22
+ const appendCssRange = (merged, start, end, count) => {
23
+ const last = merged[merged.length - 1];
24
+ if (last && last.end === start && last.count === count) {
25
+ last.end = end;
26
+ } else {
27
+ merged.push({
28
+ start, end, count
29
+ });
30
+ }
31
+ };
32
+
33
+ const sweepCssBytes = (groups, uncoveredWins) => {
34
+ const points = getCssRangePoints(groups);
35
+ const merged = [];
36
+ let start = 0;
37
+ let covered = 0;
38
+ let uncovered = 0;
39
+ for (const point of points) {
40
+ const end = point.offset;
41
+ if (start < end && (covered > 0 || uncovered > 0)) {
42
+ const count = uncoveredWins ? Number(uncovered === 0) : Number(covered > 0);
43
+ appendCssRange(merged, start, end, count);
44
+ }
45
+ covered += point.covered;
46
+ uncovered += point.uncovered;
47
+ start = end;
48
+ }
49
+ return merged;
50
+ };
51
+
52
+ const isSortedDisjoint = (ranges) => {
53
+ let end = -Infinity;
54
+ for (const range of ranges) {
55
+ if (!(range.start >= end && range.start < range.end)) {
56
+ return false;
57
+ }
58
+ end = range.end;
59
+ }
60
+ return true;
61
+ };
62
+
63
+ // Within one bundle an uncovered child overrides its covered wrapper. Only
64
+ // after that interpretation may coverage from separate bundles be unioned.
65
+ // Unmapped gaps stay absent from this union; this function does not turn them
66
+ // into covered ranges. Byte summaries still derive covered bytes from total
67
+ // length minus explicit uncovered ranges, so gaps without an uncovered range
68
+ // are reported as covered. Use the line/code views when gaps matter.
69
+ const mergeCssBytes = (groups) => {
70
+ // Ordered, disjoint ranges already have unambiguous per-bundle coverage.
71
+ // Reuse them without allocating intermediate events and normalized ranges.
72
+ const normalized = groups.map((ranges) => (isSortedDisjoint(ranges) ? ranges : sweepCssBytes([ranges], true)));
73
+ return sweepCssBytes(normalized, false);
74
+ };
75
+
76
+ export { mergeCssBytes };
@@ -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
 
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
+
980
1188
  export { findOriginalRange };
@@ -4,7 +4,9 @@ import { Util } from '../utils/util.js';
4
4
 
5
5
  import { pathToFileURL, fileURLToPath } from 'node:url';
6
6
 
7
- import { normalizeSourcePath, getSourcePathReplacer } from '../utils/source-path.js';
7
+ import {
8
+ normalizeSourcePath, getSourcePathReplacer, getSourceType, cssTypes
9
+ } from '../utils/source-path.js';
8
10
 
9
11
  // ========================================================================================================
10
12
 
@@ -67,11 +69,7 @@ const resolveFileType = (fileType, filePath) => {
67
69
  if (fileType === 'js' || fileType === 'css') {
68
70
  return fileType;
69
71
  }
70
- const extname = path.extname(filePath);
71
- if (['.css', '.scss', '.sass', '.less'].includes(extname)) {
72
- return 'css';
73
- }
74
- return 'js';
72
+ return cssTypes.has(getSourceType(filePath)) ? 'css' : 'js';
75
73
  };
76
74
 
77
75
  const saveUntestedFileSource = async (entryFile, options) => {
@@ -111,6 +111,11 @@ const defaultOptions = {
111
111
  // onEntry: async (entry) => {}
112
112
  onEntry: null,
113
113
 
114
+ // (mcr test only) {function} Optional hook before test result publication, onReady and coverage generation.
115
+ // Results are always collected. Redact sensitive messages/names here before raw/debug export.
116
+ // onTestEnd: async (testResults) => {}
117
+ onTestEnd: null,
118
+
114
119
  // {function} onEnd hook
115
120
  // onEnd: async (reportData) => {}
116
121
  onEnd: null
package/lib/generate.js CHANGED
@@ -303,8 +303,9 @@ const addZipData = async (mcr, dataList, sourceCache, dir) => {
303
303
  }
304
304
  const entryName = entry.name;
305
305
  const filename = path.basename(entryName);
306
- // console.log('============================', filename);
307
- if (filename.endsWith('.json')) {
306
+ // Do not decompress or parse test results (or other unrelated JSON)
307
+ // when merging coverage. Match the cache reader's accepted prefixes.
308
+ if (filename.endsWith('.json') && (filename.startsWith('coverage-') || filename.startsWith('source-'))) {
308
309
  const buf = await zip.entryData(entryName);
309
310
  const json = JSON.parse(buf.toString('utf-8'));
310
311
  await addJsonData(mcr, dataList, sourceCache, json, filename);