monocart-coverage-reports 3.0.0 → 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.
- package/README.md +47 -2
- package/README.zh-Hans.md +47 -2
- package/lib/converter/ast.js +23 -2
- package/lib/converter/collect-source-maps.js +33 -12
- package/lib/converter/converter.js +84 -15
- package/lib/converter/css-coverage.js +76 -0
- package/lib/converter/find-original-range.js +211 -3
- package/lib/converter/untested.js +4 -6
- package/lib/packages/monocart-coverage-assets.js +1 -1
- package/lib/packages/monocart-coverage-converter.js +11 -11
- package/lib/packages/monocart-coverage-vendor.js +1 -1
- package/lib/packages/monocart-coverage-vendor.js.LICENSE +3 -3
- package/lib/platform/share.js +7 -0
- package/lib/utils/source-path.js +21 -6
- package/lib/utils/util.js +6 -1
- package/package.json +161 -153
- package/skills/monocart-coverage-reports/SKILL.md +2 -2
- package/skills/monocart-coverage-reports/references/best-practices.md +218 -0
- package/skills/monocart-coverage-reports/references/collection.md +2 -0
- package/skills/monocart-coverage-reports/references/repository-map.md +7 -3
- package/lib/packages/chunks/chunk-RVOSNMVU.js +0 -2
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
> A JavaScript code coverage tool to generate native [V8](https://v8.dev/blog/javascript-code-coverage) reports or [Istanbul](https://istanbul.js.org/) reports.
|
|
14
14
|
|
|
15
15
|
* [Usage](#usage)
|
|
16
|
+
* [Best Practices](#best-practices)
|
|
16
17
|
* [Options](#options)
|
|
17
18
|
* [Available Reports](#available-reports)
|
|
18
19
|
* [Compare Reports](#compare-reports)
|
|
@@ -103,8 +104,22 @@ mcr node my-app.js -r v8,console-details
|
|
|
103
104
|
```
|
|
104
105
|
For more information, see [Command Line](#command-line)
|
|
105
106
|
|
|
107
|
+
## Best Practices
|
|
108
|
+
|
|
109
|
+
For new test setups, prefer native coverage collection without an instrumentation step:
|
|
110
|
+
|
|
111
|
+
| Target | Recommended setup | Responsibility |
|
|
112
|
+
| --- | --- | --- |
|
|
113
|
+
| Node.js | **Node.js native test runner + `mcr test`** | `node:test` runs assertions; MCR automatically collects native V8 JavaScript coverage and generates reports. |
|
|
114
|
+
| Browser | **Playwright Test + MCR** | Playwright runs assertions; automatic fixtures collect Chromium JS/CSS coverage; MCR merges it and generates reports. |
|
|
115
|
+
| Browser with integrated test reporting | **Playwright Test + [monocart-reporter](https://github.com/cenfun/monocart-reporter#code-coverage-report)** | The reporter already integrates MCR; configure its coverage option and submit coverage from fixtures for unified test and coverage reporting. |
|
|
116
|
+
|
|
117
|
+
For detailed setup instructions and examples, see the [best-practices guide](skills/monocart-coverage-reports/references/best-practices.md).
|
|
118
|
+
|
|
106
119
|
## AI Agent Skill
|
|
107
120
|
|
|
121
|
+
For AI-assisted development in a source checkout, start with [AGENTS.md](https://github.com/cenfun/monocart-coverage-reports/blob/main/AGENTS.md). It describes the source layout, development commands, validation, and safety boundaries. `.github/copilot-instructions.md` provides a self-contained summary of the core rules for GitHub Copilot. These checkout-only guides are not included in the npm package and are separate from the reusable integration skill below.
|
|
122
|
+
|
|
108
123
|
The reusable [Monocart Coverage Reports skill](skills/monocart-coverage-reports/SKILL.md) helps AI coding agents collect coverage, configure the CLI, debug sourcemaps and filters, merge raw reports, and enforce CI thresholds. It uses a main `SKILL.md` with task-specific references loaded on demand, targeting MCR 3.x only and checking its runtime requirements.
|
|
109
124
|
|
|
110
125
|
The npm package includes `skills/monocart-coverage-reports/`. Copy this **entire directory**, including `references/`, into your agent's supported skills location, or ask the agent to read `SKILL.md` directly. Installing MCR does not automatically register the skill or modify agent settings. The instructions are in English; requests and responses can use any language.
|
|
@@ -354,11 +369,18 @@ const [jsCoverage, cssCoverage] = await Promise.all([
|
|
|
354
369
|
const coverageData = [... jsCoverage, ... cssCoverage];
|
|
355
370
|
|
|
356
371
|
```
|
|
357
|
-
|
|
372
|
+
For Playwright Test automatic fixtures, see [fixtures.js](https://github.com/cenfun/playwright-coverage/blob/main/fixtures.js) in the complete [playwright-coverage example](https://github.com/cenfun/playwright-coverage). The [best-practices guide](skills/monocart-coverage-reports/references/best-practices.md#complete-project-example) explains the integration.
|
|
358
373
|
For more examples, see [./test/test-v8.js](./test/test-v8.js), [css](./test/test-css.js)
|
|
359
374
|
|
|
360
375
|
|
|
361
376
|
### Collecting Raw V8 Coverage Data with Puppeteer
|
|
377
|
+
|
|
378
|
+
> Depending on your package manager, Puppeteer may not download a browser during installation. If no browser is available, install one manually:
|
|
379
|
+
>
|
|
380
|
+
> ```sh
|
|
381
|
+
> npx puppeteer browsers install chrome
|
|
382
|
+
> ```
|
|
383
|
+
|
|
362
384
|
```js
|
|
363
385
|
await Promise.all([
|
|
364
386
|
page.coverage.startJSCoverage({
|
|
@@ -716,6 +738,8 @@ const coverageOptions = {
|
|
|
716
738
|
}
|
|
717
739
|
};
|
|
718
740
|
```
|
|
741
|
+
Built-in report path normalization is best-effort; it cannot fully reconcile every nonstandard, virtual, or foreign-platform path. Use `sourcePath` to correct these special cases rather than relying on automatic inference. The callback receives the normalized `filePath`; for V8 entries and sourcemap sources, `info.url` provides the original URL before report path normalization (including the sourcemap's `sourceRoot`, when present). This rewrites report paths, not the filesystem paths used to load missing source content. When `sourcesContent` is missing, local sources are loaded relative to the external map's URL (or the generated file for inline maps), with `sourceRoot` applied as a directory.
|
|
742
|
+
|
|
719
743
|
It also supports simple key/value replacement:
|
|
720
744
|
```js
|
|
721
745
|
const coverageOptions = {
|
|
@@ -741,7 +765,7 @@ const coverageOptions = {
|
|
|
741
765
|
```
|
|
742
766
|
|
|
743
767
|
## Adding Empty Coverage for Untested Files
|
|
744
|
-
|
|
768
|
+
Untested files are excluded by default. Use `all` to include them with 0% coverage, even when no coverage data was collected. For the same original source, real V8 coverage takes precedence over empty entries.
|
|
745
769
|
```js
|
|
746
770
|
const coverageOptions = {
|
|
747
771
|
all: './src'
|
|
@@ -1243,6 +1267,27 @@ const coverageOptions = {
|
|
|
1243
1267
|
process.env.MCR_LOG_TIME = true;
|
|
1244
1268
|
```
|
|
1245
1269
|
|
|
1270
|
+
### CSS coverage issues
|
|
1271
|
+
|
|
1272
|
+
CSS coverage from multiple bundles is merged as a union for each original source.
|
|
1273
|
+
|
|
1274
|
+
For preprocessor syntax that `all` cannot parse directly, use `all.transformer` to compile to CSS, set `entry.type = 'css'`, and preserve the sourcemap.
|
|
1275
|
+
|
|
1276
|
+
#### Sass mixins and `@include`
|
|
1277
|
+
|
|
1278
|
+
Sass `@include` can map a generated rule to non-contiguous source locations. When a rule cannot be reliably mapped, MCR reports the **whole bundle as generated CSS** instead of the preprocessor sources. That bundle's coverage is not merged into the original-source coverage.
|
|
1279
|
+
|
|
1280
|
+
#### Line coverage of untested preprocessor sources
|
|
1281
|
+
|
|
1282
|
+
Untested files included through `all` have `0%` byte coverage, including sourcemap originals. Lines outside parsed or mapped rules (such as variables or unused mixins) may appear covered; use **bytes** coverage for these files.
|
|
1283
|
+
|
|
1284
|
+
#### Source map can be inaccurate after minification
|
|
1285
|
+
CSS minification can produce imprecise sourcemaps and trigger fallback to generated CSS. If source mapping is inaccurate, try **disabling CSS minification**.
|
|
1286
|
+
|
|
1287
|
+
#### V8 covered the whole block
|
|
1288
|
+
|
|
1289
|
+
Chromium may mark an entire media query or nested CSS block as covered. Consider [postcss-atomic-selectors](https://www.npmjs.com/package/postcss-atomic-selectors) as a potential workaround.
|
|
1290
|
+
|
|
1246
1291
|
## Integration with Any Testing Framework
|
|
1247
1292
|
- API
|
|
1248
1293
|
- Collecting coverage data when any stage of the test is completed, and adding the coverage data to the coverage reporter. `await mcr.add(coverageData)`
|
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)
|
|
@@ -103,8 +104,22 @@ mcr node my-app.js -r v8,console-details
|
|
|
103
104
|
```
|
|
104
105
|
参见 [命令行](#command-line)
|
|
105
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
|
+
|
|
106
119
|
## AI Agent Skill
|
|
107
120
|
|
|
121
|
+
使用 AI 辅助开发本仓库源码时,请先阅读 [AGENTS.md](https://github.com/cenfun/monocart-coverage-reports/blob/main/AGENTS.md),了解源码结构、开发命令、验证流程和安全边界。`.github/copilot-instructions.md` 为 GitHub Copilot 提供可独立使用的核心规则摘要。这些指南仅用于源码仓库,不随 npm 包发布,与下面可复用的集成技能相互独立。
|
|
122
|
+
|
|
108
123
|
项目提供可复用的 [Monocart Coverage Reports 技能](skills/monocart-coverage-reports/SKILL.md),帮助 AI coding agent 采集覆盖率、配置 CLI、排查 sourcemap 和过滤问题、合并 raw 报告及设置 CI 阈值。结构为主 `SKILL.md` 加按需加载的专题参考文档,仅面向 MCR 3.x,并检查其运行环境要求。
|
|
109
124
|
|
|
110
125
|
npm 包包含 `skills/monocart-coverage-reports/`。请将**整个目录**(包括 `references/`)复制到 agent 支持的 skills 目录,或直接让 agent 读取 `SKILL.md`。安装 MCR 不会自动注册技能或修改 agent 配置。技能说明使用英文,但提问和回复可以使用中文或其他语言。
|
|
@@ -356,11 +371,18 @@ const [jsCoverage, cssCoverage] = await Promise.all([
|
|
|
356
371
|
const coverageData = [... jsCoverage, ... cssCoverage];
|
|
357
372
|
|
|
358
373
|
```
|
|
359
|
-
|
|
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)。
|
|
360
375
|
参见例子 [./test/test-v8.js](./test/test-v8.js), [css](./test/test-css.js)
|
|
361
376
|
|
|
362
377
|
|
|
363
378
|
### Collecting Raw V8 Coverage Data with Puppeteer
|
|
379
|
+
|
|
380
|
+
> 取决于包管理器配置,Puppeteer 可能不会在安装依赖时下载浏览器。如果没有可用浏览器,请手动安装:
|
|
381
|
+
>
|
|
382
|
+
> ```sh
|
|
383
|
+
> npx puppeteer browsers install chrome
|
|
384
|
+
> ```
|
|
385
|
+
|
|
364
386
|
使用Puppeteer的覆盖接口收集覆盖率数据,注意Puppeteer默认不会提供原生V8的覆盖率数据,需要设置`includeRawScriptCoverage`
|
|
365
387
|
```js
|
|
366
388
|
await Promise.all([
|
|
@@ -719,6 +741,8 @@ const coverageOptions = {
|
|
|
719
741
|
}
|
|
720
742
|
};
|
|
721
743
|
```
|
|
744
|
+
内置报告路径规范化采用尽力而为的原则,无法完全兼容所有非标准、虚拟或来自其他操作系统的路径。此类特殊情况应通过 `sourcePath` 自行修正,而不是依赖自动推断。回调接收规范化后的 `filePath`;对于 V8 入口和 sourcemap 源文件,`info.url` 提供报告路径规范化前的原始 URL(sourcemap 存在 `sourceRoot` 时包含该前缀)。它修改的是报告路径,不是读取缺失源码时使用的文件系统路径。缺少 `sourcesContent` 时,本地源码相对于外部 map 的 URL 加载(内联 map 则相对于生成文件),并将 `sourceRoot` 作为目录应用。
|
|
745
|
+
|
|
722
746
|
它也支持简单key/value的替换:
|
|
723
747
|
```js
|
|
724
748
|
const coverageOptions = {
|
|
@@ -744,7 +768,7 @@ const coverageOptions = {
|
|
|
744
768
|
```
|
|
745
769
|
|
|
746
770
|
## Adding Empty Coverage for Untested Files
|
|
747
|
-
|
|
771
|
+
默认不包含未测试文件。使用 `all` 可将它们以 0% 覆盖率加入报告,即使没有采集到任何覆盖率数据也可生成报告。同一原始源码的真实 V8 覆盖率优先于空覆盖率条目。
|
|
748
772
|
```js
|
|
749
773
|
const coverageOptions = {
|
|
750
774
|
all: './src'
|
|
@@ -1246,6 +1270,27 @@ const coverageOptions = {
|
|
|
1246
1270
|
process.env.MCR_LOG_TIME = true;
|
|
1247
1271
|
```
|
|
1248
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
|
+
|
|
1249
1294
|
## Integration with Any Testing Framework
|
|
1250
1295
|
通用集成方案
|
|
1251
1296
|
- 通过API接口在程序集成
|
package/lib/converter/ast.js
CHANGED
|
@@ -345,8 +345,29 @@ const getCssAstInfo = (item, coverageList) => {
|
|
|
345
345
|
endOffset: it.end
|
|
346
346
|
};
|
|
347
347
|
});
|
|
348
|
-
|
|
349
|
-
|
|
348
|
+
let ast;
|
|
349
|
+
try {
|
|
350
|
+
ast = parseCss(source);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
// Untested files may use preprocessor syntax that postcss cannot parse.
|
|
353
|
+
// Mirror the JS empty-file fallback: report the whole file as uncovered
|
|
354
|
+
// instead of aborting report generation.
|
|
355
|
+
if (empty) {
|
|
356
|
+
coverageList.push({
|
|
357
|
+
start: 0,
|
|
358
|
+
end: source.length,
|
|
359
|
+
count: 0
|
|
360
|
+
});
|
|
361
|
+
return {
|
|
362
|
+
functions: [],
|
|
363
|
+
branches: [],
|
|
364
|
+
statements: []
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
throw new Error(`parse css error in ${item.sourcePath}`, {
|
|
368
|
+
cause: error
|
|
369
|
+
});
|
|
370
|
+
}
|
|
350
371
|
|
|
351
372
|
addCssRules(ast.nodes, coverageList, coveredRanges, empty);
|
|
352
373
|
|
|
@@ -121,27 +121,41 @@ const resolveMapUrl = (filename, url) => {
|
|
|
121
121
|
|
|
122
122
|
const resolveSourcesContent = (data, url) => {
|
|
123
123
|
|
|
124
|
-
const {
|
|
124
|
+
const {
|
|
125
|
+
sources, sourcesContent, sourceRoot
|
|
126
|
+
} = data;
|
|
127
|
+
// sourceRoot is a directory, even when its trailing slash is omitted.
|
|
128
|
+
let rootUrl;
|
|
129
|
+
if (typeof sourceRoot === 'string' && sourceRoot) {
|
|
130
|
+
rootUrl = Util.resolveUrl(sourceRoot.endsWith('/') ? sourceRoot : `${sourceRoot}/`, url);
|
|
131
|
+
}
|
|
125
132
|
|
|
126
133
|
// sources [1,2,3]
|
|
127
134
|
// sourcesContent could be [null, null, "content"]
|
|
128
135
|
// some of contents could be missed
|
|
129
136
|
|
|
130
137
|
let hasSourceContent = false;
|
|
138
|
+
const missingSources = [];
|
|
131
139
|
sources.forEach((file, i) => {
|
|
132
140
|
if (typeof sourcesContent[i] === 'string') {
|
|
133
141
|
hasSourceContent = true;
|
|
134
142
|
return;
|
|
135
143
|
}
|
|
136
144
|
|
|
137
|
-
const sourceUrl = Util.resolveUrl(file, url);
|
|
145
|
+
const sourceUrl = Util.resolveUrl(file, rootUrl?.href || url);
|
|
138
146
|
if (sourceUrl) {
|
|
139
|
-
let
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
147
|
+
let content;
|
|
148
|
+
try {
|
|
149
|
+
let sourcePath = sourceUrl.toString();
|
|
150
|
+
// Read actual local paths, not normalized virtual report paths.
|
|
151
|
+
if (sourceUrl.protocol === 'file:') {
|
|
152
|
+
sourcePath = fileURLToPath(sourceUrl);
|
|
153
|
+
}
|
|
154
|
+
content = Util.readFileSync(path.resolve(sourcePath));
|
|
155
|
+
} catch {
|
|
156
|
+
// A foreign or malformed file URL is just an unavailable source.
|
|
157
|
+
// Keep resolving the other sources instead of discarding the map.
|
|
143
158
|
}
|
|
144
|
-
const content = Util.readFileSync(path.resolve(sourcePath));
|
|
145
159
|
if (typeof content === 'string') {
|
|
146
160
|
sourcesContent[i] = content;
|
|
147
161
|
hasSourceContent = true;
|
|
@@ -149,13 +163,18 @@ const resolveSourcesContent = (data, url) => {
|
|
|
149
163
|
}
|
|
150
164
|
}
|
|
151
165
|
|
|
152
|
-
|
|
166
|
+
missingSources.push(i);
|
|
153
167
|
Util.logDebug(EC.red(`failed to load source content: ${file}`));
|
|
154
168
|
|
|
155
169
|
});
|
|
156
170
|
|
|
157
171
|
|
|
158
172
|
if (hasSourceContent) {
|
|
173
|
+
// Only commit placeholders for accepted maps. A rejected map may be
|
|
174
|
+
// cached by the resolver and must not look like valid empty sources later.
|
|
175
|
+
missingSources.forEach((i) => {
|
|
176
|
+
sourcesContent[i] = '';
|
|
177
|
+
});
|
|
159
178
|
return data;
|
|
160
179
|
}
|
|
161
180
|
};
|
|
@@ -233,7 +252,7 @@ const collectSourceMaps = async (v8list, options) => {
|
|
|
233
252
|
for (const item of v8list) {
|
|
234
253
|
|
|
235
254
|
const {
|
|
236
|
-
|
|
255
|
+
url, id, source, sourceMap
|
|
237
256
|
} = item;
|
|
238
257
|
|
|
239
258
|
// source and sourceMap will be saved as separated file (could be cached)
|
|
@@ -258,7 +277,7 @@ const collectSourceMaps = async (v8list, options) => {
|
|
|
258
277
|
|
|
259
278
|
// An explicitly supplied, usable map takes precedence over sourceMappingURL.
|
|
260
279
|
// Keep the original source so V8 offsets remain valid.
|
|
261
|
-
if (
|
|
280
|
+
if (!isUsableSourceMap(sourceData.sourceMap)) {
|
|
262
281
|
delete sourceData.sourceMap;
|
|
263
282
|
const candidates = getSourceMapCandidates(source);
|
|
264
283
|
if (candidates.length) {
|
|
@@ -279,7 +298,7 @@ const collectSourceMaps = async (v8list, options) => {
|
|
|
279
298
|
const { url } = sourceData;
|
|
280
299
|
for (const candidate of candidates) {
|
|
281
300
|
let data;
|
|
282
|
-
let sourceMapUrl;
|
|
301
|
+
let sourceMapUrl = url;
|
|
283
302
|
try {
|
|
284
303
|
if (candidate.inline) {
|
|
285
304
|
data = convertSourceMap.fromComment(candidate.comment).sourcemap;
|
|
@@ -293,7 +312,9 @@ const collectSourceMaps = async (v8list, options) => {
|
|
|
293
312
|
if (!isUsableSourceMap(data)) {
|
|
294
313
|
continue;
|
|
295
314
|
}
|
|
296
|
-
|
|
315
|
+
// Inline maps inherit the generated file's URL. External maps
|
|
316
|
+
// resolve their sources relative to the map, not the bundle.
|
|
317
|
+
const sourceMap = resolveSourceMap(data, sourceMapUrl);
|
|
297
318
|
if (!sourceMap) {
|
|
298
319
|
continue;
|
|
299
320
|
}
|
|
@@ -11,16 +11,18 @@ import { Util } from '../utils/util.js';
|
|
|
11
11
|
|
|
12
12
|
import { findOriginalRange } from './find-original-range.js';
|
|
13
13
|
import { isUncoveredRange } from './source-range-merge.js';
|
|
14
|
+
import { mergeCssBytes } from './css-coverage.js';
|
|
14
15
|
import { handleUncoveredInCovered } from './fake-source-coverage.js';
|
|
15
16
|
import { getJsAstInfo, getCssAstInfo } from './ast.js';
|
|
16
17
|
import { getIgnoredRanges } from './ignore.js';
|
|
17
18
|
|
|
18
19
|
import { getUntestedList } from './untested.js';
|
|
19
|
-
|
|
20
20
|
import {
|
|
21
21
|
sortRanges, dedupeCountRanges, mergeRangesWith
|
|
22
22
|
} from '../utils/dedupe.js';
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
getSourceType, initSourceMapSourcesPath, cssTypes
|
|
25
|
+
} from '../utils/source-path.js';
|
|
24
26
|
|
|
25
27
|
import { decode } from '@jridgewell/sourcemap-codec';
|
|
26
28
|
|
|
@@ -831,16 +833,71 @@ const handleOriginalEmptyBytesCoverage = (state, originalStateMap) => {
|
|
|
831
833
|
});
|
|
832
834
|
};
|
|
833
835
|
|
|
834
|
-
|
|
835
836
|
const handleAllOriginalBytesCoverage = (state, originalStateMap) => {
|
|
836
837
|
|
|
837
|
-
const {
|
|
838
|
+
const {
|
|
839
|
+
js, coverageList, type
|
|
840
|
+
} = state;
|
|
841
|
+
|
|
842
|
+
if (cssTypes.has(type)) {
|
|
843
|
+
// An intentional exclusion is not a broken source map, even when all
|
|
844
|
+
// sources were excluded and there is nothing left to map.
|
|
845
|
+
if (state.filteredSourceIndexes.size > 0 && state.filteredSourceIndexes.size === state.sourceMap.sources.length) {
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
const positionMap = new Map();
|
|
849
|
+
const couldMapAllRanges = coverageList.every((range) => {
|
|
850
|
+
const {
|
|
851
|
+
start, end, originalState, error, filtered
|
|
852
|
+
} = findOriginalRange(range.start, range.end, state, originalStateMap, {
|
|
853
|
+
fixOriginalRange: true
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
if (filtered) {
|
|
857
|
+
return true;
|
|
858
|
+
}
|
|
859
|
+
if (error) {
|
|
860
|
+
return false;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// Offsets are local to each source, not unique across the bundle.
|
|
864
|
+
let positions = positionMap.get(originalState);
|
|
865
|
+
if (!positions) {
|
|
866
|
+
positions = new Set();
|
|
867
|
+
positionMap.set(originalState, positions);
|
|
868
|
+
}
|
|
869
|
+
const positionKey = `${start}-${end}`;
|
|
870
|
+
if (positions.has(positionKey)) {
|
|
871
|
+
return false;
|
|
872
|
+
}
|
|
873
|
+
positions.add(positionKey);
|
|
874
|
+
originalState.bytes.push({
|
|
875
|
+
start, end, count: range.count
|
|
876
|
+
});
|
|
877
|
+
return true;
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
// make sure every range mapped correctly,
|
|
881
|
+
// otherwise discard changes and fall back to dist file to show at least some meaningful coverage.
|
|
882
|
+
// Sass `@include` is a common case: one generated rule maps to both the
|
|
883
|
+
// call site and the mixin body, which cannot be one contiguous original
|
|
884
|
+
// range. See "Sass mixins and @include" in README.
|
|
885
|
+
if (!couldMapAllRanges) {
|
|
886
|
+
Util.logDebug(`Could not map all ranges, fallback to dist file for ${EC.yellow(state.sourcePath)}`);
|
|
887
|
+
state.v8Data.dedupe = false;
|
|
888
|
+
// The fallback is now a real report file, not a debug-only copy.
|
|
889
|
+
delete state.v8Data.debug;
|
|
890
|
+
state.originalList = [];
|
|
891
|
+
originalStateMap.clear();
|
|
892
|
+
generateCoverageForDist(state);
|
|
893
|
+
}
|
|
838
894
|
|
|
839
|
-
// only for js, no sourcemap for css for now
|
|
840
|
-
if (!js) {
|
|
841
895
|
return;
|
|
842
896
|
}
|
|
843
897
|
|
|
898
|
+
if (!js) {
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
844
901
|
// v8 coverage
|
|
845
902
|
coverageList.forEach((block) => {
|
|
846
903
|
block.ranges.forEach((range) => {
|
|
@@ -1046,7 +1103,6 @@ const getOriginalDecodedMappings = (originalDecodedMap, sourceIndex, locator) =>
|
|
|
1046
1103
|
};
|
|
1047
1104
|
|
|
1048
1105
|
// ========================================================================================================
|
|
1049
|
-
|
|
1050
1106
|
const initOriginalList = (state, originalDecodedMap, options) => {
|
|
1051
1107
|
|
|
1052
1108
|
// source filter
|
|
@@ -1054,6 +1110,9 @@ const initOriginalList = (state, originalDecodedMap, options) => {
|
|
|
1054
1110
|
|
|
1055
1111
|
// create original content mappings
|
|
1056
1112
|
const originalStateMap = new Map();
|
|
1113
|
+
if (!state.js) {
|
|
1114
|
+
state.filteredSourceIndexes = new Set();
|
|
1115
|
+
}
|
|
1057
1116
|
|
|
1058
1117
|
const { sources, sourcesContent } = state.sourceMap;
|
|
1059
1118
|
|
|
@@ -1065,7 +1124,7 @@ const initOriginalList = (state, originalDecodedMap, options) => {
|
|
|
1065
1124
|
// filter
|
|
1066
1125
|
// do not change for sourceIndex
|
|
1067
1126
|
if (!sourceFilter(sourcePath)) {
|
|
1068
|
-
|
|
1127
|
+
state.filteredSourceIndexes?.add(sourceIndex);
|
|
1069
1128
|
return;
|
|
1070
1129
|
}
|
|
1071
1130
|
// console.log(sourcePath);
|
|
@@ -1082,14 +1141,12 @@ const initOriginalList = (state, originalDecodedMap, options) => {
|
|
|
1082
1141
|
|
|
1083
1142
|
const decodedMappings = getOriginalDecodedMappings(originalDecodedMap, sourceIndex, locator);
|
|
1084
1143
|
|
|
1085
|
-
// unpacked file always is js
|
|
1086
|
-
|
|
1087
1144
|
const type = getSourceType(sourcePath);
|
|
1088
1145
|
|
|
1089
1146
|
const originalState = {
|
|
1090
1147
|
original: true,
|
|
1091
|
-
//
|
|
1092
|
-
js:
|
|
1148
|
+
// Only known style types are CSS; preserve JS coverage for all other sources.
|
|
1149
|
+
js: !cssTypes.has(type),
|
|
1093
1150
|
type,
|
|
1094
1151
|
source: sourceContent,
|
|
1095
1152
|
sourcePath,
|
|
@@ -1149,6 +1206,12 @@ const collectOriginalList = (state, originalStateMap) => {
|
|
|
1149
1206
|
source
|
|
1150
1207
|
};
|
|
1151
1208
|
|
|
1209
|
+
// Sourcemap gaps must not count as covered bytes for untested files.
|
|
1210
|
+
// Keep the marker so real coverage can replace this entry during merge.
|
|
1211
|
+
if (state.v8Data.empty) {
|
|
1212
|
+
sourceItem.empty = true;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1152
1215
|
// save v8 data and add to originalList
|
|
1153
1216
|
originalState.v8Data = sourceItem;
|
|
1154
1217
|
state.originalList.push(originalState);
|
|
@@ -1695,8 +1758,14 @@ const generateV8DataList = (stateList, options, needsIstanbul) => {
|
|
|
1695
1758
|
// exists item
|
|
1696
1759
|
const prevState = stateMap.get(id);
|
|
1697
1760
|
if (prevState) {
|
|
1698
|
-
//
|
|
1699
|
-
|
|
1761
|
+
// Empty entries and debug copies must not replace real original
|
|
1762
|
+
// coverage, nor contribute a duplicate execution to its merge.
|
|
1763
|
+
if (!prevState.v8Data.empty && !prevState.v8Data.debug) {
|
|
1764
|
+
// An untested build may contain different mapped AST nodes
|
|
1765
|
+
// (for example ES5 helpers). It must not alter real metrics.
|
|
1766
|
+
if (v8Data.empty) {
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1700
1769
|
if (mergeMap.has(id)) {
|
|
1701
1770
|
mergeMap.get(id).push(originalState);
|
|
1702
1771
|
} else {
|
|
@@ -1715,7 +1784,7 @@ const generateV8DataList = (stateList, options, needsIstanbul) => {
|
|
|
1715
1784
|
if (state.js) {
|
|
1716
1785
|
mergeV8Data(state, mergeMap.get(id));
|
|
1717
1786
|
} else {
|
|
1718
|
-
|
|
1787
|
+
state.bytes = mergeCssBytes(mergeMap.get(id).map((it) => it.bytes));
|
|
1719
1788
|
}
|
|
1720
1789
|
}
|
|
1721
1790
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const getCssRangePoints = (groups) => {
|
|
2
|
+
const points = [];
|
|
3
|
+
groups.forEach((ranges) => {
|
|
4
|
+
ranges.forEach(({
|
|
5
|
+
start, end, count
|
|
6
|
+
}) => {
|
|
7
|
+
if (start >= end) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const covered = count > 0 ? 1 : 0;
|
|
11
|
+
const uncovered = count > 0 ? 0 : 1;
|
|
12
|
+
points.push({
|
|
13
|
+
offset: start, covered, uncovered
|
|
14
|
+
}, {
|
|
15
|
+
offset: end, covered: -covered, uncovered: -uncovered
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
return points.sort((a, b) => a.offset - b.offset);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const appendCssRange = (merged, start, end, count) => {
|
|
23
|
+
const last = merged[merged.length - 1];
|
|
24
|
+
if (last && last.end === start && last.count === count) {
|
|
25
|
+
last.end = end;
|
|
26
|
+
} else {
|
|
27
|
+
merged.push({
|
|
28
|
+
start, end, count
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const sweepCssBytes = (groups, uncoveredWins) => {
|
|
34
|
+
const points = getCssRangePoints(groups);
|
|
35
|
+
const merged = [];
|
|
36
|
+
let start = 0;
|
|
37
|
+
let covered = 0;
|
|
38
|
+
let uncovered = 0;
|
|
39
|
+
for (const point of points) {
|
|
40
|
+
const end = point.offset;
|
|
41
|
+
if (start < end && (covered > 0 || uncovered > 0)) {
|
|
42
|
+
const count = uncoveredWins ? Number(uncovered === 0) : Number(covered > 0);
|
|
43
|
+
appendCssRange(merged, start, end, count);
|
|
44
|
+
}
|
|
45
|
+
covered += point.covered;
|
|
46
|
+
uncovered += point.uncovered;
|
|
47
|
+
start = end;
|
|
48
|
+
}
|
|
49
|
+
return merged;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const isSortedDisjoint = (ranges) => {
|
|
53
|
+
let end = -Infinity;
|
|
54
|
+
for (const range of ranges) {
|
|
55
|
+
if (!(range.start >= end && range.start < range.end)) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
end = range.end;
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Within one bundle an uncovered child overrides its covered wrapper. Only
|
|
64
|
+
// after that interpretation may coverage from separate bundles be unioned.
|
|
65
|
+
// Unmapped gaps stay absent from this union; this function does not turn them
|
|
66
|
+
// into covered ranges. Byte summaries still derive covered bytes from total
|
|
67
|
+
// length minus explicit uncovered ranges, so gaps without an uncovered range
|
|
68
|
+
// are reported as covered. Use the line/code views when gaps matter.
|
|
69
|
+
const mergeCssBytes = (groups) => {
|
|
70
|
+
// Ordered, disjoint ranges already have unambiguous per-bundle coverage.
|
|
71
|
+
// Reuse them without allocating intermediate events and normalized ranges.
|
|
72
|
+
const normalized = groups.map((ranges) => (isSortedDisjoint(ranges) ? ranges : sweepCssBytes([ranges], true)));
|
|
73
|
+
return sweepCssBytes(normalized, false);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export { mergeCssBytes };
|