@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 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
- # Temporary Holding Version
1
+ # @diff-cov/vitest
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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":[]}
@@ -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 };
@@ -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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
  }