import lighthouse from 'lighthouse'; import desktopConfig from 'lighthouse/core/config/desktop-config.js'; import { chromium } from 'playwright'; import { mkdir, writeFile } from 'node:fs/promises'; import net from 'node:net'; const origin = process.env.CMS_URL || 'http://127.0.0.1:8080'; // Eine Startseite ist nicht die Website. Geprüft wird, was CMS_PATHS nennt - mehrere Adressen mit // Komma getrennt; ohne Angabe die Startseite. const paths = (process.env.CMS_PATHS || '/').split(',').map(p => p.trim()).filter(Boolean); const output = process.env.CMS_AUDIT_DIR || 'reports/lighthouse'; const preflight = await fetch(origin); if (!preflight.ok || /noindex/i.test(preflight.headers.get('x-robots-tag') || '')) { throw new Error('Audit requires an indexable HTTP 200 page in production mode. Check APP_ENV and APP_DEBUG.'); } await mkdir(output, { recursive: true }); const portProbe = net.createServer(); await new Promise(resolve => portProbe.listen(0, '127.0.0.1', resolve)); const port = portProbe.address().port; await new Promise(resolve => portProbe.close(resolve)); // CMS_HOST_RULES, etwa "MAP localhost 127.0.0.1": für eingebettete Seiten, die das Einbetten nur // von localhost erlauben, während der Server auf 127.0.0.1 lauscht (Kantarci-Markt-App). const chrome = await chromium.launch({ args: [`--remote-debugging-port=${port}`, ...(process.env.CMS_HOST_RULES ? [`--host-resolver-rules=${process.env.CMS_HOST_RULES}`] : [])] }); const summary = []; try { for (const path of paths) { const label = '/' === path ? 'home' : path.replace(/^\//, '').replace(/\//g, '-'); for (const profile of ['mobile', 'desktop']) { const runs = []; for (let run = 1; run <= 3; run++) { const result = await lighthouse(origin + ('/' === path ? '' : path), { port, output: 'json', logLevel: 'error', }, profile === 'desktop' ? desktopConfig : undefined); const lhr = result.lhr; if (lhr.configSettings.formFactor !== profile) { throw new Error(`Requested ${profile}, measured ${lhr.configSettings.formFactor}`); } await writeFile(`${output}/${label}-${profile}-${run}.json`, result.report); runs.push(lhr); console.log(label, profile, run, Object.fromEntries(Object.entries(lhr.categories).map(([key, value]) => [key, value.score]))); } const scores = {}; for (const category of Object.keys(runs[0].categories)) { const values = runs.map(r => r.categories[category].score).filter(v => v !== null).sort((a, b) => a - b); scores[category] = values.length ? values[Math.floor(values.length / 2)] : null; } summary.push({ path, profile, scores, lighthouseVersion: runs[0].lighthouseVersion, settings: runs[0].configSettings, environment: runs[0].environment, note: 'Local prototype only. No customer-site acceptance or field-data claim.' }); } } await writeFile(`${output}/summary.json`, JSON.stringify(summary, null, 2)); for (const row of summary) { console.log(`${row.path} ${row.profile}: ${Object.entries(row.scores).map(([k, v]) => k + ' ' + Math.round(v * 100)).join(', ')}`); } const passed = summary.every(r => r.scores.performance >= .95 && ['accessibility', 'best-practices', 'seo'].every(c => r.scores[c] === 1) && (r.scores['agentic-browsing'] === undefined || r.scores['agentic-browsing'] === 1)); if (!passed) process.exitCode = 1; } finally { await chrome.close(); }