160 lines
5.9 KiB
TypeScript
160 lines
5.9 KiB
TypeScript
import axios from 'axios';
|
|
import * as cheerio from 'cheerio';
|
|
import { execSync } from 'child_process';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
|
|
/**
|
|
* PageSpeed Test Script
|
|
*
|
|
* 1. Fetches sitemap.xml from the target URL
|
|
* 2. Extracts all URLs
|
|
* 3. Runs Lighthouse CI on those URLs
|
|
*/
|
|
|
|
const targetUrl = process.argv[2] || process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
|
|
const limit = process.env.PAGESPEED_LIMIT ? parseInt(process.env.PAGESPEED_LIMIT) : 20; // Default limit to avoid infinite runs
|
|
const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'klz2026';
|
|
|
|
async function main() {
|
|
console.log(`\n🚀 Starting PageSpeed test for: ${targetUrl}`);
|
|
console.log(`📊 Limit: ${limit} pages\n`);
|
|
|
|
try {
|
|
// 1. Fetch Sitemap
|
|
const sitemapUrl = `${targetUrl.replace(/\/$/, '')}/sitemap.xml`;
|
|
console.log(`📥 Fetching sitemap from ${sitemapUrl}...`);
|
|
|
|
// We might need to bypass gatekeeper for the sitemap fetch too
|
|
const response = await axios.get(sitemapUrl, {
|
|
headers: {
|
|
Cookie: `klz_gatekeeper_session=${gatekeeperPassword}`,
|
|
},
|
|
validateStatus: (status) => status < 400,
|
|
});
|
|
|
|
const $ = cheerio.load(response.data, { xmlMode: true });
|
|
let urls = $('url loc')
|
|
.map((i, el) => $(el).text())
|
|
.get();
|
|
|
|
// Cleanup, filter and normalize domains to targetUrl
|
|
const urlPattern = /https?:\/\/[^\/]+/;
|
|
urls = [...new Set(urls)]
|
|
.filter((u) => u.startsWith('http'))
|
|
.map((u) => u.replace(urlPattern, targetUrl.replace(/\/$/, '')))
|
|
.sort();
|
|
|
|
console.log(`✅ Found ${urls.length} URLs in sitemap.`);
|
|
|
|
if (urls.length === 0) {
|
|
console.error('❌ No URLs found in sitemap. Is the site up?');
|
|
process.exit(1);
|
|
}
|
|
|
|
if (urls.length > limit) {
|
|
console.log(
|
|
`⚠️ Too many pages (${urls.length}). Limiting to ${limit} representative pages.`,
|
|
);
|
|
// Try to pick a variety: home, some products, some blog posts
|
|
const homeEN = urls.filter((u) => u.endsWith('/en') || u === targetUrl);
|
|
const homeDE = urls.filter((u) => u.endsWith('/de'));
|
|
const others = urls.filter((u) => !homeEN.includes(u) && !homeDE.includes(u));
|
|
urls = [...homeEN, ...homeDE, ...others.slice(0, limit - (homeEN.length + homeDE.length))];
|
|
}
|
|
|
|
console.log(`🧪 Pages to be tested:`);
|
|
urls.forEach((u) => console.log(` - ${u}`));
|
|
|
|
// 2. Prepare LHCI command
|
|
// We use --collect.url multiple times
|
|
const urlArgs = urls.map((u) => `--collect.url="${u}"`).join(' ');
|
|
|
|
// Handle authentication for staging/testing
|
|
// Lighthouse can set cookies via --collect.settings.extraHeaders
|
|
const extraHeaders = JSON.stringify({
|
|
Cookie: `klz_gatekeeper_session=${gatekeeperPassword}`,
|
|
});
|
|
|
|
const chromePath = process.env.CHROME_PATH || process.env.PUPPETEER_EXECUTABLE_PATH;
|
|
const chromePathArg = chromePath ? `--collect.chromePath="${chromePath}"` : '';
|
|
|
|
// Clean up old reports
|
|
if (fs.existsSync('.lighthouseci')) {
|
|
fs.rmSync('.lighthouseci', { recursive: true, force: true });
|
|
}
|
|
|
|
// Using a more robust way to execute and capture output
|
|
// We remove 'npx lhci upload' to keep everything local and avoid Google-hosted reports
|
|
const lhciCommand = `npx lhci collect ${urlArgs} ${chromePathArg} --config=config/lighthouserc.json --collect.settings.extraHeaders='${extraHeaders}' --collect.settings.chromeFlags="--no-sandbox --disable-setuid-sandbox --disable-dev-shm-usage" && npx lhci assert --config=config/lighthouserc.json`;
|
|
|
|
console.log(`💻 Executing LHCI...`);
|
|
|
|
try {
|
|
execSync(lhciCommand, {
|
|
encoding: 'utf8',
|
|
stdio: 'inherit',
|
|
});
|
|
} catch (err: any) {
|
|
console.warn('⚠️ LHCI assertion finished with warnings or errors.');
|
|
// We continue to show the table even if assertions failed
|
|
}
|
|
|
|
// 3. Summarize Results (Local & Independent)
|
|
const manifestPath = path.join(process.cwd(), '.lighthouseci', 'manifest.json');
|
|
if (fs.existsSync(manifestPath)) {
|
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
console.log(`\n📊 PageSpeed Summary (FOSS - Local Report):\n`);
|
|
|
|
const summaryTable = manifest.map((entry: any) => {
|
|
const s = entry.summary;
|
|
return {
|
|
URL: entry.url.replace(targetUrl, ''),
|
|
Perf: Math.round(s.performance * 100),
|
|
Acc: Math.round(s.accessibility * 100),
|
|
BP: Math.round(s['best-practices'] * 100),
|
|
SEO: Math.round(s.seo * 100),
|
|
};
|
|
});
|
|
|
|
console.table(summaryTable);
|
|
|
|
// Calculate Average
|
|
const avg = {
|
|
Perf: Math.round(
|
|
summaryTable.reduce((acc: any, curr: any) => acc + curr.Perf, 0) / summaryTable.length,
|
|
),
|
|
Acc: Math.round(
|
|
summaryTable.reduce((acc: any, curr: any) => acc + curr.Acc, 0) / summaryTable.length,
|
|
),
|
|
BP: Math.round(
|
|
summaryTable.reduce((acc: any, curr: any) => acc + curr.BP, 0) / summaryTable.length,
|
|
),
|
|
SEO: Math.round(
|
|
summaryTable.reduce((acc: any, curr: any) => acc + curr.SEO, 0) / summaryTable.length,
|
|
),
|
|
};
|
|
|
|
console.log(`\n📈 Average Scores:`);
|
|
console.log(` Performance: ${avg.Perf > 90 ? '✅' : '⚠️'} ${avg.Perf}`);
|
|
console.log(` Accessibility: ${avg.Acc > 90 ? '✅' : '⚠️'} ${avg.Acc}`);
|
|
console.log(` Best Practices: ${avg.BP > 90 ? '✅' : '⚠️'} ${avg.BP}`);
|
|
console.log(` SEO: ${avg.SEO > 90 ? '✅' : '⚠️'} ${avg.SEO}`);
|
|
}
|
|
|
|
console.log(`\n✨ PageSpeed tests completed successfully!`);
|
|
} catch (error: any) {
|
|
console.error(`\n❌ Error during PageSpeed test:`);
|
|
if (axios.isAxiosError(error)) {
|
|
console.error(`Status: ${error.response?.status}`);
|
|
console.error(`StatusText: ${error.response?.statusText}`);
|
|
console.error(`URL: ${error.config?.url}`);
|
|
} else {
|
|
console.error(error.message);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
main();
|