#!/usr/bin/env tsx /** * V8 to Istanbul Coverage Converter * * Converts Playwright's V8 coverage format to Istanbul format * so it can be merged with Jest coverage data. * * Usage: * npm run coverage:convert * # or directly: * tsx scripts/convert-v8-to-istanbul.ts * * Input: * - V8 coverage files in: ./coverage-e2e/raw/*.json * - Generated by Playwright's page.coverage API * * Output: * - Istanbul coverage in: ./coverage-e2e/.nyc_output/e2e-coverage.json * - Ready to merge with Jest coverage * * Prerequisites: * - npm install -D v8-to-istanbul * - E2E tests must collect coverage using page.coverage API */ import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; // V8 coverage entry format interface V8CoverageEntry { url: string; scriptId: string; source?: string; functions: Array<{ functionName: string; ranges: Array<{ startOffset: number; endOffset: number; count: number; }>; isBlockCoverage: boolean; }>; } // Istanbul coverage format interface IstanbulCoverage { [filePath: string]: { path: string; statementMap: Record; fnMap: Record; branchMap: Record; s: Record; f: Record; b: Record; }; } async function convertV8ToIstanbul() { console.log('\nšŸ”„ Converting V8 Coverage to Istanbul Format...\n'); const rawDir = path.join(process.cwd(), 'coverage-e2e/raw'); const outputDir = path.join(process.cwd(), 'coverage-e2e/.nyc_output'); // Check if raw directory exists try { await fs.access(rawDir); } catch { console.log('āŒ No V8 coverage found at:', rawDir); console.log('\nā„¹ļø To generate V8 coverage:'); console.log(' 1. Add coverage helpers to E2E tests (see E2E_COVERAGE_GUIDE.md)'); console.log(' 2. Run: E2E_COVERAGE=true npm run test:e2e'); console.log('\nā„¹ļø Alternatively, use Istanbul instrumentation (see guide)'); return; } // Create output directory await fs.mkdir(outputDir, { recursive: true }); // Check for v8-to-istanbul dependency let v8toIstanbul: any; try { // Dynamic import to handle both scenarios (installed vs not installed) const module = await import('v8-to-istanbul'); v8toIstanbul = module.default || module; } catch (error) { console.log('āŒ v8-to-istanbul not installed\n'); console.log('šŸ“¦ Install it with:'); console.log(' npm install -D v8-to-istanbul\n'); console.log('āš ļø Note: V8 coverage approach requires this dependency.'); console.log(' Alternatively, use Istanbul instrumentation (no extra deps needed).\n'); process.exit(1); } // Read all V8 coverage files const files = await fs.readdir(rawDir); const jsonFiles = files.filter(f => f.endsWith('.json')); if (jsonFiles.length === 0) { console.log('āš ļø No coverage files found in:', rawDir); console.log('\nRun E2E tests with coverage enabled:'); console.log(' E2E_COVERAGE=true npm run test:e2e\n'); return; } console.log(`šŸ“ Found ${jsonFiles.length} V8 coverage file(s)\n`); const istanbulCoverage: IstanbulCoverage = {}; const projectRoot = process.cwd(); let totalConverted = 0; let totalSkipped = 0; // Process each V8 coverage file for (const file of jsonFiles) { const filePath = path.join(rawDir, file); console.log(`šŸ“„ Processing: ${file}`); try { const content = await fs.readFile(filePath, 'utf-8'); const v8Coverage: V8CoverageEntry[] = JSON.parse(content); for (const entry of v8Coverage) { try { // Skip non-source files if ( !entry.url.startsWith('http://localhost') && !entry.url.startsWith('file://') ) { continue; } // Skip node_modules, .next, and other irrelevant files if ( entry.url.includes('node_modules') || entry.url.includes('/.next/') || entry.url.includes('/_next/') || entry.url.includes('/webpack/') ) { totalSkipped++; continue; } // Convert URL to file path let sourcePath: string; if (entry.url.startsWith('file://')) { sourcePath = fileURLToPath(entry.url); } else { // HTTP URL - extract path const url = new URL(entry.url); // Try to map to source file sourcePath = path.join(projectRoot, 'src', url.pathname); } // Skip if not in src/ if (!sourcePath.includes('/src/')) { totalSkipped++; continue; } // Verify file exists try { await fs.access(sourcePath); } catch { totalSkipped++; continue; } // Convert using v8-to-istanbul const converter = v8toIstanbul(sourcePath); await converter.load(); converter.applyCoverage(entry.functions); const converted = converter.toIstanbul(); // Merge into combined coverage Object.assign(istanbulCoverage, converted); totalConverted++; } catch (error: any) { console.log(` āš ļø Skipped ${entry.url}: ${error.message}`); totalSkipped++; } } } catch (error: any) { console.log(` āŒ Failed to process ${file}: ${error.message}`); } } // Write Istanbul coverage const outputPath = path.join(outputDir, 'e2e-coverage.json'); await fs.writeFile(outputPath, JSON.stringify(istanbulCoverage, null, 2)); console.log('\n' + '='.repeat(70)); console.log('āœ… Conversion Complete'); console.log('='.repeat(70)); console.log(`\n Files converted: ${totalConverted}`); console.log(` Files skipped: ${totalSkipped}`); console.log(` Output location: ${outputPath}\n`); if (totalConverted === 0) { console.log('āš ļø No files were converted. Possible reasons:'); console.log(' • V8 coverage doesn\'t contain source files from src/'); console.log(' • Coverage was collected for build artifacts instead of source'); console.log(' • Source maps are not correctly configured\n'); console.log('šŸ’” Consider using Istanbul instrumentation instead (see guide)\n'); } else { console.log('āœ… Ready to merge with Jest coverage:'); console.log(' npm run coverage:merge\n'); } } // Run the conversion convertV8ToIstanbul().catch((error) => { console.error('\nāŒ Error converting coverage:', error); process.exit(1); });