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,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();
@@ -345,8 +345,29 @@ const getCssAstInfo = (item, coverageList) => {
345
345
  endOffset: it.end
346
346
  };
347
347
  });
348
-
349
- const ast = parseCss(source);
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
- module.exports = {
382
+ export {
362
383
  getJsAstInfo,
363
384
  getCssAstInfo
364
385
  };
@@ -1,12 +1,12 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const EC = require('eight-colors');
4
- const { fileURLToPath, pathToFileURL } = require('url');
5
- const Concurrency = require('../platform/concurrency.js');
6
- const { convertSourceMap } = require('../packages/monocart-coverage-vendor.js');
7
- const { flattenSourceMaps } = require('./flatten-source-maps.js');
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
- const Util = require('../utils/util.js');
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 { sources, sourcesContent } = data;
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 sourcePath = sourceUrl.toString();
140
- // could be no `file:`
141
- if (sourcePath.startsWith('file:')) {
142
- sourcePath = fileURLToPath(sourcePath);
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
- sourcesContent[i] = '';
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
- type, url, id, source, sourceMap
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 (type === 'js' && !isUsableSourceMap(sourceData.sourceMap)) {
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
- const sourceMap = resolveSourceMap(data, url);
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
- module.exports = {
343
+ export {
323
344
  collectSourceMaps,
324
345
  resolveSourceMap
325
346
  };