monocart-coverage-reports 2.13.1 → 3.0.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 (68) hide show
  1. package/README.md +267 -114
  2. package/README.zh-Hans.md +268 -115
  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 +6 -6
  10. package/lib/converter/collect-source-maps.js +119 -92
  11. package/lib/converter/converter.js +49 -117
  12. package/lib/converter/fake-source-coverage.js +64 -0
  13. package/lib/converter/find-original-range.js +3 -3
  14. package/lib/converter/flatten-source-maps.js +2 -4
  15. package/lib/converter/ignore.js +2 -4
  16. package/lib/converter/info-branch.js +5 -3
  17. package/lib/converter/info-function.js +5 -3
  18. package/lib/converter/info-statement.js +5 -3
  19. package/lib/converter/range-index.js +78 -0
  20. package/lib/converter/source-range-merge.js +103 -0
  21. package/lib/converter/untested.js +27 -55
  22. package/lib/default/options.js +13 -1
  23. package/lib/generate.js +44 -26
  24. package/lib/index.d.ts +52 -4
  25. package/lib/index.js +35 -41
  26. package/lib/istanbul/istanbul-summary.js +2 -2
  27. package/lib/istanbul/istanbul.js +35 -13
  28. package/lib/packages/chunks/chunk-RVOSNMVU.js +2 -0
  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 +1 -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 +5 -5
  55. package/lib/utils/util.js +61 -41
  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 +153 -149
  60. package/skills/monocart-coverage-reports/SKILL.md +106 -0
  61. package/skills/monocart-coverage-reports/references/cli.md +108 -0
  62. package/skills/monocart-coverage-reports/references/collection.md +152 -0
  63. package/skills/monocart-coverage-reports/references/filtering-and-sourcemaps.md +170 -0
  64. package/skills/monocart-coverage-reports/references/reports-and-ci.md +219 -0
  65. package/skills/monocart-coverage-reports/references/repository-map.md +55 -0
  66. package/lib/index.mjs +0 -9
  67. package/lib/register/hooks.mjs +0 -3
  68. package/lib/register/register.mjs +0 -3
@@ -1,63 +1,141 @@
1
- const { EventEmitter } = require('events');
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
- ws.on('message', (data, isBinary) => {
10
-
11
- const message = JSON.parse(data);
12
- // console.log(message);
13
-
14
- const { id, method } = message;
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
- if (method) {
25
- this.emit(method, message.params, message.sessionId);
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
- if (!this.ws) {
34
- reject(new Error('Invalid websocket'));
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 message = {
39
- id,
40
- method,
41
- params: params || {}
84
+ const pending = {
85
+ resolve, reject, method, timer: null
42
86
  };
43
- this.ws.send(JSON.stringify(message), (err) => {
44
- if (err) {
45
- reject(err);
46
- return;
47
- }
48
- this.requestCache.set(id, {
49
- resolve
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
- detach() {
56
- if (this.ws) {
57
- this.ws.terminate();
58
- this.ws = null;
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
- module.exports = WSSession;
141
+ export { WSSession };
@@ -1,4 +1,5 @@
1
- const Util = require('../utils/util.js');
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 = Util.findInRanges(start, end, blockUncoveredRanges, 'startOffset', 'endOffset');
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
- const coveredBlocks = blockCoveredRanges.filter((it) => start >= it.startOffset && end <= it.endOffset);
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
- module.exports = {
804
+ export {
807
805
  BranchTypes,
808
806
  collectAstInfo
809
807
  };
@@ -1,9 +1,9 @@
1
- const acorn = require('acorn');
2
- const acornLoose = require('acorn-loose');
3
- const { parseCss } = require('../packages/monocart-coverage-vendor.js');
4
- const Util = require('../utils/util.js');
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
- const { collectAstInfo } = require('./ast-visitor.js');
6
+ import { collectAstInfo } from './ast-visitor.js';
7
7
 
8
8
  const getCoverageInfo = (coverageList) => {
9
9
  const functionMap = new Map();
@@ -358,7 +358,7 @@ const getCssAstInfo = (item, coverageList) => {
358
358
  };
359
359
 
360
360
 
361
- module.exports = {
361
+ export {
362
362
  getJsAstInfo,
363
363
  getCssAstInfo
364
364
  };
@@ -1,23 +1,79 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const EC = require('eight-colors');
4
- const { CommentParser } = require('monocart-locator');
5
- const { fileURLToPath, pathToFileURL } = require('url');
6
- const Concurrency = require('../platform/concurrency.js');
7
- const { convertSourceMap } = require('../packages/monocart-coverage-vendor.js');
8
- const { flattenSourceMaps } = require('./flatten-source-maps.js');
9
-
10
- const Util = require('../utils/util.js');
11
-
12
- const getSourceMapComments = (source) => {
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
+
9
+ import { Util } from '../utils/util.js';
10
+
11
+ const getSourceMapCandidates = (source) => {
13
12
  if (!source.includes('sourceMappingURL=')) {
14
13
  return [];
15
14
  }
16
- return new CommentParser(source).comments
17
- .map((item) => item.text)
18
- .filter((comment) => (/^\/[/*][@#][ \t]+sourceMappingURL=/).test(comment));
15
+
16
+ // Match directive-shaped text without parsing JS; valid maps in strings can be selected.
17
+ const candidates = [];
18
+ for (const match of source.matchAll(convertSourceMap.commentRegex)) {
19
+ candidates.push({
20
+ index: match.index,
21
+ comment: match[0],
22
+ inline: true
23
+ });
24
+ }
25
+ for (const match of source.matchAll(convertSourceMap.mapFileCommentRegex)) {
26
+ const filename = match[1] || match[2];
27
+ if (filename && !filename.startsWith('data:')) {
28
+ candidates.push({
29
+ index: match.index,
30
+ filename
31
+ });
32
+ }
33
+ }
34
+ // Like 2.13.0, try inline maps before external ones. Within each kind,
35
+ // start at the last directive and fall back when it is unusable.
36
+ return candidates.sort((a, b) => {
37
+ if (a.inline !== b.inline) {
38
+ return a.inline ? -1 : 1;
39
+ }
40
+ return b.index - a.index;
41
+ });
19
42
  };
20
43
 
44
+ // Only check the fields required to resolve a map; decoding is deferred to
45
+ // the downstream converter. A map without sources cannot map to original code.
46
+ const isValidBasicMap = (data) => Array.isArray(data.sources)
47
+ && data.sources.every((source) => typeof source === 'string')
48
+ && typeof data.mappings === 'string'
49
+ && (data.sources.length > 0 || data.mappings === '');
50
+
51
+ const isValidSection = (section) => section && section.offset
52
+ && Number.isInteger(section.offset.line) && section.offset.line >= 0
53
+ && Number.isInteger(section.offset.column) && section.offset.column >= 0
54
+ && isValidSourceMap(section.map);
55
+
56
+ const isValidSourceMap = (data) => {
57
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
58
+ return false;
59
+ }
60
+ if (Array.isArray(data.sections)) {
61
+ return data.sections.every(isValidSection);
62
+ }
63
+ return isValidBasicMap(data);
64
+ };
65
+
66
+ // An empty map cannot map coverage to any original source. Try the next
67
+ // candidate instead, including when an indexed map has only empty sections.
68
+ const hasMappings = (data) => {
69
+ if (Array.isArray(data.sections)) {
70
+ return data.sections.some((section) => hasMappings(section.map));
71
+ }
72
+ return data.mappings.length > 0;
73
+ };
74
+
75
+ const isUsableSourceMap = (data) => isValidSourceMap(data) && hasMappings(data);
76
+
21
77
  const defaultSourceMapResolver = async (url = '') => {
22
78
 
23
79
  if (url.startsWith('file:')) {
@@ -63,24 +119,6 @@ const resolveMapUrl = (filename, url) => {
63
119
  return urlObj?.toString();
64
120
  };
65
121
 
66
- const getSourceMapUrl = (comments, url) => {
67
- for (let i = comments.length - 1; i >= 0; i--) {
68
- const r = convertSourceMap.mapFileCommentRegex.exec(comments[i]);
69
- if (!r || r.index !== 0) {
70
- continue;
71
- }
72
- // //# captures in 1 and /* */ in 2
73
- const filename = r[1] || r[2];
74
- if (!filename || filename.startsWith('data:')) {
75
- continue;
76
- }
77
- const mapUrl = resolveMapUrl(filename, url);
78
- if (mapUrl) {
79
- return mapUrl;
80
- }
81
- }
82
- };
83
-
84
122
  const resolveSourcesContent = (data, url) => {
85
123
 
86
124
  const { sources, sourcesContent } = data;
@@ -125,7 +163,7 @@ const resolveSourcesContent = (data, url) => {
125
163
  const checkSourcesContent = (data) => {
126
164
  const { sourcesContent, sources } = data;
127
165
 
128
- if (!sourcesContent) {
166
+ if (!Array.isArray(sourcesContent)) {
129
167
  data.sourcesContent = [];
130
168
  return false;
131
169
  }
@@ -146,15 +184,7 @@ const resolveSectionedSourceMap = (data, url, sections) => {
146
184
  // offset: { line: 1, column: 0 },
147
185
  // map: { sources, sourcesContent }
148
186
 
149
- const map = item.map;
150
-
151
- if (checkSourcesContent(map)) {
152
- hasSourceContent = true;
153
- return;
154
- }
155
-
156
- const done = resolveSourcesContent(map, url);
157
- if (done) {
187
+ if (resolveSourceMap(item.map, url)) {
158
188
  hasSourceContent = true;
159
189
  }
160
190
 
@@ -169,6 +199,10 @@ const resolveSourceMap = (data, url) => {
169
199
  if (!data) {
170
200
  return;
171
201
  }
202
+ // Optional metadata may be absent or malformed in otherwise usable maps.
203
+ if (!Array.isArray(data.names)) {
204
+ data.names = [];
205
+ }
172
206
  const {
173
207
  sections, sources, mappings
174
208
  } = data;
@@ -177,7 +211,7 @@ const resolveSourceMap = (data, url) => {
177
211
  return resolveSectionedSourceMap(data, url, sections);
178
212
  }
179
213
 
180
- if (!sources || !mappings) {
214
+ if (!Array.isArray(sources) || typeof mappings !== 'string') {
181
215
  return;
182
216
  }
183
217
 
@@ -191,21 +225,6 @@ const resolveSourceMap = (data, url) => {
191
225
 
192
226
  };
193
227
 
194
- const getInlineSourceMap = (comments) => {
195
- for (let i = comments.length - 1; i >= 0; i--) {
196
- const comment = comments[i];
197
- const match = convertSourceMap.commentRegex.exec(comment);
198
- if (!match || match.index !== 0) {
199
- continue;
200
- }
201
- try {
202
- return convertSourceMap.fromComment(comment);
203
- } catch (e) {
204
- // Ignore invalid inline maps.
205
- }
206
- }
207
- };
208
-
209
228
  const collectSourceMaps = async (v8list, options) => {
210
229
 
211
230
  const sourceList = [];
@@ -237,52 +256,60 @@ const collectSourceMaps = async (v8list, options) => {
237
256
  sourceMap
238
257
  };
239
258
 
259
+ // An explicitly supplied, usable map takes precedence over sourceMappingURL.
240
260
  // Keep the original source so V8 offsets remain valid.
241
- // check sourceMap only for js
242
- if (type === 'js' && !sourceData.sourceMap) {
243
- const comments = getSourceMapComments(source);
244
- // from inline sync
245
- const smc = getInlineSourceMap(comments);
246
- if (smc) {
247
- sourceData.sourceMap = resolveSourceMap(smc.sourcemap, url);
248
- sourcemapList.push({
249
- url,
250
- inline: true
251
- });
252
- sourceList.push(sourceData);
253
- continue;
254
- }
255
- // from url async
256
- const sourceMapUrl = getSourceMapUrl(comments, url);
257
- if (sourceMapUrl) {
261
+ if (type === 'js' && !isUsableSourceMap(sourceData.sourceMap)) {
262
+ delete sourceData.sourceMap;
263
+ const candidates = getSourceMapCandidates(source);
264
+ if (candidates.length) {
258
265
  concurrency.addItem({
259
- sourceMapUrl,
266
+ candidates,
260
267
  sourceData
261
268
  });
262
- continue;
263
269
  }
264
270
  }
265
271
 
266
- // no need check sourceMap
272
+ // Keep source entries in input order while maps load concurrently.
267
273
  sourceList.push(sourceData);
268
274
 
269
275
  }
270
276
 
271
- // from url concurrency
272
277
  await concurrency.start(async (item) => {
273
- const { sourceMapUrl, sourceData } = item;
278
+ const { candidates, sourceData } = item;
274
279
  const { url } = sourceData;
275
- const data = await loadSourceMap(sourceMapUrl, options);
276
- if (data) {
277
- sourceData.sourceMap = resolveSourceMap(data, url);
278
- sourcemapList.push({
279
- url,
280
- sourceMapUrl
281
- });
280
+ for (const candidate of candidates) {
281
+ let data;
282
+ let sourceMapUrl;
283
+ try {
284
+ if (candidate.inline) {
285
+ data = convertSourceMap.fromComment(candidate.comment).sourcemap;
286
+ } else {
287
+ sourceMapUrl = resolveMapUrl(candidate.filename, url);
288
+ if (!sourceMapUrl) {
289
+ continue;
290
+ }
291
+ data = await loadSourceMap(sourceMapUrl, options);
292
+ }
293
+ if (!isUsableSourceMap(data)) {
294
+ continue;
295
+ }
296
+ const sourceMap = resolveSourceMap(data, url);
297
+ if (!sourceMap) {
298
+ continue;
299
+ }
300
+ sourceData.sourceMap = sourceMap;
301
+ sourcemapList.push(candidate.inline ? {
302
+ url,
303
+ inline: true
304
+ } : {
305
+ url,
306
+ sourceMapUrl
307
+ });
308
+ break;
309
+ } catch (e) {
310
+ // Try an earlier sourceMappingURL if parsing or loading fails.
311
+ }
282
312
  }
283
-
284
- sourceList.push(sourceData);
285
-
286
313
  });
287
314
 
288
315
  return {
@@ -292,7 +319,7 @@ const collectSourceMaps = async (v8list, options) => {
292
319
 
293
320
  };
294
321
 
295
- module.exports = {
322
+ export {
296
323
  collectSourceMaps,
297
324
  resolveSourceMap
298
325
  };