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
JavaScript
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