monocart-coverage-reports 2.7.1 → 2.7.2

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
@@ -6,15 +6,14 @@
6
6
  ![](https://img.shields.io/github/license/cenfun/monocart-coverage-reports)
7
7
  ![](https://img.shields.io/github/actions/workflow/status/cenfun/monocart-coverage-reports/static.yml)
8
8
 
9
-
10
- > A code coverage tool to generate native [V8](https://v8.dev/blog/javascript-code-coverage) reports or [Istanbul](https://istanbul.js.org/) reports. As early as 2017, V8 started offering native support for coverage data. However, we have essentially only been able to see coverage reports from Istanbul. Nowadays, we can directly generate V8 coverage reports with `monocart-coverage-reports`.
9
+ > A code coverage tool to generate native [V8](https://v8.dev/blog/javascript-code-coverage) reports or [Istanbul](https://istanbul.js.org/) reports.
11
10
 
12
11
  * [Usage](#usage)
13
12
  * [Default Options](#default-options)
14
13
  * [Available Reports](#available-reports)
15
14
  * [Using `entryFilter` and `sourceFilter` to filter the results for V8 report](#using-entryfilter-and-sourcefilter-to-filter-the-results-for-v8-report)
16
15
  * [onEnd Hook](#onend-hook)
17
- * [`mcr` CLI](#mcr-cli)
16
+ * [Command Line](#command-line)
18
17
  * [Compare Reports](#compare-reports)
19
18
  * [Compare Workflows](#compare-workflows)
20
19
  * [Collecting Istanbul Coverage Data](#collecting-istanbul-coverage-data)
@@ -275,7 +274,7 @@ const coverageOptions = {
275
274
  }
276
275
  ```
277
276
 
278
- ## `mcr` CLI
277
+ ## Command Line
279
278
  > 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`.
280
279
 
281
280
  - Installing globally
@@ -324,10 +323,10 @@ Options:
324
323
  - `mcr.config.json` - json format
325
324
  - `.mcrrc.js`
326
325
  - `.mcrrc` - json format
327
- - `mcr.config.ts`
326
+ - `mcr.config.ts` (requires preloading the typescript module)
328
327
 
329
- - Working with `ts-node`, see [mcr-ts-node](https://github.com/cenfun/mcr-ts-node)
330
328
  - Working with `tsx`, see [mcr-tsx](https://github.com/cenfun/mcr-tsx)
329
+ - Working with `ts-node`, see [mcr-ts-node](https://github.com/cenfun/mcr-ts-node)
331
330
 
332
331
  ## Compare Reports
333
332
  | | Istanbul | V8 | V8 to Istanbul |
@@ -455,7 +454,7 @@ Possible solutions:
455
454
  > node [./test/test-node-koa.js](./test/test-node-koa.js)
456
455
 
457
456
  - [Child Process](https://nodejs.org/docs/latest/api/child_process.html) + NODE_V8_COVERAGE
458
- - see [`mcr` CLI](#mcr-cli)
457
+ - see [Command Line](#command-line)
459
458
 
460
459
  ## Collecting V8 Coverage Data with `CDPClient` API
461
460
  - Work with node debugger `--inspect=9229`
@@ -815,6 +814,7 @@ const coverageOptions = {
815
814
  - [cypress-monocart-coverage](https://github.com/cenfun/cypress-monocart-coverage) - A Cypress plugin for coverage reports
816
815
 
817
816
  ### [CodeceptJS](https://github.com/codeceptjs/CodeceptJS)
817
+ - CodeceptJS has integrated MCR from version [3.5.15](https://github.com/codeceptjs/CodeceptJS/releases/tag/3.5.15)
818
818
  - [codeceptjs-monocart-coverage](https://github.com/cenfun/codeceptjs-monocart-coverage) - A CodeceptJS plugin for coverage reports
819
819
 
820
820
  ### [WebdriverIO](https://github.com/webdriverio/webdriverio)
package/lib/cli.js CHANGED
@@ -3,7 +3,6 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const EC = require('eight-colors');
6
- const { fileURLToPath } = require('url');
7
6
 
8
7
  const {
9
8
  foregroundChild,
@@ -12,120 +11,104 @@ const {
12
11
 
13
12
  const MCR = require('./index.js');
14
13
  const Util = require('./utils/util.js');
15
- const { resolveSourceMap } = require('./converter/collect-source-maps.js');
16
14
 
17
15
  const version = require('../package.json').version;
18
16
 
19
- const resolveEntrySource = (entry, sourceMapCache = {}) => {
20
- let source;
21
- const filePath = fileURLToPath(entry.url);
22
- const extname = path.extname(filePath);
23
- if (fs.existsSync(filePath)) {
24
- source = fs.readFileSync(filePath).toString('utf8');
17
+ const getRegisterPath = (filename) => {
18
+ const rel = Util.relativePath(path.resolve(__dirname, 'register', filename));
19
+ if (rel.startsWith('.')) {
20
+ return rel;
25
21
  }
22
+ return `./${rel}`;
23
+ };
26
24
 
27
- // not for typescript, but it could be jsx even it is `.js`
28
- if (source && !['.ts', '.tsx'].includes(extname)) {
29
- entry.source = source;
30
- return;
25
+ const isFeatureSupported = (nv) => {
26
+ // module register added in Node.js: v20.6.0
27
+ const [major, minor] = nv.split('.').map((s) => parseInt(s));
28
+ if (major < 20) {
29
+ return false;
31
30
  }
32
-
33
- const sourcemapData = sourceMapCache[entry.url];
34
- const lineLengths = sourcemapData && sourcemapData.lineLengths;
35
-
36
- // for fake source file (can not parse to AST)
37
- if (lineLengths) {
38
- let fakeSource = '';
39
- sourcemapData.lineLengths.forEach((length) => {
40
- fakeSource += `${''.padEnd(length, '*')}\n`;
41
- });
42
- entry.fake = true;
43
- entry.source = fakeSource;
31
+ if (major > 20) {
32
+ return true;
44
33
  }
45
-
46
- // Note: no runtime code in source map cache
47
- // This is a problem for typescript
48
-
49
- };
50
-
51
- const resolveEntrySourceMap = (entry, sourceMapCache = {}) => {
52
- // sourcemap data
53
- const sourcemapData = sourceMapCache[entry.url];
54
- if (sourcemapData) {
55
- if (sourcemapData.data) {
56
- entry.sourceMap = resolveSourceMap(sourcemapData.data, entry.url);
57
- }
34
+ if (minor < 6) {
35
+ return false;
58
36
  }
37
+ return true;
59
38
  };
60
39
 
61
- const collectCoverageData = (coverageList, entryFilter, sourceMapCache = {}) => {
62
-
63
- if (!Util.isList(coverageList)) {
40
+ const initNodeOptions = async (cliOptions) => {
41
+ const nv = process.versions.node;
42
+ if (!isFeatureSupported(nv)) {
43
+ Util.logInfo(EC.yellow(`The current Node.js version "${nv}" does NOT support "module.register", it requires "20.6.0" or higher.`));
64
44
  return;
65
45
  }
66
46
 
67
- // filter node internal files
68
- coverageList = coverageList.filter((entry) => entry.url && entry.url.startsWith('file:'));
69
- coverageList = coverageList.filter(entryFilter);
70
-
71
- if (!Util.isList(coverageList)) {
72
- return;
47
+ const nodeOptions = [];
48
+ if (process.env.NODE_OPTIONS) {
49
+ nodeOptions.push(process.env.NODE_OPTIONS);
73
50
  }
74
51
 
75
- for (const entry of coverageList) {
76
- resolveEntrySource(entry, sourceMapCache);
77
- resolveEntrySourceMap(entry, sourceMapCache);
52
+ if (cliOptions.import) {
53
+ nodeOptions.push('--import');
54
+ nodeOptions.push(cliOptions.import);
55
+ await import(cliOptions.import);
56
+ } else if (cliOptions.require) {
57
+ nodeOptions.push('--require');
58
+ nodeOptions.push(cliOptions.require);
59
+ await import(cliOptions.require);
78
60
  }
79
61
 
80
- return coverageList;
81
- };
62
+ // console.log(nodeOptions);
82
63
 
83
- const addCoverageData = async (coverageReport, nodeV8CoverageDir) => {
64
+ // export source after
65
+ const hasImport = nodeOptions.find((it) => typeof it === 'string' && it.includes('--import'));
66
+ if (hasImport) {
67
+ const importPath = getRegisterPath('register.mjs');
68
+ const hasImportPath = nodeOptions.find((it) => typeof it === 'string' && it.includes(importPath));
69
+ if (!hasImportPath) {
70
+ nodeOptions.push('--import');
71
+ nodeOptions.push(importPath);
72
+ }
73
+ } else {
74
+ const requirePath = getRegisterPath('register.js');
75
+ const hasRequirePath = nodeOptions.find((it) => typeof it === 'string' && it.includes(requirePath));
76
+ if (!hasRequirePath) {
77
+ nodeOptions.push('--require');
78
+ nodeOptions.push(requirePath);
79
+ }
80
+ }
84
81
 
85
- let added = false;
82
+ // console.log(nodeOptions);
86
83
 
87
- const files = fs.readdirSync(nodeV8CoverageDir);
88
- if (!files.length) {
89
- return added;
90
- }
84
+ process.env.NODE_OPTIONS = nodeOptions.join(' ');
91
85
 
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
- }
86
+ };
108
87
 
109
- return added;
88
+ const initNodeV8CoverageDir = (coverageOptions) => {
89
+ // dir for node v8 coverage
90
+ const nodeV8CoverageDir = Util.relativePath(path.resolve(coverageOptions.outputDir, '.v8-coverage'));
91
+ process.env.NODE_V8_COVERAGE = nodeV8CoverageDir;
92
+ // clean v8 cache before running
93
+ Util.rmSync(nodeV8CoverageDir);
94
+ // Util.logInfo(`V8 coverage dir: ${EC.cyan(nodeV8CoverageDir)}`);
95
+
96
+ return nodeV8CoverageDir;
110
97
  };
111
98
 
112
99
  const executeCommand = async (command, cliOptions) => {
113
100
 
114
101
  Util.logInfo(`Execute: ${EC.cyan(command)}`);
115
102
 
103
+ await initNodeOptions(cliOptions);
104
+
116
105
  // console.log(options);
117
106
  const coverageReport = MCR(cliOptions);
118
107
  await coverageReport.loadConfig(cliOptions.config);
119
108
  coverageReport.cleanCache();
120
109
 
121
110
  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)}`);
111
+ const nodeV8CoverageDir = initNodeV8CoverageDir(coverageOptions);
129
112
 
130
113
  // =========================================
131
114
  // onStart hook
@@ -141,11 +124,6 @@ const executeCommand = async (command, cliOptions) => {
141
124
 
142
125
  // generate coverage even it is failed. code != 0
143
126
 
144
- if (!fs.existsSync(nodeV8CoverageDir)) {
145
- EC.logRed(`Not found V8 coverage dir: ${Util.relativePath(nodeV8CoverageDir)}`);
146
- return 1;
147
- }
148
-
149
127
  // =========================================
150
128
  // onReady hook before adding coverage data.
151
129
  // Sometimes, the child process has not yet finished writing the coverage data, and it needs to wait here.
@@ -155,7 +133,12 @@ const executeCommand = async (command, cliOptions) => {
155
133
  }
156
134
  // =========================================
157
135
 
158
- await addCoverageData(coverageReport, nodeV8CoverageDir);
136
+ if (!fs.existsSync(nodeV8CoverageDir)) {
137
+ EC.logRed(`Invalid V8 coverage dir: ${Util.relativePath(nodeV8CoverageDir)}`);
138
+ return 1;
139
+ }
140
+
141
+ await coverageReport.addFromDir(nodeV8CoverageDir);
159
142
  await coverageReport.generate();
160
143
 
161
144
  // remove nodeV8CoverageDir
@@ -196,6 +179,9 @@ program
196
179
 
197
180
  .option('--lcov', 'generate lcov.info file')
198
181
 
182
+ .option('--import <module>', 'preload module at startup')
183
+ .option('--require <module>', 'preload module at startup')
184
+
199
185
  .action((_command, cliOptions) => {
200
186
  const command = program.args.join(' ');
201
187
  executeCommand(command, cliOptions);
package/lib/generate.js CHANGED
@@ -2,7 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const CG = require('console-grid');
4
4
  const EC = require('eight-colors');
5
- const { pathToFileURL } = require('url');
5
+ const { pathToFileURL, fileURLToPath } = require('url');
6
6
 
7
7
  const Util = require('./utils/util.js');
8
8
  const {
@@ -12,6 +12,7 @@ const {
12
12
  const { mergeV8Coverage, saveV8Report } = require('./v8/v8.js');
13
13
 
14
14
  const { convertV8List } = require('./converter/converter.js');
15
+ const { resolveSourceMap } = require('./converter/collect-source-maps.js');
15
16
 
16
17
  const { minimatch } = require('./packages/monocart-coverage-vendor.js');
17
18
  const { getGroupedRows } = require('./utils/snapshot.js');
@@ -720,7 +721,135 @@ const getInputData = async (inputList, all, cacheDir, addHandler) => {
720
721
  };
721
722
  };
722
723
 
724
+ // ========================================================================================================
725
+
726
+ const resolveEntrySource = (entry, sourceMapCache, sourceCache) => {
727
+
728
+ const url = entry.url;
729
+
730
+ // source from `source-id.json`
731
+ const sourceData = sourceCache[url];
732
+ if (sourceData) {
733
+ entry.source = sourceData.source;
734
+ return;
735
+ }
736
+
737
+ // source for typescript file from source map cache
738
+ // Note: no runtime code but lineLengths
739
+ const tsExtensionsPattern = /\.([cm]?ts|[tj]sx)($|\?)/;
740
+ if (tsExtensionsPattern.test(url)) {
741
+ const sourcemapData = sourceMapCache[url];
742
+ const lineLengths = sourcemapData && sourcemapData.lineLengths;
743
+
744
+ // for fake source file (can not parse to AST)
745
+ if (lineLengths) {
746
+ let fakeSource = '';
747
+ sourcemapData.lineLengths.forEach((length) => {
748
+ fakeSource += `${''.padEnd(length, '*')}\n`;
749
+ });
750
+ entry.fake = true;
751
+ entry.source = fakeSource;
752
+ return;
753
+ }
754
+
755
+ }
756
+
757
+ // Note: it could be jsx format even extname is `.js`
758
+ const filePath = fileURLToPath(url);
759
+ if (fs.existsSync(filePath)) {
760
+ entry.source = fs.readFileSync(filePath).toString('utf8');
761
+ }
762
+
763
+ };
764
+
765
+ const resolveEntrySourceMap = (entry, sourceMapCache) => {
766
+ // sourcemap data
767
+ const sourcemapData = sourceMapCache[entry.url];
768
+ if (sourcemapData) {
769
+ if (sourcemapData.data) {
770
+ entry.sourceMap = resolveSourceMap(sourcemapData.data, entry.url);
771
+ }
772
+ }
773
+ };
774
+
775
+ const readCoverageData = (filePath, entryFilter, sourceCache) => {
776
+
777
+ const content = fs.readFileSync(filePath).toString('utf-8');
778
+ if (!content) {
779
+ return;
780
+ }
781
+
782
+ const json = JSON.parse(content);
783
+ let coverageData = json.result;
784
+ if (!Util.isList(coverageData)) {
785
+ return;
786
+ }
787
+
788
+ // filter node internal files, should no anonymous for nodejs
789
+ coverageData = coverageData.filter((entry) => entry.url && entry.url.startsWith('file:'));
790
+ coverageData = coverageData.filter(entryFilter);
791
+
792
+ if (!Util.isList(coverageData)) {
793
+ // Util.logDebug('No coverage data after filter');
794
+ return;
795
+ }
796
+
797
+ const sourceMapCache = json['source-map-cache'] || {};
798
+ for (const entry of coverageData) {
799
+ resolveEntrySource(entry, sourceMapCache, sourceCache);
800
+ resolveEntrySourceMap(entry, sourceMapCache);
801
+ }
802
+
803
+ return coverageData;
804
+ };
805
+
806
+ const readFromDir = async (dir, entryFilter, addHandler) => {
807
+
808
+ if (!dir || !fs.existsSync(dir)) {
809
+ Util.logError(`Invalid V8 coverage dir: ${dir}`);
810
+ return;
811
+ }
812
+
813
+ const files = fs.readdirSync(dir);
814
+ if (!files.length) {
815
+ Util.logInfo(`No coverage files in the dir: ${dir}`);
816
+ return;
817
+ }
818
+
819
+ const coverageFiles = [];
820
+ const sourceCache = {};
821
+ files.forEach((filename) => {
822
+ const isCoverage = filename.startsWith('coverage-');
823
+ if (isCoverage) {
824
+ coverageFiles.push(filename);
825
+ return;
826
+ }
827
+ const isSource = filename.startsWith('source-');
828
+ if (isSource) {
829
+ const filePath = path.resolve(dir, filename);
830
+ const content = fs.readFileSync(filePath).toString('utf-8');
831
+ if (content) {
832
+ const json = JSON.parse(content);
833
+ if (json.url) {
834
+ sourceCache[json.url] = json;
835
+ }
836
+ }
837
+ }
838
+ });
839
+
840
+ // filter before adding
841
+ for (const filename of coverageFiles) {
842
+ const filePath = path.resolve(dir, filename);
843
+ const coverageData = readCoverageData(filePath, entryFilter, sourceCache);
844
+ if (coverageData) {
845
+ await addHandler(coverageData);
846
+ }
847
+ }
848
+
849
+ };
850
+
723
851
  module.exports = {
724
852
  getInputData,
853
+ readFromDir,
725
854
  generateCoverageReports
726
855
  };
package/lib/index.d.ts CHANGED
@@ -347,6 +347,12 @@ declare namespace MCR {
347
347
  * @param coverageData {array} V8 format, {object} Istanbul format */
348
348
  add: (coverageData: any[] | any) => Promise<AddedResults>;
349
349
 
350
+ /**
351
+ * add V8 coverage from a dir
352
+ * @param dir node v8 coverage dir
353
+ */
354
+ addFromDir: (dir: string) => Promise<void>;
355
+
350
356
  /** generate report */
351
357
  generate: () => Promise<CoverageResults>;
352
358
 
package/lib/index.js CHANGED
@@ -7,7 +7,9 @@ const defaultOptions = require('./default/options.js');
7
7
  const Util = require('./utils/util.js');
8
8
 
9
9
  const { initV8ListAndSourcemap, getEntryFilter } = require('./v8/v8.js');
10
- const { getInputData, generateCoverageReports } = require('./generate.js');
10
+ const {
11
+ getInputData, readFromDir, generateCoverageReports
12
+ } = require('./generate.js');
11
13
  const { getSnapshot, diffSnapshot } = require('./utils/snapshot.js');
12
14
  const CDPClient = require('./client/cdp-client.js');
13
15
 
@@ -113,6 +115,14 @@ class CoverageReport {
113
115
  return results;
114
116
  }
115
117
 
118
+ // add coverage from dir
119
+ addFromDir(dir) {
120
+ const entryFilter = this.getEntryFilter();
121
+ return readFromDir(dir, entryFilter, (coverageData) => {
122
+ return this.add(coverageData);
123
+ });
124
+ }
125
+
116
126
  // generate report
117
127
  async generate() {
118
128