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
text/typescript
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;
}
}