@diff-cov/vitest 0.0.0-stage → 0.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/LICENSE +21 -0
- package/README.md +40 -2
- package/dist/index.cjs +139 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +38 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +124 -0
- package/dist/index.js.map +1 -0
- package/package.json +46 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DiffCov contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,41 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @diff-cov/vitest
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Vitest 4 reporter for diff coverage.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pnpm add -D @diff-cov/vitest
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
// vitest.config.ts
|
|
11
|
+
import { defineConfig } from 'vitest/config'
|
|
12
|
+
import DiffCovReporter from '@diff-cov/vitest'
|
|
13
|
+
|
|
14
|
+
export default defineConfig({
|
|
15
|
+
test: {
|
|
16
|
+
coverage: {
|
|
17
|
+
provider: 'v8',
|
|
18
|
+
include: ['src/**/*.{ts,tsx}'],
|
|
19
|
+
reporter: ['lcov'],
|
|
20
|
+
},
|
|
21
|
+
reporters: ['default', new DiffCovReporter()],
|
|
22
|
+
},
|
|
23
|
+
})
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The reporter reads the in-memory coverage map, not the lcov file. It diffs the
|
|
27
|
+
worktree against the base, so committed, staged, unstaged, and untracked changes
|
|
28
|
+
all count. This is the development-time view, before a commit exists.
|
|
29
|
+
|
|
30
|
+
It is report-only by default. To enforce the threshold:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
new DiffCovReporter({ threshold: 90, failOnError: true })
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Options: `base`, `include`, `exclude`, `threshold`, `perFile`, `missing`,
|
|
37
|
+
`failOnError`, `enabledInWatch`, `config`, `report`.
|
|
38
|
+
|
|
39
|
+
Requires Vitest 4.
|
|
40
|
+
|
|
41
|
+
MIT licensed.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
DiffCovReporter: () => DiffCovReporter,
|
|
24
|
+
default: () => index_default
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
var import_node_fs = require("fs");
|
|
28
|
+
var import_promises = require("fs/promises");
|
|
29
|
+
var import_node_path = require("path");
|
|
30
|
+
var import_core = require("@diff-cov/core");
|
|
31
|
+
var DiffCovReporter = class {
|
|
32
|
+
options;
|
|
33
|
+
ctx = null;
|
|
34
|
+
coverageMap = null;
|
|
35
|
+
runEnded = false;
|
|
36
|
+
done = false;
|
|
37
|
+
reason = "passed";
|
|
38
|
+
constructor(options = {}) {
|
|
39
|
+
this.options = options;
|
|
40
|
+
}
|
|
41
|
+
onInit(ctx) {
|
|
42
|
+
this.ctx = ctx;
|
|
43
|
+
}
|
|
44
|
+
onTestRunStart() {
|
|
45
|
+
this.coverageMap = null;
|
|
46
|
+
this.runEnded = false;
|
|
47
|
+
this.done = false;
|
|
48
|
+
this.reason = "passed";
|
|
49
|
+
}
|
|
50
|
+
onCoverage(coverage) {
|
|
51
|
+
this.coverageMap = coverage;
|
|
52
|
+
if (this.runEnded) void this.finish();
|
|
53
|
+
}
|
|
54
|
+
async onTestRunEnd(_testModules, _unhandledErrors, reason) {
|
|
55
|
+
this.runEnded = true;
|
|
56
|
+
this.reason = reason;
|
|
57
|
+
await this.finish();
|
|
58
|
+
}
|
|
59
|
+
async finish() {
|
|
60
|
+
if (this.done || !this.ctx) return;
|
|
61
|
+
this.done = true;
|
|
62
|
+
const ctx = this.ctx;
|
|
63
|
+
const root = ctx.config.root;
|
|
64
|
+
const watch = Boolean(ctx.config.watch);
|
|
65
|
+
if (watch && !this.options.enabledInWatch) return;
|
|
66
|
+
if (this.reason === "interrupted") return;
|
|
67
|
+
if (!this.coverageMap) {
|
|
68
|
+
ctx.logger.log("diff-cov: coverage was not collected; run Vitest with --coverage.");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const config = await (0, import_core.loadConfig)(root, this.options.config);
|
|
73
|
+
const projectRoot = (() => {
|
|
74
|
+
try {
|
|
75
|
+
return (0, import_node_fs.realpathSync)(root);
|
|
76
|
+
} catch {
|
|
77
|
+
return root;
|
|
78
|
+
}
|
|
79
|
+
})();
|
|
80
|
+
const gitRoot = await (0, import_core.getRepoRoot)(projectRoot);
|
|
81
|
+
const relativeRoot = (0, import_node_path.relative)(gitRoot, projectRoot);
|
|
82
|
+
const pathPrefix = relativeRoot && !relativeRoot.startsWith("..") ? (0, import_core.toPosix)(relativeRoot) : void 0;
|
|
83
|
+
const base = await (0, import_core.resolveBase)(projectRoot, this.options.base ?? config.base ?? "auto");
|
|
84
|
+
const changes = await (0, import_core.collectChanges)(projectRoot, base, {
|
|
85
|
+
scope: "worktree",
|
|
86
|
+
includeUntracked: true
|
|
87
|
+
});
|
|
88
|
+
const report = (0, import_core.buildReport)({
|
|
89
|
+
changes,
|
|
90
|
+
bases: [base],
|
|
91
|
+
scope: "worktree",
|
|
92
|
+
coverage: (0, import_core.fromCoverageMap)(this.coverageMap, {
|
|
93
|
+
root: gitRoot,
|
|
94
|
+
projectRoot,
|
|
95
|
+
reportDir: projectRoot
|
|
96
|
+
}),
|
|
97
|
+
include: this.options.include ?? config.include,
|
|
98
|
+
exclude: this.options.exclude ?? config.exclude,
|
|
99
|
+
missing: this.options.missing ?? config.missing ?? "fail",
|
|
100
|
+
pathPrefix
|
|
101
|
+
});
|
|
102
|
+
const vitestConfig = ctx.config;
|
|
103
|
+
if (vitestConfig.changed || vitestConfig.coverage?.changed) {
|
|
104
|
+
report.warnings.push(
|
|
105
|
+
"Coverage was collected for a subset of tests; files that did not run may be under-reported."
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
const threshold = this.options.threshold ?? config.threshold ?? 0;
|
|
109
|
+
const perFile = this.options.perFile ?? config.perFile;
|
|
110
|
+
const verdict = (0, import_core.evaluateThreshold)(report, {
|
|
111
|
+
total: threshold,
|
|
112
|
+
...perFile !== void 0 ? { perFile } : {}
|
|
113
|
+
});
|
|
114
|
+
ctx.logger.log("");
|
|
115
|
+
ctx.logger.log((0, import_core.formatReport)(report, verdict));
|
|
116
|
+
const reportPath = this.options.report ?? config.report;
|
|
117
|
+
if (reportPath) {
|
|
118
|
+
const path = (0, import_node_path.resolve)(projectRoot, reportPath);
|
|
119
|
+
await (0, import_promises.mkdir)((0, import_node_path.dirname)(path), { recursive: true });
|
|
120
|
+
await (0, import_promises.writeFile)(path, JSON.stringify({ ...report, verdict }, null, 2) + "\n", "utf8");
|
|
121
|
+
}
|
|
122
|
+
if (this.options.failOnError && !verdict.passed && this.reason !== "failed" && !watch) {
|
|
123
|
+
process.exitCode = 1;
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
127
|
+
ctx.logger.error(`diff-cov: ${message}`);
|
|
128
|
+
if (this.options.failOnError && !watch) {
|
|
129
|
+
process.exitCode = 1;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
var index_default = DiffCovReporter;
|
|
135
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
136
|
+
0 && (module.exports = {
|
|
137
|
+
DiffCovReporter
|
|
138
|
+
});
|
|
139
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { realpathSync } from 'node:fs'\nimport { mkdir, writeFile } from 'node:fs/promises'\nimport { dirname, relative, resolve } from 'node:path'\nimport {\n buildReport,\n collectChanges,\n evaluateThreshold,\n formatReport,\n fromCoverageMap,\n getRepoRoot,\n loadConfig,\n resolveBase,\n toPosix,\n type BaseSpec,\n type CoverageMapLike,\n type DiffCovConfig,\n type MissingPolicy,\n type Verdict,\n} from '@diff-cov/core'\nimport type { Reporter, SerializedError, TestModule, TestRunEndReason, Vitest } from 'vitest/node'\n\nexport interface DiffCovReporterOptions {\n /** Base ref, \"auto\" or \"new\". Defaults to the config file, then \"auto\". */\n base?: BaseSpec\n include?: string[]\n exclude?: string[]\n /** Total threshold. Defaults to the config file, then 0 (report only). */\n threshold?: number\n perFile?: number\n missing?: MissingPolicy\n /** Set the process exit code when the threshold fails. */\n failOnError?: boolean\n /** Report in watch mode too. Off by default. */\n enabledInWatch?: boolean\n /** Config file path relative to the project root. */\n config?: string\n /** Write a JSON report to this path. */\n report?: string | null\n}\n\nexport class DiffCovReporter implements Reporter {\n private readonly options: DiffCovReporterOptions\n private ctx: Vitest | null = null\n private coverageMap: CoverageMapLike | null = null\n private runEnded = false\n private done = false\n private reason: TestRunEndReason = 'passed'\n\n constructor(options: DiffCovReporterOptions = {}) {\n this.options = options\n }\n\n onInit(ctx: Vitest): void {\n this.ctx = ctx\n }\n\n onTestRunStart(): void {\n this.coverageMap = null\n this.runEnded = false\n this.done = false\n this.reason = 'passed'\n }\n\n onCoverage(coverage: unknown): void {\n this.coverageMap = coverage as CoverageMapLike\n // Vitest 3 calls onCoverage after onTestRunEnd; Vitest 4 calls it before.\n if (this.runEnded) void this.finish()\n }\n\n async onTestRunEnd(\n _testModules: ReadonlyArray<TestModule>,\n _unhandledErrors: ReadonlyArray<SerializedError>,\n reason: TestRunEndReason,\n ): Promise<void> {\n this.runEnded = true\n this.reason = reason\n await this.finish()\n }\n\n private async finish(): Promise<void> {\n if (this.done || !this.ctx) return\n this.done = true\n\n const ctx = this.ctx\n const root = ctx.config.root\n const watch = Boolean((ctx.config as { watch?: boolean }).watch)\n if (watch && !this.options.enabledInWatch) return\n if (this.reason === 'interrupted') return\n\n if (!this.coverageMap) {\n ctx.logger.log('diff-cov: coverage was not collected; run Vitest with --coverage.')\n return\n }\n\n try {\n const config: DiffCovConfig = await loadConfig(root, this.options.config)\n const projectRoot = (() => {\n try {\n return realpathSync(root)\n } catch {\n return root\n }\n })()\n const gitRoot = await getRepoRoot(projectRoot)\n const relativeRoot = relative(gitRoot, projectRoot)\n const pathPrefix =\n relativeRoot && !relativeRoot.startsWith('..') ? toPosix(relativeRoot) : undefined\n const base = await resolveBase(projectRoot, this.options.base ?? config.base ?? 'auto')\n const changes = await collectChanges(projectRoot, base, {\n scope: 'worktree',\n includeUntracked: true,\n })\n const report = buildReport({\n changes,\n bases: [base],\n scope: 'worktree',\n coverage: fromCoverageMap(this.coverageMap, {\n root: gitRoot,\n projectRoot,\n reportDir: projectRoot,\n }),\n include: this.options.include ?? config.include,\n exclude: this.options.exclude ?? config.exclude,\n missing: this.options.missing ?? config.missing ?? 'fail',\n pathPrefix,\n })\n\n const vitestConfig = ctx.config as {\n changed?: boolean | string\n coverage?: { changed?: boolean | string }\n }\n if (vitestConfig.changed || vitestConfig.coverage?.changed) {\n report.warnings.push(\n 'Coverage was collected for a subset of tests; files that did not run may be under-reported.',\n )\n }\n\n const threshold = this.options.threshold ?? config.threshold ?? 0\n const perFile = this.options.perFile ?? config.perFile\n const verdict = evaluateThreshold(report, {\n total: threshold,\n ...(perFile !== undefined ? { perFile } : {}),\n })\n\n ctx.logger.log('')\n ctx.logger.log(formatReport(report, verdict))\n\n const reportPath = this.options.report ?? config.report\n if (reportPath) {\n const path = resolve(projectRoot, reportPath)\n await mkdir(dirname(path), { recursive: true })\n await writeFile(path, JSON.stringify({ ...report, verdict }, null, 2) + '\\n', 'utf8')\n }\n\n if (this.options.failOnError && !verdict.passed && this.reason !== 'failed' && !watch) {\n process.exitCode = 1\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n ctx.logger.error(`diff-cov: ${message}`)\n if (this.options.failOnError && !watch) {\n process.exitCode = 1\n }\n }\n }\n}\n\nexport default DiffCovReporter\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { BaseSpec, MissingPolicy } from '@diff-cov/core';
|
|
2
|
+
import { Reporter, Vitest, TestModule, SerializedError, TestRunEndReason } from 'vitest/node';
|
|
3
|
+
|
|
4
|
+
interface DiffCovReporterOptions {
|
|
5
|
+
/** Base ref, "auto" or "new". Defaults to the config file, then "auto". */
|
|
6
|
+
base?: BaseSpec;
|
|
7
|
+
include?: string[];
|
|
8
|
+
exclude?: string[];
|
|
9
|
+
/** Total threshold. Defaults to the config file, then 0 (report only). */
|
|
10
|
+
threshold?: number;
|
|
11
|
+
perFile?: number;
|
|
12
|
+
missing?: MissingPolicy;
|
|
13
|
+
/** Set the process exit code when the threshold fails. */
|
|
14
|
+
failOnError?: boolean;
|
|
15
|
+
/** Report in watch mode too. Off by default. */
|
|
16
|
+
enabledInWatch?: boolean;
|
|
17
|
+
/** Config file path relative to the project root. */
|
|
18
|
+
config?: string;
|
|
19
|
+
/** Write a JSON report to this path. */
|
|
20
|
+
report?: string | null;
|
|
21
|
+
}
|
|
22
|
+
declare class DiffCovReporter implements Reporter {
|
|
23
|
+
private readonly options;
|
|
24
|
+
private ctx;
|
|
25
|
+
private coverageMap;
|
|
26
|
+
private runEnded;
|
|
27
|
+
private done;
|
|
28
|
+
private reason;
|
|
29
|
+
constructor(options?: DiffCovReporterOptions);
|
|
30
|
+
onInit(ctx: Vitest): void;
|
|
31
|
+
onTestRunStart(): void;
|
|
32
|
+
onCoverage(coverage: unknown): void;
|
|
33
|
+
onTestRunEnd(_testModules: ReadonlyArray<TestModule>, _unhandledErrors: ReadonlyArray<SerializedError>, reason: TestRunEndReason): Promise<void>;
|
|
34
|
+
private finish;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { DiffCovReporter, DiffCovReporter as default };
|
|
38
|
+
export type { DiffCovReporterOptions };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { BaseSpec, MissingPolicy } from '@diff-cov/core';
|
|
2
|
+
import { Reporter, Vitest, TestModule, SerializedError, TestRunEndReason } from 'vitest/node';
|
|
3
|
+
|
|
4
|
+
interface DiffCovReporterOptions {
|
|
5
|
+
/** Base ref, "auto" or "new". Defaults to the config file, then "auto". */
|
|
6
|
+
base?: BaseSpec;
|
|
7
|
+
include?: string[];
|
|
8
|
+
exclude?: string[];
|
|
9
|
+
/** Total threshold. Defaults to the config file, then 0 (report only). */
|
|
10
|
+
threshold?: number;
|
|
11
|
+
perFile?: number;
|
|
12
|
+
missing?: MissingPolicy;
|
|
13
|
+
/** Set the process exit code when the threshold fails. */
|
|
14
|
+
failOnError?: boolean;
|
|
15
|
+
/** Report in watch mode too. Off by default. */
|
|
16
|
+
enabledInWatch?: boolean;
|
|
17
|
+
/** Config file path relative to the project root. */
|
|
18
|
+
config?: string;
|
|
19
|
+
/** Write a JSON report to this path. */
|
|
20
|
+
report?: string | null;
|
|
21
|
+
}
|
|
22
|
+
declare class DiffCovReporter implements Reporter {
|
|
23
|
+
private readonly options;
|
|
24
|
+
private ctx;
|
|
25
|
+
private coverageMap;
|
|
26
|
+
private runEnded;
|
|
27
|
+
private done;
|
|
28
|
+
private reason;
|
|
29
|
+
constructor(options?: DiffCovReporterOptions);
|
|
30
|
+
onInit(ctx: Vitest): void;
|
|
31
|
+
onTestRunStart(): void;
|
|
32
|
+
onCoverage(coverage: unknown): void;
|
|
33
|
+
onTestRunEnd(_testModules: ReadonlyArray<TestModule>, _unhandledErrors: ReadonlyArray<SerializedError>, reason: TestRunEndReason): Promise<void>;
|
|
34
|
+
private finish;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export { DiffCovReporter, DiffCovReporter as default };
|
|
38
|
+
export type { DiffCovReporterOptions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { realpathSync } from "fs";
|
|
3
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
4
|
+
import { dirname, relative, resolve } from "path";
|
|
5
|
+
import {
|
|
6
|
+
buildReport,
|
|
7
|
+
collectChanges,
|
|
8
|
+
evaluateThreshold,
|
|
9
|
+
formatReport,
|
|
10
|
+
fromCoverageMap,
|
|
11
|
+
getRepoRoot,
|
|
12
|
+
loadConfig,
|
|
13
|
+
resolveBase,
|
|
14
|
+
toPosix
|
|
15
|
+
} from "@diff-cov/core";
|
|
16
|
+
var DiffCovReporter = class {
|
|
17
|
+
options;
|
|
18
|
+
ctx = null;
|
|
19
|
+
coverageMap = null;
|
|
20
|
+
runEnded = false;
|
|
21
|
+
done = false;
|
|
22
|
+
reason = "passed";
|
|
23
|
+
constructor(options = {}) {
|
|
24
|
+
this.options = options;
|
|
25
|
+
}
|
|
26
|
+
onInit(ctx) {
|
|
27
|
+
this.ctx = ctx;
|
|
28
|
+
}
|
|
29
|
+
onTestRunStart() {
|
|
30
|
+
this.coverageMap = null;
|
|
31
|
+
this.runEnded = false;
|
|
32
|
+
this.done = false;
|
|
33
|
+
this.reason = "passed";
|
|
34
|
+
}
|
|
35
|
+
onCoverage(coverage) {
|
|
36
|
+
this.coverageMap = coverage;
|
|
37
|
+
if (this.runEnded) void this.finish();
|
|
38
|
+
}
|
|
39
|
+
async onTestRunEnd(_testModules, _unhandledErrors, reason) {
|
|
40
|
+
this.runEnded = true;
|
|
41
|
+
this.reason = reason;
|
|
42
|
+
await this.finish();
|
|
43
|
+
}
|
|
44
|
+
async finish() {
|
|
45
|
+
if (this.done || !this.ctx) return;
|
|
46
|
+
this.done = true;
|
|
47
|
+
const ctx = this.ctx;
|
|
48
|
+
const root = ctx.config.root;
|
|
49
|
+
const watch = Boolean(ctx.config.watch);
|
|
50
|
+
if (watch && !this.options.enabledInWatch) return;
|
|
51
|
+
if (this.reason === "interrupted") return;
|
|
52
|
+
if (!this.coverageMap) {
|
|
53
|
+
ctx.logger.log("diff-cov: coverage was not collected; run Vitest with --coverage.");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const config = await loadConfig(root, this.options.config);
|
|
58
|
+
const projectRoot = (() => {
|
|
59
|
+
try {
|
|
60
|
+
return realpathSync(root);
|
|
61
|
+
} catch {
|
|
62
|
+
return root;
|
|
63
|
+
}
|
|
64
|
+
})();
|
|
65
|
+
const gitRoot = await getRepoRoot(projectRoot);
|
|
66
|
+
const relativeRoot = relative(gitRoot, projectRoot);
|
|
67
|
+
const pathPrefix = relativeRoot && !relativeRoot.startsWith("..") ? toPosix(relativeRoot) : void 0;
|
|
68
|
+
const base = await resolveBase(projectRoot, this.options.base ?? config.base ?? "auto");
|
|
69
|
+
const changes = await collectChanges(projectRoot, base, {
|
|
70
|
+
scope: "worktree",
|
|
71
|
+
includeUntracked: true
|
|
72
|
+
});
|
|
73
|
+
const report = buildReport({
|
|
74
|
+
changes,
|
|
75
|
+
bases: [base],
|
|
76
|
+
scope: "worktree",
|
|
77
|
+
coverage: fromCoverageMap(this.coverageMap, {
|
|
78
|
+
root: gitRoot,
|
|
79
|
+
projectRoot,
|
|
80
|
+
reportDir: projectRoot
|
|
81
|
+
}),
|
|
82
|
+
include: this.options.include ?? config.include,
|
|
83
|
+
exclude: this.options.exclude ?? config.exclude,
|
|
84
|
+
missing: this.options.missing ?? config.missing ?? "fail",
|
|
85
|
+
pathPrefix
|
|
86
|
+
});
|
|
87
|
+
const vitestConfig = ctx.config;
|
|
88
|
+
if (vitestConfig.changed || vitestConfig.coverage?.changed) {
|
|
89
|
+
report.warnings.push(
|
|
90
|
+
"Coverage was collected for a subset of tests; files that did not run may be under-reported."
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const threshold = this.options.threshold ?? config.threshold ?? 0;
|
|
94
|
+
const perFile = this.options.perFile ?? config.perFile;
|
|
95
|
+
const verdict = evaluateThreshold(report, {
|
|
96
|
+
total: threshold,
|
|
97
|
+
...perFile !== void 0 ? { perFile } : {}
|
|
98
|
+
});
|
|
99
|
+
ctx.logger.log("");
|
|
100
|
+
ctx.logger.log(formatReport(report, verdict));
|
|
101
|
+
const reportPath = this.options.report ?? config.report;
|
|
102
|
+
if (reportPath) {
|
|
103
|
+
const path = resolve(projectRoot, reportPath);
|
|
104
|
+
await mkdir(dirname(path), { recursive: true });
|
|
105
|
+
await writeFile(path, JSON.stringify({ ...report, verdict }, null, 2) + "\n", "utf8");
|
|
106
|
+
}
|
|
107
|
+
if (this.options.failOnError && !verdict.passed && this.reason !== "failed" && !watch) {
|
|
108
|
+
process.exitCode = 1;
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
112
|
+
ctx.logger.error(`diff-cov: ${message}`);
|
|
113
|
+
if (this.options.failOnError && !watch) {
|
|
114
|
+
process.exitCode = 1;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
var index_default = DiffCovReporter;
|
|
120
|
+
export {
|
|
121
|
+
DiffCovReporter,
|
|
122
|
+
index_default as default
|
|
123
|
+
};
|
|
124
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { realpathSync } from 'node:fs'\nimport { mkdir, writeFile } from 'node:fs/promises'\nimport { dirname, relative, resolve } from 'node:path'\nimport {\n buildReport,\n collectChanges,\n evaluateThreshold,\n formatReport,\n fromCoverageMap,\n getRepoRoot,\n loadConfig,\n resolveBase,\n toPosix,\n type BaseSpec,\n type CoverageMapLike,\n type DiffCovConfig,\n type MissingPolicy,\n type Verdict,\n} from '@diff-cov/core'\nimport type { Reporter, SerializedError, TestModule, TestRunEndReason, Vitest } from 'vitest/node'\n\nexport interface DiffCovReporterOptions {\n /** Base ref, \"auto\" or \"new\". Defaults to the config file, then \"auto\". */\n base?: BaseSpec\n include?: string[]\n exclude?: string[]\n /** Total threshold. Defaults to the config file, then 0 (report only). */\n threshold?: number\n perFile?: number\n missing?: MissingPolicy\n /** Set the process exit code when the threshold fails. */\n failOnError?: boolean\n /** Report in watch mode too. Off by default. */\n enabledInWatch?: boolean\n /** Config file path relative to the project root. */\n config?: string\n /** Write a JSON report to this path. */\n report?: string | null\n}\n\nexport class DiffCovReporter implements Reporter {\n private readonly options: DiffCovReporterOptions\n private ctx: Vitest | null = null\n private coverageMap: CoverageMapLike | null = null\n private runEnded = false\n private done = false\n private reason: TestRunEndReason = 'passed'\n\n constructor(options: DiffCovReporterOptions = {}) {\n this.options = options\n }\n\n onInit(ctx: Vitest): void {\n this.ctx = ctx\n }\n\n onTestRunStart(): void {\n this.coverageMap = null\n this.runEnded = false\n this.done = false\n this.reason = 'passed'\n }\n\n onCoverage(coverage: unknown): void {\n this.coverageMap = coverage as CoverageMapLike\n // Vitest 3 calls onCoverage after onTestRunEnd; Vitest 4 calls it before.\n if (this.runEnded) void this.finish()\n }\n\n async onTestRunEnd(\n _testModules: ReadonlyArray<TestModule>,\n _unhandledErrors: ReadonlyArray<SerializedError>,\n reason: TestRunEndReason,\n ): Promise<void> {\n this.runEnded = true\n this.reason = reason\n await this.finish()\n }\n\n private async finish(): Promise<void> {\n if (this.done || !this.ctx) return\n this.done = true\n\n const ctx = this.ctx\n const root = ctx.config.root\n const watch = Boolean((ctx.config as { watch?: boolean }).watch)\n if (watch && !this.options.enabledInWatch) return\n if (this.reason === 'interrupted') return\n\n if (!this.coverageMap) {\n ctx.logger.log('diff-cov: coverage was not collected; run Vitest with --coverage.')\n return\n }\n\n try {\n const config: DiffCovConfig = await loadConfig(root, this.options.config)\n const projectRoot = (() => {\n try {\n return realpathSync(root)\n } catch {\n return root\n }\n })()\n const gitRoot = await getRepoRoot(projectRoot)\n const relativeRoot = relative(gitRoot, projectRoot)\n const pathPrefix =\n relativeRoot && !relativeRoot.startsWith('..') ? toPosix(relativeRoot) : undefined\n const base = await resolveBase(projectRoot, this.options.base ?? config.base ?? 'auto')\n const changes = await collectChanges(projectRoot, base, {\n scope: 'worktree',\n includeUntracked: true,\n })\n const report = buildReport({\n changes,\n bases: [base],\n scope: 'worktree',\n coverage: fromCoverageMap(this.coverageMap, {\n root: gitRoot,\n projectRoot,\n reportDir: projectRoot,\n }),\n include: this.options.include ?? config.include,\n exclude: this.options.exclude ?? config.exclude,\n missing: this.options.missing ?? config.missing ?? 'fail',\n pathPrefix,\n })\n\n const vitestConfig = ctx.config as {\n changed?: boolean | string\n coverage?: { changed?: boolean | string }\n }\n if (vitestConfig.changed || vitestConfig.coverage?.changed) {\n report.warnings.push(\n 'Coverage was collected for a subset of tests; files that did not run may be under-reported.',\n )\n }\n\n const threshold = this.options.threshold ?? config.threshold ?? 0\n const perFile = this.options.perFile ?? config.perFile\n const verdict = evaluateThreshold(report, {\n total: threshold,\n ...(perFile !== undefined ? { perFile } : {}),\n })\n\n ctx.logger.log('')\n ctx.logger.log(formatReport(report, verdict))\n\n const reportPath = this.options.report ?? config.report\n if (reportPath) {\n const path = resolve(projectRoot, reportPath)\n await mkdir(dirname(path), { recursive: true })\n await writeFile(path, JSON.stringify({ ...report, verdict }, null, 2) + '\\n', 'utf8')\n }\n\n if (this.options.failOnError && !verdict.passed && this.reason !== 'failed' && !watch) {\n process.exitCode = 1\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n ctx.logger.error(`diff-cov: ${message}`)\n if (this.options.failOnError && !watch) {\n process.exitCode = 1\n }\n }\n }\n}\n\nexport default DiffCovReporter\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diff-cov/vitest",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Vitest 4 reporter for diff coverage.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"coverage",
|
|
9
|
+
"diff",
|
|
10
|
+
"vitest",
|
|
11
|
+
"reporter"
|
|
12
|
+
],
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"main": "./dist/index.cjs",
|
|
17
|
+
"module": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js",
|
|
23
|
+
"require": "./dist/index.cjs"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@diff-cov/core": "0.1.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"vitest": ">=4.1.0 <5"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"tsup": "^9.0.0",
|
|
41
|
+
"typescript": "^5.9.3",
|
|
42
|
+
"vitest": "4.1.9"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsup",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"typecheck": "tsc --noEmit"
|
|
48
|
+
}
|
|
6
49
|
}
|