lighthouse fixes

This commit is contained in:
2026-02-10 22:37:29 -05:00
parent 26a8c97841
commit 07d8787972
785 changed files with 166486 additions and 77 deletions

View File

@@ -0,0 +1,260 @@
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { spawn } from "node:child_process";
import { setTimeout as sleep } from "node:timers/promises";
import lighthouse from "lighthouse";
import { launch } from "chrome-launcher";
type Theme = "light" | "dark" | "high-contrast";
type Device = "mobile" | "desktop";
type Variant = {
id: `${Theme}-${Device}`;
theme: Theme;
device: Device;
screenEmulation: {
mobile: boolean;
width: number;
height: number;
deviceScaleFactor: number;
disabled: false;
};
};
type CategoryKey = "performance" | "accessibility" | "best-practices" | "seo";
const VARIANTS: Variant[] = [
{
id: "light-mobile",
theme: "light",
device: "mobile",
screenEmulation: { mobile: true, width: 360, height: 800, deviceScaleFactor: 2, disabled: false },
},
{
id: "dark-mobile",
theme: "dark",
device: "mobile",
screenEmulation: { mobile: true, width: 360, height: 800, deviceScaleFactor: 2, disabled: false },
},
{
id: "high-contrast-mobile",
theme: "high-contrast",
device: "mobile",
screenEmulation: { mobile: true, width: 360, height: 800, deviceScaleFactor: 2, disabled: false },
},
{
id: "light-desktop",
theme: "light",
device: "desktop",
screenEmulation: { mobile: false, width: 1350, height: 940, deviceScaleFactor: 1, disabled: false },
},
{
id: "dark-desktop",
theme: "dark",
device: "desktop",
screenEmulation: { mobile: false, width: 1350, height: 940, deviceScaleFactor: 1, disabled: false },
},
{
id: "high-contrast-desktop",
theme: "high-contrast",
device: "desktop",
screenEmulation: { mobile: false, width: 1350, height: 940, deviceScaleFactor: 1, disabled: false },
},
];
const CATEGORIES: CategoryKey[] = ["performance", "accessibility", "best-practices", "seo"];
function hasFlag(flag: string): boolean {
return process.argv.includes(flag);
}
function getArg(name: string): string | undefined {
const prefix = `${name}=`;
const value = process.argv.find((arg) => arg.startsWith(prefix));
return value ? value.slice(prefix.length) : undefined;
}
function median(values: number[]): number {
const sorted = [...values].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
}
async function startPreviewServerIfNeeded(targetUrl: URL): Promise<(() => void) | null> {
const forceNoPreview = process.env.LIGHTHOUSE_SKIP_PREVIEW === "true";
if (forceNoPreview) return null;
const isLocal = targetUrl.hostname === "127.0.0.1" || targetUrl.hostname === "localhost";
if (!isLocal) return null;
const port = Number(targetUrl.port || "4321");
const astroCli = path.join(process.cwd(), "node_modules", "astro", "astro.js");
const child = spawn(process.execPath, [astroCli, "preview", "--host", "127.0.0.1", "--port", String(port)], {
cwd: process.cwd(),
stdio: "pipe",
env: process.env,
});
let ready = false;
child.stdout.on("data", (buf) => {
const line = String(buf);
if (line.includes("Local") || line.includes("localhost") || line.includes("127.0.0.1")) {
ready = true;
}
});
child.stderr.on("data", () => {});
for (let i = 0; i < 30; i += 1) {
if (ready) break;
await sleep(500);
}
await sleep(1500);
return () => {
child.kill("SIGTERM");
};
}
async function run() {
const outputDir = getArg("--out") || process.env.LIGHTHOUSE_OUTPUT_DIR || "lighthouse-reports";
const target = getArg("--url") || process.env.LIGHTHOUSE_TARGET_URL || "http://127.0.0.1:4321/";
const runCount = Number(getArg("--runs") || process.env.LIGHTHOUSE_RUNS || "3");
const assert100 = hasFlag("--assert-100");
if (!Number.isInteger(runCount) || runCount < 1) {
throw new Error(`Invalid run count: ${runCount}`);
}
const targetUrl = new URL(target);
await mkdir(outputDir, { recursive: true });
const stopPreview = await startPreviewServerIfNeeded(targetUrl);
try {
const summary: Record<string, unknown> = {
target,
runCount,
generatedAt: new Date().toISOString(),
variants: {},
};
let gateFailed = false;
for (const variant of VARIANTS) {
const byCategory: Record<CategoryKey, number[]> = {
performance: [],
accessibility: [],
"best-practices": [],
seo: [],
};
for (let runIndex = 1; runIndex <= runCount; runIndex += 1) {
let chrome:
| Awaited<ReturnType<typeof launch>>
| undefined;
for (let attempt = 1; attempt <= 3; attempt += 1) {
try {
chrome = await launch({ chromeFlags: ["--headless=new", "--disable-gpu", "--no-sandbox"] });
break;
} catch (error) {
const message = String(error || "");
if (!message.includes("EPERM") || attempt === 3) {
throw error;
}
await sleep(250 * attempt);
}
}
if (!chrome) {
throw new Error(`Failed to launch Chrome for ${variant.id} run ${runIndex}.`);
}
try {
let result:
| Awaited<ReturnType<typeof lighthouse>>
| undefined;
for (let attempt = 1; attempt <= 3; attempt += 1) {
try {
result = await lighthouse(target, {
port: chrome.port,
logLevel: "error",
output: "json",
onlyCategories: CATEGORIES,
formFactor: variant.device,
screenEmulation: variant.screenEmulation,
throttlingMethod: "simulate",
disableStorageReset: true,
extraHeaders: { Cookie: `site_theme=${encodeURIComponent(variant.theme)}` },
});
break;
} catch (error) {
const message = String(error || "");
if (!message.includes("EPERM") || attempt === 3) {
throw error;
}
await sleep(250 * attempt);
}
}
if (!result?.lhr || typeof result.report !== "string") {
throw new Error(`Lighthouse did not produce a report for ${variant.id} run ${runIndex}.`);
}
const lhr = result.lhr;
const reportPath = path.join(outputDir, `${variant.id}.run-${runIndex}.json`);
await writeFile(reportPath, result.report, "utf8");
for (const category of CATEGORIES) {
const score = lhr.categories?.[category]?.score ?? 0;
byCategory[category].push(Math.round(score * 100));
}
} finally {
chrome.kill();
}
}
const medians = Object.fromEntries(
CATEGORIES.map((category) => [category, median(byCategory[category])]),
) as Record<CategoryKey, number>;
if (assert100) {
for (const category of CATEGORIES) {
if (medians[category] < 100) {
gateFailed = true;
}
}
}
summary.variants = {
...(summary.variants as Record<string, unknown>),
[variant.id]: {
theme: variant.theme,
device: variant.device,
scoresByRun: byCategory,
medianScores: medians,
},
};
}
await writeFile(path.join(outputDir, "summary.json"), JSON.stringify(summary, null, 2), "utf8");
if (assert100 && gateFailed) {
throw new Error("Lighthouse quality gate failed: one or more variant category medians are below 100.");
}
// eslint-disable-next-line no-console
console.log(`[lighthouse] wrote reports to ${outputDir}`);
} finally {
stopPreview?.();
}
}
run().catch((e) => {
// eslint-disable-next-line no-console
console.error(`[lighthouse] ${String(e)}`);
process.exitCode = 1;
});