monocart-coverage-reports 2.6.2 → 2.6.4

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)
@@ -258,16 +259,19 @@ const coverageOptions = {
258
259
 
259
260
  ## `mcr` CLI
260
261
  > The CLI will run the program as a [child process](https://nodejs.org/docs/latest/api/child_process.html) with `NODE_V8_COVERAGE=dir` until it exits gracefully, and generate the coverage report with the coverage data from the `dir`.
262
+
261
263
  - Installing globally
262
264
  ```sh
263
265
  npm i monocart-coverage-reports -g
264
- mcr node ./test/test-node-env.js -r v8,console-summary --lcov
266
+ mcr node ./test/specs/node.test.js -r v8,console-summary --lcov
265
267
  ```
268
+
266
269
  - Locally in your project
267
270
  ```sh
268
271
  npm i monocart-coverage-reports
269
- npx mcr node ./test/test-node-env.js -r v8,console-summary --lcov
272
+ npx mcr node ./test/specs/node.test.js -r v8,console-summary --lcov
270
273
  ```
274
+
271
275
  - CLI Options
272
276
  ```sh
273
277
  Usage: mcr [options] <command>
@@ -279,10 +283,11 @@ Arguments:
279
283
 
280
284
  Options:
281
285
  -V, --version output the version number
282
- -c, --config <path> custom config path
283
- -o, --outputDir <dir> output dir for reports
284
- -r, --reports <name[,name]> coverage reports to use
286
+ -c, --config <path> custom config file path
287
+ --logging <logging> off, error, info, debug
285
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
286
291
  -i, --inputDir <dir> input dir for merging raw files
287
292
  --entryFilter <pattern> entry url filter
288
293
  --sourceFilter <pattern> source path filter
@@ -290,19 +295,20 @@ Options:
290
295
  --inline inline html for v8 report
291
296
  --assetsPath <path> assets path if not inline
292
297
  --lcov generate lcov.info file
293
- --logging <logging> off, error, info, debug
294
298
  -h, --help display help for command
295
299
  ```
296
- - Loading default configuration file
297
- - `.mcrrc`
298
- - `mcr.config.json`
299
- - `mcr.config.mjs`
300
- - `mcr.config.cjs`
300
+
301
+ - Loading config file by priority:
302
+ - custom config file with `-c` or `--config`
301
303
  - `mcr.config.js`
302
- - Or loading custom configuration file with `-c` or `--config`
303
- ```sh
304
- mcr node ./test.js -c path-to/my-custom-config.js
305
- ```
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"`
310
+
311
+ - Working with `ts-node`, see [mcr-ts-node](https://github.com/cenfun/mcr-ts-node)
306
312
 
307
313
  ## Compare Reports
308
314
  | | Istanbul | V8 | V8 to Istanbul |
@@ -699,6 +705,28 @@ When `logging` is `debug`, the raw report data will be preserved in `[outputDir]
699
705
 
700
706
  ![](./assets/debug-sourcemap.png)
701
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
+
702
730
  ## Integration
703
731
 
704
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,93 +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 cleanDir = (dir) => {
21
- Util.rmSync(dir);
22
- };
23
-
24
- const resolveConfigPath = (customConfigPath) => {
25
-
26
- const configList = [
27
- '.mcrrc',
28
- 'mcr.config.json',
29
- 'mcr.config.mjs',
30
- 'mcr.config.cjs',
31
- 'mcr.config.js'
32
- ];
33
-
34
- if (customConfigPath) {
35
- configList.push(customConfigPath);
36
- }
37
-
38
- const existsList = configList.filter((p) => fs.existsSync(p));
39
- if (!existsList.length) {
40
- return;
41
- }
42
-
43
- // last one
44
- const configPath = existsList.pop();
45
- Util.logInfo(`Config: ${EC.cyan(configPath)}`);
46
-
47
- return configPath;
48
- };
49
-
50
- const resolveConfigOptions = async (configPath) => {
51
-
52
- // json format
53
- const ext = path.extname(configPath);
54
- if (ext === '.json' || configPath.slice(-2) === 'rc') {
55
- return JSON.parse(Util.readFileSync(configPath));
56
- }
57
-
58
- let configOptions;
59
- let err;
60
- try {
61
- configOptions = await import(pathToFileURL(configPath));
62
- } catch (ee) {
63
- err = ee;
64
- }
65
-
66
- if (!configOptions) {
67
- Util.logError(`ERROR: failed to import "${configPath}" ${err.message} `);
68
- return;
69
- }
70
-
71
- configOptions = configOptions.default || configOptions;
72
-
73
- return configOptions;
74
- };
75
-
76
- const initOptions = async (cliOptions) => {
77
-
78
- const configPath = resolveConfigPath(cliOptions.config);
79
- if (configPath) {
80
- const configOptions = await resolveConfigOptions(configPath);
81
- if (configOptions) {
82
- cliOptions = {
83
- ... configOptions,
84
- ... cliOptions
85
- };
86
- }
87
- }
88
-
89
- // report options
90
- const options = {
91
- ... defaultOptions
92
- };
93
- Object.keys(cliOptions).forEach((k) => {
94
- if (Util.hasOwn(options, k)) {
95
- options[k] = cliOptions[k];
96
- }
97
- });
98
-
99
- return options;
100
- };
101
-
102
19
  const resolveEntrySource = (entry, sourceMapCache = {}) => {
103
20
  let source;
104
21
  const filePath = fileURLToPath(entry.url);
@@ -107,7 +24,7 @@ const resolveEntrySource = (entry, sourceMapCache = {}) => {
107
24
  source = fs.readFileSync(filePath).toString('utf8');
108
25
  }
109
26
 
110
- // not for typescript
27
+ // not for typescript, but it could be jsx even it is `.js`
111
28
  if (source && !['.ts', '.tsx'].includes(extname)) {
112
29
  entry.source = source;
113
30
  return;
@@ -118,7 +35,6 @@ const resolveEntrySource = (entry, sourceMapCache = {}) => {
118
35
 
119
36
  // for fake source file (can not parse to AST)
120
37
  if (lineLengths) {
121
- // get runtime code with ts-node
122
38
  let fakeSource = '';
123
39
  sourcemapData.lineLengths.forEach((length) => {
124
40
  fakeSource += `${''.padEnd(length, '*')}\n`;
@@ -142,7 +58,7 @@ const resolveEntrySourceMap = (entry, sourceMapCache = {}) => {
142
58
  }
143
59
  };
144
60
 
145
- const collectCoverageData = (coverageList, entryFilterHandler, sourceMapCache = {}) => {
61
+ const collectCoverageData = (coverageList, entryFilter, sourceMapCache = {}) => {
146
62
 
147
63
  if (!Util.isList(coverageList)) {
148
64
  return;
@@ -150,7 +66,7 @@ const collectCoverageData = (coverageList, entryFilterHandler, sourceMapCache =
150
66
 
151
67
  // filter node internal files
152
68
  coverageList = coverageList.filter((entry) => entry.url && entry.url.startsWith('file:'));
153
- coverageList = coverageList.filter(entryFilterHandler);
69
+ coverageList = coverageList.filter(entryFilter);
154
70
 
155
71
  if (!Util.isList(coverageList)) {
156
72
  return;
@@ -164,57 +80,87 @@ const collectCoverageData = (coverageList, entryFilterHandler, sourceMapCache =
164
80
  return coverageList;
165
81
  };
166
82
 
167
- const executeCommand = async (command, cliOptions) => {
83
+ const addCoverageData = async (coverageReport, nodeV8CoverageDir) => {
168
84
 
169
- const options = await initOptions(cliOptions);
85
+ let added = false;
170
86
 
171
- Util.logInfo(`Execute: ${EC.cyan(command)}`);
172
- // console.log(options);
87
+ const files = fs.readdirSync(nodeV8CoverageDir);
88
+ if (!files.length) {
89
+ return added;
90
+ }
173
91
 
174
- const nodeV8CoverageDir = path.resolve(options.outputDir, '.v8-coverage');
175
- process.env.NODE_V8_COVERAGE = nodeV8CoverageDir;
92
+ // filter before adding
93
+ const entryFilter = coverageReport.getEntryFilter();
94
+ for (const filename of files) {
95
+ const filePath = path.resolve(nodeV8CoverageDir, filename);
96
+ const content = fs.readFileSync(filePath).toString('utf-8');
97
+ if (content) {
98
+ const json = JSON.parse(content);
99
+ const coverageList = json.result;
100
+ const sourceMapCache = json['source-map-cache'];
101
+ const coverageData = collectCoverageData(coverageList, entryFilter, sourceMapCache);
102
+ if (coverageData) {
103
+ await coverageReport.add(coverageData);
104
+ added = true;
105
+ }
106
+ }
107
+ }
176
108
 
177
- cleanDir(nodeV8CoverageDir);
109
+ return added;
110
+ };
178
111
 
179
- // console.log('nodeV8CoverageDir', nodeV8CoverageDir);
112
+ const executeCommand = async (command, cliOptions) => {
180
113
 
181
- const coverageReport = MCR(options);
114
+ Util.logInfo(`Execute: ${EC.cyan(command)}`);
115
+
116
+ // console.log(options);
117
+ const coverageReport = MCR(cliOptions);
118
+ await coverageReport.loadConfig(cliOptions.config);
182
119
  coverageReport.cleanCache();
183
120
 
184
- // on start hook
185
- const onStart = cliOptions.onStart;
121
+ const coverageOptions = coverageReport.options;
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;
126
+ // clean v8 cache before running
127
+ Util.rmSync(nodeV8CoverageDir);
128
+ // Util.logInfo(`V8 coverage dir: ${EC.cyan(nodeV8CoverageDir)}`);
129
+
130
+ // =========================================
131
+ // onStart hook
132
+ const onStart = coverageOptions.onStart;
186
133
  if (typeof onStart === 'function') {
187
134
  await onStart(coverageReport);
188
135
  }
136
+ // =========================================
189
137
 
190
- foregroundChild(command, {
138
+ const subprocess = foregroundChild(command, {
191
139
  shell: true
192
- }, async () => {
140
+ }, async (code, signal) => {
141
+
142
+ // generate coverage even it is failed. code != 0
193
143
 
194
144
  if (!fs.existsSync(nodeV8CoverageDir)) {
195
- EC.logRed(`Not found coverage data: ${Util.relativePath(nodeV8CoverageDir)}`);
145
+ EC.logRed(`Not found V8 coverage dir: ${Util.relativePath(nodeV8CoverageDir)}`);
196
146
  return 1;
197
147
  }
198
148
 
199
- const entryFilterHandler = coverageReport.getEntryFilter();
200
-
201
- const files = fs.readdirSync(nodeV8CoverageDir);
202
- for (const filename of files) {
203
- const content = fs.readFileSync(path.resolve(nodeV8CoverageDir, filename)).toString('utf-8');
204
- const json = JSON.parse(content);
205
- const coverageList = json.result;
206
- const sourceMapCache = json['source-map-cache'];
207
- const coverageData = await collectCoverageData(coverageList, entryFilterHandler, sourceMapCache);
208
- if (coverageData) {
209
- await coverageReport.add(coverageData);
210
- }
149
+ // =========================================
150
+ // onReady hook before adding coverage data.
151
+ // Sometimes, the child process has not yet finished writing the coverage data, and it needs to wait here.
152
+ const onReady = coverageOptions.onReady;
153
+ if (typeof onReady === 'function') {
154
+ await onReady(coverageReport, nodeV8CoverageDir, subprocess);
211
155
  }
156
+ // =========================================
212
157
 
158
+ await addCoverageData(coverageReport, nodeV8CoverageDir);
213
159
  await coverageReport.generate();
214
160
 
215
161
  // remove nodeV8CoverageDir
216
- if (options.logging !== 'debug') {
217
- cleanDir(nodeV8CoverageDir);
162
+ if (!Util.isDebug()) {
163
+ Util.rmSync(nodeV8CoverageDir);
218
164
  }
219
165
 
220
166
  return process.exitCode;
@@ -222,16 +168,23 @@ const executeCommand = async (command, cliOptions) => {
222
168
 
223
169
  };
224
170
 
171
+ process.on('uncaughtException', function(err) {
172
+ Util.logError(`ERROR: ${err.message || err}`);
173
+ });
174
+
225
175
  program
226
176
  .name('mcr')
227
177
  .description('CLI to generate coverage reports')
228
178
  .version(version)
229
179
  .argument('<command>', 'command to execute')
230
180
  .allowUnknownOption()
231
- .option('-c, --config <path>', 'custom config path')
232
- .option('-o, --outputDir <dir>', 'output dir for reports')
233
- .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
+
234
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')
235
188
  .option('-i, --inputDir <dir>', 'input dir for merging raw files')
236
189
 
237
190
  .option('--entryFilter <pattern>', 'entry url filter')
@@ -243,8 +196,6 @@ program
243
196
 
244
197
  .option('--lcov', 'generate lcov.info file')
245
198
 
246
- .option('--logging <logging>', 'off, error, info, debug')
247
-
248
199
  .action((_command, cliOptions) => {
249
200
  const command = program.args.join(' ');
250
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,
@@ -369,7 +369,8 @@ const addJsBytesCoverage = (state, range) => {
369
369
  // add bytes range
370
370
  const byte = {
371
371
  start: startOffset,
372
- end: endOffset,
372
+ // the end could be > source.length
373
+ end: Math.min(endOffset, state.maxContentLength),
373
374
  count
374
375
  };
375
376
  // for debug
@@ -982,6 +983,7 @@ const initOriginalList = (state, originalDecodedMap, options) => {
982
983
  }
983
984
 
984
985
  const locator = new Locator(sourceContent);
986
+ const maxContentLength = sourceContent.length;
985
987
 
986
988
  const decodedMappings = getOriginalDecodedMappings(originalDecodedMap, sourceIndex, locator);
987
989
 
@@ -997,6 +999,7 @@ const initOriginalList = (state, originalDecodedMap, options) => {
997
999
  source: sourceContent,
998
1000
  sourcePath,
999
1001
  locator,
1002
+ maxContentLength,
1000
1003
  decodedMappings,
1001
1004
  // coverage info
1002
1005
  bytes: [],
@@ -1411,6 +1414,7 @@ const convertV8List = (v8list, options) => {
1411
1414
 
1412
1415
  // source mapping
1413
1416
  const locator = new Locator(source);
1417
+ const maxContentLength = source.length;
1414
1418
 
1415
1419
  // ============================
1416
1420
  // move sourceMap
@@ -1453,6 +1457,7 @@ const convertV8List = (v8list, options) => {
1453
1457
  sourcePath,
1454
1458
  sourceMap,
1455
1459
  locator,
1460
+ maxContentLength,
1456
1461
  decodedMappings: [],
1457
1462
  // coverage info
1458
1463
  bytes: [],
@@ -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
@@ -21,20 +21,16 @@ const { getGroupedRows } = require('./utils/snapshot.js');
21
21
  // maybe v8 or to istanbul reports
22
22
  const generateV8ListReports = async (v8list, coverageData, fileSources, options) => {
23
23
 
24
- let istanbulCoverageResults;
24
+ let istanbulReportPath;
25
25
  // v8 to istanbul reports
26
26
  if (options.reportGroup.istanbul) {
27
- istanbulCoverageResults = await saveIstanbulReports(coverageData, fileSources, options);
27
+ const istanbulCoverageResults = await saveIstanbulReports(coverageData, fileSources, options);
28
+ istanbulReportPath = istanbulCoverageResults.reportPath;
28
29
  }
29
30
 
30
31
  // v8 reports and v8 coverage results
31
32
  // could be no v8 or v8-json, but requires v8 coverage results
32
- const v8CoverageResults = await saveV8Report(v8list, options);
33
-
34
- // fix reportPath
35
- if (!v8CoverageResults.reportPath && istanbulCoverageResults) {
36
- v8CoverageResults.reportPath = istanbulCoverageResults.reportPath;
37
- }
33
+ const v8CoverageResults = await saveV8Report(v8list, options, istanbulReportPath);
38
34
 
39
35
  return v8CoverageResults;
40
36
  };
@@ -292,12 +288,15 @@ const handleConsoleDetailsReport = (reportData, reportOptions, options) => {
292
288
  } = reportData;
293
289
 
294
290
  if (name) {
295
- EC.logCyan(name);
291
+ Util.logInfo(EC.cyan(name));
296
292
  }
297
293
 
298
294
  const metrics = Util.getMetrics(cdOptions.metrics, type);
299
295
  const rows = getDetailsRows(files, metrics, cdOptions);
300
296
  const summaryRow = getRowData('Summary', summary, metrics);
297
+ if (summaryRow.nameStatus) {
298
+ summaryRow.name = Util.getColorStrByStatus(summaryRow.name, summaryRow.nameStatus);
299
+ }
301
300
  // no rows if skipped all by skipPercent
302
301
  if (rows.length) {
303
302
  rows.push({
@@ -343,7 +342,7 @@ const handleConsoleSummaryReport = (reportData, reportOptions, options) => {
343
342
  } = reportData;
344
343
 
345
344
  if (name) {
346
- EC.logCyan(name);
345
+ Util.logInfo(EC.cyan(name));
347
346
  }
348
347
 
349
348
  const metrics = Util.getMetrics(csOptions.metrics, type);