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

471 lines • 20 kB
export class AuditEngine { async performAudit(page, categories = ['all']) { const results = {}; if (categories.includes('all') || categories.includes('accessibility')) { results.accessibility = await this.auditAccessibility(page); } if (categories.includes('all') || categories.includes('performance')) { results.performance = await this.auditPerformance(page); } if (categories.includes('all') || categories.includes('seo')) { results.seo = await this.auditSEO(page); } if (categories.includes('all') || categories.includes('bestPractices')) { results.bestPractices = await this.auditBestPractices(page); } if (categories.includes('all') || categories.includes('security')) { results.security = await this.auditSecurity(page); } return results; } async auditAccessibility(page) { const issues = []; const suggestions = []; const audit = await page.evaluate(() => { const results = { imagesWithoutAlt: 0, buttonsWithoutText: 0, inputsWithoutLabels: 0, lowContrastElements: 0, missingLandmarks: false, formIssues: 0, }; // Check images without alt text document.querySelectorAll('img:not([alt])').forEach((img) => { results.imagesWithoutAlt++; }); // Check buttons without accessible text document.querySelectorAll('button').forEach((button) => { if (!button.textContent?.trim() && !button.getAttribute('aria-label')) { results.buttonsWithoutText++; } }); // Check inputs without labels document.querySelectorAll('input:not([type="hidden"])').forEach((input) => { const id = input.id; if (!id || !document.querySelector(`label[for="${id}"]`)) { if (!input.getAttribute('aria-label')) { results.inputsWithoutLabels++; } } }); // Check for main landmark if (!document.querySelector('main, [role="main"]')) { results.missingLandmarks = true; } // Check form accessibility document.querySelectorAll('form').forEach((form) => { if (!form.querySelector('button[type="submit"], input[type="submit"]')) { results.formIssues++; } }); return results; }); // Calculate score (0-100) let score = 100; if (audit.imagesWithoutAlt > 0) { score -= Math.min(20, audit.imagesWithoutAlt * 5); issues.push({ severity: 'error', message: `${audit.imagesWithoutAlt} images missing alt text`, details: { count: audit.imagesWithoutAlt } }); suggestions.push('Add descriptive alt text to all images'); } if (audit.buttonsWithoutText > 0) { score -= Math.min(15, audit.buttonsWithoutText * 5); issues.push({ severity: 'error', message: `${audit.buttonsWithoutText} buttons without accessible text`, details: { count: audit.buttonsWithoutText } }); suggestions.push('Add text content or aria-label to buttons'); } if (audit.inputsWithoutLabels > 0) { score -= Math.min(15, audit.inputsWithoutLabels * 3); issues.push({ severity: 'error', message: `${audit.inputsWithoutLabels} form inputs without labels`, details: { count: audit.inputsWithoutLabels } }); suggestions.push('Associate labels with form inputs using "for" attribute'); } if (audit.missingLandmarks) { score -= 10; issues.push({ severity: 'warning', message: 'Missing main landmark', details: { landmark: 'main' } }); suggestions.push('Add <main> element or role="main" to primary content'); } return { category: 'accessibility', score: Math.max(0, score), issues, suggestions }; } async auditPerformance(page) { const issues = []; const suggestions = []; // Get performance metrics const metrics = await page.evaluate(() => { const navigation = performance.getEntriesByType('navigation')[0]; const paint = performance.getEntriesByType('paint'); return { domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart, loadTime: navigation.loadEventEnd - navigation.loadEventStart, firstPaint: paint.find(p => p.name === 'first-paint')?.startTime || 0, firstContentfulPaint: paint.find(p => p.name === 'first-contentful-paint')?.startTime || 0, resources: performance.getEntriesByType('resource').length, totalSize: performance.getEntriesByType('resource').reduce((acc, r) => acc + (r.transferSize || 0), 0), largeImages: Array.from(document.querySelectorAll('img')).filter((img) => { return img.naturalWidth > 1920 || img.naturalHeight > 1080; }).length, scripts: document.querySelectorAll('script').length, stylesheets: document.querySelectorAll('link[rel="stylesheet"]').length, }; }); // Calculate score let score = 100; // Check load time if (metrics.loadTime > 3000) { score -= 20; issues.push({ severity: 'warning', message: `Page load time is ${(metrics.loadTime / 1000).toFixed(2)}s (target: <3s)`, details: { loadTime: metrics.loadTime } }); suggestions.push('Optimize resource loading and reduce page weight'); } // Check FCP if (metrics.firstContentfulPaint > 1800) { score -= 15; issues.push({ severity: 'warning', message: `First Contentful Paint is ${(metrics.firstContentfulPaint / 1000).toFixed(2)}s (target: <1.8s)`, details: { fcp: metrics.firstContentfulPaint } }); suggestions.push('Reduce render-blocking resources'); } // Check resource count if (metrics.resources > 50) { score -= 10; issues.push({ severity: 'info', message: `Loading ${metrics.resources} resources (consider bundling)`, details: { resourceCount: metrics.resources } }); suggestions.push('Bundle and minify resources'); } // Check for large images if (metrics.largeImages > 0) { score -= 10; issues.push({ severity: 'warning', message: `${metrics.largeImages} oversized images detected`, details: { largeImages: metrics.largeImages } }); suggestions.push('Optimize images - use appropriate sizes and formats (WebP, AVIF)'); } // Check total size const totalSizeMB = metrics.totalSize / (1024 * 1024); if (totalSizeMB > 5) { score -= 15; issues.push({ severity: 'warning', message: `Total page size is ${totalSizeMB.toFixed(2)}MB (target: <5MB)`, details: { totalSize: metrics.totalSize } }); suggestions.push('Reduce page weight through compression and optimization'); } return { category: 'performance', score: Math.max(0, score), issues, suggestions }; } async auditSEO(page) { const issues = []; const suggestions = []; const seoData = await page.evaluate(() => { const title = document.querySelector('title')?.textContent || ''; const metaDescription = document.querySelector('meta[name="description"]')?.getAttribute('content') || ''; const h1Count = document.querySelectorAll('h1').length; const canonicalLink = document.querySelector('link[rel="canonical"]')?.getAttribute('href'); const viewport = document.querySelector('meta[name="viewport"]')?.getAttribute('content'); const ogTags = { title: document.querySelector('meta[property="og:title"]')?.getAttribute('content'), description: document.querySelector('meta[property="og:description"]')?.getAttribute('content'), image: document.querySelector('meta[property="og:image"]')?.getAttribute('content'), }; return { title, titleLength: title.length, metaDescription, descriptionLength: metaDescription.length, h1Count, hasCanonical: !!canonicalLink, hasViewport: !!viewport, hasOgTags: !!(ogTags.title && ogTags.description), hasRobotsTxt: false, // Would need server check }; }); let score = 100; // Check title if (!seoData.title) { score -= 20; issues.push({ severity: 'error', message: 'Missing page title', details: { field: 'title' } }); suggestions.push('Add a descriptive <title> tag'); } else if (seoData.titleLength < 30 || seoData.titleLength > 60) { score -= 10; issues.push({ severity: 'warning', message: `Title length is ${seoData.titleLength} characters (ideal: 30-60)`, details: { titleLength: seoData.titleLength } }); suggestions.push('Optimize title length for search results'); } // Check meta description if (!seoData.metaDescription) { score -= 15; issues.push({ severity: 'error', message: 'Missing meta description', details: { field: 'description' } }); suggestions.push('Add a compelling meta description'); } else if (seoData.descriptionLength < 120 || seoData.descriptionLength > 160) { score -= 5; issues.push({ severity: 'warning', message: `Meta description length is ${seoData.descriptionLength} characters (ideal: 120-160)`, details: { descriptionLength: seoData.descriptionLength } }); } // Check H1 if (seoData.h1Count === 0) { score -= 15; issues.push({ severity: 'error', message: 'No H1 tag found', details: { h1Count: 0 } }); suggestions.push('Add a single H1 tag with primary keyword'); } else if (seoData.h1Count > 1) { score -= 5; issues.push({ severity: 'warning', message: `Multiple H1 tags found (${seoData.h1Count})`, details: { h1Count: seoData.h1Count } }); suggestions.push('Use only one H1 tag per page'); } // Check mobile viewport if (!seoData.hasViewport) { score -= 15; issues.push({ severity: 'error', message: 'Missing viewport meta tag', details: { field: 'viewport' } }); suggestions.push('Add <meta name="viewport" content="width=device-width, initial-scale=1">'); } // Check Open Graph if (!seoData.hasOgTags) { score -= 5; issues.push({ severity: 'info', message: 'Missing Open Graph tags for social sharing', details: { field: 'og:tags' } }); suggestions.push('Add Open Graph meta tags for better social media sharing'); } return { category: 'seo', score: Math.max(0, score), issues, suggestions }; } async auditBestPractices(page) { const issues = []; const suggestions = []; const practices = await page.evaluate(() => { const hasHttps = window.location.protocol === 'https:'; const hasLang = !!document.documentElement.getAttribute('lang'); const hasCharset = !!document.querySelector('meta[charset]'); const consoleErrors = window.__AI_DEBUG__?.events?.filter((e) => e.type === 'console.error').length || 0; const hasFavicon = !!document.querySelector('link[rel="icon"], link[rel="shortcut icon"]'); // Check for common bad practices const inlineStyles = document.querySelectorAll('[style]').length; const inlineScripts = document.querySelectorAll('script:not([src])').length; return { hasHttps, hasLang, hasCharset, consoleErrors, hasFavicon, inlineStyles, inlineScripts, }; }); let score = 100; if (!practices.hasLang) { score -= 10; issues.push({ severity: 'warning', message: 'Missing lang attribute on html element', details: { field: 'lang' } }); suggestions.push('Add lang attribute to <html> element'); } if (!practices.hasCharset) { score -= 10; issues.push({ severity: 'warning', message: 'Missing charset meta tag', details: { field: 'charset' } }); suggestions.push('Add <meta charset="UTF-8">'); } if (practices.consoleErrors > 0) { score -= Math.min(20, practices.consoleErrors * 5); issues.push({ severity: 'error', message: `${practices.consoleErrors} console errors detected`, details: { errorCount: practices.consoleErrors } }); suggestions.push('Fix JavaScript errors in console'); } if (!practices.hasFavicon) { score -= 5; issues.push({ severity: 'info', message: 'Missing favicon', details: { field: 'favicon' } }); suggestions.push('Add a favicon for better branding'); } if (practices.inlineStyles > 10) { score -= 10; issues.push({ severity: 'warning', message: `${practices.inlineStyles} inline styles found`, details: { inlineStyles: practices.inlineStyles } }); suggestions.push('Move inline styles to CSS files'); } if (practices.inlineScripts > 2) { score -= 10; issues.push({ severity: 'warning', message: `${practices.inlineScripts} inline scripts found`, details: { inlineScripts: practices.inlineScripts } }); suggestions.push('Move inline scripts to external files'); } return { category: 'bestPractices', score: Math.max(0, score), issues, suggestions }; } async auditSecurity(page) { const issues = []; const suggestions = []; const security = await page.evaluate(() => { const forms = document.querySelectorAll('form'); const passwordInputs = document.querySelectorAll('input[type="password"]'); const httpLinks = Array.from(document.querySelectorAll('a[href^="http://"]')).length; const externalScripts = Array.from(document.querySelectorAll('script[src]')) .filter(s => { const src = s.getAttribute('src') || ''; return src.startsWith('http') && !src.includes(window.location.hostname); }).length; // Check for sensitive data in URLs const urlParams = new URLSearchParams(window.location.search); const sensitiveParams = ['password', 'token', 'key', 'secret', 'auth'].filter(param => urlParams.has(param)); return { formCount: forms.length, httpsFormActions: Array.from(forms).filter(f => { const action = f.getAttribute('action') || ''; return !action || action.startsWith('https://') || action.startsWith('/'); }).length, passwordInputs: passwordInputs.length, autocompleteOffPasswords: Array.from(passwordInputs).filter(p => p.getAttribute('autocomplete') === 'off' || p.getAttribute('autocomplete') === 'new-password').length, httpLinks, externalScripts, sensitiveParams, }; }); let score = 100; // Check forms use HTTPS if (security.formCount > security.httpsFormActions) { score -= 20; issues.push({ severity: 'error', message: 'Forms submitting over insecure HTTP', details: { insecureForms: security.formCount - security.httpsFormActions } }); suggestions.push('Ensure all forms submit over HTTPS'); } // Check password autocomplete if (security.passwordInputs > security.autocompleteOffPasswords) { score -= 10; issues.push({ severity: 'warning', message: 'Password fields allow autocomplete', details: { count: security.passwordInputs - security.autocompleteOffPasswords } }); suggestions.push('Set autocomplete="new-password" on password fields'); } // Check for HTTP links if (security.httpLinks > 0) { score -= 15; issues.push({ severity: 'warning', message: `${security.httpLinks} links use insecure HTTP`, details: { httpLinks: security.httpLinks } }); suggestions.push('Update all links to use HTTPS'); } // Check external scripts if (security.externalScripts > 5) { score -= 10; issues.push({ severity: 'info', message: `${security.externalScripts} external scripts loaded`, details: { externalScripts: security.externalScripts } }); suggestions.push('Audit external scripts and consider using Subresource Integrity (SRI)'); } // Check for sensitive data in URL if (security.sensitiveParams.length > 0) { score -= 25; issues.push({ severity: 'error', message: `Sensitive data in URL parameters: ${security.sensitiveParams.join(', ')}`, details: { params: security.sensitiveParams } }); suggestions.push('Never pass sensitive data in URL parameters'); } return { category: 'security', score: Math.max(0, score), issues, suggestions }; } } //# sourceMappingURL=audit-engine.js.map