supercov 0.0.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.
@@ -0,0 +1,187 @@
1
+ import { createHash } from "node:crypto";
2
+ import { spawnSync } from "node:child_process";
3
+ import {
4
+ existsSync,
5
+ readFileSync,
6
+ readdirSync,
7
+ statSync,
8
+ } from "node:fs";
9
+ import { relative, resolve, sep } from "node:path";
10
+ import type { CoverageProject } from "./project.ts";
11
+ import type {
12
+ CoverageRunFingerprint,
13
+ CoverageRunIntegrity,
14
+ } from "./types.ts";
15
+
16
+ export const COVERAGE_REPORT_SCHEMA_VERSION = 2;
17
+ export const COVERAGE_INSTRUMENTER_VERSION = "2.0.0";
18
+
19
+ const SOURCE_PATTERN = /\.[cm]?[jt]sx?$/;
20
+ const TEST_PATTERN = /(?:^|[/_.-])(test|spec)(?:[/_.-]|$).*\.[cm]?[jt]sx?$/i;
21
+ const SKIPPED_DIRECTORIES = new Set([
22
+ ".git",
23
+ ".supercov",
24
+ "build",
25
+ "coverage",
26
+ "dist",
27
+ "node_modules",
28
+ "results",
29
+ "test-results",
30
+ ]);
31
+
32
+ function filesUnder(directory: string): string[] {
33
+ if (!existsSync(directory) || !statSync(directory).isDirectory()) return [];
34
+ return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
35
+ if (entry.isDirectory() && SKIPPED_DIRECTORIES.has(entry.name)) return [];
36
+ const path = resolve(directory, entry.name);
37
+ if (entry.isDirectory()) return filesUnder(path);
38
+ return entry.isFile() ? [path] : [];
39
+ });
40
+ }
41
+
42
+ function normalized(root: string, path: string): string {
43
+ return relative(root, path).split(sep).join("/");
44
+ }
45
+
46
+ function digestFiles(root: string, paths: Iterable<string>): string {
47
+ const hash = createHash("sha256");
48
+ for (const path of [...new Set(paths)].sort()) {
49
+ if (!existsSync(path) || !statSync(path).isFile()) continue;
50
+ hash.update(normalized(root, path));
51
+ hash.update("\0");
52
+ hash.update(readFileSync(path));
53
+ hash.update("\0");
54
+ }
55
+ return hash.digest("hex");
56
+ }
57
+
58
+ function git(root: string): CoverageRunIntegrity["git"] {
59
+ const revision = spawnSync("git", ["rev-parse", "HEAD"], {
60
+ cwd: root,
61
+ encoding: "utf8",
62
+ });
63
+ const status = spawnSync("git", ["status", "--porcelain=v1"], {
64
+ cwd: root,
65
+ encoding: "utf8",
66
+ });
67
+ if (revision.status !== 0 && status.status !== 0) return undefined;
68
+ return {
69
+ ...(revision.status === 0
70
+ ? { revision: revision.stdout.trim() }
71
+ : {}),
72
+ dirty: status.status !== 0 || status.stdout.trim().length > 0,
73
+ };
74
+ }
75
+
76
+ export function createRunIntegrity(
77
+ root: string,
78
+ project: CoverageProject,
79
+ toolSourceDirectory: string,
80
+ ): CoverageRunIntegrity {
81
+ const sourceFiles = project.sourceRoots.flatMap((directory) =>
82
+ filesUnder(resolve(root, directory)).filter((path) => SOURCE_PATTERN.test(path)),
83
+ );
84
+ const explicitTests = ["test", "tests", "__tests__"].flatMap((directory) =>
85
+ filesUnder(resolve(root, directory)).filter((path) => SOURCE_PATTERN.test(path)),
86
+ );
87
+ const colocatedTests = project.sourceRoots.flatMap((directory) =>
88
+ filesUnder(resolve(root, directory)).filter((path) =>
89
+ TEST_PATTERN.test(normalized(root, path)),
90
+ ),
91
+ );
92
+ const testFiles = [...new Set([...explicitTests, ...colocatedTests])];
93
+ const dependencyFiles = [
94
+ "package.json",
95
+ "package-lock.json",
96
+ "npm-shrinkwrap.json",
97
+ "pnpm-lock.yaml",
98
+ "yarn.lock",
99
+ "bun.lock",
100
+ "bun.lockb",
101
+ ]
102
+ .map((path) => resolve(root, path))
103
+ .filter(existsSync);
104
+ const configurationFiles = [
105
+ "playwright.config.ts",
106
+ "playwright.config.mts",
107
+ "playwright.config.js",
108
+ "playwright.config.mjs",
109
+ "vitest.config.ts",
110
+ "vitest.config.mts",
111
+ "vitest.config.js",
112
+ "vitest.config.mjs",
113
+ "vite.config.ts",
114
+ "vite.config.mts",
115
+ "vite.config.js",
116
+ "vite.config.mjs",
117
+ "tsconfig.json",
118
+ ".npmrc",
119
+ ]
120
+ .map((path) => resolve(root, path))
121
+ .concat(
122
+ [project.playwrightConfig, project.vitestConfig].filter(
123
+ (value): value is string => Boolean(value),
124
+ ),
125
+ )
126
+ .filter(existsSync);
127
+ const instrumenterFiles = filesUnder(toolSourceDirectory).filter((path) =>
128
+ /\.[cm]?[jt]s$/.test(path),
129
+ );
130
+ const source = digestFiles(root, sourceFiles);
131
+ const tests = digestFiles(root, testFiles);
132
+ const dependencies = digestFiles(root, dependencyFiles);
133
+ const configuration = digestFiles(root, configurationFiles);
134
+ const instrumenter = digestFiles(toolSourceDirectory, instrumenterFiles);
135
+ const combined = createHash("sha256")
136
+ .update(
137
+ JSON.stringify({
138
+ schema: COVERAGE_REPORT_SCHEMA_VERSION,
139
+ version: COVERAGE_INSTRUMENTER_VERSION,
140
+ source,
141
+ tests,
142
+ dependencies,
143
+ configuration,
144
+ instrumenter,
145
+ }),
146
+ )
147
+ .digest("hex");
148
+ const fingerprint: CoverageRunFingerprint = {
149
+ algorithm: "sha256",
150
+ source,
151
+ tests,
152
+ dependencies,
153
+ configuration,
154
+ instrumenter,
155
+ combined,
156
+ sourceFiles: sourceFiles.length,
157
+ testFiles: testFiles.length,
158
+ };
159
+ const gitState = git(root);
160
+ return {
161
+ schemaVersion: COVERAGE_REPORT_SCHEMA_VERSION,
162
+ instrumenterVersion: COVERAGE_INSTRUMENTER_VERSION,
163
+ ...(gitState ? { git: gitState } : {}),
164
+ fingerprint,
165
+ };
166
+ }
167
+
168
+ export function compareRunIntegrity(
169
+ stored: CoverageRunIntegrity | undefined,
170
+ current: CoverageRunIntegrity,
171
+ ): { stale: boolean; reasons: string[] } {
172
+ if (!stored) return { stale: true, reasons: ["run predates integrity fingerprints"] };
173
+ const reasons: string[] = [];
174
+ if (stored.schemaVersion !== current.schemaVersion)
175
+ reasons.push("report schema changed");
176
+ if (stored.fingerprint.instrumenter !== current.fingerprint.instrumenter)
177
+ reasons.push("instrumenter changed");
178
+ if (stored.fingerprint.source !== current.fingerprint.source)
179
+ reasons.push("instrumented source changed");
180
+ if (stored.fingerprint.tests !== current.fingerprint.tests)
181
+ reasons.push("test files changed");
182
+ if (stored.fingerprint.dependencies !== current.fingerprint.dependencies)
183
+ reasons.push("dependencies or lockfile changed");
184
+ if (stored.fingerprint.configuration !== current.fingerprint.configuration)
185
+ reasons.push("test/build configuration changed");
186
+ return { stale: reasons.length > 0, reasons };
187
+ }