Files
Nils-Johan Gynther 69bcc3e342
Test Suite / backend-pr-quick (push) Has been skipped
Test Suite / quick-import-pr-quick (push) Has been skipped
Test Suite / backend-full (push) Successful in 14m6s
Test Suite / flutter-quality (push) Failing after 4m44s
feat(web): improve web build configuration and accessibility
- Add source maps and web renderer build arguments with defaults
- Configure Caddy with CSP headers, cache policies, and service worker handling
- Defer loading of import screen for performance optimization
- Add semantic labels to icons for accessibility
- Update web index.html with Swedish language, meta tags, and description
- Add robots.txt and lighthouse configuration
- Add new planning documents and archive entries
2026-05-23 18:04:27 +02:00

190 lines
6.0 KiB
JavaScript

#!/usr/bin/env node
import { execFileSync } from 'node:child_process';
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
function parseArgs(argv) {
const args = {
url: '',
label: 'baseline',
runs: 3,
formFactor: 'mobile',
outDir: 'flutter/lighthouse/results',
};
for (let i = 2; i < argv.length; i += 1) {
const current = argv[i];
const next = argv[i + 1];
if (current === '--url' && next) {
args.url = next;
i += 1;
} else if (current === '--label' && next) {
args.label = next;
i += 1;
} else if (current === '--runs' && next) {
args.runs = Number.parseInt(next, 10);
i += 1;
} else if (current === '--form-factor' && next) {
args.formFactor = next;
i += 1;
} else if (current === '--out-dir' && next) {
args.outDir = next;
i += 1;
}
}
if (!args.url) {
throw new Error('Missing --url, example: node flutter/lighthouse/collect.mjs --url http://localhost:5000');
}
if (!Number.isFinite(args.runs) || args.runs < 1) {
throw new Error('--runs must be >= 1');
}
return args;
}
function median(values) {
if (values.length === 0) return null;
const ordered = [...values].sort((a, b) => a - b);
const center = Math.floor(ordered.length / 2);
if (ordered.length % 2 === 0) {
return (ordered[center - 1] + ordered[center]) / 2;
}
return ordered[center];
}
function toMs(value) {
if (typeof value !== 'number') return null;
return Math.round(value);
}
function readMetric(lhr, id) {
const audit = lhr.audits[id];
return audit && typeof audit.numericValue === 'number' ? audit.numericValue : null;
}
function readResourceSummary(lhr) {
const details = lhr.audits['resource-summary']?.details;
const items = details?.items;
if (!Array.isArray(items)) return { transferKb: null, requests: null };
const total = items.find((item) => item.resourceType === 'total');
if (!total) return { transferKb: null, requests: null };
return {
transferKb: typeof total.transferSize === 'number' ? Math.round(total.transferSize / 1024) : null,
requests: typeof total.requestCount === 'number' ? total.requestCount : null,
};
}
function formatNullable(value) {
return value == null ? 'n/a' : String(value);
}
function main() {
const args = parseArgs(process.argv);
const outputRoot = resolve(args.outDir);
mkdirSync(outputRoot, { recursive: true });
const runRows = [];
for (let runIndex = 1; runIndex <= args.runs; runIndex += 1) {
const outputPath = join(outputRoot, `${args.label}-run-${runIndex}.json`);
const flags = [
'lighthouse',
args.url,
'--only-categories=performance,accessibility,seo',
'--throttling-method=simulate',
`--form-factor=${args.formFactor}`,
'--screenEmulation.mobile=true',
'--output=json',
`--output-path=${outputPath}`,
'--chrome-flags=--headless=new --no-sandbox',
'--quiet',
];
execFileSync('npx', flags, { stdio: 'inherit' });
const lhr = JSON.parse(readFileSync(outputPath, 'utf8'));
const summary = readResourceSummary(lhr);
runRows.push({
run: runIndex,
performance: Math.round((lhr.categories.performance.score ?? 0) * 100),
accessibility: Math.round((lhr.categories.accessibility.score ?? 0) * 100),
seo: Math.round((lhr.categories.seo.score ?? 0) * 100),
lcpMs: toMs(readMetric(lhr, 'largest-contentful-paint')),
tbtMs: toMs(readMetric(lhr, 'total-blocking-time')),
inpMs: toMs(readMetric(lhr, 'interaction-to-next-paint')),
transferKb: summary.transferKb,
requests: summary.requests,
fetchTime: lhr.fetchTime,
finalUrl: lhr.finalUrl,
});
}
const medians = {
performance: median(runRows.map((r) => r.performance)),
accessibility: median(runRows.map((r) => r.accessibility)),
seo: median(runRows.map((r) => r.seo)),
lcpMs: median(runRows.filter((r) => r.lcpMs != null).map((r) => r.lcpMs)),
tbtMs: median(runRows.filter((r) => r.tbtMs != null).map((r) => r.tbtMs)),
inpMs: median(runRows.filter((r) => r.inpMs != null).map((r) => r.inpMs)),
transferKb: median(runRows.filter((r) => r.transferKb != null).map((r) => r.transferKb)),
requests: median(runRows.filter((r) => r.requests != null).map((r) => r.requests)),
};
const summaryPath = join(outputRoot, `${args.label}-summary.json`);
writeFileSync(
summaryPath,
JSON.stringify(
{
url: args.url,
label: args.label,
runs: args.runs,
formFactor: args.formFactor,
medians,
results: runRows,
},
null,
2,
),
);
const markdownPath = join(outputRoot, `${args.label}-summary.md`);
const markdown = [
`# Lighthouse summary: ${args.label}`,
'',
`- URL: ${args.url}`,
`- Runs: ${args.runs}`,
`- Form factor: ${args.formFactor}`,
'',
'| Metric | Median |',
'| --- | ---: |',
`| Performance | ${formatNullable(medians.performance)} |`,
`| Accessibility | ${formatNullable(medians.accessibility)} |`,
`| SEO | ${formatNullable(medians.seo)} |`,
`| LCP (ms) | ${formatNullable(medians.lcpMs)} |`,
`| TBT (ms) | ${formatNullable(medians.tbtMs)} |`,
`| INP (ms) | ${formatNullable(medians.inpMs)} |`,
`| Transfer (KB) | ${formatNullable(medians.transferKb)} |`,
`| Requests | ${formatNullable(medians.requests)} |`,
'',
'## Runs',
'',
'| Run | Perf | A11y | SEO | LCP | TBT | INP | KB | Req |',
'| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |',
...runRows.map(
(row) =>
`| ${row.run} | ${row.performance} | ${row.accessibility} | ${row.seo} | ${formatNullable(row.lcpMs)} | ${formatNullable(row.tbtMs)} | ${formatNullable(row.inpMs)} | ${formatNullable(row.transferKb)} | ${formatNullable(row.requests)} |`,
),
'',
].join('\n');
writeFileSync(markdownPath, markdown);
process.stdout.write(`Wrote ${summaryPath}\n`);
process.stdout.write(`Wrote ${markdownPath}\n`);
}
main();