import puppeteer, { HTTPResponse } from 'puppeteer'; import axios from 'axios'; import * as cheerio from 'cheerio'; const targetUrl = process.argv[2] || process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'; const gatekeeperPassword = process.env.GATEKEEPER_PASSWORD || 'klz2026'; async function main() { console.log(`\nšŸš€ Starting Strict Asset Integrity Check for: ${targetUrl}`); // 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: `klz_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.`); } catch (err: any) { console.error(`āŒ Failed to fetch sitemap: ${err.message}`); process.exit(1); } // 2. Launch Headless Browser console.log(`\nšŸ•·ļø Launching Puppeteer Headless Engine...`); const browser = await puppeteer.launch({ headless: true, 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: 'klz_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) => { consoleErrorsList.push({ type: 'PAGE_ERROR', error: err.message, page: page.url(), }); hasConsoleErrors = true; }); // Listen for console.error and console.warn messages (like Next.js Image warnings, hydration errors, CSP blocks) page.on('console', (msg) => { const type = msg.type(); if (type === 'error' || type === 'warning') { 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: any) { console.error(`āš ļø Timeout or navigation error on ${u}: ${err.message}`); // 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();