monocart-coverage-reports 2.7.8 → 2.7.10

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
@@ -28,6 +28,8 @@
28
28
  * [Command Line](#command-line)
29
29
  * [Config File](#config-file)
30
30
  * [Merge Coverage Reports](#merge-coverage-reports)
31
+ - [Automatic Merging](#automatic-merging)
32
+ - [Manual Merging](#manual-merging)
31
33
  * [Common issues](#common-issues)
32
34
  - [Unexpected coverage](#unexpected-coverage)
33
35
  - [Unparsable source](#unparsable-source)
@@ -56,6 +58,10 @@
56
58
 
57
59
  ## Usage
58
60
  > It's recommended to use [Node.js 20+](https://nodejs.org/).
61
+ - Install
62
+ ```sh
63
+ npm install monocart-coverage-reports
64
+ ```
59
65
  - API
60
66
  ```js
61
67
  const MCR = require('monocart-coverage-reports');
@@ -83,16 +89,8 @@ mcr node my-app.js -r v8,console-details
83
89
  For more information, see [Command Line](#command-line)
84
90
 
85
91
  ## Options
86
- - Default Options: [lib/default/options.js](./lib/default/options.js)
87
- - `reports` [Available Reports](#available-reports)
88
- - `entryFilter` and `sourceFilter` [Using `entryFilter` and `sourceFilter` to filter the results for V8 report](#using-entryfilter-and-sourcefilter-to-filter-the-results-for-v8-report)
89
- - `sourcePath` [Resolve `sourcePath` for the Source Files](#resolve-sourcepath-for-the-source-files)
90
- - `all` [Adding Empty Coverage for Untested Files](#adding-empty-coverage-for-untested-files)
91
- - `inputDir` [Merge Coverage Reports](#merge-coverage-reports)
92
- - `logging` [Debug for Coverage and Sourcemap](#debug-for-coverage-and-sourcemap)
93
- - `onEnd` [onEnd Hook](#onend-hook)
94
-
95
- - Declaration: [lib/index.d.ts](./lib/index.d.ts)
92
+ - Default options: [lib/default/options.js](./lib/default/options.js)
93
+ - Options declaration see `CoverageReportOptions` [lib/index.d.ts](./lib/index.d.ts)
96
94
  - [Config file](#config-file)
97
95
 
98
96
  ## Available Reports
@@ -255,9 +253,11 @@ const mcr = MCR(coverageOptions);
255
253
  ```js
256
254
  await Promise.all([
257
255
  page.coverage.startJSCoverage({
256
+ // reportAnonymousScripts: true,
258
257
  resetOnNavigation: false
259
258
  }),
260
259
  page.coverage.startCSSCoverage({
260
+ // Note, anonymous styles (without sourceURLs) are not supported, alternatively, you can use CDPClient
261
261
  resetOnNavigation: false
262
262
  })
263
263
  ]);
@@ -274,10 +274,12 @@ const coverageData = [... jsCoverage, ... cssCoverage];
274
274
  ```
275
275
  For more examples, see [./test/test-v8.js](./test/test-v8.js), and [anonymous](./test/test-anonymous.js), [css](./test/test-css.js)
276
276
 
277
+
277
278
  ### Collecting Raw V8 Coverage Data with Puppeteer
278
279
  ```js
279
280
  await Promise.all([
280
281
  page.coverage.startJSCoverage({
282
+ // reportAnonymousScripts: true,
281
283
  resetOnNavigation: false,
282
284
  // provide raw v8 coverage data
283
285
  includeRawScriptCoverage: true
@@ -322,6 +324,8 @@ Possible solutions:
322
324
  - Taking coverage data and adding it to the report.
323
325
  - Example:
324
326
  > node [./test/test-node-ins.js](./test/test-node-ins.js)
327
+ - vm Example (scriptOffset):
328
+ > node [./test/test-node-vm.js](./test/test-node-vm.js)
325
329
 
326
330
  - [CDP](https://chromedevtools.github.io/devtools-protocol/) API
327
331
  - Enabling [Node Debugging](https://nodejs.org/en/guides/debugging-getting-started/).
@@ -487,23 +491,42 @@ const coverageOptions = {
487
491
  sourceFilter: (sourcePath) => sourcePath.search(/src\//) !== -1
488
492
  };
489
493
  ```
490
- Or using `minimatch` pattern:
494
+ Or using [`minimatch`](https://github.com/isaacs/minimatch) pattern:
491
495
  ```js
492
496
  const coverageOptions = {
493
497
  entryFilter: "**/main.js",
494
498
  sourceFilter: "**/src/**"
495
499
  };
496
- // supports multiple patterns:
500
+ ```
501
+ Supports multiple patterns:
502
+ ```js
497
503
  const coverageOptions = {
498
504
  entryFilter: {
505
+ '**/node_modules/**': false,
499
506
  '**/vendor.js': false,
500
- '**/main.js': true
507
+ '**/src/**': true
501
508
  },
502
509
  sourceFilter: {
503
- '**/src/**': true
510
+ '**/node_modules/**': false,
511
+ '**/**': true
504
512
  }
505
513
  };
506
514
  ```
515
+ In fact, the `minimatch` patterns will be transformed to a function like:
516
+ ```js
517
+ const coverageOptions = {
518
+ // '**/node_modules/**': false,
519
+ // '**/vendor.js': false,
520
+ // '**/src/**': true
521
+ entryFilter: (entry) => {
522
+ if (minimatch(entry.url, '**/node_modules/**')) { return false; }
523
+ if (minimatch(entry.url, '**/vendor.js')) { return false; }
524
+ if (minimatch(entry.url, '**/src/**')) { return true; }
525
+ return false; // else unmatched
526
+ }
527
+ // Note, the order of the patterns will impact the results
528
+ };
529
+ ```
507
530
 
508
531
  ## Resolve `sourcePath` for the Source Files
509
532
  If the source file comes from the sourcemap, then its path is a virtual path. Using the `sourcePath` option to resolve a custom path.
@@ -544,7 +567,7 @@ const coverageOptions = {
544
567
  }
545
568
  };
546
569
  ```
547
- The filter also supports `minimatch` pattern:
570
+ The filter also supports [`minimatch`](https://github.com/isaacs/minimatch) pattern:
548
571
  ```js
549
572
  const coverageOptions = {
550
573
  all: {
@@ -684,7 +707,7 @@ Arguments:
684
707
  command command to execute
685
708
 
686
709
  Options:
687
- -V, --version output the version number
710
+ -v, --version output the current version
688
711
  -c, --config <path> custom config file path
689
712
  -l, --logging <logging> off, error, info, debug
690
713
  -n, --name <name> report name for title
@@ -728,13 +751,19 @@ Loading config file by priority:
728
751
 
729
752
  ## Merge Coverage Reports
730
753
  The following usage scenarios may require merging coverage reports:
731
- - When the code is executed in different environments, like Node.js Server Side and browser Client Side (Next.js for instance). Each environment may generate its own coverage report. Merging them can give a more comprehensive view of the test coverage. see example [nextjs-with-playwright](https://github.com/cenfun/nextjs-with-playwright) for automatic report merging.
754
+ - When the code is executed in different environments, like Node.js server side and browser client side (Next.js for instance). Each environment may generate its own coverage report. Merging them can give a more comprehensive view of the test coverage.
732
755
  - When the code is subjected to different kinds of testing. For example, unit tests with Jest might cover certain parts of the code, while end-to-end tests with Playwright might cover other parts. Merging these different coverage reports can provide a holistic view of what code has been tested.
733
- - When tests are run on different machines or different shards, each might produce its own coverage report. Merging these can give a complete picture of the test coverage across all machines or shards.
756
+ - When tests are run on different machines or containers, each might produce its own coverage report. Merging these can give a complete picture of the test coverage across all machines or shards.
734
757
 
758
+ ### Automatic Merging
759
+ - `MCR` will automatically merge all the added coverage data when executing `generate()`. And it supports adding coverage data asynchronously across processes, see [Multiprocessing Support](#multiprocessing-support)
760
+ - For Next.js, it can actually add coverage data including both server side and client side before executing `generate()`, see example [nextjs-with-playwright](https://github.com/cenfun/nextjs-with-playwright)
761
+ - Use Codecov, a popular online code coverage service, which supports automatic merging of reports. Please use report `codecov`, it will generate report file `codecov.json`. If multiple `codecov.json` files are generated, upload all these files, they will be automatically merged. see [Codecov](#codecov) and [merging reports](https://docs.codecov.com/docs/merging-reports)
762
+
763
+ ### Manual Merging
735
764
  If the reports cannot be merged automatically, then here is how to manually merge the reports.
736
765
  First, using the `raw` report to export the original coverage data to the specified directory.
737
- For example, we have `raw` coverage data from unit tests:
766
+ - For example, we have `raw` coverage data from unit tests, which is output to `./coverage-reports/unit/raw`
738
767
  ```js
739
768
  const coverageOptions = {
740
769
  name: 'My Unit Test Coverage Report',
@@ -750,8 +779,8 @@ const coverageOptions = {
750
779
  ]
751
780
  };
752
781
  ```
753
- We also have `raw` coverage data from e2e tests, which is output to `./coverage-reports/e2e/raw`.
754
- After all the tests are completed, generate a merged report with option `inputDir`:
782
+ - We also have `raw` coverage data from e2e tests, which is output to `./coverage-reports/e2e/raw`
783
+ - Create a script `merge-coverage.js` to generate a merged report with option `inputDir`. After all the tests are completed, running script `node path/to/merge-coverage.js`
755
784
  ```js
756
785
  // merge-coverage.js
757
786
  const fs = require('fs');
@@ -801,20 +830,32 @@ const coverageOptions = {
801
830
  };
802
831
  await new CoverageReport(coverageOptions).generate();
803
832
  ```
833
+ - All the command scripts are probably like following:
834
+ ```json
835
+ {
836
+ "scripts": {
837
+ "test:unit": "jest",
838
+ "test:e2e": "playwright test",
839
+ "merge-coverage": "node path/to/merge-coverage.js",
840
+ "test": "npm run test:unit && npm run test:e2e && npm run merge-coverage"
841
+ }
842
+ }
843
+ ```
804
844
 
805
845
  ## Common issues
806
846
  ### Unexpected coverage
807
- In most cases, it happens when the coverage of the generated code is converted to the coverage of the original code through a sourcemap. In other words, it's an issue with the sourcemap. Most of the time, we can solve this by setting `minify` to `false` in the build tools configuration. Let's take a look at an example:
847
+ In most cases, it happens when the coverage of the generated code is converted to the coverage of the original code through a sourcemap. In other words, it's an issue with the sourcemap. Most of the time, we can solve this by setting `minify` to `false` in the configuration of build tools. Let's take a look at an example:
808
848
  ```js
809
849
  const a = tf ? 'true' : 'false';
810
850
  ^ ^ ^
811
851
  m1 p m2
812
852
  ```
813
- `m1` and `m2` are two consecutive mappings, `p` is the position we looking for. However, we can only get the position of the `m1` or `m2` if we don't fix it to `p`. Especially the generated code is different from the original code, such as the code was minified, compressed or converted, it is difficult to find the exact position. You can try [Debug for Coverage and Sourcemap](#debug-for-coverage-and-sourcemap)
853
+ In the generated code, there is a position `p`, and we need to find out its corresponding position in the original code. Unfortunately, there is no matched mapping for the position `p`. Instead, it has two adjacent upstream and downstream mappings `m1` and `m2`, so, the original position of `p` that we are looking for, might not be able to be precisely located. Especially, the generated code is different from the original code, such as the code was minified, compressed or converted, it is difficult to find the exact original position without matched mapping.
854
+ - Further understanding of sourcemap, try [Debug for Coverage and Sourcemap](#debug-for-coverage-and-sourcemap)
814
855
 
815
856
  How `MCR` Works:
816
- - 1, Trying to fix the middle original position with string comparison and [`diff-sequences`](https://github.com/jestjs/jest/tree/main/packages/diff-sequences). However, for non-JS code, such as Vue template, JSX, etc., it might be hard to find a perfect solution.
817
- - 2, Finding all functions, statements and branches by parsing the source code [AST](https://github.com/acornjs/acorn). However, there's a small issue, which is the V8 cannot provide effective branch coverage information for `AssignmentPattern`.
857
+ - 1, Trying to fix the original position with string comparison and [`diff-sequences`](https://github.com/jestjs/jest/tree/main/packages/diff-sequences). However, for non-JS code, such as Vue template, JSX, etc., it might be hard to find a perfect solution.
858
+ - 2, Finding all functions, statements and branches by parsing the source code [AST](https://github.com/acornjs/acorn). (There is a small issue is the V8 cannot provide effective branch coverage information for `AssignmentPattern`)
818
859
 
819
860
 
820
861
  ### Unparsable source
package/lib/cli.js CHANGED
@@ -21,53 +21,78 @@ const getRegisterPath = (filename) => {
21
21
  return `./${rel}`;
22
22
  };
23
23
 
24
- const initNodeOptions = async (cliOptions) => {
25
-
26
- // module register added in Node.js: v20.6.0
27
- const nv = process.versions.node;
28
- if (Util.cmpVersion(nv, '20.6.0') < 0) {
29
- Util.logInfo(`The current Node.js version "${nv}" does NOT support "module.register", it requires "20.6.0" or higher.`);
30
- return;
31
- }
32
-
24
+ const getInitNodeOptions = async (cliOptions) => {
33
25
  const nodeOptions = [];
34
26
  if (process.env.NODE_OPTIONS) {
35
27
  nodeOptions.push(process.env.NODE_OPTIONS);
36
28
  }
37
29
 
38
30
  if (cliOptions.import) {
39
- nodeOptions.push('--import');
40
- nodeOptions.push(cliOptions.import);
31
+ nodeOptions.push(`--import ${cliOptions.import}`);
32
+ // for load mcr.config.ts
41
33
  await import(cliOptions.import);
42
34
  } else if (cliOptions.require) {
43
- nodeOptions.push('--require');
44
- nodeOptions.push(cliOptions.require);
35
+ nodeOptions.push(`--require ${cliOptions.require}`);
36
+ // for load mcr.config.ts
45
37
  await import(cliOptions.require);
46
38
  }
47
39
 
40
+ return nodeOptions;
41
+ };
42
+
43
+ const getPreloadType = (nodeOptions) => {
44
+ const hasImport = nodeOptions.find((it) => it.includes('--import'));
45
+ if (hasImport) {
46
+ return '--import';
47
+ }
48
+ return '--require';
49
+ };
50
+
51
+ const checkRegisterFeature = () => {
52
+ const nv = process.versions.node;
53
+
54
+ // "module.register" added in Node.js: v20.6.0
55
+ // if (Util.cmpVersion(nv, '20.6.0') >= 0) {
56
+ // return true;
57
+ // }
58
+ // but also added in: v18.19.0
59
+ const requiredNV = '18.19.0';
60
+ if (Util.cmpVersion(nv, requiredNV) < 0) {
61
+ Util.logInfo(`The current Node.js version "${nv}" does NOT support "module.register", it requires "${requiredNV}" or higher.`);
62
+ return false;
63
+ }
64
+
65
+ // could be < 20.6.0 but just ignore it, please using latest minor version
66
+
67
+ return true;
68
+ };
69
+
70
+ const initNodeOptions = async (cliOptions) => {
71
+
72
+ const supportRegister = checkRegisterFeature();
73
+ if (!supportRegister) {
74
+ return;
75
+ }
76
+
77
+ const nodeOptions = await getInitNodeOptions(cliOptions);
48
78
  // console.log(nodeOptions);
49
79
 
80
+ const preloadType = getPreloadType(nodeOptions);
81
+
50
82
  // export source after
51
- const hasImport = nodeOptions.find((it) => typeof it === 'string' && it.includes('--import'));
52
- if (hasImport) {
83
+ if (preloadType === '--import') {
53
84
  const importPath = getRegisterPath('register.mjs');
54
- const hasImportPath = nodeOptions.find((it) => typeof it === 'string' && it.includes(importPath));
55
- if (!hasImportPath) {
56
- nodeOptions.push('--import');
57
- nodeOptions.push(importPath);
58
- }
85
+ nodeOptions.push(`--import ${importPath}`);
59
86
  } else {
60
87
  const requirePath = getRegisterPath('register.js');
61
- const hasRequirePath = nodeOptions.find((it) => typeof it === 'string' && it.includes(requirePath));
62
- if (!hasRequirePath) {
63
- nodeOptions.push('--require');
64
- nodeOptions.push(requirePath);
65
- }
88
+ nodeOptions.push(`--require ${requirePath}`);
66
89
  }
67
90
 
68
91
  // console.log(nodeOptions);
92
+ const nodeOptionsStr = nodeOptions.join(' ');
93
+ Util.logDebug(`node options: ${EC.cyan(nodeOptionsStr)}`);
69
94
 
70
- process.env.NODE_OPTIONS = nodeOptions.join(' ');
95
+ process.env.NODE_OPTIONS = nodeOptionsStr;
71
96
 
72
97
  };
73
98
 
@@ -86,6 +111,7 @@ const executeCommand = async (command, cliOptions) => {
86
111
 
87
112
  Util.logInfo(`Execute: ${EC.cyan(command)}`);
88
113
 
114
+ // before load config
89
115
  await initNodeOptions(cliOptions);
90
116
 
91
117
  // console.log(options);
@@ -133,7 +159,8 @@ const executeCommand = async (command, cliOptions) => {
133
159
  };
134
160
 
135
161
  process.on('uncaughtException', function(err) {
136
- Util.logError(`Process uncaughtException: ${err.message || err}`);
162
+ Util.logError(`Process uncaughtException: ${err.message}`);
163
+ console.log(err.stack);
137
164
  });
138
165
 
139
166
  // the -- separator
@@ -154,7 +181,7 @@ process.argv.forEach((it) => {
154
181
  program
155
182
  .name('mcr')
156
183
  .description('CLI to generate coverage reports')
157
- .version(version)
184
+ .version(version, '-v, --version', 'output the current version')
158
185
  .argument('[command]', 'command to execute')
159
186
  .allowUnknownOption()
160
187
  .option('-c, --config <path>', 'custom config file path')
@@ -606,6 +606,12 @@ const getOriginalExclusiveEnd = (cache, state) => {
606
606
  const { end } = cache;
607
607
  const { decodedMappings } = state;
608
608
  const endMappings = findMapping(decodedMappings, end);
609
+ if (!endMappings) {
610
+ return {
611
+ error: true,
612
+ errors: ['not found end mappings']
613
+ };
614
+ }
609
615
  if (Array.isArray(endMappings)) {
610
616
  return getFixedOriginalEnd(end, endMappings, state, cache);
611
617
  }
@@ -853,6 +859,9 @@ const findOriginalRange = (start, end, state, originalMap) => {
853
859
  cache.originalStart = originalStart;
854
860
 
855
861
  const originalEndResult = getOriginalEndPosition(cache, state);
862
+ if (originalEndResult.error) {
863
+ return createMappingError(originalEndResult.errors);
864
+ }
856
865
  const { originalEnd } = originalEndResult;
857
866
 
858
867
  // range start > end