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

506 lines • 23.1 kB
export class NextJSDebugEngine { page; rscPayloads = []; serverClientBoundaries = []; dataFetching = []; pageTransitions = []; async attachToPage(page) { this.page = page; await this.injectNextJSMonitoring(); await this.setupRSCMonitoring(); await this.setupImageOptimizationTracking(); } async detectNextJS(page) { return await page.evaluate(() => { // Check for Next.js specific globals const hasNextData = !!window.__NEXT_DATA__; const hasNextRouter = !!window.next?.router; const hasNextHydrated = !!window.__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; }); } async injectNextJSMonitoring() { if (!this.page) return; await this.page.addInitScript(() => { window.__NEXTJS_DEBUG__ = { pageTransitions: [], errors: [], buildInfo: null, serverClientBoundaries: [], dataFetching: [], captureTransition: function (from, to, type, startTime) { this.pageTransitions.push({ from, to, type, duration: Date.now() - startTime, timestamp: new Date().toISOString() }); }, captureError: function (error, errorInfo) { 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') }); }, captureServerClientBoundary: function (component, type, props) { this.serverClientBoundaries.push({ component, type, props, location: new Error().stack?.split('\n')[2] || 'unknown', timestamp: new Date().toISOString() }); }, captureDataFetching: function (type, path, data, duration) { this.dataFetching.push({ type, path, data, duration, timestamp: new Date().toISOString() }); } }; // Capture build info if (window.__NEXT_DATA__) { window.__NEXTJS_DEBUG__.buildInfo = { buildId: window.__NEXT_DATA__.buildId, page: window.__NEXT_DATA__.page, query: window.__NEXT_DATA__.query, assetPrefix: window.__NEXT_DATA__.assetPrefix, runtimeConfig: window.__NEXT_DATA__.runtimeConfig, dynamicIds: window.__NEXT_DATA__.dynamicIds, gssp: window.__NEXT_DATA__.gssp, gip: window.__NEXT_DATA__.gip, gsp: window.__NEXT_DATA__.gsp, isPreview: window.__NEXT_DATA__.isPreview }; } // Monitor Next.js router if (window.next?.router) { const router = window.next.router; const originalPush = router.push; const originalReplace = router.replace; router.push = async function (...args) { const startTime = Date.now(); const from = window.location.pathname; const result = await originalPush.apply(this, args); const to = window.location.pathname; window.__NEXTJS_DEBUG__.captureTransition(from, to, 'client', startTime); return result; }; router.replace = async function (...args) { const startTime = Date.now(); const from = window.location.pathname; const result = await originalReplace.apply(this, args); const to = window.location.pathname; window.__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.__NEXTJS_DEBUG__.captureError(new Error(errorStr), { source: 'hydration' }); } return originalError.apply(console, args); }; // Track server/client component boundaries if (window.React && window.React.createElement) { const originalCreateElement = window.React.createElement; window.React.createElement = function (type, props, ...children) { // Detect use client directive if (type && typeof type === 'function') { const componentStr = type.toString(); const isClientComponent = componentStr.includes('use client') || componentStr.includes('useClient') || props?._isClient; if (isClientComponent || props?._isServer) { window.__NEXTJS_DEBUG__.captureServerClientBoundary(type.name || 'Anonymous', isClientComponent ? 'client' : 'server', props); } } return originalCreateElement.call(this, type, props, ...children); }; } // Monitor fetch for server actions and data fetching const originalFetch = window.fetch; window.fetch = async function (...args) { const [url, options] = args; const urlStr = typeof url === 'string' ? url : url.toString(); // Detect Next.js specific fetches const headers = options?.headers; if (urlStr.includes('_next/data/') || urlStr.includes('/api/') || headers?.['Next-Action']) { const startTime = Date.now(); const type = headers?.['Next-Action'] ? 'server-action' : 'fetch'; try { const response = await originalFetch(...args); const duration = Date.now() - startTime; // Clone to read body const cloned = response.clone(); const data = await cloned.json().catch(() => null); window.__NEXTJS_DEBUG__.captureDataFetching(type, urlStr, data, duration); return response; } catch (error) { window.__NEXTJS_DEBUG__.captureDataFetching(type, urlStr, { error: error instanceof Error ? error.message : String(error) }, Date.now() - startTime); throw error; } } return originalFetch(...args); }; }); } async setupRSCMonitoring() { 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 } } }); } async setupImageOptimizationTracking() { if (!this.page) return; await this.page.addInitScript(() => { window.__NEXTJS_IMAGE_DEBUG__ = { unoptimizedImages: [], lazyLoadObserver: null, init: function () { // Track all images const checkImage = (img) => { 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); } else if (node instanceof Element) { node.querySelectorAll('img').forEach(checkImage); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); }, recordUnoptimized: function (img, displayed) { 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.__NEXTJS_IMAGE_DEBUG__.init(); }); } else { window.__NEXTJS_IMAGE_DEBUG__.init(); } }); } async getPageInfo() { if (!this.page) return null; return await this.page.evaluate(() => { const nextData = window.__NEXT_DATA__; const debug = window.__NEXTJS_DEBUG__; if (!nextData) return null; const pageInfo = { path: nextData.page, type: 'static', dataFetching: [], hasLayout: false, isAppDir: nextData.page.includes('/app/') || !!window.__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; // Add server/client component info if (debug?.serverClientBoundaries) { const boundaries = debug.serverClientBoundaries; pageInfo.serverComponents = [...new Set(boundaries.filter((b) => b.type === 'server').map((b) => b.component))]; pageInfo.clientComponents = [...new Set(boundaries.filter((b) => b.type === 'client').map((b) => b.component))]; } // Add data flow info if (debug?.dataFetching) { const serverData = debug.dataFetching .filter((d) => ['getServerSideProps', 'getStaticProps'].includes(d.type)) .map((d) => d.data); const clientData = debug.dataFetching .filter((d) => ['fetch', 'server-action'].includes(d.type)) .map((d) => d.data); pageInfo.dataFlow = { serverData: serverData.length > 0 ? serverData[0] : undefined, clientData: clientData.length > 0 ? clientData : undefined, propsPassed: nextData.props || {} }; } return pageInfo; }); } async getBuildInfo() { if (!this.page) return null; return await this.page.evaluate(() => { const debug = window.__NEXTJS_DEBUG__; return debug?.buildInfo || null; }); } async getHydrationErrors() { if (!this.page) return []; const errors = await this.page.evaluate(() => { const debug = window.__NEXTJS_DEBUG__; return debug?.errors.filter((e) => e.source === 'hydration') || []; }); return errors; } async getPageTransitions() { if (!this.page) return []; return await this.page.evaluate(() => { const debug = window.__NEXTJS_DEBUG__; return debug?.pageTransitions || []; }); } async getRSCPayloads() { return this.rscPayloads; } async getServerClientBoundaries() { if (!this.page) return []; const boundaries = await this.page.evaluate(() => { const debug = window.__NEXTJS_DEBUG__; return debug?.serverClientBoundaries || []; }); return boundaries; } async getDataFetching() { if (!this.page) return []; const fetching = await this.page.evaluate(() => { const debug = window.__NEXTJS_DEBUG__; return debug?.dataFetching || []; }); return fetching; } async analyzeServerClientFlow() { const boundaries = await this.getServerClientBoundaries(); const dataFetching = await this.getDataFetching(); const serverCount = boundaries.filter((b) => b.type === 'server').length; const clientCount = boundaries.filter((b) => b.type === 'client').length; // Analyze data passing const dataPassing = []; boundaries.forEach((boundary) => { if (boundary.props && Object.keys(boundary.props).length > 0) { const size = JSON.stringify(boundary.props).length; dataPassing.push({ from: boundary.type === 'server' ? 'server' : 'client', to: boundary.component, size }); } }); // Generate recommendations const recommendations = []; if (clientCount > serverCount * 2) { recommendations.push('Consider moving more components to server components to reduce bundle size'); } const largeProps = dataPassing.filter(d => d.size > 10000); if (largeProps.length > 0) { recommendations.push('Large props detected - consider data fetching on client or pagination'); } const serverActions = dataFetching.filter((d) => d.type === 'server-action'); if (serverActions.length === 0 && clientCount > 5) { recommendations.push('Consider using Server Actions for form submissions and mutations'); } return { serverComponents: serverCount, clientComponents: clientCount, dataPassing, recommendations }; } async getImageOptimizationIssues() { if (!this.page) return { unoptimizedImages: [], lazyLoadFailures: [], nextImageUsage: 0 }; const data = await this.page.evaluate(() => { const imageDebug = window.__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() { const problems = []; // Check for hydration errors const hydrationErrors = await this.getHydrationErrors(); if (hydrationErrors.length > 0) { problems.push({ problem: 'Next.js Hydration Errors', severity: 'high', 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', 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' : 'medium', 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', 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', 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', 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; } } //# sourceMappingURL=nextjs-debug-engine.js.map