monocart-coverage-reports 2.5.9 → 2.6.1

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/lib/v8/v8.js CHANGED
@@ -1,630 +1,400 @@
1
- const path = require('path');
2
- const CG = require('console-grid');
3
- const EC = require('eight-colors');
4
- const Util = require('../utils/util.js');
5
- const { getV8Summary } = require('./v8-summary.js');
6
- const { dedupeFlatRanges } = require('../utils/dedupe.js');
7
- const { getSourcePath, getSourcePathHandler } = require('../utils/source-path.js');
8
- const { mergeScriptCovs, minimatch } = require('../packages/monocart-coverage-vendor.js');
9
- const { collectSourceMaps } = require('../converter/collect-source-maps.js');
10
- const version = require('../../package.json').version;
11
-
12
- const getWrapperSource = (offset, source) => {
13
- // NO \n because it break line number in mappings
14
- let startStr = '';
15
- if (offset >= 4) {
16
- // fill comments
17
- const spaces = ''.padEnd(offset - 4, '*');
18
- startStr = `/*${spaces}*/`;
19
- } else {
20
- // fill spaces (should no chance)
21
- startStr += ''.padEnd(offset, ' ');
22
- }
23
- return startStr + source;
24
- };
25
-
26
- const getEntryFilter = (options) => {
27
- const input = options.entryFilter;
28
-
29
- // for function handler
30
- if (typeof input === 'function') {
31
- return input;
32
- }
33
-
34
- // for single minimatch pattern
35
- if (input && typeof input === 'string') {
36
- return (entry) => {
37
- return minimatch(entry.url, input);
38
- };
39
- }
40
-
41
- // for patterns
42
- if (input && typeof input === 'object') {
43
- const patterns = Object.keys(input);
44
- return (entry) => {
45
- const url = entry.url;
46
- for (const pattern of patterns) {
47
- if (minimatch(url, pattern)) {
48
- return input[pattern];
49
- }
50
- }
51
- // false if not matched
52
- };
53
- }
54
-
55
- // default
56
- return () => true;
57
- };
58
-
59
- const initV8ListAndSourcemap = async (v8list, options) => {
60
-
61
- // entry filter
62
- const entryFilter = getEntryFilter(options);
63
-
64
- // filter no source or text
65
- // init type and css source from text
66
- // keep functions
67
- v8list = v8list.filter((item) => {
68
-
69
- // filter entry
70
- if (!entryFilter(item)) {
71
- // console.log('-', item.url);
72
- return false;
73
- }
74
- // console.log(item.url);
75
-
76
- if (typeof item.source === 'string' && item.functions) {
77
- item.type = 'js';
78
- return true;
79
- }
80
- if (typeof item.text === 'string' && item.ranges) {
81
- item.type = 'css';
82
- return true;
83
- }
84
- // allow empty coverage
85
- if (item.empty) {
86
- if (!['js', 'css'].includes(item.type)) {
87
- item.type = 'js';
88
- }
89
- return true;
90
- }
91
- // 404 css, text will be empty
92
- // Util.logError(`Invalid source: ${item.url}`);
93
- // console.log(item);
94
- });
95
-
96
-
97
- // do not change original v8list, to work for multiple APIs
98
-
99
- const sourcePathHandler = getSourcePathHandler(options);
100
-
101
- // init id, sourcePath
102
- v8list = v8list.map((item, i) => {
103
-
104
- // it could be a empty file
105
- let source = `${item.source || item.text || ''}`;
106
-
107
- // fix source with script offset
108
- let scriptOffset;
109
- const offset = Util.toNum(item.scriptOffset, true);
110
- if (offset > 0) {
111
- scriptOffset = offset;
112
- source = getWrapperSource(offset, source);
113
- }
114
-
115
- const data = {
116
- url: item.url,
117
- type: item.type,
118
- source,
119
- // script offset >= 0, for vm script
120
- scriptOffset,
121
- // Manually Resolve the Sourcemap
122
- sourceMap: item.sourceMap,
123
- // fake source with `lineLengths`
124
- fake: item.fake,
125
- // empty coverage
126
- empty: item.empty,
127
- // match source if using empty coverage
128
- distFile: item.distFile
129
- };
130
-
131
- // coverage
132
- if (data.type === 'js') {
133
- data.functions = item.functions;
134
- } else {
135
- data.ranges = item.ranges;
136
- }
137
-
138
- // resolve source path
139
- let sourcePath = getSourcePath(data.url, i + 1, data.type);
140
- if (sourcePathHandler) {
141
- const newSourcePath = sourcePathHandler(sourcePath, data.url);
142
- if (typeof newSourcePath === 'string' && newSourcePath) {
143
- sourcePath = newSourcePath;
144
- }
145
- }
146
- data.sourcePath = sourcePath;
147
-
148
- // calculate source id
149
- data.id = Util.calculateSha1(sourcePath + data.source);
150
-
151
- return data;
152
- });
153
-
154
- // collect sourcemap first
155
- const time_start = Date.now();
156
- const smList = await collectSourceMaps(v8list, options);
157
- if (smList.length) {
158
- // debug level time
159
- Util.logTime(`loaded sourcemaps: ${smList.length}`, time_start);
160
- // console.log(smList);
161
- }
162
-
163
- return v8list;
164
- };
165
-
166
- // ========================================================================================================
167
-
168
- // force to async
169
- const mergeCssRanges = (itemList) => {
170
- return new Promise((resolve) => {
171
-
172
- let concatRanges = [];
173
- itemList.forEach((item) => {
174
- concatRanges = concatRanges.concat(item.ranges);
175
- });
176
-
177
- // ranges: [ {start, end} ]
178
- const ranges = dedupeFlatRanges(concatRanges);
179
-
180
- resolve(ranges || []);
181
- });
182
- };
183
-
184
- const mergeJsFunctions = (itemList) => {
185
- return new Promise((resolve) => {
186
-
187
- const res = mergeScriptCovs(itemList);
188
- const functions = res && res.functions;
189
-
190
- resolve(functions || []);
191
- });
192
- };
193
-
194
- const mergeV8Coverage = async (dataList, sourceCache, options) => {
195
-
196
- let allList = [];
197
- dataList.forEach((d) => {
198
- allList = allList.concat(d.data);
199
- });
200
-
201
- // remove empty items
202
- const coverageList = allList.filter((it) => !it.empty);
203
-
204
- // connect all functions and ranges
205
- const itemMap = {};
206
- const mergeMap = {};
207
- coverageList.forEach((item) => {
208
- const { id } = item;
209
- const prev = itemMap[id];
210
- if (prev) {
211
- if (mergeMap[id]) {
212
- mergeMap[id].push(item);
213
- } else {
214
- mergeMap[id] = [prev, item];
215
- }
216
- } else {
217
- itemMap[id] = item;
218
- }
219
- });
220
-
221
- // merge functions and ranges
222
- const mergeIds = Object.keys(mergeMap);
223
- for (const id of mergeIds) {
224
- const itemList = mergeMap[id];
225
- const item = itemMap[id];
226
- if (item.type === 'js') {
227
- item.functions = await mergeJsFunctions(itemList);
228
- } else {
229
- item.ranges = await mergeCssRanges(itemList);
230
- }
231
- }
232
-
233
- // first time filter for empty, (not for sources)
234
- // empty and not in item map
235
- const emptyList = allList.filter((it) => it.empty).filter((it) => !itemMap[it.id]);
236
-
237
- // empty list + merged list
238
- const mergedList = emptyList.concat(Object.values(itemMap));
239
-
240
- // try to load coverage and source by id
241
- for (const item of mergedList) {
242
- const {
243
- id, type, empty
244
- } = item;
245
- const json = sourceCache.get(id);
246
- if (json) {
247
- item.source = json.source;
248
- item.sourceMap = json.sourceMap;
249
- } else {
250
- Util.logError(`Not found source data: ${Util.relativePath(item.sourcePath)}`);
251
- item.source = '';
252
- }
253
-
254
- // add empty coverage after merged
255
- if (empty) {
256
- if (type === 'js') {
257
- item.functions = [{
258
- functionName: '',
259
- ranges: [{
260
- startOffset: 0,
261
- endOffset: item.source.length,
262
- count: 0
263
- }]
264
- }];
265
- } else {
266
- // empty css
267
- item.ranges = [];
268
- }
269
- }
270
-
271
- }
272
-
273
- return mergedList;
274
- };
275
-
276
- // ============================================================
277
-
278
- const mergeSingleSubGroups = (item) => {
279
-
280
- if (!item.subs) {
281
- return;
282
- }
283
- if (item.subs.length === 1) {
284
- const sub = item.subs[0];
285
- if (!sub.subs) {
286
- return;
287
- }
288
- item.name = [item.name, sub.name].filter((it) => it).join('/');
289
- item.subs = sub.subs;
290
- mergeSingleSubGroups(item);
291
- return;
292
- }
293
-
294
- item.subs.forEach((sub) => {
295
- mergeSingleSubGroups(sub);
296
- });
297
-
298
- };
299
-
300
- const getRowData = (rowName, summary, metrics) => {
301
- const summaryRow = {};
302
- let lowest = {
303
- pct: 100,
304
- status: 'high'
305
- };
306
- metrics.map((k) => {
307
- const s = summary[k];
308
- const percent = s.pct;
309
- if (typeof percent !== 'number') {
310
- return;
311
- }
312
- summaryRow[k] = Util.getColorStrByStatus(Util.PSF(percent, 100, 2), s.status);
313
- if (percent < lowest.pct) {
314
- lowest = s;
315
- }
316
- });
317
- summaryRow.name = Util.getColorStrByStatus(rowName, lowest.status);
318
- return summaryRow;
319
- };
320
-
321
- const getUncoveredLines = (file) => {
322
- const lines = [];
323
-
324
- const dataLines = file.data.lines;
325
-
326
- let startLine;
327
- let endLine;
328
-
329
- const addLines = () => {
330
- if (!startLine) {
331
- return;
332
- }
333
- if (endLine) {
334
- // range
335
- const link = startLine.color === 'yellow' && endLine.color === 'yellow' ? EC.yellow('-') : EC.red('-');
336
- lines.push(EC[startLine.color](startLine.line) + link + EC[endLine.color](endLine.line));
337
- startLine = null;
338
- endLine = null;
339
- } else {
340
- // only start
341
- lines.push(EC[startLine.color](startLine.line));
342
- startLine = null;
343
- }
344
- };
345
-
346
- const setLines = (line, color) => {
347
- if (startLine) {
348
- endLine = {
349
- line,
350
- color
351
- };
352
- return;
353
- }
354
- startLine = {
355
- line,
356
- color
357
- };
358
- };
359
-
360
- Object.keys(dataLines).forEach((line) => {
361
- const count = dataLines[line];
362
- if (count === 0) {
363
- setLines(line, 'red');
364
- return;
365
- }
366
- // 0 < count < 1
367
- if (typeof count === 'string') {
368
- setLines(line, 'yellow');
369
- return;
370
- }
371
- // count >= 1
372
- addLines();
373
- });
374
- addLines();
375
-
376
- return lines.join(',');
377
- };
378
-
379
- const getGroupedRows = (files, metrics, cdOptions) => {
380
-
381
- const skipPercent = cdOptions.skipPercent;
382
- if (typeof skipPercent === 'number' && skipPercent > 0) {
383
- files = files.filter((file) => {
384
- const { summary } = file;
385
- for (const k of metrics) {
386
- const percent = summary[k].pct;
387
- if (typeof percent === 'number' && percent < skipPercent) {
388
- return true;
389
- }
390
- }
391
- return false;
392
- });
393
- }
394
-
395
- let groups = [];
396
-
397
- files.forEach((file) => {
398
- const { sourcePath, summary } = file;
399
- const pathList = sourcePath.split('/');
400
- const lastName = pathList.pop();
401
- let subs = groups;
402
- pathList.forEach((key) => {
403
- const item = subs.find((it) => it.name === key && it.subs);
404
- if (item) {
405
- subs = item.subs;
406
- return;
407
- }
408
- const sub = {
409
- name: key,
410
- subs: []
411
- };
412
- subs.push(sub);
413
- subs = sub.subs;
414
- });
415
- const fileRow = getRowData(lastName, summary, metrics);
416
- fileRow.uncoveredLines = getUncoveredLines(file);
417
- subs.push(fileRow);
418
- });
419
-
420
- const group = {
421
- subs: groups
422
- };
423
- mergeSingleSubGroups(group);
424
-
425
- if (group.name) {
426
- groups = [group];
427
- }
428
-
429
- return groups;
430
- };
431
-
432
- const handleConsoleDetailsReport = (reportData, reportOptions, options) => {
433
- const cdOptions = {
434
- maxCols: 50,
435
- skipPercent: 0,
436
- metrics: [],
437
- ... reportOptions
438
- };
439
-
440
- const {
441
- name, summary, files
442
- } = reportData;
443
-
444
- if (name) {
445
- EC.logCyan(name);
446
- }
447
-
448
- const metrics = Util.getMetrics('v8', cdOptions.metrics);
449
- const rows = getGroupedRows(files, metrics, cdOptions);
450
- const summaryRow = getRowData('Summary', summary, metrics);
451
- // no rows if skipped all by skipPercent
452
- if (rows.length) {
453
- rows.push({
454
- innerBorder: true
455
- });
456
- }
457
- rows.push(summaryRow);
458
-
459
- const columns = [{
460
- id: 'name',
461
- name: 'Name'
462
- }, ... metrics.map((m) => {
463
- return {
464
- id: m,
465
- name: Util.capitalizeFirstLetter(m),
466
- align: 'right'
467
- };
468
- }), {
469
- id: 'uncoveredLines',
470
- name: 'Uncovered Lines'
471
- }];
472
-
473
- CG({
474
- options: {
475
- nullPlaceholder: '',
476
- defaultMaxWidth: cdOptions.maxCols
477
- },
478
- columns,
479
- rows
480
- });
481
- };
482
-
483
- const handleCodecovReport = async (reportData, reportOptions, options) => {
484
- const codecovOptions = {
485
- outputFile: 'codecov.json',
486
- ... reportOptions
487
- };
488
-
489
- const jsonPath = path.resolve(options.outputDir, codecovOptions.outputFile);
490
-
491
- // https://docs.codecov.com/docs/codecov-custom-coverage-format
492
- const coverage = {};
493
- reportData.files.forEach((item) => {
494
- const { sourcePath, data } = item;
495
- coverage[sourcePath] = data.lines;
496
- });
497
- const codecovData = {
498
- coverage
499
- };
500
-
501
- await Util.writeFile(jsonPath, JSON.stringify(codecovData));
502
- return Util.relativePath(jsonPath);
503
- };
504
-
505
- const handleV8JsonReport = async (reportData, reportOptions, options) => {
506
- const v8JsonOptions = {
507
- outputFile: 'coverage-report.json',
508
- ... reportOptions
509
- };
510
-
511
- // console.log(mergedOptions);
512
- const jsonPath = path.resolve(options.outputDir, v8JsonOptions.outputFile);
513
- await Util.writeFile(jsonPath, JSON.stringify(reportData));
514
- return Util.relativePath(jsonPath);
515
- };
516
-
517
-
518
- const handleV8HtmlReport = async (reportData, reportOptions, options) => {
519
-
520
- // V8 only options, merged with root options
521
- const v8HtmlOptions = {
522
- outputFile: options.outputFile,
523
- inline: options.inline,
524
- assetsPath: options.assetsPath,
525
- metrics: options.metrics,
526
- ... reportOptions
527
- };
528
-
529
- // add metrics to data for UI
530
- reportData.metrics = v8HtmlOptions.metrics;
531
-
532
- const {
533
- outputFile, inline, assetsPath
534
- } = v8HtmlOptions;
535
-
536
- // resolve full report path
537
- const reportPath = path.resolve(options.outputDir, outputFile);
538
- const outputDir = path.dirname(reportPath);
539
- const htmlFile = path.basename(reportPath);
540
-
541
- // deps
542
- const jsFiles = ['monocart-code-viewer', 'monocart-formatter', 'turbogrid'].map((it) => {
543
- return Util.resolveNodeModule(`${it}/dist/${it}.js`);
544
- });
545
-
546
- // package v8 ui
547
- jsFiles.push(Util.resolvePackage('monocart-coverage-v8.js'));
548
-
549
- // console.log(jsFiles);
550
-
551
- const htmlOptions = {
552
- reportData,
553
- jsFiles,
554
- inline,
555
- assetsPath,
556
- outputDir,
557
- htmlFile,
558
-
559
- reportDataFile: 'coverage-data.js'
560
- };
561
-
562
- const htmlPath = await Util.saveHtmlReport(htmlOptions);
563
-
564
- return htmlPath;
565
- };
566
-
567
- const saveV8Report = async (v8list, options) => {
568
-
569
- const defaultWatermarks = {
570
- bytes: [50, 80],
571
- statements: [50, 80],
572
- branches: [50, 80],
573
- functions: [50, 80],
574
- lines: [50, 80]
575
- };
576
- const watermarks = Util.resolveWatermarks(defaultWatermarks, options.watermarks);
577
-
578
- const summary = getV8Summary(v8list, watermarks);
579
-
580
- const reportData = {
581
- name: options.name,
582
- version,
583
- watermarks,
584
- summary,
585
- files: v8list
586
- };
587
-
588
- // v8 reports
589
- const buildInV8Reports = {
590
- 'v8': handleV8HtmlReport,
591
- 'v8-json': handleV8JsonReport,
592
- 'codecov': handleCodecovReport,
593
- 'console-details': handleConsoleDetailsReport
594
- };
595
-
596
- const outputs = {};
597
- const v8Group = options.reportGroup.v8;
598
- const v8Reports = Object.keys(v8Group);
599
- for (const reportName of v8Reports) {
600
- const reportOptions = v8Group[reportName];
601
- const buildInHandler = buildInV8Reports[reportName];
602
- if (buildInHandler) {
603
- outputs[reportName] = await buildInHandler(reportData, reportOptions, options);
604
- } else {
605
- outputs[reportName] = await Util.runCustomReporter(reportName, reportData, reportOptions, options);
606
- }
607
- }
608
-
609
- // outputPath, should be html or json
610
- const reportPath = Util.resolveReportPath(options, () => {
611
- return outputs.v8 || outputs['v8-json'] || Object.values(outputs).filter((it) => it && typeof it === 'string').shift();
612
- });
613
-
614
- const coverageResults = {
615
- type: 'v8',
616
- reportPath,
617
- name: options.name,
618
- watermarks,
619
- summary,
620
- files: v8list
621
- };
622
-
623
- return coverageResults;
624
- };
625
-
626
- module.exports = {
627
- initV8ListAndSourcemap,
628
- mergeV8Coverage,
629
- saveV8Report
630
- };
1
+ const path = require('path');
2
+ const Util = require('../utils/util.js');
3
+ const { getV8Summary } = require('./v8-summary.js');
4
+ const { dedupeFlatRanges } = require('../utils/dedupe.js');
5
+ const { getSourcePath, getSourcePathHandler } = require('../utils/source-path.js');
6
+ const { mergeScriptCovs, minimatch } = require('../packages/monocart-coverage-vendor.js');
7
+ const { collectSourceMaps } = require('../converter/collect-source-maps.js');
8
+ const version = require('../../package.json').version;
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
+
24
+ const getEntryFilter = (options) => {
25
+ const input = options.entryFilter;
26
+
27
+ // for function handler
28
+ if (typeof input === 'function') {
29
+ return input;
30
+ }
31
+
32
+ // for single minimatch pattern
33
+ if (input && typeof input === 'string') {
34
+ return (entry) => {
35
+ return minimatch(entry.url, input);
36
+ };
37
+ }
38
+
39
+ // for patterns
40
+ if (input && typeof input === 'object') {
41
+ const patterns = Object.keys(input);
42
+ return (entry) => {
43
+ const url = entry.url;
44
+ for (const pattern of patterns) {
45
+ if (minimatch(url, pattern)) {
46
+ return input[pattern];
47
+ }
48
+ }
49
+ // false if not matched
50
+ };
51
+ }
52
+
53
+ // default
54
+ return () => true;
55
+ };
56
+
57
+ const initV8ListAndSourcemap = async (v8list, options) => {
58
+
59
+ // entry filter
60
+ const entryFilter = getEntryFilter(options);
61
+
62
+ // filter no source or text
63
+ // init type and css source from text
64
+ // keep functions
65
+ v8list = v8list.filter((item) => {
66
+
67
+ // filter entry
68
+ if (!entryFilter(item)) {
69
+ // console.log('-', item.url);
70
+ return false;
71
+ }
72
+ // console.log(item.url);
73
+
74
+ if (typeof item.source === 'string' && item.functions) {
75
+ item.type = 'js';
76
+ return true;
77
+ }
78
+ if (typeof item.text === 'string' && item.ranges) {
79
+ item.type = 'css';
80
+ return true;
81
+ }
82
+ // allow empty coverage
83
+ if (item.empty) {
84
+ if (!['js', 'css'].includes(item.type)) {
85
+ item.type = 'js';
86
+ }
87
+ return true;
88
+ }
89
+ // 404 css, text will be empty
90
+ // Util.logError(`Invalid source: ${item.url}`);
91
+ // console.log(item);
92
+ });
93
+
94
+
95
+ // do not change original v8list, to work for multiple APIs
96
+
97
+ const sourcePathHandler = getSourcePathHandler(options);
98
+
99
+ // init id, sourcePath
100
+ v8list = v8list.map((item, i) => {
101
+
102
+ // it could be a empty file
103
+ let source = `${item.source || item.text || ''}`;
104
+
105
+ // fix source with script offset
106
+ let scriptOffset;
107
+ const offset = Util.toNum(item.scriptOffset, true);
108
+ if (offset > 0) {
109
+ scriptOffset = offset;
110
+ source = getWrapperSource(offset, source);
111
+ }
112
+
113
+ const data = {
114
+ url: item.url,
115
+ type: item.type,
116
+ source,
117
+ // script offset >= 0, for vm script
118
+ scriptOffset,
119
+ // Manually Resolve the Sourcemap
120
+ sourceMap: item.sourceMap,
121
+ // fake source with `lineLengths`
122
+ fake: item.fake,
123
+ // empty coverage
124
+ empty: item.empty,
125
+ // match source if using empty coverage
126
+ distFile: item.distFile
127
+ };
128
+
129
+ // coverage
130
+ if (data.type === 'js') {
131
+ data.functions = item.functions;
132
+ } else {
133
+ data.ranges = item.ranges;
134
+ }
135
+
136
+ // resolve source path
137
+ let sourcePath = getSourcePath(data.url, i + 1, data.type);
138
+ if (sourcePathHandler) {
139
+ const newSourcePath = sourcePathHandler(sourcePath, data.url);
140
+ if (typeof newSourcePath === 'string' && newSourcePath) {
141
+ sourcePath = newSourcePath;
142
+ }
143
+ }
144
+ data.sourcePath = sourcePath;
145
+
146
+ // calculate source id
147
+ data.id = Util.calculateSha1(sourcePath + data.source);
148
+
149
+ return data;
150
+ });
151
+
152
+ // collect sourcemap first
153
+ const time_start = Date.now();
154
+ const smList = await collectSourceMaps(v8list, options);
155
+ if (smList.length) {
156
+ // debug level time
157
+ Util.logTime(`loaded sourcemaps: ${smList.length}`, time_start);
158
+ // console.log(smList);
159
+ }
160
+
161
+ return v8list;
162
+ };
163
+
164
+ // ========================================================================================================
165
+
166
+ // force to async
167
+ const mergeCssRanges = (itemList) => {
168
+ return new Promise((resolve) => {
169
+
170
+ let concatRanges = [];
171
+ itemList.forEach((item) => {
172
+ concatRanges = concatRanges.concat(item.ranges);
173
+ });
174
+
175
+ // ranges: [ {start, end} ]
176
+ const ranges = dedupeFlatRanges(concatRanges);
177
+
178
+ resolve(ranges || []);
179
+ });
180
+ };
181
+
182
+ const mergeJsFunctions = (itemList) => {
183
+ return new Promise((resolve) => {
184
+
185
+ const res = mergeScriptCovs(itemList);
186
+ const functions = res && res.functions;
187
+
188
+ resolve(functions || []);
189
+ });
190
+ };
191
+
192
+ const mergeV8Coverage = async (dataList, sourceCache, options) => {
193
+
194
+ let allList = [];
195
+ dataList.forEach((d) => {
196
+ allList = allList.concat(d.data);
197
+ });
198
+
199
+ // remove empty items
200
+ const coverageList = allList.filter((it) => !it.empty);
201
+
202
+ // connect all functions and ranges
203
+ const itemMap = {};
204
+ const mergeMap = {};
205
+ coverageList.forEach((item) => {
206
+ const { id } = item;
207
+ const prev = itemMap[id];
208
+ if (prev) {
209
+ if (mergeMap[id]) {
210
+ mergeMap[id].push(item);
211
+ } else {
212
+ mergeMap[id] = [prev, item];
213
+ }
214
+ } else {
215
+ itemMap[id] = item;
216
+ }
217
+ });
218
+
219
+ // merge functions and ranges
220
+ const mergeIds = Object.keys(mergeMap);
221
+ for (const id of mergeIds) {
222
+ const itemList = mergeMap[id];
223
+ const item = itemMap[id];
224
+ if (item.type === 'js') {
225
+ item.functions = await mergeJsFunctions(itemList);
226
+ } else {
227
+ item.ranges = await mergeCssRanges(itemList);
228
+ }
229
+ }
230
+
231
+ // first time filter for empty, (not for sources)
232
+ // empty and not in item map
233
+ const emptyList = allList.filter((it) => it.empty).filter((it) => !itemMap[it.id]);
234
+
235
+ // empty list + merged list
236
+ const mergedList = emptyList.concat(Object.values(itemMap));
237
+
238
+ // try to load coverage and source by id
239
+ for (const item of mergedList) {
240
+ const {
241
+ id, type, empty
242
+ } = item;
243
+ const json = sourceCache.get(id);
244
+ if (json) {
245
+ item.source = json.source;
246
+ item.sourceMap = json.sourceMap;
247
+ } else {
248
+ Util.logError(`Not found source data: ${Util.relativePath(item.sourcePath)}`);
249
+ item.source = '';
250
+ }
251
+
252
+ // add empty coverage after merged
253
+ if (empty) {
254
+ if (type === 'js') {
255
+ item.functions = [{
256
+ functionName: '',
257
+ ranges: [{
258
+ startOffset: 0,
259
+ endOffset: item.source.length,
260
+ count: 0
261
+ }]
262
+ }];
263
+ } else {
264
+ // empty css
265
+ item.ranges = [];
266
+ }
267
+ }
268
+
269
+ }
270
+
271
+ return mergedList;
272
+ };
273
+
274
+ // ============================================================
275
+
276
+ const handleV8JsonReport = async (reportData, reportOptions, options) => {
277
+ const v8JsonOptions = {
278
+ outputFile: 'coverage-report.json',
279
+ ... reportOptions
280
+ };
281
+
282
+ // console.log(mergedOptions);
283
+ const jsonPath = path.resolve(options.outputDir, v8JsonOptions.outputFile);
284
+ await Util.writeFile(jsonPath, JSON.stringify(reportData));
285
+ return Util.relativePath(jsonPath);
286
+ };
287
+
288
+
289
+ const handleV8HtmlReport = async (reportData, reportOptions, options) => {
290
+
291
+ // V8 only options, merged with root options
292
+ const v8HtmlOptions = {
293
+ outputFile: options.outputFile,
294
+ inline: options.inline,
295
+ assetsPath: options.assetsPath,
296
+ metrics: options.metrics,
297
+ ... reportOptions
298
+ };
299
+
300
+ // add metrics to data for UI
301
+ reportData.metrics = v8HtmlOptions.metrics;
302
+
303
+ const {
304
+ outputFile, inline, assetsPath
305
+ } = v8HtmlOptions;
306
+
307
+ // resolve full report path
308
+ const reportPath = path.resolve(options.outputDir, outputFile);
309
+ const outputDir = path.dirname(reportPath);
310
+ const htmlFile = path.basename(reportPath);
311
+
312
+ // deps
313
+ const jsFiles = ['monocart-code-viewer', 'monocart-formatter', 'turbogrid'].map((it) => {
314
+ return Util.resolveNodeModule(`${it}/dist/${it}.js`);
315
+ });
316
+
317
+ // package v8 ui
318
+ jsFiles.push(Util.resolvePackage('monocart-coverage-v8.js'));
319
+
320
+ // console.log(jsFiles);
321
+
322
+ const htmlOptions = {
323
+ reportData,
324
+ jsFiles,
325
+ inline,
326
+ assetsPath,
327
+ outputDir,
328
+ htmlFile,
329
+
330
+ reportDataFile: 'coverage-data.js'
331
+ };
332
+
333
+ const htmlPath = await Util.saveHtmlReport(htmlOptions);
334
+
335
+ return htmlPath;
336
+ };
337
+
338
+ const saveV8Report = async (v8list, options) => {
339
+
340
+ const defaultWatermarks = {
341
+ bytes: [50, 80],
342
+ statements: [50, 80],
343
+ branches: [50, 80],
344
+ functions: [50, 80],
345
+ lines: [50, 80]
346
+ };
347
+ const watermarks = Util.resolveWatermarks(defaultWatermarks, options.watermarks);
348
+
349
+ const summary = getV8Summary(v8list, watermarks);
350
+
351
+ const reportData = {
352
+ name: options.name,
353
+ version,
354
+ watermarks,
355
+ summary,
356
+ files: v8list
357
+ };
358
+
359
+ // v8 reports
360
+ const buildInV8Reports = {
361
+ 'v8': handleV8HtmlReport,
362
+ 'v8-json': handleV8JsonReport
363
+ };
364
+
365
+ const outputs = {};
366
+ const v8Group = options.reportGroup.v8;
367
+ const v8Reports = Object.keys(v8Group);
368
+ for (const reportName of v8Reports) {
369
+ const reportOptions = v8Group[reportName];
370
+ const buildInHandler = buildInV8Reports[reportName];
371
+ if (buildInHandler) {
372
+ outputs[reportName] = await buildInHandler(reportData, reportOptions, options);
373
+ } else {
374
+ outputs[reportName] = await Util.runCustomReporter(reportName, reportData, reportOptions, options);
375
+ }
376
+ }
377
+
378
+ // outputPath, should be html or json
379
+ const reportPath = Util.resolveReportPath(options, () => {
380
+ return outputs.v8 || outputs['v8-json'] || Object.values(outputs).filter((it) => it && typeof it === 'string').shift();
381
+ });
382
+
383
+ const coverageResults = {
384
+ type: 'v8',
385
+ reportPath,
386
+ name: options.name,
387
+ watermarks,
388
+ summary,
389
+ files: v8list
390
+ };
391
+
392
+ return coverageResults;
393
+ };
394
+
395
+ module.exports = {
396
+ initV8ListAndSourcemap,
397
+ mergeV8Coverage,
398
+ saveV8Report,
399
+ getEntryFilter
400
+ };