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.
- package/README.md +314 -116
- package/README.zh-Hans.md +315 -117
- package/lib/assets.js +6 -6
- package/lib/cli.js +191 -95
- package/lib/client/cdp-client.js +74 -43
- package/lib/client/coverage-client.js +24 -8
- package/lib/client/ws-session.js +115 -37
- package/lib/converter/ast-visitor.js +5 -7
- package/lib/converter/ast.js +29 -8
- package/lib/converter/collect-source-maps.js +42 -21
- package/lib/converter/converter.js +124 -129
- package/lib/converter/css-coverage.js +76 -0
- package/lib/converter/fake-source-coverage.js +64 -0
- package/lib/converter/find-original-range.js +214 -6
- package/lib/converter/flatten-source-maps.js +2 -4
- package/lib/converter/ignore.js +2 -4
- package/lib/converter/info-branch.js +5 -3
- package/lib/converter/info-function.js +5 -3
- package/lib/converter/info-statement.js +5 -3
- package/lib/converter/range-index.js +78 -0
- package/lib/converter/source-range-merge.js +103 -0
- package/lib/converter/untested.js +30 -60
- package/lib/default/options.js +13 -1
- package/lib/generate.js +44 -26
- package/lib/index.d.ts +52 -4
- package/lib/index.js +35 -41
- package/lib/istanbul/istanbul-summary.js +2 -2
- package/lib/istanbul/istanbul.js +35 -13
- package/lib/packages/monocart-coverage-assets.js +4 -3
- package/lib/packages/monocart-coverage-converter.js +27 -0
- package/lib/packages/monocart-coverage-vendor.js +2 -41
- package/lib/packages/monocart-coverage-vendor.js.LICENSE +3 -221
- package/lib/platform/concurrency.js +47 -43
- package/lib/platform/share.js +8 -1
- package/lib/register/hooks.js +19 -13
- package/lib/register/register.js +5 -3
- package/lib/reports/codacy.js +3 -5
- package/lib/reports/codecov.js +3 -5
- package/lib/reports/console-details.js +5 -5
- package/lib/reports/console-summary.js +4 -4
- package/lib/reports/custom.js +8 -24
- package/lib/reports/markdown-details.js +4 -6
- package/lib/reports/markdown-summary.js +4 -6
- package/lib/reports/raw.js +10 -12
- package/lib/reports/v8-json.js +3 -5
- package/lib/reports/v8.js +3 -5
- package/lib/utils/dedupe.js +1 -1
- package/lib/utils/gc.js +30 -14
- package/lib/utils/markdown.js +2 -2
- package/lib/utils/merge/merge.js +2 -4
- package/lib/utils/merge/range-tree.js +1 -1
- package/lib/utils/request.js +85 -73
- package/lib/utils/snapshot.js +4 -4
- package/lib/utils/source-path.js +26 -11
- package/lib/utils/util.js +67 -42
- package/lib/v8/v8-summary.js +2 -4
- package/lib/v8/v8.js +11 -11
- package/migrate-to-3.md +175 -0
- package/package.json +54 -42
- package/skills/monocart-coverage-reports/SKILL.md +106 -0
- package/skills/monocart-coverage-reports/references/best-practices.md +218 -0
- package/skills/monocart-coverage-reports/references/cli.md +108 -0
- package/skills/monocart-coverage-reports/references/collection.md +154 -0
- package/skills/monocart-coverage-reports/references/filtering-and-sourcemaps.md +170 -0
- package/skills/monocart-coverage-reports/references/reports-and-ci.md +219 -0
- package/skills/monocart-coverage-reports/references/repository-map.md +59 -0
- package/lib/index.mjs +0 -9
- package/lib/register/hooks.mjs +0 -3
- package/lib/register/register.mjs +0 -3
package/lib/client/ws-session.js
CHANGED
|
@@ -1,63 +1,141 @@
|
|
|
1
|
-
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
|
|
3
|
+
const parseMessage = (data) => {
|
|
4
|
+
const message = JSON.parse(data);
|
|
5
|
+
if (!message || typeof message !== 'object' || Array.isArray(message)) {
|
|
6
|
+
throw new Error('Expected a CDP message object');
|
|
7
|
+
}
|
|
8
|
+
return message;
|
|
9
|
+
};
|
|
2
10
|
|
|
3
11
|
class WSSession extends EventEmitter {
|
|
4
|
-
constructor(ws) {
|
|
12
|
+
constructor(ws, { timeout = 30000 } = {}) {
|
|
5
13
|
super();
|
|
14
|
+
if (!Number.isFinite(timeout) || timeout < 0) {
|
|
15
|
+
throw new RangeError('WebSocket request timeout must be a non-negative finite number');
|
|
16
|
+
}
|
|
6
17
|
this.ws = ws;
|
|
18
|
+
this.timeout = timeout;
|
|
7
19
|
this.requestId = 1;
|
|
8
20
|
this.requestCache = new Map();
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (id) {
|
|
16
|
-
const request = this.requestCache.get(id);
|
|
17
|
-
this.requestCache.delete(id);
|
|
18
|
-
if (request) {
|
|
19
|
-
request.resolve(message.result);
|
|
20
|
-
}
|
|
21
|
-
return;
|
|
21
|
+
this.events = {
|
|
22
|
+
message: (data) => this.onMessage(data),
|
|
23
|
+
error: (error) => this.disconnect(error),
|
|
24
|
+
close: () => {
|
|
25
|
+
this.disconnect(new Error('WebSocket closed'));
|
|
26
|
+
this.unbind(ws);
|
|
22
27
|
}
|
|
28
|
+
};
|
|
29
|
+
for (const [name, handler] of Object.entries(this.events)) {
|
|
30
|
+
ws.on(name, handler);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
23
33
|
|
|
24
|
-
|
|
25
|
-
|
|
34
|
+
onMessage(data) {
|
|
35
|
+
let message;
|
|
36
|
+
try {
|
|
37
|
+
message = parseMessage(data);
|
|
38
|
+
} catch (cause) {
|
|
39
|
+
this.disconnect(new Error('Invalid WebSocket JSON message', {
|
|
40
|
+
cause
|
|
41
|
+
}));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const { id, method } = message;
|
|
45
|
+
if (id) {
|
|
46
|
+
const pending = this.requestCache.get(id);
|
|
47
|
+
if (!pending) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
let error;
|
|
51
|
+
if (message.error) {
|
|
52
|
+
error = new Error(`Protocol error (${pending.method}): ${message.error.message}`);
|
|
53
|
+
error.code = message.error.code;
|
|
54
|
+
error.data = message.error.data;
|
|
26
55
|
}
|
|
56
|
+
this.settleRequest(id, error, message.result);
|
|
57
|
+
} else if (typeof method === 'string') {
|
|
58
|
+
this.emit(method, message.params, message.sessionId);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
27
61
|
|
|
28
|
-
|
|
62
|
+
settleRequest(id, error, result) {
|
|
63
|
+
const pending = this.requestCache.get(id);
|
|
64
|
+
if (!pending) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
this.requestCache.delete(id);
|
|
68
|
+
clearTimeout(pending.timer);
|
|
69
|
+
if (error) {
|
|
70
|
+
pending.reject(error);
|
|
71
|
+
} else {
|
|
72
|
+
pending.resolve(result);
|
|
73
|
+
}
|
|
29
74
|
}
|
|
30
75
|
|
|
31
76
|
send(method, params) {
|
|
32
77
|
return new Promise((resolve, reject) => {
|
|
33
|
-
|
|
34
|
-
|
|
78
|
+
const ws = this.ws;
|
|
79
|
+
if (!ws || (typeof ws.readyState === 'number' && ws.readyState !== 1)) {
|
|
80
|
+
reject(new Error('Invalid or closed websocket'));
|
|
35
81
|
return;
|
|
36
82
|
}
|
|
37
83
|
const id = this.requestId++;
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
method,
|
|
41
|
-
params: params || {}
|
|
84
|
+
const pending = {
|
|
85
|
+
resolve, reject, method, timer: null
|
|
42
86
|
};
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
87
|
+
// Register before send: a response may arrive before its callback.
|
|
88
|
+
this.requestCache.set(id, pending);
|
|
89
|
+
if (this.timeout > 0) {
|
|
90
|
+
pending.timer = setTimeout(() => {
|
|
91
|
+
const error = new Error(`WebSocket request timed out: ${method}`);
|
|
92
|
+
error.code = 'ETIMEDOUT';
|
|
93
|
+
this.settleRequest(id, error);
|
|
94
|
+
}, this.timeout);
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
ws.send(JSON.stringify({
|
|
98
|
+
id, method, params: params || {}
|
|
99
|
+
}), (error) => {
|
|
100
|
+
if (error) {
|
|
101
|
+
this.settleRequest(id, error);
|
|
102
|
+
}
|
|
50
103
|
});
|
|
51
|
-
})
|
|
104
|
+
} catch (error) {
|
|
105
|
+
this.settleRequest(id, error);
|
|
106
|
+
}
|
|
52
107
|
});
|
|
53
108
|
}
|
|
54
109
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
110
|
+
disconnect(error) {
|
|
111
|
+
const ws = this.ws;
|
|
112
|
+
this.ws = null;
|
|
113
|
+
for (const id of this.requestCache.keys()) {
|
|
114
|
+
this.settleRequest(id, error);
|
|
59
115
|
}
|
|
116
|
+
if (ws) {
|
|
117
|
+
ws.off('message', this.events.message);
|
|
118
|
+
if (ws.readyState === 3) {
|
|
119
|
+
this.unbind(ws);
|
|
120
|
+
} else {
|
|
121
|
+
// Keep error/close handlers until close to consume termination errors.
|
|
122
|
+
ws.terminate();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
unbind(ws) {
|
|
128
|
+
if (this.events) {
|
|
129
|
+
for (const [name, handler] of Object.entries(this.events)) {
|
|
130
|
+
ws.off(name, handler);
|
|
131
|
+
}
|
|
132
|
+
this.events = null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
detach() {
|
|
137
|
+
this.disconnect(new Error('WebSocket detached'));
|
|
60
138
|
}
|
|
61
139
|
}
|
|
62
140
|
|
|
63
|
-
|
|
141
|
+
export { WSSession };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
import { Util } from '../utils/util.js';
|
|
2
|
+
import { findContainingRange } from './range-index.js';
|
|
2
3
|
|
|
3
4
|
const BranchTypes = {
|
|
4
5
|
ConditionalExpression: 'ConditionalExpression',
|
|
@@ -83,17 +84,14 @@ const getFunctionBlock = (start, end, functionState) => {
|
|
|
83
84
|
return block;
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
const uncoveredBlock =
|
|
87
|
+
const uncoveredBlock = findContainingRange(blockUncoveredRanges, start, end);
|
|
87
88
|
if (uncoveredBlock) {
|
|
88
89
|
return uncoveredBlock;
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
// the block is not exact correct, if there is a block wrapped
|
|
92
93
|
// [x8]{ var a = b || [x4]c }, b is no block, but it can be found in x8 block
|
|
93
|
-
|
|
94
|
-
if (coveredBlocks.length) {
|
|
95
|
-
return coveredBlocks.pop();
|
|
96
|
-
}
|
|
94
|
+
return findContainingRange(blockCoveredRanges, start, end);
|
|
97
95
|
|
|
98
96
|
};
|
|
99
97
|
|
|
@@ -803,7 +801,7 @@ const collectAstInfo = (ast, coverageInfo) => {
|
|
|
803
801
|
|
|
804
802
|
};
|
|
805
803
|
|
|
806
|
-
|
|
804
|
+
export {
|
|
807
805
|
BranchTypes,
|
|
808
806
|
collectAstInfo
|
|
809
807
|
};
|
package/lib/converter/ast.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import * as acorn from 'acorn';
|
|
2
|
+
import * as acornLoose from 'acorn-loose';
|
|
3
|
+
import { parseCss } from '../packages/monocart-coverage-converter.js';
|
|
4
|
+
import { Util } from '../utils/util.js';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
import { collectAstInfo } from './ast-visitor.js';
|
|
7
7
|
|
|
8
8
|
const getCoverageInfo = (coverageList) => {
|
|
9
9
|
const functionMap = new Map();
|
|
@@ -345,8 +345,29 @@ const getCssAstInfo = (item, coverageList) => {
|
|
|
345
345
|
endOffset: it.end
|
|
346
346
|
};
|
|
347
347
|
});
|
|
348
|
-
|
|
349
|
-
|
|
348
|
+
let ast;
|
|
349
|
+
try {
|
|
350
|
+
ast = parseCss(source);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
// Untested files may use preprocessor syntax that postcss cannot parse.
|
|
353
|
+
// Mirror the JS empty-file fallback: report the whole file as uncovered
|
|
354
|
+
// instead of aborting report generation.
|
|
355
|
+
if (empty) {
|
|
356
|
+
coverageList.push({
|
|
357
|
+
start: 0,
|
|
358
|
+
end: source.length,
|
|
359
|
+
count: 0
|
|
360
|
+
});
|
|
361
|
+
return {
|
|
362
|
+
functions: [],
|
|
363
|
+
branches: [],
|
|
364
|
+
statements: []
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
throw new Error(`parse css error in ${item.sourcePath}`, {
|
|
368
|
+
cause: error
|
|
369
|
+
});
|
|
370
|
+
}
|
|
350
371
|
|
|
351
372
|
addCssRules(ast.nodes, coverageList, coveredRanges, empty);
|
|
352
373
|
|
|
@@ -358,7 +379,7 @@ const getCssAstInfo = (item, coverageList) => {
|
|
|
358
379
|
};
|
|
359
380
|
|
|
360
381
|
|
|
361
|
-
|
|
382
|
+
export {
|
|
362
383
|
getJsAstInfo,
|
|
363
384
|
getCssAstInfo
|
|
364
385
|
};
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import EC from 'eight-colors';
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
|
+
import { Concurrency } from '../platform/concurrency.js';
|
|
6
|
+
import convertSourceMap from 'convert-source-map';
|
|
7
|
+
import { flattenSourceMaps } from './flatten-source-maps.js';
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
import { Util } from '../utils/util.js';
|
|
10
10
|
|
|
11
11
|
const getSourceMapCandidates = (source) => {
|
|
12
12
|
if (!source.includes('sourceMappingURL=')) {
|
|
@@ -121,27 +121,41 @@ const resolveMapUrl = (filename, url) => {
|
|
|
121
121
|
|
|
122
122
|
const resolveSourcesContent = (data, url) => {
|
|
123
123
|
|
|
124
|
-
const {
|
|
124
|
+
const {
|
|
125
|
+
sources, sourcesContent, sourceRoot
|
|
126
|
+
} = data;
|
|
127
|
+
// sourceRoot is a directory, even when its trailing slash is omitted.
|
|
128
|
+
let rootUrl;
|
|
129
|
+
if (typeof sourceRoot === 'string' && sourceRoot) {
|
|
130
|
+
rootUrl = Util.resolveUrl(sourceRoot.endsWith('/') ? sourceRoot : `${sourceRoot}/`, url);
|
|
131
|
+
}
|
|
125
132
|
|
|
126
133
|
// sources [1,2,3]
|
|
127
134
|
// sourcesContent could be [null, null, "content"]
|
|
128
135
|
// some of contents could be missed
|
|
129
136
|
|
|
130
137
|
let hasSourceContent = false;
|
|
138
|
+
const missingSources = [];
|
|
131
139
|
sources.forEach((file, i) => {
|
|
132
140
|
if (typeof sourcesContent[i] === 'string') {
|
|
133
141
|
hasSourceContent = true;
|
|
134
142
|
return;
|
|
135
143
|
}
|
|
136
144
|
|
|
137
|
-
const sourceUrl = Util.resolveUrl(file, url);
|
|
145
|
+
const sourceUrl = Util.resolveUrl(file, rootUrl?.href || url);
|
|
138
146
|
if (sourceUrl) {
|
|
139
|
-
let
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
147
|
+
let content;
|
|
148
|
+
try {
|
|
149
|
+
let sourcePath = sourceUrl.toString();
|
|
150
|
+
// Read actual local paths, not normalized virtual report paths.
|
|
151
|
+
if (sourceUrl.protocol === 'file:') {
|
|
152
|
+
sourcePath = fileURLToPath(sourceUrl);
|
|
153
|
+
}
|
|
154
|
+
content = Util.readFileSync(path.resolve(sourcePath));
|
|
155
|
+
} catch {
|
|
156
|
+
// A foreign or malformed file URL is just an unavailable source.
|
|
157
|
+
// Keep resolving the other sources instead of discarding the map.
|
|
143
158
|
}
|
|
144
|
-
const content = Util.readFileSync(path.resolve(sourcePath));
|
|
145
159
|
if (typeof content === 'string') {
|
|
146
160
|
sourcesContent[i] = content;
|
|
147
161
|
hasSourceContent = true;
|
|
@@ -149,13 +163,18 @@ const resolveSourcesContent = (data, url) => {
|
|
|
149
163
|
}
|
|
150
164
|
}
|
|
151
165
|
|
|
152
|
-
|
|
166
|
+
missingSources.push(i);
|
|
153
167
|
Util.logDebug(EC.red(`failed to load source content: ${file}`));
|
|
154
168
|
|
|
155
169
|
});
|
|
156
170
|
|
|
157
171
|
|
|
158
172
|
if (hasSourceContent) {
|
|
173
|
+
// Only commit placeholders for accepted maps. A rejected map may be
|
|
174
|
+
// cached by the resolver and must not look like valid empty sources later.
|
|
175
|
+
missingSources.forEach((i) => {
|
|
176
|
+
sourcesContent[i] = '';
|
|
177
|
+
});
|
|
159
178
|
return data;
|
|
160
179
|
}
|
|
161
180
|
};
|
|
@@ -233,7 +252,7 @@ const collectSourceMaps = async (v8list, options) => {
|
|
|
233
252
|
for (const item of v8list) {
|
|
234
253
|
|
|
235
254
|
const {
|
|
236
|
-
|
|
255
|
+
url, id, source, sourceMap
|
|
237
256
|
} = item;
|
|
238
257
|
|
|
239
258
|
// source and sourceMap will be saved as separated file (could be cached)
|
|
@@ -258,7 +277,7 @@ const collectSourceMaps = async (v8list, options) => {
|
|
|
258
277
|
|
|
259
278
|
// An explicitly supplied, usable map takes precedence over sourceMappingURL.
|
|
260
279
|
// Keep the original source so V8 offsets remain valid.
|
|
261
|
-
if (
|
|
280
|
+
if (!isUsableSourceMap(sourceData.sourceMap)) {
|
|
262
281
|
delete sourceData.sourceMap;
|
|
263
282
|
const candidates = getSourceMapCandidates(source);
|
|
264
283
|
if (candidates.length) {
|
|
@@ -279,7 +298,7 @@ const collectSourceMaps = async (v8list, options) => {
|
|
|
279
298
|
const { url } = sourceData;
|
|
280
299
|
for (const candidate of candidates) {
|
|
281
300
|
let data;
|
|
282
|
-
let sourceMapUrl;
|
|
301
|
+
let sourceMapUrl = url;
|
|
283
302
|
try {
|
|
284
303
|
if (candidate.inline) {
|
|
285
304
|
data = convertSourceMap.fromComment(candidate.comment).sourcemap;
|
|
@@ -293,7 +312,9 @@ const collectSourceMaps = async (v8list, options) => {
|
|
|
293
312
|
if (!isUsableSourceMap(data)) {
|
|
294
313
|
continue;
|
|
295
314
|
}
|
|
296
|
-
|
|
315
|
+
// Inline maps inherit the generated file's URL. External maps
|
|
316
|
+
// resolve their sources relative to the map, not the bundle.
|
|
317
|
+
const sourceMap = resolveSourceMap(data, sourceMapUrl);
|
|
297
318
|
if (!sourceMap) {
|
|
298
319
|
continue;
|
|
299
320
|
}
|
|
@@ -319,7 +340,7 @@ const collectSourceMaps = async (v8list, options) => {
|
|
|
319
340
|
|
|
320
341
|
};
|
|
321
342
|
|
|
322
|
-
|
|
343
|
+
export {
|
|
323
344
|
collectSourceMaps,
|
|
324
345
|
resolveSourceMap
|
|
325
346
|
};
|