import { readdir, readFile } from 'fs/promises'; import { join } from 'path'; /** * Walk the outputFolder looking for the pageSummary JSON files * produced by sitespeed.io. The structure is: * outputFolder/pages///.pageSummary.json */ async function findPageSummaries(outputFolder) { const pagesDir = join(outputFolder, 'pages'); const summaries = {}; async function walk(dir) { let entries; try { entries = await readdir(dir, { withFileTypes: true }); } catch { return; } for (const e of entries) { const full = join(dir, e.name); if (e.isDirectory()) { await walk(full); } else if (e.name.endsWith('.pageSummary.json')) { const plugin = e.name.replace('.pageSummary.json', ''); if (!summaries[plugin]) summaries[plugin] = []; summaries[plugin].push(full); } } } await walk(pagesDir); return summaries; } async function readJson(filePath) { const raw = await readFile(filePath, 'utf8'); return JSON.parse(raw); } function safe(obj, ...path) { let cur = obj; for (const key of path) { if (cur == null || typeof cur !== 'object') return null; cur = cur[key]; } return cur ?? null; } function median(obj) { return safe(obj, 'median') ?? safe(obj, 'mean') ?? null; } export async function parseResults(outputFolder, _url) { const summaries = await findPageSummaries(outputFolder); const metrics = {}; // ─── browsertime.pageSummary ─────────────────────────────────────────────── const btFiles = summaries['browsertime'] || []; if (btFiles.length > 0) { const bt = await readJson(btFiles[0]); const stats = safe(bt, 'statistics'); const timings = safe(stats, 'timings'); const pageTimings = safe(timings, 'pageTimings'); const userTimings = safe(timings, 'userTimings'); const visualMetrics = safe(stats, 'visualMetrics'); const cpu = safe(stats, 'cpu'); const axe = safe(bt, 'accessibility', 'summary'); // Core Web Vitals / timings if (timings) { metrics.ttfb = median(safe(timings, 'timeToFirstByte')); metrics.fcp = median(safe(timings, 'firstContentfulPaint')); metrics.lcp = median(safe(timings, 'largestContentfulPaint')); metrics.cls = median(safe(timings, 'cumulativeLayoutShift')); metrics.tbt = median(safe(timings, 'totalBlockingTime')); metrics.max_potential_fid = median(safe(timings, 'maxPotentialFID')); } if (pageTimings) { metrics.page_load_time = median(safe(pageTimings, 'pageLoadTime')); metrics.fully_loaded = median(safe(pageTimings, 'fullyLoaded')); metrics.dom_content_loaded = median(safe(pageTimings, 'domContentLoadedEventEnd')); metrics.dom_interactive = median(safe(pageTimings, 'domInteractive')); metrics.front_end_time = median(safe(pageTimings, 'frontEndTime')); metrics.back_end_time = median(safe(pageTimings, 'backEndTime')); metrics.time_to_first_byte = median(safe(pageTimings, 'timeToFirstByte')) ?? metrics.ttfb; } if (visualMetrics) { metrics.speed_index = median(safe(visualMetrics, 'SpeedIndex')); metrics.first_visual_change = median(safe(visualMetrics, 'FirstVisualChange')); metrics.last_visual_change = median(safe(visualMetrics, 'LastVisualChange')); metrics.visual_complete_85 = median(safe(visualMetrics, 'VisualComplete85')); metrics.perceptual_speed_index = median(safe(visualMetrics, 'PerceptualSpeedIndex')); } // CPU / Long Tasks if (cpu) { metrics.long_tasks_count = median(safe(cpu, 'longTasks', 'tasks')); metrics.long_tasks_duration = median(safe(cpu, 'longTasks', 'totalDuration')); } // Axe accessibility if (axe) { metrics.axe_critical = safe(axe, 'critical') ?? 0; metrics.axe_serious = safe(axe, 'serious') ?? 0; metrics.axe_moderate = safe(axe, 'moderate') ?? 0; metrics.axe_minor = safe(axe, 'minor') ?? 0; } } // ─── coach.pageSummary ───────────────────────────────────────────────────── const coachFiles = summaries['coach'] || []; if (coachFiles.length > 0) { const coach = await readJson(coachFiles[0]); const advice = safe(coach, 'advice'); if (advice) { metrics.score_overall = safe(advice, 'score') ?? safe(advice, 'overall', 'score'); metrics.score_performance = safe(advice, 'performance', 'score'); metrics.score_accessibility = safe(advice, 'accessibility', 'score'); metrics.score_bestpractice = safe(advice, 'bestpractice', 'score'); metrics.score_privacy = safe(advice, 'privacy', 'score'); } } // ─── pagexray.pageSummary ────────────────────────────────────────────────── const xrayFiles = summaries['pagexray'] || []; if (xrayFiles.length > 0) { const xray = await readJson(xrayFiles[0]); // pagexray has multiple runs — use the first or median-like object const page = Array.isArray(xray) ? xray[0] : xray; const ct = safe(page, 'contentTypes'); if (ct) { metrics.transfer_total = safe(page, 'transferSize'); metrics.requests_total = safe(page, 'requests'); metrics.transfer_html = safe(ct, 'html', 'transferSize'); metrics.transfer_js = safe(ct, 'javascript', 'transferSize'); metrics.transfer_css = safe(ct, 'css', 'transferSize'); metrics.transfer_image = safe(ct, 'image', 'transferSize'); metrics.transfer_font = safe(ct, 'font', 'transferSize'); metrics.requests_js = safe(ct, 'javascript', 'requests'); metrics.requests_css = safe(ct, 'css', 'requests'); metrics.requests_image = safe(ct, 'image', 'requests'); metrics.requests_font = safe(ct, 'font', 'requests'); } const tp = safe(page, 'thirdParty'); if (tp) { metrics.third_party_requests = safe(tp, 'requests'); metrics.third_party_transfer = safe(tp, 'transferSize'); } } // ─── sustainable.pageSummary ─────────────────────────────────────────────── const sustainFiles = summaries['sustainable'] || []; if (sustainFiles.length > 0) { const sust = await readJson(sustainFiles[0]); metrics.co2_per_page_view = safe(sust, 'co2PerPageView') ?? safe(sust, 'statistics', 'co2PerPageView', 'median'); metrics.co2_total = safe(sust, 'totalCO2') ?? safe(sust, 'statistics', 'totalCO2', 'median'); metrics.co2_first_party = safe(sust, 'firstParty', 'co2') ?? safe(sust, 'statistics', 'firstParty', 'co2', 'median'); metrics.co2_third_party = safe(sust, 'thirdParty', 'co2') ?? safe(sust, 'statistics', 'thirdParty', 'co2', 'median'); } // Remove null values to avoid overwriting real DB values with NULL return Object.fromEntries( Object.entries(metrics).filter(([, v]) => v !== null && v !== undefined) ); }