monocart-coverage-reports 2.2.1 → 2.3.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.
@@ -109,6 +109,19 @@ const Util = {
109
109
  return false;
110
110
  },
111
111
 
112
+ toList: function(data, separator) {
113
+ if (data instanceof Array) {
114
+ return data;
115
+ }
116
+ if (typeof data === 'string' && (typeof separator === 'string' || separator instanceof RegExp)) {
117
+ return data.split(separator).map((str) => str.trim()).filter((str) => str);
118
+ }
119
+ if (typeof data === 'undefined' || data === null) {
120
+ return [];
121
+ }
122
+ return [data];
123
+ },
124
+
112
125
  forEach: function(rootList, callback) {
113
126
  const isBreak = (res) => {
114
127
  return res === 'break' || res === false;
@@ -14,7 +14,7 @@ const filterRanges = (ranges) => {
14
14
  };
15
15
 
16
16
  // apply directly to css ranges
17
- const dedupeRanges = (ranges) => {
17
+ const dedupeFlatRanges = (ranges) => {
18
18
 
19
19
  ranges = filterRanges(ranges);
20
20
 
@@ -59,11 +59,10 @@ const dedupeRanges = (ranges) => {
59
59
  return ranges;
60
60
  };
61
61
 
62
- // apply to js count ranges
63
- const dedupeCountRanges = (ranges) => {
62
+ const mergeRangesWith = (ranges, comparer, handler) => {
64
63
 
65
- // count ranges format
66
- // { start: 0, end: 6, count: 0 }
64
+ // ranges format
65
+ // { start: 0, end: 6, ... }
67
66
 
68
67
  ranges = filterRanges(ranges);
69
68
 
@@ -77,9 +76,10 @@ const dedupeCountRanges = (ranges) => {
77
76
 
78
77
  // merge count for same range
79
78
  ranges.reduce((lastRange, range) => {
80
- if (range.start === lastRange.start && range.end === lastRange.end) {
79
+ if (comparer(lastRange, range)) {
81
80
  range.dedupe = true;
82
- lastRange.count += range.count;
81
+
82
+ handler(lastRange, range);
83
83
 
84
84
  hasDedupe = true;
85
85
 
@@ -98,8 +98,21 @@ const dedupeCountRanges = (ranges) => {
98
98
  return ranges;
99
99
  };
100
100
 
101
+ // apply to js count ranges
102
+ const dedupeCountRanges = (ranges) => {
103
+ const comparer = (lastRange, range) => {
104
+ return lastRange.start === range.start && lastRange.end === range.end;
105
+ };
106
+ const handler = (lastRange, range) => {
107
+ lastRange.count += range.count;
108
+ };
109
+ return mergeRangesWith(ranges, comparer, handler);
110
+ };
111
+
101
112
 
102
113
  module.exports = {
103
- dedupeRanges,
114
+ sortRanges,
115
+ dedupeFlatRanges,
116
+ mergeRangesWith,
104
117
  dedupeCountRanges
105
118
  };
@@ -39,6 +39,11 @@ const getSourcePath = (url, index = '', type = '') => {
39
39
  url = pathToFileURL(['anonymous', index ? `-${index}` : '', type ? `.${type}` : ''].join('')).toString();
40
40
  }
41
41
 
42
+ // url could be a absolute path
43
+ if (path.isAbsolute(url)) {
44
+ url = pathToFileURL(url).toString();
45
+ }
46
+
42
47
  if (url.startsWith('file:')) {
43
48
  const relPath = Util.relativePath(fileURLToPath(url));
44
49
  return filterPath(relPath);
package/lib/utils/util.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const { writeFile, readFile } = require('fs/promises');
3
3
  const path = require('path');
4
+ const { pathToFileURL } = require('url');
4
5
  const os = require('os');
5
6
  const crypto = require('crypto');
6
7
  const EC = require('eight-colors');
@@ -48,7 +49,7 @@ const Util = {
48
49
  return str;
49
50
  },
50
51
 
51
- calculateSha1(buffer) {
52
+ calculateSha1: (buffer) => {
52
53
  const hash = crypto.createHash('sha1');
53
54
  hash.update(buffer);
54
55
  return hash.digest('hex');
@@ -204,6 +205,34 @@ const Util = {
204
205
  return Util.relativePath(htmlPath);
205
206
  },
206
207
 
208
+ runCustomReporter: async (reportName, reportData, reportOptions, globalOptions) => {
209
+ let CustomReporter;
210
+ let err;
211
+ try {
212
+ CustomReporter = await import(reportName);
213
+ } catch (e) {
214
+ err = e;
215
+ try {
216
+ CustomReporter = await import(pathToFileURL(reportName));
217
+ } catch (ee) {
218
+ err = ee;
219
+ }
220
+ }
221
+
222
+ if (!CustomReporter) {
223
+ Util.logError(err.message);
224
+ return;
225
+ }
226
+
227
+ CustomReporter = CustomReporter.default || CustomReporter;
228
+
229
+ const reporter = new CustomReporter(reportOptions, globalOptions);
230
+
231
+ const results = await reporter.generate(reportData);
232
+
233
+ return results;
234
+ },
235
+
207
236
  getEOL: function(content) {
208
237
  if (!content) {
209
238
  return os.EOL;
package/lib/v8/v8.js CHANGED
@@ -1,12 +1,26 @@
1
1
  const path = require('path');
2
2
  const Util = require('../utils/util.js');
3
3
  const { getV8Summary } = require('./v8-summary.js');
4
- const { dedupeRanges } = require('../utils/dedupe.js');
4
+ const { dedupeFlatRanges } = require('../utils/dedupe.js');
5
5
  const { getSourcePath } = require('../utils/source-path.js');
6
6
  const { mergeScriptCovs } = require('../packages/monocart-coverage-vendor.js');
7
7
  const collectSourceMaps = require('../converter/collect-source-maps.js');
8
8
  const version = require('../../package.json').version;
9
9
 
10
+ const getWrapperSource = (offset, source) => {
11
+ // NO \n because it break line number in mappings
12
+ let startStr = '';
13
+ if (offset >= 4) {
14
+ // fill comments
15
+ const spaces = ''.padEnd(offset - 4, '*');
16
+ startStr = `/*${spaces}*/`;
17
+ } else {
18
+ // fill spaces (should no chance)
19
+ startStr += ''.padEnd(offset, ' ');
20
+ }
21
+ return startStr + source;
22
+ };
23
+
10
24
  const initV8ListAndSourcemap = async (v8list, options) => {
11
25
 
12
26
  // filter list first
@@ -18,12 +32,20 @@ const initV8ListAndSourcemap = async (v8list, options) => {
18
32
  // filter no source or text
19
33
  // init type and css source from text
20
34
  // keep functions
21
-
22
35
  v8list = v8list.filter((item) => {
23
36
  if (typeof item.source === 'string' && item.functions) {
37
+ item.type = 'js';
24
38
  return true;
25
39
  }
26
40
  if (typeof item.text === 'string' && item.ranges) {
41
+ item.type = 'css';
42
+ return true;
43
+ }
44
+ // allow empty coverage
45
+ if (item.empty) {
46
+ if (!['js', 'css'].includes(item.type)) {
47
+ item.type = 'js';
48
+ }
27
49
  return true;
28
50
  }
29
51
  // 404 css, text will be empty
@@ -39,30 +61,38 @@ const initV8ListAndSourcemap = async (v8list, options) => {
39
61
  // init id, sourcePath
40
62
  v8list = v8list.map((item, i) => {
41
63
 
64
+ let source = item.source || item.text;
65
+
66
+ // fix source with script offset
67
+ let scriptOffset;
68
+ const offset = Util.toNum(item.scriptOffset, true);
69
+ if (offset > 0) {
70
+ scriptOffset = offset;
71
+ source = getWrapperSource(offset, source);
72
+ }
73
+
42
74
  const data = {
43
- url: item.url
75
+ url: item.url,
76
+ type: item.type,
77
+ source,
78
+ // script offset >= 0, for vm script
79
+ scriptOffset,
80
+ // Manually Resolve the Sourcemap
81
+ sourceMap: item.sourceMap,
82
+ // empty coverage
83
+ empty: item.empty,
84
+ // match source if using empty coverage
85
+ distFile: item.distFile
44
86
  };
45
87
 
46
- if (item.functions) {
47
- data.type = 'js';
88
+ // coverage
89
+ if (data.type === 'js') {
48
90
  data.functions = item.functions;
49
- data.source = item.source;
50
- // could be existed
51
- data.sourceMap = item.sourceMap;
52
- } else if (item.ranges) {
53
- data.type = 'css';
91
+ } else {
54
92
  data.ranges = item.ranges;
55
- data.source = item.text;
56
- }
57
-
58
- const idList = [];
59
- // could be existed
60
- const distFile = item.distFile;
61
- if (distFile) {
62
- data.distFile = distFile;
63
- idList.push(distFile);
64
93
  }
65
94
 
95
+ // resolve source path
66
96
  let sourcePath = getSourcePath(data.url, i + 1, data.type);
67
97
  if (typeof sourcePathHandler === 'function') {
68
98
  const newSourcePath = sourcePathHandler(sourcePath, data.url);
@@ -70,13 +100,10 @@ const initV8ListAndSourcemap = async (v8list, options) => {
70
100
  sourcePath = newSourcePath;
71
101
  }
72
102
  }
73
-
74
103
  data.sourcePath = sourcePath;
75
104
 
76
- idList.push(sourcePath);
77
- idList.push(data.source);
78
-
79
- data.id = Util.calculateSha1(idList.join(''));
105
+ // calculate source id
106
+ data.id = Util.calculateSha1(sourcePath + data.source);
80
107
 
81
108
  return data;
82
109
  });
@@ -86,7 +113,7 @@ const initV8ListAndSourcemap = async (v8list, options) => {
86
113
  const count = await collectSourceMaps(v8list, options);
87
114
  if (count) {
88
115
  // debug level time
89
- Util.logTime(`loaded ${count} sourcemaps`, time_start);
116
+ Util.logTime(`loaded sourcemaps: ${count}`, time_start);
90
117
  }
91
118
 
92
119
  return v8list;
@@ -104,7 +131,7 @@ const mergeCssRanges = (itemList) => {
104
131
  });
105
132
 
106
133
  // ranges: [ {start, end} ]
107
- const ranges = dedupeRanges(concatRanges);
134
+ const ranges = dedupeFlatRanges(concatRanges);
108
135
 
109
136
  resolve(ranges || []);
110
137
  });
@@ -120,19 +147,20 @@ const mergeJsFunctions = (itemList) => {
120
147
  });
121
148
  };
122
149
 
123
- const mergeV8Coverage = async (dataList, options) => {
150
+ const mergeV8Coverage = async (dataList, sourceCache, options) => {
124
151
 
125
- let list = [];
152
+ let allList = [];
126
153
  dataList.forEach((d) => {
127
- list = list.concat(d.data);
154
+ allList = allList.concat(d.data);
128
155
  });
129
156
 
130
- // console.log('before merge', list.map((it) => it.url));
157
+ // remove empty items
158
+ const coverageList = allList.filter((it) => !it.empty);
131
159
 
132
160
  // connect all functions and ranges
133
161
  const itemMap = {};
134
162
  const mergeMap = {};
135
- list.forEach((item) => {
163
+ coverageList.forEach((item) => {
136
164
  const { id } = item;
137
165
  const prev = itemMap[id];
138
166
  if (prev) {
@@ -158,44 +186,99 @@ const mergeV8Coverage = async (dataList, options) => {
158
186
  }
159
187
  }
160
188
 
161
- list = Object.values(itemMap);
189
+ // first time filter for empty, (not for sources)
190
+ // empty and not in item map
191
+ const emptyList = allList.filter((it) => it.empty).filter((it) => !itemMap[it.id]);
192
+
193
+ // empty list + merged list
194
+ const mergedList = emptyList.concat(Object.values(itemMap));
162
195
 
163
196
  // try to load coverage and source by id
164
- for (const item of list) {
165
- const { id } = item;
166
- const sourcePath = Util.resolveCacheSourcePath(options.cacheDir, id);
167
- const content = await Util.readFile(sourcePath);
168
- if (content) {
169
- const json = JSON.parse(content);
197
+ for (const item of mergedList) {
198
+ const {
199
+ id, type, empty
200
+ } = item;
201
+ const json = sourceCache.get(id);
202
+ if (json) {
170
203
  item.source = json.source;
171
204
  item.sourceMap = json.sourceMap;
172
205
  } else {
173
- Util.logError(`failed to read source: ${item.url}`);
206
+ Util.logError(`Not found source data: ${Util.relativePath(item.sourcePath)}`);
174
207
  item.source = '';
175
208
  }
209
+
210
+ // add empty coverage after merged
211
+ if (empty) {
212
+ if (type === 'js') {
213
+ item.functions = [{
214
+ functionName: '',
215
+ ranges: [{
216
+ startOffset: 0,
217
+ endOffset: item.source.length,
218
+ count: 0
219
+ }]
220
+ }];
221
+ } else {
222
+ // empty css
223
+ item.ranges = [];
224
+ }
225
+ }
226
+
176
227
  }
177
228
 
178
- return list;
229
+ return mergedList;
179
230
  };
180
231
 
181
232
  // ============================================================
182
233
 
183
- const saveV8JsonReport = async (reportData, options, v8JsonOptions) => {
184
- const v8MergedOptions = {
234
+ const saveCodecovReport = async (reportData, reportOptions, options) => {
235
+ const codecovOptions = {
236
+ outputFile: 'codecov.json',
237
+ ... reportOptions
238
+ };
239
+
240
+ const jsonPath = path.resolve(options.outputDir, codecovOptions.outputFile);
241
+
242
+ // https://docs.codecov.com/docs/codecov-custom-coverage-format
243
+ const coverage = {};
244
+ reportData.files.forEach((item) => {
245
+ const { sourcePath, data } = item;
246
+ coverage[sourcePath] = data.lines;
247
+ });
248
+ const codecovData = {
249
+ coverage
250
+ };
251
+
252
+ await Util.writeFile(jsonPath, JSON.stringify(codecovData));
253
+ return Util.relativePath(jsonPath);
254
+ };
255
+
256
+ const saveV8JsonReport = async (reportData, reportOptions, options) => {
257
+ const v8JsonOptions = {
185
258
  outputFile: 'coverage-report.json',
186
- ... v8JsonOptions
259
+ ... reportOptions
187
260
  };
188
261
 
189
- // console.log(v8MergedOptions);
190
- const jsonPath = path.resolve(options.outputDir, v8MergedOptions.outputFile);
191
- // console.log(reportPath);
262
+ // console.log(mergedOptions);
263
+ const jsonPath = path.resolve(options.outputDir, v8JsonOptions.outputFile);
192
264
  await Util.writeFile(jsonPath, JSON.stringify(reportData));
193
-
194
265
  return Util.relativePath(jsonPath);
195
266
  };
196
267
 
197
268
 
198
- const saveV8HtmlReport = async (reportData, options, v8HtmlOptions) => {
269
+ const saveV8HtmlReport = async (reportData, reportOptions, options) => {
270
+
271
+ // V8 only options, merged with root options
272
+ const v8HtmlOptions = {
273
+ outputFile: options.outputFile,
274
+ inline: options.inline,
275
+ assetsPath: options.assetsPath,
276
+ metrics: options.metrics,
277
+ ... reportOptions
278
+ };
279
+
280
+ // add metrics to data for UI
281
+ reportData.metrics = v8HtmlOptions.metrics;
199
282
 
200
283
  const {
201
284
  outputFile, inline, assetsPath
@@ -252,38 +335,29 @@ const saveV8Report = async (v8list, options) => {
252
335
  files: v8list
253
336
  };
254
337
 
255
- let outputPath;
256
-
257
338
  // v8 reports
258
- const v8Map = options.reportGroup.v8;
259
-
260
- // v8 html
261
- const v8Options = v8Map.v8;
262
- if (v8Options) {
263
-
264
- // V8 only options, merged with root options
265
- const v8HtmlOptions = {
266
- outputFile: options.outputFile,
267
- inline: options.inline,
268
- assetsPath: options.assetsPath,
269
- metrics: options.metrics,
270
- ... v8Options
271
- };
272
-
273
- // add metrics to data
274
- reportData.metrics = v8HtmlOptions.metrics;
275
-
276
- outputPath = await saveV8HtmlReport(reportData, options, v8HtmlOptions);
277
- }
339
+ const buildInV8Reports = {
340
+ 'v8': saveV8HtmlReport,
341
+ 'v8-json': saveV8JsonReport,
342
+ 'codecov': saveCodecovReport
343
+ };
278
344
 
279
- // v8 json
280
- const v8JsonOptions = v8Map['v8-json'];
281
- if (v8JsonOptions) {
282
- outputPath = await saveV8JsonReport(reportData, options, v8JsonOptions);
345
+ const outputs = {};
346
+ const v8Group = options.reportGroup.v8;
347
+ const v8Reports = Object.keys(v8Group);
348
+ for (const reportName of v8Reports) {
349
+ const reportOptions = v8Group[reportName];
350
+ const buildInHandler = buildInV8Reports[reportName];
351
+ if (buildInHandler) {
352
+ outputs[reportName] = await buildInHandler(reportData, reportOptions, options);
353
+ } else {
354
+ outputs[reportName] = await Util.runCustomReporter(reportName, reportData, reportOptions, options);
355
+ }
283
356
  }
284
357
 
358
+ // outputPath, should be html or json
285
359
  const reportPath = Util.resolveReportPath(options, () => {
286
- return outputPath;
360
+ return outputs.v8 || outputs['v8-json'] || Object.values(outputs).filter((it) => it && typeof it === 'string').shift();
287
361
  });
288
362
 
289
363
  const coverageResults = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "monocart-coverage-reports",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "description": "Monocart coverage reports",
5
5
  "main": "./lib/index.js",
6
6
  "bin": {
@@ -20,16 +20,18 @@
20
20
  "build-docs": "node ./scripts/build-docs.js",
21
21
  "build-test": "node ./scripts/build-test.js",
22
22
  "build": "sf lint && sf b -p && npm run build-test",
23
- "test-node": "npm run test-node-env && npm run test-node-api && npm run test-node-ins && npm run test-node-cdp && npm run test-node-koa",
23
+ "test-node": "npm run test-node-env && npm run test-node-api && npm run test-node-ins && npm run test-node-cdp && npm run test-node-koa && npm run test-vm",
24
24
  "test-node-env": "cross-env NODE_V8_COVERAGE=.temp/v8-coverage-env node ./test/test-node-env.js && node ./test/generate-node-report.js",
25
25
  "test-node-api": "cross-env NODE_V8_COVERAGE=.temp/v8-coverage-api node ./test/test-node-api.js",
26
26
  "test-node-fgc": "node ./test/test-node-fgc.js",
27
27
  "test-node-ins": "node ./test/test-node-ins.js",
28
28
  "test-node-cdp": "node --inspect=9229 ./test/test-node-cdp.js",
29
29
  "test-node-koa": "node ./test/test-node-koa.js",
30
+ "test-vm": "node ./test/test-vm.js",
30
31
  "test-browser": "node ./test/test.js",
31
- "test-cli": "npx mcr \"node ./test/test-node-env.js\" -o docs/cli -r v8,console-summary --lcov -c test/cli-options.js",
32
- "test": "npm run test-browser && npm run test-node && npm run test-cli && npm run build-docs",
32
+ "test-cli": "npx mcr \"node ./test/test-node-env.js\" -o docs/cli -c test/cli-options.js",
33
+ "test-merge": "node ./test/test-merge.js",
34
+ "test": "npm run test-browser && npm run test-node && npm run test-cli && npm run test-merge && npm run build-docs",
33
35
  "dev": "sf d v8",
34
36
  "open": "node ./scripts/open.js",
35
37
  "patch": "npm run build && sf publish patch -r"
@@ -1,9 +0,0 @@
1
- const { parentPort } = require('worker_threads');
2
- const { decode } = require('../packages/monocart-coverage-vendor.js');
3
-
4
- parentPort.on('message', (message) => {
5
- const result = decode(message);
6
- parentPort.postMessage(result);
7
- });
8
-
9
- parentPort.postMessage('workerReady');
@@ -1,35 +0,0 @@
1
- const path = require('path');
2
- const { Worker } = require('worker_threads');
3
- const Util = require('./util.js');
4
-
5
- // do NOT use isMainThread because it could be executed in worker too. (worker in worker)
6
-
7
- const decodeMappings = (mappings = '') => {
8
- if (typeof text !== 'string') {
9
- mappings = String(mappings);
10
- }
11
-
12
- return new Promise((resolve) => {
13
-
14
- const workerPath = path.resolve(__dirname, 'decode-mappings-worker.js');
15
- const worker = new Worker(workerPath);
16
-
17
- worker.on('message', (message) => {
18
- if (message === 'workerReady') {
19
- worker.postMessage(mappings);
20
- return;
21
- }
22
- resolve(message);
23
- worker.terminate();
24
- });
25
-
26
- worker.on('error', (e) => {
27
- Util.logError(e.message);
28
- resolve([]);
29
- worker.terminate();
30
- });
31
-
32
- });
33
- };
34
-
35
- module.exports = decodeMappings;