import fs from 'fs-extra'; import upath from 'upath'; import { z } from 'zod/v4'; import type { CoverageInfo } from './types.ts'; const StatementLocation = z.object({ start: z.object({ line: z.number() }), end: z.object({ line: z.number() }), }); const CoverageEntry = z.object({ statementMap: z.record(z.string(), StatementLocation), s: z.record(z.string(), z.number()), }); const CoverageJson = z.record(z.string(), CoverageEntry); type CoverageJson = z.infer; function groupConsecutiveNumbers(numbers: number[]): string[] { if (numbers.length === 0) { return []; } const sorted = [...numbers].sort((a, b) => a - b); const ranges: string[] = []; let start = sorted[0]; let end = sorted[0]; for (let i = 1; i < sorted.length; i++) { if (sorted[i] === end + 1) { end = sorted[i]; } else { ranges.push(start === end ? `${start}` : `${start}-${end}`); start = sorted[i]; end = sorted[i]; } } ranges.push(start === end ? `${start}` : `${start}-${end}`); return ranges; } async function loadCoverageFile( coverageDir: string, ): Promise { const coverageFile = upath.resolve(coverageDir, 'coverage-final.json'); if (!(await fs.pathExists(coverageFile))) { return null; } try { const rawJson: unknown = JSON.parse( await fs.readFile(coverageFile, 'utf8'), ); const parseResult = CoverageJson.safeParse(rawJson); return parseResult.success ? parseResult.data : null; } catch { return null; } } function normalizeCoveragePaths( data: CoverageJson, ): Map { const normalized = new Map(); for (const [key, value] of Object.entries(data)) { normalized.set(upath.normalize(key), value); } return normalized; } function isSourceFile(path: string): boolean { return path.endsWith('.ts') && !path.endsWith('.spec.ts'); } function extractFileCoverage( file: string, coverage: Map, ): CoverageInfo | null { const absPath = upath.resolve(process.cwd(), file); const entry = coverage.get(absPath); if (!entry) { return null; } const { statementMap, s: statementCoverage } = entry; const totalStatements = Object.keys(statementMap).length; if (totalStatements === 0) { return null; } const coveredStatements = Object.values(statementCoverage).filter( (count) => count > 0, ).length; const percentage = (coveredStatements / totalStatements) * 100; if (percentage >= 100) { return null; } const uncoveredLineSet = new Set(); for (const [stmtId, count] of Object.entries(statementCoverage)) { if (count !== 0) { continue; } const stmt = statementMap[stmtId]; if (!stmt) { continue; } for (let line = stmt.start.line; line <= stmt.end.line; line++) { uncoveredLineSet.add(line); } } return { file, percentage, uncoveredLines: groupConsecutiveNumbers(Array.from(uncoveredLineSet)), }; } export async function loadCoverage( coverageDir: string, ): Promise | null> { const data = await loadCoverageFile(coverageDir); if (!data) { return null; } return normalizeCoveragePaths(data); } export function getCoverageForFiles( coverage: Map, files: string[], ): CoverageInfo[] { const results: CoverageInfo[] = []; for (const file of files) { if (!isSourceFile(file)) { continue; } const info = extractFileCoverage(file, coverage); if (info) { results.push(info); } } return results; } export function getCoverageForDir( coverage: Map, dir: string, ): CoverageInfo[] { const absDir = `${upath.resolve(process.cwd(), dir)}/`; const results: CoverageInfo[] = []; for (const absPath of coverage.keys()) { if (!absPath.startsWith(absDir)) { continue; } const relPath = upath.relative(process.cwd(), absPath); if (!isSourceFile(relPath)) { continue; } const info = extractFileCoverage(relPath, coverage); if (info) { results.push(info); } } return results; }