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.find((arg) => !arg.startsWith('--') && arg.startsWith('http')) || process.env.NEXT_PUBLIC_BASE_URL || process.env.LHCI_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}`, }); // Detect Chrome path from Puppeteer installation if not provided let chromePath = process.env.CHROME_PATH || process.env.PUPPETEER_EXECUTABLE_PATH; if (!chromePath) { try { console.log('๐Ÿ” Attempting to detect Puppeteer Chrome path...'); const puppeteerInfo = execSync('npx puppeteer browsers latest chrome', { encoding: 'utf8', }); console.log(`๐Ÿ“ฆ Puppeteer info: ${puppeteerInfo}`); const match = puppeteerInfo.match(/executablePath: (.*)/); if (match && match[1]) { chromePath = match[1].trim(); console.log(`โœ… Detected Puppeteer Chrome at: ${chromePath}`); } } catch (e: any) { console.warn(`โš ๏ธ Could not detect Puppeteer Chrome path via command: ${e.message}`); } // Fallback to known paths if still not found if (!chromePath) { const fallbacks = [ '/root/.cache/puppeteer/chrome/linux-145.0.7632.77/chrome-linux64/chrome', '/home/runner/.cache/puppeteer/chrome/linux-145.0.7632.77/chrome-linux64/chrome', path.join( process.cwd(), 'node_modules', '.puppeteer', 'chrome', 'linux-145.0.7632.77', 'chrome-linux64', 'chrome', ), ]; for (const fallback of fallbacks) { if (fs.existsSync(fallback)) { chromePath = fallback; console.log(`โœ… Found Puppeteer Chrome at fallback: ${chromePath}`); break; } } } } else { console.log(`โ„น๏ธ Using existing Chrome path: ${chromePath}`); } if (!chromePath) { console.warn('โŒ CHROME_PATH is still undefined. Lighthouse might fail.'); } 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 use a puppeteer script to set cookies which is more reliable than extraHeaders for LHCI const lhciCommand = `npx lhci collect ${urlArgs} ${chromePathArg} --config=config/lighthouserc.json --collect.puppeteerScript="scripts/lhci-puppeteer-setup.js" --collect.settings.chromeFlags="--no-sandbox --disable-setuid-sandbox --disable-dev-shm-usage" && npx lhci assert --config=config/lighthouserc.json`; console.log(`๐Ÿ’ป Executing LHCI with CHROME_PATH="${chromePath}" and Puppeteer Auth...`); try { execSync(lhciCommand, { encoding: 'utf8', stdio: 'inherit', env: { ...process.env, CHROME_PATH: chromePath }, }); } 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();