import puppeteer, { HTTPResponse } from "puppeteer"; import axios from "axios"; import * as cheerio from "cheerio"; const targetUrl = process.argv.find((arg) => !arg.startsWith("--") && arg.startsWith("http")) || process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"; const limit = process.env.ASSET_CHECK_LIMIT ? parseInt(process.env.ASSET_CHECK_LIMIT) : 20; const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || "mbgrid"; async function main() { console.log(`\nšŸš€ Starting Strict Asset Integrity Check for: ${targetUrl}`); console.log(`šŸ“Š Limit: ${limit} pages\n`); // 1. Fetch Sitemap to discover all routes const sitemapUrl = `${targetUrl.replace(/\/$/, "")}/sitemap.xml`; let urls: string[] = []; try { console.log(`šŸ“„ Fetching sitemap from ${sitemapUrl}...`); const response = await axios.get(sitemapUrl, { headers: { Cookie: `mintel_gatekeeper_session=${gatekeeperPassword}` }, }); const $ = cheerio.load(response.data, { xmlMode: true }); urls = $("url loc") .map((i, el) => $(el).text()) .get(); // Normalize to target URL instance 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} target URLs.`); if (urls.length > limit) { console.log( `āš ļø Too many pages (${urls.length}). Limiting to ${limit} representative pages.`, ); // Simplify selection: home pages + a slice of the rest const homeDE = urls.filter((u) => u.endsWith("/de") || u === targetUrl); const homeEN = urls.filter((u) => u.endsWith("/en")); const others = urls.filter( (u) => !homeEN.includes(u) && !homeDE.includes(u), ); urls = [ ...homeDE, ...homeEN, ...others.slice(0, limit - (homeEN.length + homeDE.length)), ]; } } catch (err: unknown) { const errorBody = err instanceof Error ? err.message : String(err); console.error(`āŒ Failed to fetch sitemap: ${errorBody}`); process.exit(1); } // 2. Launch Headless Browser console.log(`\nšŸ•·ļø Launching Puppeteer Headless Engine...`); const browser = await puppeteer.launch({ headless: true, executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || process.env.CHROME_PATH || undefined, args: [ "--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage", ], }); const page = await browser.newPage(); // Inject Gatekeeper session bypassing auth screens await page.setCookie({ name: "mintel_gatekeeper_session", value: gatekeeperPassword, domain: new URL(targetUrl).hostname, path: "/", httpOnly: true, secure: targetUrl.startsWith("https://"), }); let hasBrokenAssets = false; let hasConsoleErrors = false; const brokenAssetsList: Array<{ url: string; status: number; page: string }> = []; const consoleErrorsList: Array<{ type: string; error: string; page: string; }> = []; // Listen for unhandled exceptions natively in the page page.on("pageerror", (err: unknown) => { const errorMessage = err instanceof Error ? err.message : String(err); consoleErrorsList.push({ type: "PAGE_ERROR", error: errorMessage, page: page.url(), }); hasConsoleErrors = true; }); // Listen for console.error and console.warn messages (like Next.js Image warnings, hydration errors, CSP blocks) // eslint-disable-next-line @typescript-eslint/no-explicit-any page.on("console", (msg: any) => { const type = msg.type(); if (type === "error" || type === "warn") { const text = msg.text(); // Exclude common browser extension noise or third party tracker warnings if ( text.includes("google-analytics") || text.includes("googletagmanager") || text.includes("SES Removing unpermitted intrinsics") || text.includes("Third-party cookie will be blocked") || text.includes("Fast Refresh") ) return; consoleErrorsList.push({ type: type.toUpperCase(), error: text, page: page.url(), }); hasConsoleErrors = true; } }); // Listen to ALL network responses page.on("response", (response: HTTPResponse) => { const status = response.status(); // Catch classic 404s and 500s on ANY fetch/image/script if ( status >= 400 && status !== 999 && !response.url().includes("google-analytics") && !response.url().includes("googletagmanager") ) { const type = response.request().resourceType(); // We explicitly care about images, stylesheets, scripts, and fetch requests (API) getting 404/500s. if (["image", "script", "stylesheet", "fetch", "xhr"].includes(type)) { brokenAssetsList.push({ url: response.url(), status: status, page: page.url(), }); hasBrokenAssets = true; } } }); // 3. Scan each page for (let i = 0; i < urls.length; i++) { const u = urls[i]; console.log(`[${i + 1}/${urls.length}] Scanning: ${u}`); try { // Wait until network is idle to ensure all Next.js hydration and image lazy-loads trigger await page.goto(u, { waitUntil: "networkidle0", timeout: 30000 }); // Force scroll to bottom to trigger any IntersectionObserver lazy-loaded images await page.evaluate(async () => { await new Promise((resolve) => { let totalHeight = 0; const distance = 100; const timer = setInterval(() => { const scrollHeight = document.body.scrollHeight; window.scrollBy(0, distance); totalHeight += distance; if (totalHeight >= scrollHeight) { clearInterval(timer); resolve(); } }, 100); }); }); // Wait a tiny bit more for final lazy loads await new Promise((r) => setTimeout(r, 1000)); } catch (err: unknown) { const errorBody = err instanceof Error ? err.message : String(err); console.error(`āš ļø Timeout or navigation error on ${u}: ${errorBody}`); // Don't fail the whole script just because one page timed out, but flag it hasBrokenAssets = true; } } await browser.close(); // 4. Report Results if (hasBrokenAssets && brokenAssetsList.length > 0) { console.error( `\nāŒ FATAL: Broken assets (404/500) detected heavily on the site!`, ); console.table(brokenAssetsList); } if (hasConsoleErrors && consoleErrorsList.length > 0) { console.error(`\nāŒ FATAL: Console Errors/Warnings detected on the site!`); console.table(consoleErrorsList); } if (hasBrokenAssets || hasConsoleErrors) { console.error( `\n🚨 The CI build will now fail to prevent bad code from reaching production.`, ); process.exit(1); } else { console.log( `\nšŸŽ‰ SUCCESS: All ${urls.length} pages rendered perfectly with 0 broken images or console errors!`, ); process.exit(0); } } main();