monocart-coverage-reports 2.6.3 → 2.6.5

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 CHANGED
@@ -33,6 +33,7 @@
33
33
  - [Using `v8-to-istanbul`](#using-v8-to-istanbul)
34
34
  - [How Monocart Works](#how-monocart-works)
35
35
  * [Debug for Coverage and Sourcemap](#debug-for-coverage-and-sourcemap)
36
+ * [Common issues](#common-issues)
36
37
  * [Integration](#integration)
37
38
  - [Playwright](#playwright)
38
39
  - [Jest](#jest)
@@ -282,10 +283,11 @@ Arguments:
282
283
 
283
284
  Options:
284
285
  -V, --version output the version number
285
- -c, --config <path> custom config path
286
- -o, --outputDir <dir> output dir for reports
287
- -r, --reports <name[,name]> coverage reports to use
286
+ -c, --config <path> custom config file path
287
+ --logging <logging> off, error, info, debug
288
288
  -n, --name <name> report name for title
289
+ -r, --reports <name[,name]> coverage reports to use
290
+ -o, --outputDir <dir> output dir for reports
289
291
  -i, --inputDir <dir> input dir for merging raw files
290
292
  --entryFilter <pattern> entry url filter
291
293
  --sourceFilter <pattern> source path filter
@@ -293,24 +295,20 @@ Options:
293
295
  --inline inline html for v8 report
294
296
  --assetsPath <path> assets path if not inline
295
297
  --lcov generate lcov.info file
296
- --logging <logging> off, error, info, debug
297
298
  -h, --help display help for command
298
299
  ```
299
300
 
300
- - Loading default configuration file
301
- - `.mcrrc`
302
- - `mcr.config.json`
303
- - `mcr.config.mjs`
304
- - `mcr.config.cjs`
301
+ - Loading config file by priority:
302
+ - custom config file with `-c` or `--config`
305
303
  - `mcr.config.js`
306
- - `mcr.config.ts`
307
-
308
- - Or loading custom configuration file with `-c` or `--config`
309
- ```sh
310
- mcr node ./test.js -c path-to/my-custom-config.js
311
- ```
304
+ - `mcr.config.cjs`
305
+ - `mcr.config.mjs`
306
+ - `mcr.config.json` - json format
307
+ - `.mcrrc.js`
308
+ - `.mcrrc` - json format
309
+ - `mcr.config.ts` - it requires `NODE_OPTIONS="--loader ts-node/esm"`
312
310
 
313
- - Work with `ts-node`, see [mcr-ts-node](https://github.com/cenfun/mcr-ts-node)
311
+ - Working with `ts-node`, see [mcr-ts-node](https://github.com/cenfun/mcr-ts-node)
314
312
 
315
313
  ## Compare Reports
316
314
  | | Istanbul | V8 | V8 to Istanbul |
@@ -707,6 +705,28 @@ When `logging` is `debug`, the raw report data will be preserved in `[outputDir]
707
705
 
708
706
  ![](./assets/debug-sourcemap.png)
709
707
 
708
+ ## Common issues
709
+ - `Unparsable source`
710
+
711
+ It happens during the parsing of the source code into AST, if the source code is not in the standard ECMAScript. For example `ts`, `jsx` and so on. There is a option to fix it, which is to manually compile the source code for these files.
712
+ ```js
713
+ import * as fs from "fs";
714
+ import * as path from "path";
715
+ import { fileURLToPath } from "url";
716
+ import * as TsNode from 'ts-node';
717
+ const coverageOptions = {
718
+ onEntry: async (entry) => {
719
+ const filePath = fileURLToPath(entry.url)
720
+ const originalSource = fs.readFileSync(filePath).toString("utf-8");
721
+ const fileName = path.basename(filePath);
722
+ const tn = TsNode.create({});
723
+ const source = tn.compile(originalSource, fileName);
724
+ entry.fake = false;
725
+ entry.source = source;
726
+ }
727
+ }
728
+ ```
729
+
710
730
  ## Integration
711
731
 
712
732
  ### [Playwright](https://github.com/microsoft/playwright)
package/lib/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const EC = require('eight-colors');
6
- const { fileURLToPath, pathToFileURL } = require('url');
6
+ const { fileURLToPath } = require('url');
7
7
 
8
8
  const {
9
9
  foregroundChild,
@@ -12,86 +12,10 @@ const {
12
12
 
13
13
  const MCR = require('./index.js');
14
14
  const Util = require('./utils/util.js');
15
- const defaultOptions = require('./default/options.js');
16
15
  const { resolveSourceMap } = require('./converter/collect-source-maps.js');
17
16
 
18
17
  const version = require('../package.json').version;
19
18
 
20
- const resolveConfigPath = (customConfigPath) => {
21
-
22
- const configList = [
23
- '.mcrrc',
24
- 'mcr.config.json',
25
- 'mcr.config.mjs',
26
- 'mcr.config.cjs',
27
- 'mcr.config.js',
28
- 'mcr.config.ts'
29
- ];
30
-
31
- if (customConfigPath) {
32
- configList.push(customConfigPath);
33
- }
34
-
35
- const existsList = configList.filter((p) => fs.existsSync(p));
36
- if (!existsList.length) {
37
- return;
38
- }
39
-
40
- // last one
41
- const configPath = existsList.pop();
42
- Util.logInfo(`Config: ${EC.cyan(configPath)}`);
43
-
44
- return configPath;
45
- };
46
-
47
- const resolveConfigOptions = async (configPath) => {
48
-
49
- // json format
50
- const ext = path.extname(configPath);
51
- if (ext === '.json' || configPath.slice(-2) === 'rc') {
52
- return JSON.parse(Util.readFileSync(configPath));
53
- }
54
-
55
- let configOptions;
56
- let err;
57
- try {
58
- configOptions = await import(pathToFileURL(configPath));
59
- } catch (ee) {
60
- err = ee;
61
- }
62
-
63
- if (err || !configOptions) {
64
- Util.logError(`ERROR: failed to import "${configPath}": ${err && err.message} `);
65
- return;
66
- }
67
-
68
- configOptions = configOptions.default || configOptions;
69
-
70
- return configOptions;
71
- };
72
-
73
- const initOptions = async (cliOptions) => {
74
-
75
- const configPath = resolveConfigPath(cliOptions.config);
76
- if (configPath) {
77
- const configOptions = await resolveConfigOptions(configPath);
78
- if (configOptions) {
79
- cliOptions = {
80
- ... configOptions,
81
- ... cliOptions
82
- };
83
- }
84
- }
85
-
86
- // report options
87
- const options = {
88
- ... defaultOptions,
89
- ... cliOptions
90
- };
91
-
92
- return options;
93
- };
94
-
95
19
  const resolveEntrySource = (entry, sourceMapCache = {}) => {
96
20
  let source;
97
21
  const filePath = fileURLToPath(entry.url);
@@ -100,7 +24,7 @@ const resolveEntrySource = (entry, sourceMapCache = {}) => {
100
24
  source = fs.readFileSync(filePath).toString('utf8');
101
25
  }
102
26
 
103
- // not for typescript
27
+ // not for typescript, but it could be jsx even it is `.js`
104
28
  if (source && !['.ts', '.tsx'].includes(extname)) {
105
29
  entry.source = source;
106
30
  return;
@@ -111,7 +35,6 @@ const resolveEntrySource = (entry, sourceMapCache = {}) => {
111
35
 
112
36
  // for fake source file (can not parse to AST)
113
37
  if (lineLengths) {
114
- // get runtime code with ts-node
115
38
  let fakeSource = '';
116
39
  sourcemapData.lineLengths.forEach((length) => {
117
40
  fakeSource += `${''.padEnd(length, '*')}\n`;
@@ -188,25 +111,25 @@ const addCoverageData = async (coverageReport, nodeV8CoverageDir) => {
188
111
 
189
112
  const executeCommand = async (command, cliOptions) => {
190
113
 
191
- const options = await initOptions(cliOptions);
192
-
193
114
  Util.logInfo(`Execute: ${EC.cyan(command)}`);
115
+
194
116
  // console.log(options);
117
+ const coverageReport = MCR(cliOptions);
118
+ await coverageReport.loadConfig(cliOptions.config);
119
+ coverageReport.cleanCache();
195
120
 
196
- const nodeV8CoverageDir = path.resolve(options.outputDir, '.v8-coverage');
197
- process.env.NODE_V8_COVERAGE = nodeV8CoverageDir;
121
+ const coverageOptions = coverageReport.options;
198
122
 
123
+ // dir for node v8 coverage
124
+ const nodeV8CoverageDir = Util.relativePath(path.resolve(coverageOptions.outputDir, '.v8-coverage'));
125
+ process.env.NODE_V8_COVERAGE = nodeV8CoverageDir;
199
126
  // clean v8 cache before running
200
127
  Util.rmSync(nodeV8CoverageDir);
201
-
202
- // console.log('nodeV8CoverageDir', nodeV8CoverageDir);
203
-
204
- const coverageReport = MCR(options);
205
- coverageReport.cleanCache();
128
+ // Util.logInfo(`V8 coverage dir: ${EC.cyan(nodeV8CoverageDir)}`);
206
129
 
207
130
  // =========================================
208
131
  // onStart hook
209
- const onStart = options.onStart;
132
+ const onStart = coverageOptions.onStart;
210
133
  if (typeof onStart === 'function') {
211
134
  await onStart(coverageReport);
212
135
  }
@@ -214,17 +137,19 @@ const executeCommand = async (command, cliOptions) => {
214
137
 
215
138
  const subprocess = foregroundChild(command, {
216
139
  shell: true
217
- }, async () => {
140
+ }, async (code, signal) => {
141
+
142
+ // generate coverage even it is failed. code != 0
218
143
 
219
144
  if (!fs.existsSync(nodeV8CoverageDir)) {
220
- EC.logRed(`Not found coverage data: ${Util.relativePath(nodeV8CoverageDir)}`);
145
+ EC.logRed(`Not found V8 coverage dir: ${Util.relativePath(nodeV8CoverageDir)}`);
221
146
  return 1;
222
147
  }
223
148
 
224
149
  // =========================================
225
150
  // onReady hook before adding coverage data.
226
151
  // Sometimes, the child process has not yet finished writing the coverage data, and it needs to wait here.
227
- const onReady = options.onReady;
152
+ const onReady = coverageOptions.onReady;
228
153
  if (typeof onReady === 'function') {
229
154
  await onReady(coverageReport, nodeV8CoverageDir, subprocess);
230
155
  }
@@ -243,16 +168,23 @@ const executeCommand = async (command, cliOptions) => {
243
168
 
244
169
  };
245
170
 
171
+ process.on('uncaughtException', function(err) {
172
+ Util.logError(`ERROR: ${err.message || err}`);
173
+ });
174
+
246
175
  program
247
176
  .name('mcr')
248
177
  .description('CLI to generate coverage reports')
249
178
  .version(version)
250
179
  .argument('<command>', 'command to execute')
251
180
  .allowUnknownOption()
252
- .option('-c, --config <path>', 'custom config path')
253
- .option('-o, --outputDir <dir>', 'output dir for reports')
254
- .option('-r, --reports <name[,name]>', 'coverage reports to use')
181
+ .option('-c, --config <path>', 'custom config file path')
182
+ .option('--logging <logging>', 'off, error, info, debug')
183
+
255
184
  .option('-n, --name <name>', 'report name for title')
185
+ .option('-r, --reports <name[,name]>', 'coverage reports to use')
186
+
187
+ .option('-o, --outputDir <dir>', 'output dir for reports')
256
188
  .option('-i, --inputDir <dir>', 'input dir for merging raw files')
257
189
 
258
190
  .option('--entryFilter <pattern>', 'entry url filter')
@@ -264,8 +196,6 @@ program
264
196
 
265
197
  .option('--lcov', 'generate lcov.info file')
266
198
 
267
- .option('--logging <logging>', 'off, error, info, debug')
268
-
269
199
  .action((_command, cliOptions) => {
270
200
  const command = program.args.join(' ');
271
201
  executeCommand(command, cliOptions);
@@ -188,7 +188,9 @@ const getJsAstInfo = (item, coverageList) => {
188
188
  }
189
189
 
190
190
  // runtime code should be parsed successful
191
- Util.logInfo(`failed to parse source file: ${item.sourcePath} ${err.message}`);
191
+ Util.logInfo(`Unparsable source: ${item.sourcePath} ${err.message}`);
192
+
193
+ // it could be jsx even it is `.js`
192
194
 
193
195
  // https://github.com/acornjs/acorn/tree/master/acorn-loose
194
196
  // It is recommended to always try a parse with the regular acorn parser first,
@@ -97,6 +97,7 @@ const getBlockEndPosition = (originalLineText) => {
97
97
  // generatedPos: 17,
98
98
  // originalText: ' }}</slot>'
99
99
 
100
+ // end marks
100
101
  const list = ['>', '}', ')'];
101
102
  for (const s of list) {
102
103
  const endBlockIndex = originalLineText.lastIndexOf(s);
@@ -116,7 +117,11 @@ const getBlockEndPosition = (originalLineText) => {
116
117
 
117
118
  };
118
119
 
119
- const getComparedPosition = (gt, gp, ot, direction) => {
120
+ const getComparedPosition = (info, direction) => {
121
+
122
+ let gt = info.generatedText;
123
+ const gp = info.generatedPos;
124
+ let ot = info.originalText;
120
125
 
121
126
  // before trim
122
127
  const oLen = ot.length;
@@ -175,45 +180,54 @@ const getComparedPosition = (gt, gp, ot, direction) => {
175
180
 
176
181
  // ============================
177
182
  // right matched
183
+ // only for original first line text
178
184
 
185
+ const olt = info.originalLineText;
179
186
  const right = gt.slice(gp);
180
- if (ot.endsWith(right)) {
187
+ if (olt.endsWith(right)) {
181
188
  return {
182
- pos: ot.length - right.length
189
+ pos: olt.length - right.length
183
190
  };
184
191
  }
185
192
 
186
193
  };
187
194
 
195
+ const initOriginalLineText = (info) => {
196
+ const originalText = info.originalText;
197
+ // never cross line, using first line of original text
198
+ // trim end remove \r\n and \n
199
+ const originalLines = originalText.split(/\n/);
200
+ if (originalLines.length === 1) {
201
+ // already single line
202
+ info.originalLineText = originalText.trimEnd();
203
+ info.originalSingle = true;
204
+ return;
205
+ }
206
+ // multiple liens
207
+ info.originalLineText = originalLines[0].trimEnd();
208
+ };
209
+
188
210
  const getSimilarPosition = (info, direction) => {
189
211
 
190
- const {
191
- generatedText, generatedPos, originalText
192
- } = info;
212
+ initOriginalLineText(info);
193
213
 
194
- const textPos = getComparedPosition(generatedText, generatedPos, originalText, direction);
214
+ const textPos = getComparedPosition(info, direction);
195
215
  if (textPos) {
196
216
  return textPos;
197
217
  }
198
218
 
199
- // never cross line, using first line of original text
200
- // trim end remove \r\n and \n
201
- const originalLines = originalText.split(/\n/);
202
- if (originalLines.length === 1) {
203
- // already single line
204
- info.originalLineText = originalText;
219
+ if (info.originalSingle) {
205
220
  return;
206
221
  }
207
222
 
208
- const originalLineText = originalLines[0].trimEnd();
209
- info.originalLineText = originalLineText;
210
-
223
+ // multiple lines
224
+ const { generatedPos, originalLineText } = info;
211
225
  // generatedPos not in first line
212
226
  if (generatedPos > originalLineText.length) {
213
227
  return;
214
228
  }
215
229
 
216
- const linePos = getComparedPosition(generatedText, generatedPos, originalLineText, direction);
230
+ const linePos = getComparedPosition(info, direction);
217
231
  if (linePos) {
218
232
  return linePos;
219
233
  }
@@ -236,6 +250,7 @@ const getFixedPosition = (info, direction) => {
236
250
  return similarPos.pos;
237
251
  }
238
252
 
253
+
239
254
  // generatedText: never cross lines
240
255
  // originalText: could be multiple lines
241
256
  // originalLineText: single line originalText
@@ -266,7 +281,7 @@ const getOriginalEndOffset = (m, rangeInfo) => {
266
281
  return m.originalEndOffset;
267
282
  };
268
283
 
269
- const getOriginalText = (m1, m2, rangeInfo, direction) => {
284
+ const getOriginalText = (m1, m2, rangeInfo) => {
270
285
  const originalLocator = rangeInfo.originalState.locator;
271
286
 
272
287
  const o1 = m1.originalOffset;
@@ -383,15 +398,17 @@ const getFixedOriginalStart = (start, mappings, state, rangeInfo) => {
383
398
  }
384
399
 
385
400
  const direction = 'start';
386
- const { originalText, originalOffset } = getOriginalText(m1, m2, rangeInfo, direction);
401
+ const { originalText, originalOffset } = getOriginalText(m1, m2, rangeInfo);
387
402
  const { generatedText, generatedPos } = getGeneratedText(m1, m2, state, start);
388
403
 
389
- const originalPos = getFixedPosition({
404
+ const info = {
390
405
  generatedText,
391
406
  generatedPos,
392
407
  originalText,
393
408
  originalState
394
- }, direction);
409
+ };
410
+
411
+ const originalPos = getFixedPosition(info, direction);
395
412
 
396
413
  // if (start === 2660) {
397
414
  // console.log('=====================================================');
@@ -437,7 +454,8 @@ const getFixedOriginalEnd = (end, mappings, state, rangeInfo) => {
437
454
  const gt = state.locator.source[end];
438
455
  const oi = m2.originalOffset - 1;
439
456
  const ot = originalState.locator.source[oi];
440
- if (gt === ot) {
457
+ // it could be ; but it quite far away
458
+ if (gt === ot && [';', ','].includes(gt)) {
441
459
  return {
442
460
  originalEnd: oi
443
461
  };
@@ -456,24 +474,28 @@ const getFixedOriginalEnd = (end, mappings, state, rangeInfo) => {
456
474
  }
457
475
 
458
476
  const direction = 'end';
459
- const { originalText, originalOffset } = getOriginalText(m1, m2, rangeInfo, direction);
477
+ const { originalText, originalOffset } = getOriginalText(m1, m2, rangeInfo);
460
478
  const { generatedText, generatedPos } = getGeneratedText(m1, m2, state, end);
461
479
 
462
- const originalPos = getFixedPosition({
480
+ const info = {
481
+ offset: end,
463
482
  generatedText,
464
483
  generatedPos,
465
484
  originalText,
466
485
  originalState
467
- }, direction);
486
+ };
468
487
 
469
- // if (end === 2858) {
488
+ const originalPos = getFixedPosition(info, direction);
489
+
490
+ // if (end === 1304578) {
470
491
  // console.log('=====================================================');
471
492
  // console.log('fixed end', originalState.sourcePath);
472
493
  // console.log({
473
494
  // generatedText,
474
495
  // generatedPos,
475
496
  // originalText,
476
- // originalPos
497
+ // originalPos,
498
+ // originalLineText: info.originalLineText
477
499
  // });
478
500
  // }
479
501
 
@@ -3,11 +3,8 @@ module.exports = {
3
3
  // {string} logging levels: off, error, info, debug
4
4
  logging: 'info',
5
5
 
6
- // {string} output dir
7
- outputDir: './coverage-reports',
8
-
9
- // {string|string[]} input raw dir(s)
10
- inputDir: null,
6
+ // {string} Report name. Defaults to "Coverage Report".
7
+ name: 'Coverage Report',
11
8
 
12
9
  // {string} v8 or html for istanbul by default
13
10
  // {array} multiple reports with options
@@ -21,17 +18,11 @@ module.exports = {
21
18
  // ],
22
19
  reports: '',
23
20
 
24
- // {string} Report name. Defaults to "Coverage Report".
25
- name: 'Coverage Report',
26
-
27
- // (V8 only) {string} Output [sub dir/]filename. Defaults to "index.html"
28
- outputFile: 'index.html',
29
-
30
- // (V8 only) {boolean} Inline all scripts to the single HTML file. Defaults to false.
31
- inline: false,
21
+ // {string} output dir
22
+ outputDir: './coverage-reports',
32
23
 
33
- // (V8 only) {string} Assets path if not inline. Defaults to "./assets"
34
- assetsPath: './assets',
24
+ // {string|string[]} input raw dir(s)
25
+ inputDir: null,
35
26
 
36
27
  // (V8 only) {function} A filter function to execute for each element in the V8 list.
37
28
  // entryFilter: (entry) => {
@@ -46,6 +37,24 @@ module.exports = {
46
37
  // sourceFilter: (sourcePath) => sourcePath.search(/src\/.+/) !== -1,
47
38
  sourceFilter: null,
48
39
 
40
+ // {function} Source path handler.
41
+ // sourcePath: (filePath) => `wwwroot/${filePath}`,
42
+ sourcePath: null,
43
+
44
+ // (V8 only) {string} Output [sub dir/]filename. Defaults to "index.html"
45
+ outputFile: 'index.html',
46
+
47
+ // (V8 only) {boolean} Inline all scripts to the single HTML file. Defaults to false.
48
+ inline: false,
49
+
50
+ // (V8 only) {string} Assets path if not inline. Defaults to "./assets"
51
+ assetsPath: './assets',
52
+
53
+ // (Istanbul only) defaultSummarizer, sourceFinder
54
+
55
+ // {boolean} Generate lcov.info file, same as lcovonly report. Defaults to false.
56
+ lcov: false,
57
+
49
58
  // options for adding empty coverage for all files
50
59
  // all: {
51
60
  // dir: ['src'],
@@ -56,15 +65,6 @@ module.exports = {
56
65
  // (V8 only) {boolean} Enable/Disable ignoring uncovered codes with the special comments: v8 ignore next/next N/start/stop
57
66
  v8Ignore: true,
58
67
 
59
- // (Istanbul only) defaultSummarizer, sourceFinder
60
-
61
- // {boolean} Generate lcov.info file, same as lcovonly report. Defaults to false.
62
- lcov: false,
63
-
64
- // {function} Source path handler.
65
- // sourcePath: (filePath) => `wwwroot/${filePath}`,
66
- sourcePath: null,
67
-
68
68
  // {string|function} Specify the report path, especially when there are multiple reports. Defaults to outputDir/index.html.
69
69
  reportPath: null,
70
70
 
@@ -78,6 +78,10 @@ module.exports = {
78
78
  // {boolean} Indicates whether to clean previous cache in output dir before generating report. Defaults to false.
79
79
  cleanCache: false,
80
80
 
81
+ // {function} onEntry hook
82
+ // onEntry: async (entry) => {}
83
+ onEntry: null,
84
+
81
85
  // {function} onEnd hook
82
86
  // onEnd: async (reportData) => {}
83
87
  onEnd: null
package/lib/generate.js CHANGED
@@ -288,12 +288,15 @@ const handleConsoleDetailsReport = (reportData, reportOptions, options) => {
288
288
  } = reportData;
289
289
 
290
290
  if (name) {
291
- EC.logCyan(name);
291
+ Util.logInfo(EC.cyan(name));
292
292
  }
293
293
 
294
294
  const metrics = Util.getMetrics(cdOptions.metrics, type);
295
295
  const rows = getDetailsRows(files, metrics, cdOptions);
296
296
  const summaryRow = getRowData('Summary', summary, metrics);
297
+ if (summaryRow.nameStatus) {
298
+ summaryRow.name = Util.getColorStrByStatus(summaryRow.name, summaryRow.nameStatus);
299
+ }
297
300
  // no rows if skipped all by skipPercent
298
301
  if (rows.length) {
299
302
  rows.push({
@@ -339,7 +342,7 @@ const handleConsoleSummaryReport = (reportData, reportOptions, options) => {
339
342
  } = reportData;
340
343
 
341
344
  if (name) {
342
- EC.logCyan(name);
345
+ Util.logInfo(EC.cyan(name));
343
346
  }
344
347
 
345
348
  const metrics = Util.getMetrics(csOptions.metrics, type);