UNPKG

ai-debug-local-mcp

Version:

🎯 ENHANCED AI GUIDANCE v4.1.2: Dramatically improved tool descriptions help AI users choose the right tools instead of 'close enough' options. Ultra-fast keyboard automation (10x speed), universal recording, multi-ecosystem debugging support, and compreh

459 lines (393 loc) • 15.5 kB
import { Page } from 'playwright'; export interface NextJSPageInfo { path: string; type: 'static' | 'ssr' | 'ssg' | 'isr'; dataFetching: ('getStaticProps' | 'getServerSideProps' | 'getStaticPaths')[]; hasLayout: boolean; isAppDir: boolean; } export interface NextJSBuildInfo { buildId: string; runtimeConfig?: any; assetPrefix?: string; basePath?: string; } export interface RSCPayload { componentName: string; props: any; timestamp: Date; size: number; } export interface NextJSImageOptimization { unoptimizedImages: Array<{ src: string; format: string; displayed: { width: number; height: number }; actual: { width: number; height: number }; improvement: string; }>; lazyLoadFailures: string[]; nextImageUsage: number; } export class NextJSDebugEngine { private page?: Page; private rscPayloads: RSCPayload[] = []; private pageTransitions: Array<{ from: string; to: string; type: 'client' | 'server'; duration: number; timestamp: Date; }> = []; async attachToPage(page: Page): Promise<void> { this.page = page; await this.injectNextJSMonitoring(); await this.setupRSCMonitoring(); await this.setupImageOptimizationTracking(); } async detectNextJS(page: Page): Promise<boolean> { return await page.evaluate(() => { // Check for Next.js specific globals const hasNextData = !!(window as any).__NEXT_DATA__; const hasNextRouter = !!(window as any).next?.router; const hasNextHydrated = !!(window as any).__NEXT_HYDRATED; // Check for Next.js specific elements const hasNextRoot = document.getElementById('__next') !== null; const hasNextBuildId = document.querySelector('meta[name="next-head-count"]') !== null; return hasNextData || hasNextRouter || hasNextHydrated || hasNextRoot || hasNextBuildId; }); } private async injectNextJSMonitoring(): Promise<void> { if (!this.page) return; await this.page.addInitScript(() => { (window as any).__NEXTJS_DEBUG__ = { pageTransitions: [], errors: [], buildInfo: null, captureTransition: function(from: string, to: string, type: string, startTime: number) { this.pageTransitions.push({ from, to, type, duration: Date.now() - startTime, timestamp: new Date().toISOString() }); }, captureError: function(error: any, errorInfo: any) { this.errors.push({ message: error.message || String(error), stack: error.stack, componentStack: errorInfo?.componentStack, timestamp: new Date().toISOString(), isChunkLoadError: error.message?.includes('ChunkLoadError'), is404: error.message?.includes('404') }); } }; // Capture build info if ((window as any).__NEXT_DATA__) { (window as any).__NEXTJS_DEBUG__.buildInfo = { buildId: (window as any).__NEXT_DATA__.buildId, page: (window as any).__NEXT_DATA__.page, query: (window as any).__NEXT_DATA__.query, assetPrefix: (window as any).__NEXT_DATA__.assetPrefix, runtimeConfig: (window as any).__NEXT_DATA__.runtimeConfig, dynamicIds: (window as any).__NEXT_DATA__.dynamicIds, gssp: (window as any).__NEXT_DATA__.gssp, gip: (window as any).__NEXT_DATA__.gip, gsp: (window as any).__NEXT_DATA__.gsp, isPreview: (window as any).__NEXT_DATA__.isPreview }; } // Monitor Next.js router if ((window as any).next?.router) { const router = (window as any).next.router; const originalPush = router.push; const originalReplace = router.replace; router.push = async function(...args: any[]) { const startTime = Date.now(); const from = window.location.pathname; const result = await originalPush.apply(this, args); const to = window.location.pathname; (window as any).__NEXTJS_DEBUG__.captureTransition(from, to, 'client', startTime); return result; }; router.replace = async function(...args: any[]) { const startTime = Date.now(); const from = window.location.pathname; const result = await originalReplace.apply(this, args); const to = window.location.pathname; (window as any).__NEXTJS_DEBUG__.captureTransition(from, to, 'client', startTime); return result; }; } // Monitor for hydration errors const originalError = console.error; console.error = function(...args) { const errorStr = args.join(' '); if (errorStr.includes('Hydration') || errorStr.includes('Text content does not match') || errorStr.includes('did not match. Server:')) { (window as any).__NEXTJS_DEBUG__.captureError( new Error(errorStr), { source: 'hydration' } ); } return originalError.apply(console, args); }; }); } private async setupRSCMonitoring(): Promise<void> { if (!this.page) return; // Monitor React Server Component payloads this.page.on('response', async (response) => { const url = response.url(); const contentType = response.headers()['content-type'] || ''; // Detect RSC payloads (they have a specific content type) if (contentType.includes('text/x-component') || url.includes('_rsc') || response.headers()['x-nextjs-data']) { try { const text = await response.text(); const size = text.length; // Parse RSC payload format const componentMatch = text.match(/^0:"([^"]+)"/m); const componentName = componentMatch ? componentMatch[1] : 'Unknown'; this.rscPayloads.push({ componentName, props: {}, // Would need more parsing for actual props timestamp: new Date(), size }); } catch (error) { // Ignore parsing errors } } }); } private async setupImageOptimizationTracking(): Promise<void> { if (!this.page) return; await this.page.addInitScript(() => { (window as any).__NEXTJS_IMAGE_DEBUG__ = { unoptimizedImages: [], lazyLoadObserver: null, init: function() { // Track all images const checkImage = (img: HTMLImageElement) => { const src = img.src; const isNextImage = img.hasAttribute('data-nimg'); // Check if using next/image if (!isNextImage && src && !src.includes('_next/image')) { const displayed = { width: img.clientWidth, height: img.clientHeight }; // Get natural dimensions when loaded if (img.complete) { this.recordUnoptimized(img, displayed); } else { img.addEventListener('load', () => { this.recordUnoptimized(img, displayed); }); } } }; // Check existing images document.querySelectorAll('img').forEach(checkImage); // Monitor new images const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeName === 'IMG') { checkImage(node as HTMLImageElement); } else if (node instanceof Element) { node.querySelectorAll('img').forEach(checkImage); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); }, recordUnoptimized: function(img: HTMLImageElement, displayed: any) { const actual = { width: img.naturalWidth, height: img.naturalHeight }; // Calculate potential improvement let improvement = 'None'; if (actual.width > displayed.width * 2 || actual.height > displayed.height * 2) { improvement = 'High - Image is 2x+ larger than displayed size'; } else if (actual.width > displayed.width * 1.5 || actual.height > displayed.height * 1.5) { improvement = 'Medium - Image is 1.5x larger than displayed size'; } // Detect format const format = img.src.split('.').pop()?.split('?')[0] || 'unknown'; this.unoptimizedImages.push({ src: img.src, format, displayed, actual, improvement }); } }; // Initialize after DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { (window as any).__NEXTJS_IMAGE_DEBUG__.init(); }); } else { (window as any).__NEXTJS_IMAGE_DEBUG__.init(); } }); } async getPageInfo(): Promise<NextJSPageInfo | null> { if (!this.page) return null; return await this.page.evaluate(() => { const nextData = (window as any).__NEXT_DATA__; if (!nextData) return null; const pageInfo: NextJSPageInfo = { path: nextData.page, type: 'static', dataFetching: [], hasLayout: false, isAppDir: nextData.page.includes('/app/') || !!(window as any).__next_app__ }; // Determine page type if (nextData.gssp) { pageInfo.type = 'ssr'; pageInfo.dataFetching.push('getServerSideProps'); } else if (nextData.gsp) { pageInfo.type = 'ssg'; pageInfo.dataFetching.push('getStaticProps'); } else if (nextData.isFallback) { pageInfo.type = 'isr'; } if (nextData.gip) { pageInfo.dataFetching.push('getStaticPaths'); } // Check for layout pageInfo.hasLayout = document.querySelector('[data-nextjs-layout]') !== null; return pageInfo; }); } async getBuildInfo(): Promise<NextJSBuildInfo | null> { if (!this.page) return null; return await this.page.evaluate(() => { const debug = (window as any).__NEXTJS_DEBUG__; return debug?.buildInfo || null; }); } async getHydrationErrors(): Promise<any[]> { if (!this.page) return []; const errors = await this.page.evaluate(() => { const debug = (window as any).__NEXTJS_DEBUG__; return debug?.errors.filter((e: any) => e.source === 'hydration') || []; }); return errors; } async getPageTransitions(): Promise<any[]> { if (!this.page) return []; return await this.page.evaluate(() => { const debug = (window as any).__NEXTJS_DEBUG__; return debug?.pageTransitions || []; }); } async getRSCPayloads(): Promise<RSCPayload[]> { return this.rscPayloads; } async getImageOptimizationIssues(): Promise<NextJSImageOptimization> { if (!this.page) return { unoptimizedImages: [], lazyLoadFailures: [], nextImageUsage: 0 }; const data = await this.page.evaluate(() => { const imageDebug = (window as any).__NEXTJS_IMAGE_DEBUG__; const allImages = document.querySelectorAll('img').length; const nextImages = document.querySelectorAll('img[data-nimg]').length; return { unoptimizedImages: imageDebug?.unoptimizedImages || [], lazyLoadFailures: [], // Would need IntersectionObserver tracking nextImageUsage: allImages > 0 ? (nextImages / allImages) * 100 : 0 }; }); return data; } async detectNextJSProblems(): Promise<Array<{ problem: string; severity: 'low' | 'medium' | 'high'; description: string; solution: string; }>> { const problems = []; // Check for hydration errors const hydrationErrors = await this.getHydrationErrors(); if (hydrationErrors.length > 0) { problems.push({ problem: 'Next.js Hydration Errors', severity: 'high' as const, description: `Found ${hydrationErrors.length} hydration errors. This breaks interactivity.`, solution: 'Ensure consistent rendering between server and client. Check for browser-only APIs in SSR/SSG pages.' }); } // Check RSC payload sizes const rscPayloads = await this.getRSCPayloads(); const largePayloads = rscPayloads.filter(p => p.size > 50 * 1024); // 50KB if (largePayloads.length > 0) { problems.push({ problem: 'Large Server Component Payloads', severity: 'medium' as const, description: `${largePayloads.length} RSC payloads exceed 50KB. Largest: ${(largePayloads[0]?.size / 1024).toFixed(2)}KB`, solution: 'Reduce data fetching in Server Components, use pagination, or move large data to client components.' }); } // Check image optimization const imageIssues = await this.getImageOptimizationIssues(); if (imageIssues.unoptimizedImages.length > 0) { const highImpact = imageIssues.unoptimizedImages.filter(img => img.improvement.includes('High') ).length; problems.push({ problem: 'Unoptimized Images', severity: highImpact > 0 ? 'high' as const : 'medium' as const, description: `${imageIssues.unoptimizedImages.length} images not using next/image. ${highImpact} have high optimization potential.`, solution: 'Use next/image component for automatic optimization, lazy loading, and responsive images.' }); } // Check page type efficiency const pageInfo = await this.getPageInfo(); if (pageInfo?.type === 'ssr' && !pageInfo.dataFetching.includes('getServerSideProps')) { problems.push({ problem: 'Unnecessary SSR', severity: 'low' as const, description: 'Page uses SSR but has no data fetching. Could be static.', solution: 'Convert to static generation (SSG) for better performance and caching.' }); } // Check for slow page transitions const transitions = await this.getPageTransitions(); const slowTransitions = transitions.filter(t => t.duration > 2000); if (slowTransitions.length > 0) { problems.push({ problem: 'Slow Page Transitions', severity: 'medium' as const, description: `${slowTransitions.length} page transitions took over 2 seconds.`, solution: 'Use next/link for prefetching, optimize getServerSideProps, or consider static generation.' }); } // Check Next.js image usage if (imageIssues.nextImageUsage < 50 && imageIssues.unoptimizedImages.length > 5) { problems.push({ problem: 'Low next/image Adoption', severity: 'medium' as const, description: `Only ${imageIssues.nextImageUsage.toFixed(1)}% of images use next/image optimization.`, solution: 'Migrate img tags to next/image for automatic optimization and better Core Web Vitals.' }); } return problems; } }