186 lines
8.8 KiB
JavaScript
186 lines
8.8 KiB
JavaScript
import { readdir, readFile, stat } from 'fs/promises';
|
|
import { join } from 'path';
|
|
|
|
/**
|
|
* Recursively find all *.pageSummary.json files.
|
|
* sitespeed.io v39 writes them to:
|
|
* <outputFolder>/pages/<hostname>/data/<plugin>.pageSummary.json
|
|
*/
|
|
async function findPageSummaries(outputFolder) {
|
|
const summaries = {};
|
|
const errors = [];
|
|
|
|
async function walk(dir) {
|
|
let names;
|
|
try {
|
|
names = await readdir(dir);
|
|
} catch (err) {
|
|
errors.push(`readdir(${dir}): ${err.message}`);
|
|
return;
|
|
}
|
|
console.log(`[parser] walk ${dir} → [${names.join(', ')}]`);
|
|
for (const name of names) {
|
|
const full = join(dir, name);
|
|
let s;
|
|
try { s = await stat(full); } catch { continue; }
|
|
if (s.isDirectory()) {
|
|
await walk(full);
|
|
} else if (name.endsWith('.pageSummary.json')) {
|
|
const plugin = name.replace('.pageSummary.json', '');
|
|
if (!summaries[plugin]) summaries[plugin] = [];
|
|
summaries[plugin].push(full);
|
|
console.log(`[parser] found: ${full}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
await walk(outputFolder);
|
|
|
|
if (errors.length > 0) {
|
|
console.warn('[parser] walk errors:', errors.join('; '));
|
|
}
|
|
|
|
const keys = Object.keys(summaries);
|
|
if (keys.length === 0) {
|
|
console.warn(`[parser] no pageSummary JSON files found under ${outputFolder}`);
|
|
} else {
|
|
console.log(`[parser] plugins found: ${keys.join(', ')}`);
|
|
}
|
|
|
|
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 med(obj) {
|
|
return safe(obj, 'median');
|
|
}
|
|
|
|
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 s = safe(bt, 'statistics');
|
|
console.log('[parser] browsertime statistics keys:', s ? Object.keys(s).join(', ') : 'NONE');
|
|
|
|
if (s) {
|
|
// Core Web Vitals — use googleWebVitals aggregate (flat, direct median)
|
|
metrics.lcp = med(safe(s, 'googleWebVitals', 'largestContentfulPaint'));
|
|
metrics.fcp = med(safe(s, 'timings', 'paintTiming', 'first-contentful-paint'));
|
|
metrics.cls = med(safe(s, 'pageinfo', 'cumulativeLayoutShift'));
|
|
metrics.tbt = med(safe(s, 'googleWebVitals', 'totalBlockingTime'));
|
|
metrics.ttfb = med(safe(s, 'googleWebVitals', 'ttfb'));
|
|
metrics.max_potential_fid = med(safe(s, 'cpu', 'longTasks', 'maxPotentialFid'));
|
|
|
|
// Navigation timings — pageTimings is nested under statistics.timings
|
|
metrics.page_load_time = med(safe(s, 'timings', 'pageTimings', 'pageLoadTime'));
|
|
metrics.fully_loaded = med(safe(s, 'timings', 'fullyLoaded'));
|
|
metrics.dom_content_loaded = med(safe(s, 'timings', 'pageTimings', 'domContentLoadedTime'));
|
|
metrics.dom_interactive = med(safe(s, 'timings', 'pageTimings', 'domInteractiveTime'));
|
|
metrics.front_end_time = med(safe(s, 'timings', 'pageTimings', 'frontEndTime'));
|
|
metrics.back_end_time = med(safe(s, 'timings', 'pageTimings', 'backEndTime'));
|
|
metrics.time_to_first_byte = metrics.ttfb;
|
|
|
|
metrics.speed_index = med(safe(s, 'visualMetrics', 'SpeedIndex'));
|
|
metrics.first_visual_change = med(safe(s, 'visualMetrics', 'FirstVisualChange'));
|
|
metrics.last_visual_change = med(safe(s, 'visualMetrics', 'LastVisualChange'));
|
|
metrics.visual_complete_85 = med(safe(s, 'visualMetrics', 'VisualComplete85'));
|
|
|
|
metrics.long_tasks_count = med(safe(s, 'cpu', 'longTasks', 'tasks'));
|
|
metrics.long_tasks_duration = med(safe(s, 'cpu', 'longTasks', 'totalDuration'));
|
|
|
|
console.log('[parser] lcp=%s fcp=%s tbt=%s cls=%s ttfb=%s si=%s',
|
|
metrics.lcp, metrics.fcp, metrics.tbt, metrics.cls, metrics.ttfb, metrics.speed_index);
|
|
}
|
|
} else {
|
|
console.warn('[parser] no browsertime.pageSummary.json found');
|
|
}
|
|
|
|
// ─── axe.pageSummary ─────────────────────────────────────────────────────
|
|
const axeFiles = summaries['axe'] || [];
|
|
if (axeFiles.length > 0) {
|
|
const axe = await readJson(axeFiles[0]);
|
|
console.log('[parser] axe top-level keys:', Object.keys(axe).join(', '));
|
|
metrics.axe_critical = med(safe(axe, 'violations', 'critical')) ?? 0;
|
|
metrics.axe_serious = med(safe(axe, 'violations', 'serious')) ?? 0;
|
|
metrics.axe_moderate = med(safe(axe, 'violations', 'moderate')) ?? 0;
|
|
metrics.axe_minor = med(safe(axe, 'violations', 'minor')) ?? 0;
|
|
}
|
|
|
|
// ─── coach.pageSummary ────────────────────────────────────────────────────
|
|
const coachFiles = summaries['coach'] || [];
|
|
if (coachFiles.length > 0) {
|
|
const coach = await readJson(coachFiles[0]);
|
|
console.log('[parser] coach top-level keys:', Object.keys(coach).join(', '));
|
|
metrics.score_overall = safe(coach, 'advice', 'score');
|
|
metrics.score_performance = safe(coach, 'advice', 'performance', 'score');
|
|
metrics.score_bestpractice = safe(coach, 'advice', 'bestpractice', 'score');
|
|
metrics.score_privacy = safe(coach, 'advice', 'privacy', 'score');
|
|
console.log('[parser] coach scores: overall=%s perf=%s',
|
|
metrics.score_overall, metrics.score_performance);
|
|
}
|
|
|
|
// ─── pagexray.pageSummary ─────────────────────────────────────────────────
|
|
const xrayFiles = summaries['pagexray'] || [];
|
|
if (xrayFiles.length > 0) {
|
|
const xray = await readJson(xrayFiles[0]);
|
|
const xs = safe(xray, 'statistics');
|
|
console.log('[parser] pagexray top-level keys:', Object.keys(xray).join(', '));
|
|
if (xs) console.log('[parser] pagexray.statistics keys:', Object.keys(xs).join(', '));
|
|
|
|
function xv(statPath, directPath) {
|
|
const fromStats = xs ? med(safe(xs, ...statPath)) : null;
|
|
if (fromStats !== null) return fromStats;
|
|
return safe(xray, ...directPath);
|
|
}
|
|
|
|
metrics.transfer_total = xv(['transferSize'], ['transferSize']);
|
|
metrics.requests_total = xv(['requests'], ['requests']);
|
|
metrics.transfer_html = xv(['contentTypes', 'html', 'transferSize'], ['contentTypes', 'html', 'transferSize']);
|
|
metrics.transfer_js = xv(['contentTypes', 'javascript', 'transferSize'], ['contentTypes', 'javascript', 'transferSize']);
|
|
metrics.transfer_css = xv(['contentTypes', 'css', 'transferSize'], ['contentTypes', 'css', 'transferSize']);
|
|
metrics.transfer_image = xv(['contentTypes', 'image', 'transferSize'], ['contentTypes', 'image', 'transferSize']);
|
|
metrics.transfer_font = xv(['contentTypes', 'font', 'transferSize'], ['contentTypes', 'font', 'transferSize']);
|
|
metrics.requests_js = xv(['contentTypes', 'javascript', 'requests'], ['contentTypes', 'javascript', 'requests']);
|
|
metrics.requests_css = xv(['contentTypes', 'css', 'requests'], ['contentTypes', 'css', 'requests']);
|
|
metrics.requests_image = xv(['contentTypes', 'image', 'requests'], ['contentTypes', 'image', 'requests']);
|
|
metrics.requests_font = xv(['contentTypes', 'font', 'requests'], ['contentTypes', 'font', 'requests']);
|
|
metrics.third_party_transfer = xv(['thirdParty', 'transferSize'], ['thirdParty', 'transferSize']);
|
|
metrics.third_party_requests = xv(['thirdParty', 'requests'], ['thirdParty', 'requests']);
|
|
|
|
console.log('[parser] transfer_total=%s requests_total=%s', metrics.transfer_total, metrics.requests_total);
|
|
}
|
|
|
|
// ─── sustainable.pageSummary ──────────────────────────────────────────────
|
|
const sustainFiles = summaries['sustainable'] || [];
|
|
if (sustainFiles.length > 0) {
|
|
const sust = await readJson(sustainFiles[0]);
|
|
console.log('[parser] sustainable top-level keys:', Object.keys(sust).join(', '));
|
|
metrics.co2_per_page_view = med(safe(sust, 'co2PerPageView'));
|
|
metrics.co2_total = med(safe(sust, 'co2PerPageView'));
|
|
metrics.co2_first_party = med(safe(sust, 'co2FirstParty'));
|
|
metrics.co2_third_party = med(safe(sust, 'co2ThirdParty'));
|
|
}
|
|
|
|
const extracted = Object.entries(metrics).filter(([, v]) => v !== null && v !== undefined);
|
|
console.log(`[parser] extracted ${extracted.length} metrics`);
|
|
|
|
return Object.fromEntries(extracted);
|
|
}
|