/** * P2: JS Router Detection * * Scans the loaded page for client-side route tables declared by React Router v6, * Vue Router, Angular Router, or custom window globals. Supplements the existing * history.pushState interception (which only captures runtime navigation) with a * static analysis of routes declared in the JS bundle. * * Called once per page after goto(). Runs entirely in-browser via page.evaluate(). * Adds discovered routes to the BFS frontier so the crawler visits them. */ import { logger as rootLogger } from './logger.js'; const logger = rootLogger.child({ module: 'js-router-detector' }); /** * Extract absolute page URLs from the current page's JS router config. * Returns deduplicated absolute URLs (same origin). Dynamic segments (:id, *) * are skipped — the crawler can't resolve them without knowing valid IDs. */ export async function detectJsRoutes(page: any, origin: string): Promise { try { const raw: string[] = await page.evaluate(() => { const found: string[] = []; // 1. Check common framework globals ───────────────────────────────────── const windowGlobals = [ '__routes__', '__ROUTER__', '__vue_router__', '__router__', 'router', '_router', '__reactRouter', '__NEXT_DATA__', ]; for (const g of windowGlobals) { try { const val = (window as any)[g]; if (!val || typeof val !== 'object') continue; // Next.js page manifest if (g === '__NEXT_DATA__' && Array.isArray(val.buildManifest?.pages)) { for (const p of val.buildManifest.pages) if (typeof p === 'string') found.push(p); continue; } // Router objects: options.routes, routes, getRoutes() const routeArr = (Array.isArray(val.options?.routes) ? val.options.routes : null) ?? (Array.isArray(val.routes) ? val.routes : null) ?? (typeof val.getRoutes === 'function' ? val.getRoutes() : null); if (!Array.isArray(routeArr)) continue; const flatRoutes = (arr: any[], prefix = ''): void => { for (const r of arr) { if (!r || typeof r !== 'object') continue; const path: string = r.path ?? r.href ?? r.url ?? ''; const full = path.startsWith('/') ? path : `${prefix}/${path}`.replace(/\/\//g, '/'); if (full && full !== '/') found.push(full); if (Array.isArray(r.children)) flatRoutes(r.children, full); } }; flatRoutes(routeArr); } catch { /* skip corrupt globals */ } } // 2. Scan inline