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.
- package/README.md +98 -2
- package/README.zh-Hans.md +98 -2
- package/lib/cli.js +25 -60
- package/lib/converter/ast.js +23 -2
- package/lib/converter/collect-source-maps.js +33 -12
- package/lib/converter/converter.js +104 -39
- package/lib/converter/css-coverage.js +76 -0
- package/lib/converter/find-original-range.js +211 -3
- package/lib/converter/untested.js +4 -6
- package/lib/default/options.js +5 -0
- package/lib/generate.js +3 -2
- package/lib/index.d.ts +67 -0
- package/lib/packages/monocart-coverage-assets.js +1 -1
- package/lib/packages/monocart-coverage-converter.js +11 -11
- package/lib/packages/monocart-coverage-vendor.js +2 -1
- package/lib/packages/monocart-coverage-vendor.js.LICENSE +69 -9
- package/lib/platform/share.js +7 -0
- package/lib/test/test-reporter.js +191 -0
- package/lib/test/test-results.js +104 -0
- package/lib/test/test-runner.js +115 -0
- package/lib/utils/source-path.js +21 -6
- package/lib/utils/util.js +6 -1
- package/migrate-to-3.md +4 -0
- package/package.json +29 -21
- package/skills/monocart-coverage-reports/SKILL.md +2 -2
- package/skills/monocart-coverage-reports/references/best-practices.md +218 -0
- package/skills/monocart-coverage-reports/references/cli.md +5 -0
- package/skills/monocart-coverage-reports/references/collection.md +2 -0
- package/skills/monocart-coverage-reports/references/reports-and-ci.md +2 -0
- package/skills/monocart-coverage-reports/references/repository-map.md +7 -3
- package/lib/packages/chunks/chunk-RVOSNMVU.js +0 -2
|
@@ -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 {
|
|
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
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
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
|
-
|
|
774
|
+
startOffset = Math.min(startOffset, mapping.generatedOffset);
|
|
775
|
+
endOffset = Math.max(endOffset, mapping.generatedOffset);
|
|
776
|
+
});
|
|
784
777
|
|
|
785
|
-
//
|
|
786
|
-
//
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
//
|
|
1092
|
-
js:
|
|
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
|
-
//
|
|
1699
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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) => {
|
package/lib/default/options.js
CHANGED
|
@@ -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
|
-
//
|
|
307
|
-
|
|
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);
|