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 || "https://testing.mintel.me"; const limit = process.env.PAGESPEED_LIMIT ? parseInt(process.env.PAGESPEED_LIMIT) : 20; const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || "mintel"; const gatekeeperCookie = process.env.AUTH_COOKIE_NAME || "mintel_gatekeeper_session"; 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: `${gatekeeperCookie}=${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 home = urls.filter( (u) => u.endsWith("/de") || u.endsWith("/en") || u === targetUrl, ); const others = urls.filter((u) => !home.includes(u)); urls = [...home, ...others.slice(0, limit - home.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: `${gatekeeperCookie}=${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} --collect.settings.chromeFlags='--no-sandbox --disable-setuid-sandbox --headless' --collect.settings.extraHeaders='${extraHeaders}' && npx lhci assert`; 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();