monocart-coverage-reports 2.13.2 → 3.1.0

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.
Files changed (69) hide show
  1. package/README.md +314 -116
  2. package/README.zh-Hans.md +315 -117
  3. package/lib/assets.js +6 -6
  4. package/lib/cli.js +191 -95
  5. package/lib/client/cdp-client.js +74 -43
  6. package/lib/client/coverage-client.js +24 -8
  7. package/lib/client/ws-session.js +115 -37
  8. package/lib/converter/ast-visitor.js +5 -7
  9. package/lib/converter/ast.js +29 -8
  10. package/lib/converter/collect-source-maps.js +42 -21
  11. package/lib/converter/converter.js +124 -129
  12. package/lib/converter/css-coverage.js +76 -0
  13. package/lib/converter/fake-source-coverage.js +64 -0
  14. package/lib/converter/find-original-range.js +214 -6
  15. package/lib/converter/flatten-source-maps.js +2 -4
  16. package/lib/converter/ignore.js +2 -4
  17. package/lib/converter/info-branch.js +5 -3
  18. package/lib/converter/info-function.js +5 -3
  19. package/lib/converter/info-statement.js +5 -3
  20. package/lib/converter/range-index.js +78 -0
  21. package/lib/converter/source-range-merge.js +103 -0
  22. package/lib/converter/untested.js +30 -60
  23. package/lib/default/options.js +13 -1
  24. package/lib/generate.js +44 -26
  25. package/lib/index.d.ts +52 -4
  26. package/lib/index.js +35 -41
  27. package/lib/istanbul/istanbul-summary.js +2 -2
  28. package/lib/istanbul/istanbul.js +35 -13
  29. package/lib/packages/monocart-coverage-assets.js +4 -3
  30. package/lib/packages/monocart-coverage-converter.js +27 -0
  31. package/lib/packages/monocart-coverage-vendor.js +2 -41
  32. package/lib/packages/monocart-coverage-vendor.js.LICENSE +3 -221
  33. package/lib/platform/concurrency.js +47 -43
  34. package/lib/platform/share.js +8 -1
  35. package/lib/register/hooks.js +19 -13
  36. package/lib/register/register.js +5 -3
  37. package/lib/reports/codacy.js +3 -5
  38. package/lib/reports/codecov.js +3 -5
  39. package/lib/reports/console-details.js +5 -5
  40. package/lib/reports/console-summary.js +4 -4
  41. package/lib/reports/custom.js +8 -24
  42. package/lib/reports/markdown-details.js +4 -6
  43. package/lib/reports/markdown-summary.js +4 -6
  44. package/lib/reports/raw.js +10 -12
  45. package/lib/reports/v8-json.js +3 -5
  46. package/lib/reports/v8.js +3 -5
  47. package/lib/utils/dedupe.js +1 -1
  48. package/lib/utils/gc.js +30 -14
  49. package/lib/utils/markdown.js +2 -2
  50. package/lib/utils/merge/merge.js +2 -4
  51. package/lib/utils/merge/range-tree.js +1 -1
  52. package/lib/utils/request.js +85 -73
  53. package/lib/utils/snapshot.js +4 -4
  54. package/lib/utils/source-path.js +26 -11
  55. package/lib/utils/util.js +67 -42
  56. package/lib/v8/v8-summary.js +2 -4
  57. package/lib/v8/v8.js +11 -11
  58. package/migrate-to-3.md +175 -0
  59. package/package.json +54 -42
  60. package/skills/monocart-coverage-reports/SKILL.md +106 -0
  61. package/skills/monocart-coverage-reports/references/best-practices.md +218 -0
  62. package/skills/monocart-coverage-reports/references/cli.md +108 -0
  63. package/skills/monocart-coverage-reports/references/collection.md +154 -0
  64. package/skills/monocart-coverage-reports/references/filtering-and-sourcemaps.md +170 -0
  65. package/skills/monocart-coverage-reports/references/reports-and-ci.md +219 -0
  66. package/skills/monocart-coverage-reports/references/repository-map.md +59 -0
  67. package/lib/index.mjs +0 -9
  68. package/lib/register/hooks.mjs +0 -3
  69. package/lib/register/register.mjs +0 -3
package/README.zh-Hans.md CHANGED
@@ -13,6 +13,7 @@
13
13
  > JS代码覆盖率工具,用来生成原生的[V8](https://v8.dev/blog/javascript-code-coverage)或者[Istanbul](https://istanbul.js.org/)代码覆盖率报告
14
14
 
15
15
  * [用法](#usage)
16
+ * [最佳实践](#best-practices)
16
17
  * [选项配置](#options)
17
18
  * [所有支持的报告类型](#available-reports)
18
19
  * [比较两种报告](#compare-reports)
@@ -31,6 +32,7 @@
31
32
  * [多进程支持](#multiprocessing-support)
32
33
  * [如何使用CLI命令行](#command-line)
33
34
  * [如何加载配置文件](#config-file)
35
+ * [使用 `mcr test` 运行测试](#run-tests-with-mcr-test)
34
36
  * [如何合并覆盖率报告](#merge-coverage-reports)
35
37
  - [自动合并](#automatic-merging)
36
38
  - [手动合并](#manual-merging)
@@ -66,18 +68,21 @@
66
68
  * [感谢](#thanks)
67
69
 
68
70
  ## Usage
69
- > 推荐使用 [Node.js 22+](https://nodejs.org/),项目将其作为持续维护的目标版本。
71
+ > **MCR 3.x 的所有 API 和 CLI 命令均要求 [Node.js >= 22.15.0](https://nodejs.org/)**,包括普通命令、`merge` 和 `test`。
72
+
73
+ **从 2.x 升级:** 请参阅 [3.x 迁移指南](migrate-to-3.md)。
74
+
70
75
  - 安装
71
76
  ```sh
72
77
  npm install monocart-coverage-reports
73
78
  ```
74
79
  - API
75
80
  ```js
76
- const MCR = require('monocart-coverage-reports');
81
+ import { MCR } from 'monocart-coverage-reports';
77
82
  const mcr = MCR({
78
83
  name: 'My Coverage Report - 2024-02-28',
79
84
  outputDir: './coverage-reports',
80
- reports: ["v8", "console-details"],
85
+ reports: ['v8', 'console-details'],
81
86
  cleanCache: true
82
87
  });
83
88
  await mcr.add(coverageData);
@@ -99,6 +104,35 @@ mcr node my-app.js -r v8,console-details
99
104
  ```
100
105
  参见 [命令行](#command-line)
101
106
 
107
+ ## Best Practices
108
+
109
+ 为新项目建立测试方案时,优先使用无需插桩的原生覆盖率采集:
110
+
111
+ | 测试目标 | 推荐方案 | 职责划分 |
112
+ | --- | --- | --- |
113
+ | Node.js 端 | **Node.js 原生 test runner + `mcr test`** | `node:test` 执行测试断言;MCR 自动采集 JavaScript 原生 V8 覆盖率并生成报告。 |
114
+ | 浏览器端 | **Playwright Test + MCR** | Playwright 执行测试断言;自动 fixture 采集 Chromium JS/CSS 覆盖率;MCR 合并并生成报告。 |
115
+ | 浏览器端,需要统一测试报告 | **Playwright Test + [monocart-reporter](https://github.com/cenfun/monocart-reporter#code-coverage-report)** | reporter 已集成 MCR;配置其 coverage 选项,并由 fixture 提交覆盖率数据,统一管理测试报告和覆盖率报告。 |
116
+
117
+ 详细配置步骤和示例见[最佳实践指南](skills/monocart-coverage-reports/references/best-practices.md)。
118
+
119
+ ## AI Agent Skill
120
+
121
+ 使用 AI 辅助开发本仓库源码时,请先阅读 [AGENTS.md](https://github.com/cenfun/monocart-coverage-reports/blob/main/AGENTS.md),了解源码结构、开发命令、验证流程和安全边界。`.github/copilot-instructions.md` 为 GitHub Copilot 提供可独立使用的核心规则摘要。这些指南仅用于源码仓库,不随 npm 包发布,与下面可复用的集成技能相互独立。
122
+
123
+ 项目提供可复用的 [Monocart Coverage Reports 技能](skills/monocart-coverage-reports/SKILL.md),帮助 AI coding agent 采集覆盖率、配置 CLI、排查 sourcemap 和过滤问题、合并 raw 报告及设置 CI 阈值。结构为主 `SKILL.md` 加按需加载的专题参考文档,仅面向 MCR 3.x,并检查其运行环境要求。
124
+
125
+ npm 包包含 `skills/monocart-coverage-reports/`。请将**整个目录**(包括 `references/`)复制到 agent 支持的 skills 目录,或直接让 agent 读取 `SKILL.md`。安装 MCR 不会自动注册技能或修改 agent 配置。技能说明使用英文,但提问和回复可以使用中文或其他语言。
126
+
127
+ 在使用 MCR 的项目根目录中定位已安装的技能和包配置(旧版本可能尚未包含技能):
128
+
129
+ ```sh
130
+ node -p "require.resolve('monocart-coverage-reports/skills/monocart-coverage-reports/SKILL.md')"
131
+ node -p "require.resolve('monocart-coverage-reports/package.json')"
132
+ ```
133
+
134
+ 读取定位到的技能文件,再按相对链接读取专题文档。请以已安装包的运行时、API 和版本要求为准,不要假设所有示例都适用于旧版本。以上命令用于定位文件,不是将 Markdown 作为 JavaScript 导入。
135
+
102
136
  ## Options
103
137
  - 默认选项: [lib/default/options.js](./lib/default/options.js)
104
138
  - 选项的类型描述,见 `CoverageReportOptions` [lib/index.d.ts](./lib/index.d.ts)
@@ -115,6 +149,7 @@ mcr node my-app.js -r v8,console-details
115
149
  | `dataDir` | `string` | `null` | `generate()` 阶段自动加载的覆盖率数据目录,作为 `addFromDir()` 的替代。 |
116
150
  | `entryFilter` | `string \| object \| function` | `null` | (仅 V8)按 URL 过滤入口文件,参见 [过滤V8覆盖率数据](#filtering-results)。 |
117
151
  | `sourceFilter` | `string \| object \| function` | `null` | (仅 V8)过滤从 sourcemap 解包出的源文件。 |
152
+ | `outputFilter` | `string \| object \| function` | `null` | (仅 V8)在 sourcemap 转换后,按最终 `sourcePath` 过滤文件。参见 [过滤V8覆盖率数据](#filtering-results)。 |
118
153
  | `filter` | `string \| object \| function` | `null` | 合并 `entryFilter` 和 `sourceFilter` 的统一过滤器。 |
119
154
  | `sourcePath` | `object \| function` | `null` | 修改源文件路径,参见 [使用 `sourcePath`](#resolve-sourcepath-for-the-source-files)。 |
120
155
  | `all` | `string \| string[] \| object` | `null` | 为未测试文件添加空覆盖率,参见 [为未测试的文件添加空的覆盖率报告](#adding-empty-coverage-for-untested-files)。 |
@@ -127,7 +162,7 @@ mcr node my-app.js -r v8,console-details
127
162
  | `watermarks` | `[number, number] \| object` | `[50, 80]` | 覆盖率高/低阈值百分比。也支持按指标配置:`{ bytes:[50,80], lines:[50,80] }`。 |
128
163
  | `clean` | `boolean` | `true` | 生成报告前清理 `outputDir` 中的旧报告(`.cache` 目录在 `cleanCache` 也为 `true` 时才会被清理)。 |
129
164
  | `cleanCache` | `boolean` | `false` | 启动时清理缓存目录。当 `clean` 和 `cleanCache` 都为 `false` 时,之前运行的缓存数据会保留,可在多进程间复用。 |
130
- | `gc` | `number` | `null` | 内存阈值(MB),在关键阶段若 RSS 超过阈值则强制触发 GC,适用于超大数据集,参见 [JavaScript heap out of memory](#javascript-heap-out-of-memory)。 |
165
+ | `gc` | `number` | `null` | 内存阈值(MB),在关键阶段若 JavaScript 堆内存使用量(`heapUsed`)超过阈值则强制触发 GC,适用于超大数据集,参见 [JavaScript heap out of memory](#javascript-heap-out-of-memory)。 |
131
166
  | `sourceMap` | `boolean` | `false` | 将源文件和 sourcemap 也保存到 cache 便于调试(需要 `logging: "debug"`)。 |
132
167
  | `sourceMapResolver` | `(url, defaultResolver) => Promise<string \| object>` | `null` | 自定义 sourcemap 加载逻辑(例如从构建缓存中读取),可调用 `defaultResolver(url)` 回退到默认解析。 |
133
168
  | `onEntry` | `(entry) => void \| Promise<void>` | `null` | (仅 V8)每个入口文件处理前触发,参见 [Hooks](#hooks)。 |
@@ -202,7 +237,7 @@ cat path-to/coverage-summary.md >> $GITHUB_STEP_SUMMARY
202
237
 
203
238
  - 自定义报告
204
239
  ```js
205
- {
240
+ const coverageOptions = {
206
241
  reports: [
207
242
  [path.resolve('./test/custom-istanbul-reporter.js'), {
208
243
  type: 'istanbul',
@@ -216,22 +251,24 @@ cat path-to/coverage-summary.md >> $GITHUB_STEP_SUMMARY
216
251
  type: 'both'
217
252
  }]
218
253
  ]
219
- }
254
+ };
220
255
  ```
221
256
  - Istanbul自定义报告
222
257
  > 例子: [./test/custom-istanbul-reporter.js](./test/custom-istanbul-reporter.js), see [istanbul built-in reporters' implementation](https://github.com/istanbuljs/istanbuljs/tree/master/packages/istanbul-reports/lib) for reference.
223
258
  - V8自定义报告
224
259
  > 例子: [./test/custom-v8-reporter.js](./test/custom-v8-reporter.js)
225
260
 
226
- `type` 选项决定报告接收的覆盖率数据格式:
227
- - `'istanbul'` — 报告接收 Istanbul 格式的覆盖率数据(`coverageResults.istanbulData`)。
228
- - `'v8'` — 报告接收原生 V8 格式的覆盖率数据(`coverageResults.v8Data`)。
229
- - `'both'` — 报告同时接收两种格式,可通过 `coverageResults.v8Data` 和 `coverageResults.istanbulData` 访问。
261
+ `type` 选项决定报告接口及执行时机:
262
+ - `'istanbul'` — 使用 Istanbul 报告接口:先调用 `new Reporter(reportOptions)`,再调用 `execute(context)`。context 提供 Istanbul 报告树和源码查找能力。V8 输入会先转换为 Istanbul 覆盖率数据。
263
+ - `'v8'`(默认值)— 对 V8 输入,先调用 `new Reporter(reportOptions, globalOptions)`,再调用 `await generate(reportData)`。`reportData` 是 V8 覆盖率结果,包含 `type`、`summary` 和 `files`;每个文件包含 V8 覆盖范围和汇总。
264
+ - `'both'` — 对 V8 或 Istanbul 输入均使用上述 `generate(reportData)` 接口。请检查 `reportData.type`,按对应的结果结构处理。它**不会**同时提供两份原始格式的数据,结果中不存在 `v8Data` 或 `istanbulData` 属性。
265
+
266
+ 报告统一使用原生 ESM `import()` 加载。ESM 报告使用 `export default Reporter`;通过 Node 内置互操作,也可加载 `module.exports = Reporter` 的 CommonJS 报告(在 ESM 项目中使用 `.cjs`)。本地路径必须明确指定文件扩展名,不补全扩展名、不解析目录入口,不提供独立的 CommonJS 加载器。报告加载或初始化失败会使报告生成失败,不会静默跳过。
230
267
 
231
268
  ### Multiple Reports:
232
269
  如何配置多个报告
233
270
  ```js
234
- const MCR = require('monocart-coverage-reports');
271
+ import { MCR } from 'monocart-coverage-reports';
235
272
  const coverageOptions = {
236
273
  outputDir: './coverage-reports',
237
274
  reports: [
@@ -248,15 +285,15 @@ const coverageOptions = {
248
285
 
249
286
  // custom reports
250
287
  // Specify reporter name with the NPM package
251
- ["custom-reporter-1"],
252
- ["custom-reporter-2", {
253
- type: "istanbul",
254
- key: "value"
288
+ ['custom-reporter-1'],
289
+ ['custom-reporter-2', {
290
+ type: 'istanbul',
291
+ key: 'value'
255
292
  }],
256
293
  // Specify reporter name with local path
257
294
  ['/absolute/path/to/custom-reporter.js']
258
295
  ]
259
- }
296
+ };
260
297
  const mcr = MCR(coverageOptions);
261
298
  ```
262
299
 
@@ -324,7 +361,7 @@ await Promise.all([
324
361
  })
325
362
  ]);
326
363
 
327
- await page.goto("your page url");
364
+ await page.goto('your page url');
328
365
 
329
366
  const [jsCoverage, cssCoverage] = await Promise.all([
330
367
  page.coverage.stopJSCoverage(),
@@ -334,11 +371,18 @@ const [jsCoverage, cssCoverage] = await Promise.all([
334
371
  const coverageData = [... jsCoverage, ... cssCoverage];
335
372
 
336
373
  ```
337
- 使用 `@playwright/test` 的 [`Automatic fixtures`](https://playwright.dev/docs/test-fixtures#automatic-fixtures)收集覆盖率数据, 见例子: [fixtures.ts](https://github.com/cenfun/playwright-coverage/blob/main/fixtures.ts)
374
+ Playwright Test 自动 fixture 参见完整 [playwright-coverage 示例](https://github.com/cenfun/playwright-coverage)中的 [fixtures.js](https://github.com/cenfun/playwright-coverage/blob/main/fixtures.js),接入方式见[最佳实践指南](skills/monocart-coverage-reports/references/best-practices.md#complete-project-example)。
338
375
  参见例子 [./test/test-v8.js](./test/test-v8.js), [css](./test/test-css.js)
339
376
 
340
377
 
341
378
  ### Collecting Raw V8 Coverage Data with Puppeteer
379
+
380
+ > 取决于包管理器配置,Puppeteer 可能不会在安装依赖时下载浏览器。如果没有可用浏览器,请手动安装:
381
+ >
382
+ > ```sh
383
+ > npx puppeteer browsers install chrome
384
+ > ```
385
+
342
386
  使用Puppeteer的覆盖接口收集覆盖率数据,注意Puppeteer默认不会提供原生V8的覆盖率数据,需要设置`includeRawScriptCoverage`
343
387
  ```js
344
388
  await Promise.all([
@@ -353,7 +397,7 @@ await Promise.all([
353
397
  })
354
398
  ]);
355
399
 
356
- await page.goto("your page url");
400
+ await page.goto('your page url');
357
401
 
358
402
  const [jsCoverage, cssCoverage] = await Promise.all([
359
403
  page.coverage.stopJSCoverage(),
@@ -406,30 +450,35 @@ const coverageData = [... jsCoverage.map((it) => {
406
450
  - 如果是子进程,可参见 [命令行](#command-line)
407
451
 
408
452
  ### Collecting V8 Coverage Data with `CDPClient` API
453
+
454
+ `timeout` 控制 HTTP 调试目标发现和 WebSocket 连接的超时(默认 10000ms);`requestTimeout` 控制内置 WebSocket 会话中每条 CDP 命令的超时(默认 30000ms)。设为 `0` 可禁用相应超时。外部 Playwright/Puppeteer 会话自行管理命令超时。自定义 socket 可使用 `new MCR.WSSession(ws, { timeout: 30000 })`。
455
+
409
456
  - `CDPClient`为`MCR`提供的内置接口类,用来更便捷的处理覆盖率相关数据,所有的API如下
410
- ```js
411
- // 开始和停止并收集JS的覆盖率数据
412
- startJSCoverage: () => Promise<void>;
413
- stopJSCoverage: () => Promise<V8CoverageEntry[]>;
457
+ ```ts
458
+ interface CoverageClient {
459
+ // 开始和停止并收集JS的覆盖率数据
460
+ startJSCoverage: () => Promise<void>;
461
+ stopJSCoverage: () => Promise<V8CoverageEntry[]>;
414
462
 
415
- // 开始和停止并收集CSS的覆盖率数据,支持匿名文件(比如style里的css)
416
- startCSSCoverage: () => Promise<void>;
417
- stopCSSCoverage: () => Promise<V8CoverageEntry[]>;
463
+ // 开始和停止并收集CSS的覆盖率数据,支持匿名文件(比如style里的css)
464
+ startCSSCoverage: () => Promise<void>;
465
+ stopCSSCoverage: () => Promise<V8CoverageEntry[]>;
418
466
 
419
- // 开始和停止并收集JS和CSS的覆盖率数据
420
- startCoverage: () => Promise<void>;
421
- stopCoverage: () => Promise<V8CoverageEntry[]>;
467
+ // 开始和停止并收集JS和CSS的覆盖率数据
468
+ startCoverage: () => Promise<void>;
469
+ stopCoverage: () => Promise<V8CoverageEntry[]>;
422
470
 
423
- /** 如果开启了NODE_V8_COVERAGE,这个接口用来手动保存当前覆盖率数据 */
424
- writeCoverage: () => Promise<string>;
471
+ /** 如果开启了NODE_V8_COVERAGE,这个接口用来手动保存当前覆盖率数据 */
472
+ writeCoverage: () => Promise<string>;
425
473
 
426
- /** 收集istanbul覆盖率数据 */
427
- getIstanbulCoverage: (coverageKey?: string) => Promise<any>;
474
+ /** 收集istanbul覆盖率数据 */
475
+ getIstanbulCoverage: (coverageKey?: string) => Promise<any>;
476
+ }
428
477
  ```
429
478
 
430
479
  - 结合使用Node调试端口`--inspect=9229` 或者浏览器调试端口 `--remote-debugging-port=9229`
431
480
  ```js
432
- const MCR = require('monocart-coverage-reports');
481
+ import { MCR } from 'monocart-coverage-reports';
433
482
  const client = await MCR.CDPClient({
434
483
  port: 9229
435
484
  });
@@ -440,8 +489,8 @@ const coverageData = await client.stopJSCoverage();
440
489
 
441
490
  - 结合使用 [Playwright CDPSession](https://playwright.dev/docs/api/class-cdpsession)
442
491
  ```js
443
- const { chromium } = require('playwright');
444
- const MCR = require('monocart-coverage-reports');
492
+ import { chromium } from 'playwright';
493
+ import { MCR } from 'monocart-coverage-reports';
445
494
  const browser = await chromium.launch();
446
495
  const page = await browser.newPage();
447
496
  const session = await page.context().newCDPSession(page);
@@ -451,14 +500,14 @@ const client = await MCR.CDPClient({
451
500
  // both js and css coverage
452
501
  await client.startCoverage();
453
502
  // run your test page here
454
- await page.goto("your page url");
503
+ await page.goto('your page url');
455
504
  const coverageData = await client.stopCoverage();
456
505
  ```
457
506
 
458
507
  - 结合使用 [Puppeteer CDPSession](https://pptr.dev/api/puppeteer.cdpsession)
459
508
  ```js
460
- const puppeteer = require('puppeteer');
461
- const MCR = require('monocart-coverage-reports');
509
+ import puppeteer from 'puppeteer';
510
+ import { MCR } from 'monocart-coverage-reports';
462
511
  const browser = await puppeteer.launch({});
463
512
  const page = await browser.newPage();
464
513
  const session = await page.target().createCDPSession();
@@ -468,20 +517,20 @@ const client = await MCR.CDPClient({
468
517
  // both js and css coverage
469
518
  await client.startCoverage();
470
519
  // run your test page here
471
- await page.goto("your page url");
520
+ await page.goto('your page url');
472
521
  const coverageData = await client.stopCoverage();
473
522
  ```
474
523
 
475
524
  - 结合使用 [Selenium Webdriver](https://www.selenium.dev/documentation/webdriver/) WebSocket (仅支持Chrome/Edge浏览器)
476
525
  ```js
477
- const { Builder, Browser } = require('selenium-webdriver');
478
- const MCR = require('monocart-coverage-reports');
526
+ import { Builder, Browser } from 'selenium-webdriver';
527
+ import { MCR } from 'monocart-coverage-reports';
479
528
  const driver = await new Builder().forBrowser(Browser.CHROME).build();
480
529
  const pageCdpConnection = await driver.createCDPConnection('page');
481
530
  const session = new MCR.WSSession(pageCdpConnection._wsConnection);
482
531
  const client = await MCR.CDPClient({
483
532
  session
484
- })
533
+ });
485
534
  ```
486
535
 
487
536
  ### V8 Coverage Data API
@@ -489,7 +538,7 @@ const client = await MCR.CDPClient({
489
538
  - [Playwright的覆盖率接口](https://playwright.dev/docs/api/class-coverage)
490
539
  - [Puppeteer的覆盖率接口](https://pptr.dev/api/puppeteer.coverage)
491
540
  - [DevTools Protocol的覆盖率接口](https://chromedevtools.github.io/devtools-protocol/tot/Profiler/#method-startPreciseCoverage) 参见 [ScriptCoverage](https://chromedevtools.github.io/devtools-protocol/tot/Profiler/#type-ScriptCoverage) 和 [v8-coverage](https://github.com/bcoe/v8-coverage)
492
- ```js
541
+ ```ts
493
542
  // Coverage data for a source range.
494
543
  export interface CoverageRange {
495
544
  // JavaScript script source offset for the range start.
@@ -578,15 +627,15 @@ export type V8CoverageData = ScriptCoverage[];
578
627
  过滤可以使用函数:
579
628
  ```js
580
629
  const coverageOptions = {
581
- entryFilter: (entry) => entry.url.indexOf("main.js") !== -1,
630
+ entryFilter: (entry) => entry.url.indexOf('main.js') !== -1,
582
631
  sourceFilter: (sourcePath) => sourcePath.search(/src\//) !== -1
583
632
  };
584
633
  ```
585
634
  也可以使用便捷的[`minimatch`](https://github.com/isaacs/minimatch)来匹配(推荐):
586
635
  ```js
587
636
  const coverageOptions = {
588
- entryFilter: "**/main.js",
589
- sourceFilter: "**/src/**"
637
+ entryFilter: '**/main.js',
638
+ sourceFilter: '**/src/**'
590
639
  };
591
640
  ```
592
641
  支持多个匹配:
@@ -611,10 +660,17 @@ mcr --sourceFilter "{'**/node_modules/**':false,'**/**':true}"
611
660
  ```js
612
661
  const coverageOptions = {
613
662
  entryFilter: (entry) => {
614
- if (minimatch(entry.url, '**/node_modules/**')) { return false; }
615
- if (minimatch(entry.url, '**/vendor.js')) { return false; }
616
- if (minimatch(entry.url, '**/src/**')) { return true; }
617
- return false; // else unmatched
663
+ if (minimatch(entry.url, '**/node_modules/**')) {
664
+ return false;
665
+ }
666
+ if (minimatch(entry.url, '**/vendor.js')) {
667
+ return false;
668
+ }
669
+ if (minimatch(entry.url, '**/src/**')) {
670
+ return true;
671
+ }
672
+ // else unmatched
673
+ return false;
618
674
  }
619
675
  };
620
676
  ```
@@ -633,6 +689,42 @@ const coverageOptions = {
633
689
  };
634
690
  ```
635
691
 
692
+ ### 使用 `outputFilter` 过滤最终文件
693
+ > **版本要求:** `outputFilter` 是 **v3.0.0** 新增的选项,v2.13.x 及更早版本不支持。
694
+
695
+ **两套过滤方案选一套即可,不要混淆。** `outputFilter` 与已有的 `entryFilter` + `sourceFilter` 都用于决定报告中保留哪些文件,功能重叠,是两种替代方案,而不是必须叠加配置的过滤器。合并选项 `filter` 只是 `entryFilter` + `sourceFilter` 的简写,不会配置 `outputFilter`。
696
+
697
+ | 方案 | 过滤时机和对象 | 优点 | 代价与注意事项 |
698
+ | :-- | :-- | :-- | :-- |
699
+ | `entryFilter` + `sourceFilter`(或 `filter`) | 从输入端过滤:在发现入口文件时,`entryFilter` 按 URL 过滤;在 sourcemap 解包源文件时,`sourceFilter` 按源路径过滤。 | 尽早排除不需要的文件,减少后续无效处理,性能更高。大型项目或 bundle 较多时,优先推荐此方案。 | 需要区分入口文件和 sourcemap 源文件,并配置两个阶段的规则。如果通过 `entryFilter` 排除整个 bundle,其中需要保留的源文件也会一起丢失。 |
700
+ | `outputFilter` | 从输出结果过滤:在 sourcemap 转换、合并和添加未测试文件(`all`)完成后、生成报告前,按最终 `sourcePath` 过滤。 | 配置更简单,一套规则直接面向最终文件列表,无需区分入口文件和解包后的源文件。 | 不需要的文件在被丢弃前仍会经历前期发现和处理,无法节省这些开销。当最终排除的文件较多时,可能带来明显的生成耗时和内存开销,存在性能问题。 |
701
+
702
+ **选择建议:** 优先考虑性能时,使用输入过滤方案(`entryFilter` + `sourceFilter`,或 `filter`);更看重配置简单、希望直接过滤最终结果,且可以接受额外处理成本时,选择 `outputFilter`。切换到 `outputFilter` 时,应移除同一范围的旧输入过滤配置:输入阶段已经排除的文件,无法通过输出过滤恢复。
703
+
704
+ 对于 V8 输入,`outputFilter` 同时作用于 V8 报告和转换后的 Istanbul 报告。默认值为 `null`,不启用过滤。
705
+
706
+ 它与 `entryFilter` 和 `sourceFilter` 使用相同的字符串和有序对象匹配规则:按顺序返回第一个匹配项的值,未匹配的文件不保留。匹配目标是经过 `sourcePath` 改写后的最终路径。函数形式接收最终文件对象,而不是路径字符串。
707
+
708
+ ```js
709
+ const coverageOptions = {
710
+ outputFilter: {
711
+ '**/node_modules/**': false,
712
+ '**/src/**': true
713
+ }
714
+ };
715
+ // 也可以使用:
716
+ // outputFilter: '**/src/**'
717
+ // outputFilter: (entry) => entry.sourcePath.startsWith('src/')
718
+ ```
719
+
720
+ 命令行示例(使用引号避免 shell 展开匹配规则):
721
+ ```sh
722
+ mcr merge --inputDir ./raw --outputFilter "**/src/**"
723
+ mcr merge --inputDir ./raw --outputFilter "{'**/node_modules/**':false,'**/src/**':true}"
724
+ ```
725
+
726
+ `outputFilter` 不改变 raw 原始覆盖率导出,也不适用于 Istanbul 格式的输入数据。
727
+
636
728
  ## Resolve `sourcePath` for the Source Files
637
729
  当一个文件从sourcemap解包,它的路径可能是个虚拟路径, 此时可以使用`sourcePath`选项来修改文件路径。比如,我们测试了多个dist包的入口文件,它们的源文件可能包含了一些共同的文件,但路径可能不同,如果我们需要相同的文件覆盖率数据可以自动合并,那么需要使用`sourcePath`来统一这些相同文件的路径
638
730
  ```js
@@ -649,38 +741,40 @@ const coverageOptions = {
649
741
  }
650
742
  };
651
743
  ```
744
+ 内置报告路径规范化采用尽力而为的原则,无法完全兼容所有非标准、虚拟或来自其他操作系统的路径。此类特殊情况应通过 `sourcePath` 自行修正,而不是依赖自动推断。回调接收规范化后的 `filePath`;对于 V8 入口和 sourcemap 源文件,`info.url` 提供报告路径规范化前的原始 URL(sourcemap 存在 `sourceRoot` 时包含该前缀)。它修改的是报告路径,不是读取缺失源码时使用的文件系统路径。缺少 `sourcesContent` 时,本地源码相对于外部 map 的 URL 加载(内联 map 则相对于生成文件),并将 `sourceRoot` 作为目录应用。
745
+
652
746
  它也支持简单key/value的替换:
653
747
  ```js
654
748
  const coverageOptions = {
655
749
  sourcePath: {
656
- 'my-dist-file1/': '',
750
+ 'my-dist-file1/': '',
657
751
  'my-dist-file2/': ''
658
752
  }
659
753
  };
660
754
  ```
661
755
  解决文件路径不完整的问题:
662
756
  ```js
663
- const path = require("path")
757
+ import path from 'node:path';
664
758
 
665
759
  // MCR coverage options
666
760
  const coverageOptions = {
667
- sourcePath: (filePath, info)=> {
761
+ sourcePath: (filePath, info) => {
668
762
  if (!filePath.includes('/') && info.distFile) {
669
763
  return `${path.dirname(info.distFile)}/${filePath}`;
670
764
  }
671
765
  return filePath;
672
766
  }
673
- }
767
+ };
674
768
  ```
675
769
 
676
770
  ## Adding Empty Coverage for Untested Files
677
- 默认,未测试的文件不会包含在覆盖率报告中。使用 `all` 选项可将它们以 0% 覆盖率加入报告。即使没有收集到任何覆盖率数据,也会为匹配的未测试文件生成 V8 报告。
771
+ 默认不包含未测试文件。使用 `all` 可将它们以 0% 覆盖率加入报告,即使没有采集到任何覆盖率数据也可生成报告。同一原始源码的真实 V8 覆盖率优先于空覆盖率条目。
678
772
  ```js
679
773
  const coverageOptions = {
680
- all: './src',
774
+ all: './src'
681
775
 
682
776
  // 支持多个目录
683
- all: ['./src', './lib'],
777
+ // all: ['./src', './lib']
684
778
  };
685
779
  ```
686
780
  未测试的文件也适用于`sourceFilter`过滤器. 而且也可以指定自己的`filter`过滤器 (可以返回文件类型来支持js或css的覆盖率格式):
@@ -693,30 +787,29 @@ const coverageOptions = {
693
787
  '**/ignored-*.js': false,
694
788
  '**/*.html': false,
695
789
  // empty css coverage
696
- '**/*.scss': "css",
790
+ '**/*.scss': 'css',
697
791
  '**/*': true
698
792
  }
699
793
  }
700
794
  };
701
795
  ```
702
- 我们可能需要编译.ts, .jsx, .vue等等这样的文件, 这样才能被默认的AST解析器解析,以得到更多的覆盖率指标的数据
796
+ 可以将未测试的文件转换成 JavaScript,以便进行 AST 分析。以下示例仅处理**不含 JSX 的 TypeScript**,并假定已安装 `@swc/core`。JSX/TSX 或 Vue 应使用匹配的编译器和解析器,这不是通用转换器。保留完整文件路径,避免不同目录下的同名文件冲突;SWC 返回的 sourcemap 是 JSON 字符串,赋值前需要解析。
703
797
  ```js
704
- const path = require("path");
705
- const swc = require("@swc/core");
798
+ import { fileURLToPath } from 'node:url';
799
+ import { transform } from '@swc/core';
706
800
  const coverageOptions = {
707
801
  all: {
708
802
  dir: ['./src'],
803
+ filter: '**/*.ts',
709
804
  transformer: async (entry) => {
710
- const { code, map } = await swc.transform(entry.source, {
711
- filename: path.basename(entry.url),
805
+ const { code, map } = await transform(entry.source, {
806
+ filename: fileURLToPath(entry.url),
712
807
  sourceMaps: true,
713
808
  isModule: true,
714
809
  jsc: {
715
810
  parser: {
716
- syntax: "typescript",
717
- jsx: true
718
- },
719
- transform: {}
811
+ syntax: 'typescript'
812
+ }
720
813
  }
721
814
  });
722
815
  entry.source = code;
@@ -729,36 +822,45 @@ const coverageOptions = {
729
822
  ## Hooks
730
823
 
731
824
  ### `onEnd` — 报告生成后触发
732
- 典型用法:检测覆盖率是否达标,低于 threshold 时抛错退出。
825
+ 典型用法:检测覆盖率是否达标,失败时抛错,让 API 调用方处理异常、CLI 返回非零状态;不要使用 `process.exit()` 提前中断清理。
733
826
  ```js
734
- const EC = require('eight-colors');
827
+ const getCoverageError = (metric, summary, minimum) => {
828
+ if (!summary || !(summary.total > 0)) {
829
+ return `${metric}: missing or empty coverage`;
830
+ }
831
+ const { pct } = summary;
832
+ if (typeof pct !== 'number' || !Number.isFinite(pct) || pct < minimum) {
833
+ return `${metric}: ${pct}%, required ${minimum}%`;
834
+ }
835
+ return '';
836
+ };
735
837
  const coverageOptions = {
736
838
  name: 'My Coverage Report',
737
839
  outputDir: './coverage-reports',
738
840
  onEnd: (coverageResults) => {
841
+ if (!coverageResults) {
842
+ throw new Error('No coverage results');
843
+ }
739
844
  const thresholds = {
740
845
  bytes: 80,
741
846
  lines: 60
742
847
  };
743
- console.log('check thresholds ...', thresholds);
744
848
  const errors = [];
745
- const { summary } = coverageResults;
746
- Object.keys(thresholds).forEach((k) => {
747
- const pct = summary[k].pct;
748
- if (pct < thresholds[k]) {
749
- errors.push(`Coverage threshold for ${k} (${pct} %) not met: ${thresholds[k]} %`);
849
+ for (const [metric, minimum] of Object.entries(thresholds)) {
850
+ const error = getCoverageError(metric, coverageResults.summary?.[metric], minimum);
851
+ if (error) {
852
+ errors.push(error);
750
853
  }
751
- });
854
+ }
752
855
  if (errors.length) {
753
- const errMsg = errors.join('\n');
754
- console.log(EC.red(errMsg));
755
- // throw new Error(errMsg);
756
- // process.exit(1);
856
+ throw new Error(errors.join('\n'));
757
857
  }
758
858
  }
759
- }
859
+ };
760
860
  ```
761
861
 
862
+ 本示例拒绝缺失或总数为零的指标;如果某项指标允许为空,应明确调整策略。没有可用输入时,`generate()` 可能直接返回 `undefined` 而不调用 `onEnd`。API 调用方必须检查返回值;普通 CLI 流程应使用干净的输出目录,并验证本次生成的非空报告。参见 [严格 CI 示例](skills/monocart-coverage-reports/references/reports-and-ci.md#reject-stale-reports-and-missing-collection-in-ci)。
863
+
762
864
  ### `onEntry` — 每个V8入口在处理前触发(仅V8)
763
865
  可以在进入AST解析前改写 entry(例如对非标准语法的源码做预编译),参见 [Unparsable source](#unparsable-source)
764
866
  ```js
@@ -775,7 +877,7 @@ const coverageOptions = {
775
877
  - `onReady(coverageReport, nodeV8CoverageDir, subprocess)` — 子进程退出后、MCR 读取 `nodeV8CoverageDir` 之前触发。用于子进程尚未写完覆盖数据需要等待的场景
776
878
  ```js
777
879
  // mcr.config.js
778
- module.exports = {
880
+ export default {
779
881
  onReady: async (coverageReport, nodeV8CoverageDir, subprocess) => {
780
882
  // 等待某个信号,或先对 raw 文件做一些预处理
781
883
  }
@@ -783,6 +885,8 @@ module.exports = {
783
885
  ```
784
886
 
785
887
  ## Ignoring Uncovered Codes
888
+ <!-- readme-lint-disable -->
889
+
786
890
  使用特定的注释,以`v8 ignore `开头可以忽略未覆盖的代码:
787
891
  - 忽略开始到结束
788
892
  ```js
@@ -817,12 +921,14 @@ function uncovered() {
817
921
  ```
818
922
  - 可以设置 `v8Ignore: false` 完全禁用基于注释的忽略机制。
819
923
 
924
+ <!-- readme-lint-enable -->
925
+
820
926
  ## Multiprocessing Support
821
927
  > 多进程支持可以很好的解决异步并行的情况。所有的覆盖率数据会保存到`[outputDir]/.cache`,在报告生成之后,这些缓存数据会被清除。除非开启了[调试模式](#debug-for-coverage-and-sourcemap),或者使用了`raw`报告
822
928
  - 主进程,初始化,清理之前的缓存
823
929
  ```js
824
- const MCR = require('monocart-coverage-reports');
825
- const coverageOptions = require('path-to/same-options.js');
930
+ import { MCR } from 'monocart-coverage-reports';
931
+ import coverageOptions from 'path-to/same-options.js';
826
932
  const mcr = MCR(coverageOptions);
827
933
  // clean previous cache before the start of testing
828
934
  // unless the running environment is new and no cache
@@ -831,16 +937,16 @@ mcr.cleanCache();
831
937
 
832
938
  - 子进程1, 测试业务1
833
939
  ```js
834
- const MCR = require('monocart-coverage-reports');
835
- const coverageOptions = require('path-to/same-options.js');
940
+ import { MCR } from 'monocart-coverage-reports';
941
+ import coverageOptions from 'path-to/same-options.js';
836
942
  const mcr = MCR(coverageOptions);
837
943
  await mcr.add(coverageData1);
838
944
  ```
839
945
 
840
946
  - 子进程2, 测试业务2
841
947
  ```js
842
- const MCR = require('monocart-coverage-reports');
843
- const coverageOptions = require('path-to/same-options.js');
948
+ import { MCR } from 'monocart-coverage-reports';
949
+ import coverageOptions from 'path-to/same-options.js';
844
950
  const mcr = MCR(coverageOptions);
845
951
  await mcr.add(coverageData2);
846
952
  ```
@@ -848,8 +954,8 @@ await mcr.add(coverageData2);
848
954
  - 主进程,所有测试完成之后
849
955
  ```js
850
956
  // generate coverage reports after the completion of testing
851
- const MCR = require('monocart-coverage-reports');
852
- const coverageOptions = require('path-to/same-options.js');
957
+ import { MCR } from 'monocart-coverage-reports';
958
+ import coverageOptions from 'path-to/same-options.js';
853
959
  const mcr = MCR(coverageOptions);
854
960
  await mcr.generate();
855
961
  ```
@@ -871,14 +977,17 @@ npx mcr node ./test/specs/node.test.js -r v8,console-details --lcov
871
977
 
872
978
  - CLI 选项
873
979
 
980
+ 命令使用 Execa 的默认选项执行,不主动开启 shell 模式。请分别传入可执行文件及参数,例如 `mcr node "./my app.js"`,不要把整条命令作为一个带引号的参数传入。MCR 保留参数边界,不展开环境变量或 glob,也不解析管道、重定向、`&&` 等 shell 语法;组合命令请放入脚本。使用 `mcr [options] -- executable ...args` 可以避免子命令参数被当作 MCR 参数解析。平台相关的命令执行由 Execa 处理,包括在 Windows 下按需通过 `cmd.exe` 调用批处理文件并转义参数。
981
+
874
982
  以下选项仅在 CLI 模式(`mcr <command>`)下可用:
875
983
 
876
984
  | 选项 | 类型 | 默认值 | 说明 |
877
985
  | :-- | :-- | :-- | :-- |
878
986
  | `-c, --config` | `string` | — | 自定义配置文件路径 |
879
- | `--import` | `string` | `null` | 通过 `NODE_OPTIONS` 预加载 ESM 模块,如 `tsx`。需要 Node.js `>= 18.19` |
880
- | `--require` | `string` | `null` | 通过 `NODE_OPTIONS` 预加载 CJS 模块 |
881
- | `--env` | `string` | `null` | 从 dotenv 文件加载环境变量(默认 `.env`)。需要 Node.js `>= 20.6` |
987
+ | `--import` | `string` | `null` | 通过 `NODE_OPTIONS` 预加载 ESM 模块,如 `tsx`。|
988
+ | `--require` | `string` | `null` | `--import` 的已弃用别名;使用 ESM 加载,不使用 CommonJS 解析 |
989
+ | `--env` | `string` | `null` | 从 dotenv 文件加载环境变量(默认 `.env`)。|
990
+ | `test`(仅配置文件) | `string \| string[] \| null` | `null` | 未指定命令行目标时,原样传给 `node --test` 的默认测试文件路径或 glob,**仅 `mcr test` 使用**;其他命令和 API 忽略。需要 Node.js `>= 22.15.0` |
882
991
  | `onStart` | `(coverageReport) => Promise<void>` | `null` | 子进程启动前触发;参见 [Hooks](#hooks) |
883
992
  | `onReady` | `(coverageReport, nodeV8CoverageDir, subprocess) => Promise<void>` | `null` | 子进程退出后、MCR 读取覆盖率前触发;参见 [Hooks](#hooks) |
884
993
 
@@ -889,16 +998,18 @@ npx mcr node ./test/specs/node.test.js -r v8,console-details --lcov
889
998
  mcr -c mcr.config.js -- sub-cli -c sub-cli.config.js
890
999
  ```
891
1000
 
892
- - `--import <module>` / `--require <module>`: 通过 `NODE_OPTIONS` 把一个预加载模块传给子进程。常用于 TypeScript/JSX 运行时(`tsx`、`ts-node` 等),也可用于加载 `mcr.config.ts`。要求 Node.js `>= 18.19`
1001
+ - `--import <module>` / `--require <module>`: 通过 `NODE_OPTIONS` 把一个预加载模块传给子进程。常用于 TypeScript/JSX 运行时(`tsx`、`ts-node` 等),也可用于加载 `mcr.config.ts`。在所有受支持的 Node.js 版本中均可用。本地路径以当前工作目录为基准,必须明确指定文件扩展名,支持包含空格的路径。包名由原生 ESM 按 MCR 模块位置解析。两个选项都使用 `import()`,并向 Node 传递 `--import`;`--require` 仅为已弃用别名,不提供 CommonJS 加载逻辑。CJS 模块仅通过 Node 内置的 import 互操作支持。
893
1002
  ```sh
894
1003
  mcr --import tsx node ./test.ts
895
1004
  ```
896
1005
 
897
- - `--env [path]`: 子进程启动前从 dotenv 文件加载环境变量(默认 `.env`)。依赖 `process.loadEnvFile`,Node.js `20.6` 起支持
1006
+ - `--env [path]`: 子进程启动前从 dotenv 文件加载环境变量(默认 `.env`)。使用 `process.loadEnvFile`,在所有受支持的 Node.js 版本中均可用
898
1007
  ```sh
899
1008
  mcr --env .env.test node ./test.js
900
1009
  ```
901
1010
 
1011
+ - `test`:运行 Node.js 测试并生成覆盖率报告,参见[使用 `mcr test` 运行测试](#run-tests-with-mcr-test)。
1012
+
902
1013
  - `merge`: 直接合并 `inputDir` 中的原始覆盖率数据并生成合并报告,无需启动子进程。
903
1014
  ```sh
904
1015
  mcr merge -r v8,console-details --inputDir ./coverage-reports/unit/raw,./coverage-reports/e2e/raw --outputDir ./coverage-reports/merged
@@ -911,6 +1022,8 @@ mcr merge -r v8,console-details --inputDir ./coverage-reports/unit/raw,./coverag
911
1022
  - [AVA](#ava)
912
1023
 
913
1024
  ## Config File
1025
+ MCR 3.x 中,显式指定的配置不存在,或找到的配置加载失败(包括 JSON 解析、模块语法及初始化错误)时,`loadConfig()` 会直接抛出异常,所有 CLI 命令也会非零退出,不再需要 `strict` 参数。未指定自定义配置且找不到默认配置时,仍使用已有选项继续执行。
1026
+
914
1027
  根据以下优先级加载配置文件
915
1028
  - 自定义配置文件(如果没有指定则加载后面的默认配置文件):
916
1029
  - CLI: `mcr --config <my-config-file-path>`
@@ -926,6 +1039,59 @@ mcr --import tsx -c mcr.config.ts node ./test.js
926
1039
 
927
1040
  > 未指定自定义路径时,MCR 会从当前工作目录向上查找默认配置文件(通过 [`find-up`](https://github.com/sindresorhus/find-up))。不在搜索路径内的文件会被忽略
928
1041
 
1042
+ ## Run Tests with `mcr test`
1043
+
1044
+ **MCR 3.x 重要更新:** 使用内置的 [Node.js 测试 runner](https://nodejs.org/docs/latest/api/test.html),一条命令即可运行测试并自动生成覆盖率报告。需要 **Node.js >= 22.15.0**。
1045
+
1046
+ 安装 MCR 后,直接指定测试文件即可运行,无需配置文件:
1047
+
1048
+ ```sh
1049
+ npm install --save-dev monocart-coverage-reports
1050
+ npx mcr test "./test/**/*.test.js" -r v8,console-summary -o ./coverage-reports
1051
+ ```
1052
+
1053
+ 此命令会运行匹配的测试,在终端打印覆盖率摘要,并将 V8 HTML 报告写入 `./coverage-reports/index.html`。请为 glob 加引号,让 Node.js 而不是 shell 处理匹配。
1054
+
1055
+ 对于需要反复执行的测试,可将测试目标和覆盖率选项保存到 `mcr.config.js`(ESM;需在 `package.json` 设置 `"type": "module"`,或将配置命名为 `mcr.config.mjs`):
1056
+
1057
+ ```js
1058
+ export default {
1059
+ test: ['./test/unit/**/*.test.js', './test/integration/**/*.test.js'],
1060
+ reports: ['v8', 'console-summary'],
1061
+ outputDir: './coverage-reports'
1062
+ };
1063
+ ```
1064
+
1065
+ ```sh
1066
+ # 运行配置中的测试目标
1067
+ npx mcr test
1068
+ # 覆盖配置中的测试目标,并在 -- 后透传 Node.js 测试 runner 参数
1069
+ npx mcr test "./test/unit/**/*.test.js" -- --test-reporter=spec
1070
+
1071
+ # 显式指定配置文件
1072
+ npx mcr test "../tests/**/*.test.js" -c ./mcr.config.js
1073
+
1074
+ # 在命令行选择报告
1075
+ npx mcr test "../tests/**/*.test.js" -r v8,console-summary
1076
+
1077
+ # 运行多个测试文件
1078
+ npx mcr test ./test/first.test.js ./test/second.test.js
1079
+
1080
+ # 按测试名称筛选,并指定 reporter
1081
+ npx mcr test "../tests/**/*.test.js" -c ./mcr.config.js -- --test-name-pattern="my test" --test-reporter=spec
1082
+
1083
+ # 将 test 命令放在初始分隔符后
1084
+ npx mcr -c ./mcr.config.js -- test "../tests/**/*.test.js" -- --test-reporter=spec
1085
+ ```
1086
+
1087
+ 命令行指定的测试目标会**覆盖**配置中的 `test`,而不是追加;未指定命令行目标时才使用配置。命令行指定目标时可以不使用配置文件。原有 MCR 参数(如 `-c`、`-r`、`-o`、`--lcov`)可以放在目标之前或之后。Node runner 参数请放在 `--` 后,原样透传,不作为 MCR 参数或目标覆盖项解析。建议为 glob 加引号,避免 shell 提前展开。使用另一种 `mcr [options] -- test ...` 形式时,所有 MCR 参数必须放在第一个 `--` 前,Node runner 参数则放在第二个 `--` 后。
1088
+
1089
+ 测试目标原样传给 `node --test`。MCR 不检查目标类型或文件是否存在,不展开目录/glob,不解析或标准化路径,不转义特殊字符,也不去重;`node --test` 原生接收测试文件路径或 glob 形式的位置参数,而不是 JavaScript 配置对象。MCR 配置中的字符串表示一个这样的参数(不会按空格拆分),字符串数组表示多个按原顺序传递的参数;`null` 仅是 MCR 跳过测试的默认值,不会传给 Node。测试发现、匹配、参数解释及错误行为均交给 Node。相对路径以当前工作目录为基准,而不是配置文件目录。不承诺直接指定目录会递归发现测试;递归匹配请使用 `"../tests/**/*.test.js"` 等 glob,行为以当前 Node 版本的原生支持为准。TypeScript 执行能力取决于 Node 版本或 `--import tsx` 等加载器。
1090
+
1091
+ `test` 默认为 `null`;未指定命令行目标,且 `test` 未配置或为 `null`、空字符串、`[]` 时跳过测试,也不生成报告。MCR 仅判断是否提供了目标,无效或未匹配目标由 Node 自行处理。如果没有产生覆盖率数据且无法生成报告,命令仍会在报告生成阶段失败。显式指定的配置文件不存在或配置加载失败时,也会失败退出,不会静默跳过测试。测试失败仍生成覆盖率报告,并保留非零退出状态;报告生成失败同样非零退出。支持现有覆盖率配置和 CLI hooks,可按需通过过滤器排除测试文件。**`test` 配置在其他 CLI 命令和 API 调用中均不生效**,包括 `merge` 和 `generate()`。
1092
+
1093
+ 其他 CLI 选项参见[命令行](#command-line)。
1094
+
929
1095
  ## Merge Coverage Reports
930
1096
  以下这些使用场景可能需要使用合并覆盖率报告:
931
1097
  - 多个执行环境,比如Node.js服务端,以及浏览器客户端,比如`Next.js`
@@ -945,12 +1111,12 @@ mcr --import tsx -c mcr.config.ts node ./test.js
945
1111
  ```js
946
1112
  const coverageOptions = {
947
1113
  name: 'My Unit Test Coverage Report',
948
- outputDir: "./coverage-reports/unit",
1114
+ outputDir: './coverage-reports/unit',
949
1115
  reports: [
950
1116
  ['raw', {
951
1117
  // relative path will be "./coverage-reports/unit/raw"
952
1118
  // defaults to raw
953
- outputDir: "raw"
1119
+ outputDir: 'raw'
954
1120
  }],
955
1121
  ['v8'],
956
1122
  ['console-details']
@@ -966,8 +1132,8 @@ const coverageOptions = {
966
1132
  - 然后创建一个`merge-coverage.js`文件,使用`inputDir`参数导入`raw`数据,来生成合并的覆盖率报告.
967
1133
  ```js
968
1134
  // merge-coverage.js
969
- const fs = require('fs');
970
- const { CoverageReport } = require('monocart-coverage-reports');
1135
+ import fs from 'fs';
1136
+ import { CoverageReport } from 'monocart-coverage-reports';
971
1137
  const inputDir = [
972
1138
  './coverage-reports/unit/raw',
973
1139
  './coverage-reports/e2e/raw'
@@ -986,7 +1152,7 @@ const coverageOptions = {
986
1152
  '**/node_modules/**': false,
987
1153
  '**/src/**': true
988
1154
  },
989
-
1155
+
990
1156
  sourcePath: (filePath, info) => {
991
1157
  // Unify the file path for the same files
992
1158
  // For example, the file index.js has different paths:
@@ -1000,7 +1166,7 @@ const coverageOptions = {
1000
1166
  ['v8'],
1001
1167
  ['console-details']
1002
1168
  ],
1003
-
1169
+
1004
1170
  onEnd: () => {
1005
1171
  // remove the raw files if no longer needed
1006
1172
  // inputDir.forEach((p) => {
@@ -1030,7 +1196,7 @@ await new CoverageReport(coverageOptions).generate();
1030
1196
  > 常见问题
1031
1197
  ### Unexpected coverage
1032
1198
  覆盖率看起来不正确,多数情况是因为sourcemap转换的问题导致的. 可以先尝试设置构建工具的 `minify=false` 也就是不要压缩代码来解决。下面来看看sourcemap存在问题的具体原因:
1033
- ```js
1199
+ ```text
1034
1200
  const a = tf ? 'true' : 'false';
1035
1201
  ^ ^ ^
1036
1202
  m1 p m2
@@ -1046,21 +1212,21 @@ const a = tf ? 'true' : 'false';
1046
1212
  ### Unparsable source
1047
1213
  源码无法解析问题。由上面我们知道`MCR`通过分析源码的AST获取更多指标的覆盖率信息,但源码如果不是标准的 ECMAScript,比如`ts`, `jsx`这些,那么分析的时候就会报错,此时我们可以手动来编译这些文件(可行但不推荐).
1048
1214
  ```js
1049
- import * as fs from "fs";
1050
- import * as path from "path";
1051
- import { fileURLToPath } from "url";
1215
+ import * as fs from 'fs';
1216
+ import * as path from 'path';
1217
+ import { fileURLToPath } from 'url';
1052
1218
  import * as TsNode from 'ts-node';
1053
1219
  const coverageOptions = {
1054
- onEntry: async (entry) => {
1055
- const filePath = fileURLToPath(entry.url)
1056
- const originalSource = fs.readFileSync(filePath).toString("utf-8");
1220
+ onEntry: (entry) => {
1221
+ const filePath = fileURLToPath(entry.url);
1222
+ const originalSource = fs.readFileSync(filePath).toString('utf-8');
1057
1223
  const fileName = path.basename(filePath);
1058
1224
  const tn = TsNode.create({});
1059
1225
  const source = tn.compile(originalSource, fileName);
1060
1226
  entry.fake = false;
1061
1227
  entry.source = source;
1062
1228
  }
1063
- }
1229
+ };
1064
1230
  ```
1065
1231
 
1066
1232
  ### JavaScript heap out of memory
@@ -1101,9 +1267,30 @@ const coverageOptions = {
1101
1267
 
1102
1268
  - 使用环境变量`MCR_LOG_TIME`显示时间日志
1103
1269
  ```js
1104
- process.env.MCR_LOG_TIME = true
1270
+ process.env.MCR_LOG_TIME = true;
1105
1271
  ```
1106
1272
 
1273
+ ### CSS 覆盖率与 sourcemap
1274
+
1275
+ 多个 bundle 映射到同一原始文件时,CSS 覆盖率取并集。
1276
+
1277
+ 对于 `all` 无法直接解析的预处理器语法,请使用 `all.transformer` 编译为 CSS,设置 `entry.type = 'css'`,并保留 sourcemap。
1278
+
1279
+ #### Sass mixin 与 `@include`
1280
+
1281
+ Sass `@include` 可能将一条生成规则映射到不连续的源码位置。规则无法可靠映射时,MCR 会将**整个 bundle 按生成的 CSS 展示**,而不是预处理器源文件;该 bundle 的覆盖率也不会合并到原始源码的覆盖率中。
1282
+
1283
+ #### 未测试预处理源文件的行覆盖率
1284
+
1285
+ 通过 `all` 加入的未测试文件,其字节覆盖率为 `0%`,包括 sourcemap 还原的原始文件。解析或映射规则之外的行(如变量、未使用的 mixin)可能显示为已覆盖;请以 **bytes** 覆盖率为准。
1286
+
1287
+ #### 压缩后 sourcemap 可能不准确
1288
+ CSS 压缩可能产生不准确的 sourcemap,导致报告回退到生成的 CSS。遇到映射不准确时,可尝试**禁用 CSS 压缩**。
1289
+
1290
+ #### V8 会将整个 block 视为已覆盖
1291
+
1292
+ Chromium 可能将整个 media query 或嵌套 CSS block 标记为已覆盖。可考虑使用 [postcss-atomic-selectors](https://www.npmjs.com/package/postcss-atomic-selectors) 作为临时解决方案。
1293
+
1107
1294
  ## Integration with Any Testing Framework
1108
1295
  通用集成方案
1109
1296
  - 通过API接口在程序集成
@@ -1185,7 +1372,7 @@ mcr mocha ./test/**/*.js
1185
1372
  ```sh
1186
1373
  cross-env NODE_OPTIONS="--import tsx" npx mcr tsx ./src/example.ts
1187
1374
  cross-env NODE_OPTIONS="--import tsx" npx mcr mocha ./test/**/*.ts
1188
- # Node.js v18.19.0 +
1375
+ # MCR 3.x: Node.js >= 22.15.0
1189
1376
  mcr --import tsx tsx ./src/example.ts
1190
1377
  mcr --import tsx mocha ./test/**/*.ts
1191
1378
  ```
@@ -1206,7 +1393,7 @@ mcr ava
1206
1393
  - Supports native `codecov` built-in report ([specification](https://docs.codecov.com/docs/codecov-custom-coverage-format))
1207
1394
  ```js
1208
1395
  const coverageOptions = {
1209
- outputDir: "./coverage-reports",
1396
+ outputDir: './coverage-reports',
1210
1397
  reports: [
1211
1398
  ['codecov']
1212
1399
  ]
@@ -1226,7 +1413,7 @@ const coverageOptions = {
1226
1413
  - Using `lcov` report:
1227
1414
  ```js
1228
1415
  const coverageOptions = {
1229
- outputDir: "./coverage-reports",
1416
+ outputDir: './coverage-reports',
1230
1417
  lcov: true
1231
1418
  };
1232
1419
  ```
@@ -1244,7 +1431,7 @@ const coverageOptions = {
1244
1431
  - Using `lcov` report:
1245
1432
  ```js
1246
1433
  const coverageOptions = {
1247
- outputDir: "./coverage-reports",
1434
+ outputDir: './coverage-reports',
1248
1435
  lcov: true
1249
1436
  };
1250
1437
  ```
@@ -1277,17 +1464,25 @@ const coverageOptions = {
1277
1464
  ```
1278
1465
 
1279
1466
  ## Contributing
1280
- - Node.js 22+(推荐的开发版本)
1467
+ - 推荐使用最新 Node.js 24 LTS 开发;包的最低运行版本为 22.15.0。开发工具可能要求高于最低运行版本的补丁版本。
1281
1468
  - VSCode (extensions: eslint/stylelint/vue)
1282
1469
  ```sh
1283
1470
  npm install
1284
1471
  npx playwright install --with-deps
1285
1472
 
1286
1473
  npm run build
1474
+ npm run lint
1287
1475
  npm run test
1288
1476
 
1289
1477
  npm run dev
1290
1478
  ```
1479
+ README 中的 JavaScript 示例通过 `npm run lint:readme` 和 `npm run lint` 使用项目的 ESLint 配置检查。仅针对省略上下文的片段关闭 `no-undef` 和 `no-unused-vars`,语法、代码风格及其他正确性规则保持启用。TypeScript 声明使用 `ts` 代码块,示意图使用 `text`。刻意保留非标准格式的演示可继续使用 `js` 高亮,并在区域前后分别添加独占一行的 `<!-- readme-lint-disable -->` 和 `<!-- readme-lint-enable -->` 隐藏注释;该区域不参与 lint 和自动修复。标记必须配对且不能嵌套,代码块内部出现的标记仅视为演示文本。
1480
+
1481
+ ```sh
1482
+ npm run lint:readme
1483
+ npm run lint:readme -- --fix
1484
+ ```
1485
+
1291
1486
  - Refreshing `eol=lf` for snapshot of test (Windows)
1292
1487
  ```sh
1293
1488
  git add . -u
@@ -1298,4 +1493,7 @@ npm run eol
1298
1493
 
1299
1494
  ## Thanks
1300
1495
  - [@bcoe](https://github.com/bcoe)
1301
- - [@edumserrano](https://github.com/edumserrano)
1496
+ - [@edumserrano](https://github.com/edumserrano)
1497
+
1498
+
1499
+ ![Monocart Coverage Reports](./assets/og-image.png)