UNPKG

@casoon/auditmysite

Version:

Professional website analysis suite with robust accessibility testing, Core Web Vitals performance monitoring, SEO analysis, and content optimization insights. Features isolated browser contexts, retry mechanisms, and comprehensive API endpoints for profe

498 lines 20.4 kB
"use strict"; /** * 🎯 Stable Audit Interface * * This interface provides a stable, reliable API for website auditing. * It abstracts internal complexity and ensures consistent behavior. * * Key Features: * - Simple, consistent API * - Built-in error handling and validation * - Progress monitoring * - Health checks * - Comprehensive results * * Usage: * ```typescript * const auditor = new StableAuditor({ * maxPages: 5, * timeout: 60000, * outputFormat: 'markdown' * }); * * const results = await auditor.auditWebsite('https://example.com/sitemap.xml'); * ``` */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.StableAuditor = void 0; exports.createStableAuditor = createStableAuditor; const accessibility_1 = require("../core/accessibility"); const parsers_1 = require("../core/parsers"); const browser_pool_manager_1 = require("../core/browser/browser-pool-manager"); const html_generator_1 = require("../generators/html-generator"); const path = __importStar(require("path")); const fs = __importStar(require("fs")); /** * Stable Auditor Class - The main interface for website auditing */ class StableAuditor { constructor(config) { this.isInitialized = false; this.healthStatus = 'healthy'; // Apply defaults and validate config this.config = this.validateAndNormalizeConfig(config); } /** * Set progress callback to monitor audit progress */ onProgress(callback) { this.progressCallback = callback; return this; } /** * Set error callback to handle recoverable errors */ onError(callback) { this.errorCallback = callback; return this; } /** * Initialize the auditor (must be called before auditing) */ async initialize() { try { this.reportProgress('initializing', 0, 0, 0, 'Initializing auditor...'); // Initialize browser pool this.browserPoolManager = new browser_pool_manager_1.BrowserPoolManager({ maxConcurrent: this.config.maxConcurrent, maxIdleTime: 30000, enableResourceOptimization: true }); await this.browserPoolManager.warmUp(1); // Initialize accessibility checker this.accessibilityChecker = new accessibility_1.AccessibilityChecker({ poolManager: this.browserPoolManager, enableComprehensiveAnalysis: true, qualityAnalysisOptions: { verbose: this.config.verbose, analysisTimeout: this.config.timeout } }); await this.accessibilityChecker.initialize(); this.isInitialized = true; this.healthStatus = 'healthy'; this.reportProgress('initializing', 100, 0, 0, 'Auditor initialized successfully'); } catch (error) { this.healthStatus = 'unhealthy'; this.reportError('SYSTEM_ERROR', 'Failed to initialize auditor', undefined, error, false); throw error; } } /** * Perform a complete website audit */ async auditWebsite(sitemapUrl) { if (!this.isInitialized) { throw new Error('Auditor not initialized. Call initialize() first.'); } const startTime = Date.now(); try { // Health check await this.performHealthCheck(); // Parse sitemap this.reportProgress('parsing', 0, 0, 0, 'Parsing sitemap...'); const urls = await this.parseSitemap(sitemapUrl); // Audit pages this.reportProgress('testing', 0, 0, urls.length, 'Starting page audits...'); const pageResults = await this.auditPages(urls); // Generate reports this.reportProgress('generating', 0, pageResults.length, pageResults.length, 'Generating reports...'); const reports = await this.generateReports(pageResults, sitemapUrl); // Calculate summary const summary = this.calculateSummary(pageResults, startTime); const performance = this.calculatePerformanceMetrics(pageResults); const result = { summary, pages: pageResults, reports, performance, metadata: { auditDate: new Date().toISOString(), version: '2.0.0-alpha.2', config: this.config, systemInfo: { nodeVersion: process.version, memoryUsage: process.memoryUsage() } } }; this.reportProgress('complete', 100, pageResults.length, pageResults.length, 'Audit completed successfully'); return result; } catch (error) { this.healthStatus = 'unhealthy'; this.reportError('SYSTEM_ERROR', 'Audit failed', sitemapUrl, error, false); throw error; } } /** * Cleanup resources (call when done) */ async cleanup() { try { if (this.accessibilityChecker) { await this.accessibilityChecker.cleanup(); } if (this.browserPoolManager) { await this.browserPoolManager.shutdown(); } this.isInitialized = false; this.healthStatus = 'healthy'; } catch (error) { console.warn('Warning during cleanup:', error); } } /** * Get current health status of the auditor */ getHealthStatus() { return { status: this.healthStatus, details: { initialized: this.isInitialized, browserPoolSize: this.browserPoolManager?.getMetrics()?.poolSize || 0, memoryUsage: process.memoryUsage(), uptime: process.uptime() } }; } // Private helper methods validateAndNormalizeConfig(config) { if (!config.maxPages || config.maxPages < 1) { throw new Error('maxPages must be a positive number'); } if (config.maxPages > 100) { console.warn('⚠️ Warning: maxPages > 100 may cause performance issues'); } return { maxPages: config.maxPages, timeout: config.timeout || 90000, maxConcurrent: Math.min(config.maxConcurrent || 3, 5), // Cap at 5 for stability outputFormat: config.outputFormat || 'html', outputDir: config.outputDir || './reports', standard: config.standard || 'WCAG2AA', verbose: config.verbose || false, reportPrefix: config.reportPrefix || 'audit' }; } async performHealthCheck() { // Check memory usage const memUsage = process.memoryUsage().heapUsed / 1024 / 1024; if (memUsage > 2048) { // 2GB threshold this.healthStatus = 'degraded'; this.reportError('SYSTEM_ERROR', 'High memory usage detected', undefined, { memoryMB: memUsage }, true); } // Check browser pool if (this.browserPoolManager) { const metrics = this.browserPoolManager.getMetrics(); if (metrics.poolSize === 0) { this.healthStatus = 'degraded'; this.reportError('BROWSER_ERROR', 'No browsers available in pool', undefined, metrics, true); } } } async parseSitemap(sitemapUrl) { try { const parser = new parsers_1.SitemapParser(); const urls = await parser.parseSitemap(sitemapUrl); if (urls.length === 0) { throw new Error('No URLs found in sitemap'); } // Filter and limit URLs const filteredUrls = parser.filterUrls(urls, { filterPatterns: ['[...slug]', '[category]', '/demo/', '/test/'] }); const limitedUrls = filteredUrls.slice(0, this.config.maxPages); if (this.config.verbose) { console.log(`📄 Sitemap parsed: ${urls.length} total, ${filteredUrls.length} after filtering, ${limitedUrls.length} selected`); } return limitedUrls.map(url => url.loc); } catch (error) { this.reportError('SITEMAP_ERROR', 'Failed to parse sitemap', sitemapUrl, error, false); throw error; } } async auditPages(urls) { if (!this.accessibilityChecker) { throw new Error('AccessibilityChecker not initialized'); } try { const multi = await this.accessibilityChecker.testMultiplePages(urls, { timeout: this.config.timeout, maxConcurrent: this.config.maxConcurrent, pa11yStandard: this.config.standard, verbose: this.config.verbose }); return multi.results.map(result => this.transformToPageAuditResult(result.accessibilityResult)); } catch (error) { this.reportError('BROWSER_ERROR', 'Failed to audit pages', undefined, error, false); throw error; } } transformToPageAuditResult(result) { // Extract scores safely const accessibilityScore = result.pa11yScore || 0; const performanceScore = result.enhancedPerformance?.score || result.performance?.score || 0; const seoScore = result.enhancedSEO?.score || 0; const mobileScore = result.mobileFriendliness?.score || 0; // Transform issues const issues = { errors: (result.errors || []).map((issue) => ({ type: 'accessibility', severity: 'error', rule: issue.code || 'unknown', message: issue.message || 'Unknown error', element: issue.element, selector: issue.selector })), warnings: (result.warnings || []).map((warning) => ({ type: 'accessibility', severity: 'warning', rule: warning.code || 'unknown', message: warning.message || 'Unknown warning', element: warning.element, selector: warning.selector })), notices: (result.notices || []).map((notice) => ({ type: 'accessibility', severity: 'notice', rule: notice.code || 'unknown', message: notice.message || 'Unknown notice', element: notice.element, selector: notice.selector })) }; return { url: result.url, title: result.title || 'Unknown Page', passed: result.passed || false, crashed: result.crashed || false, duration: result.duration || 0, scores: { accessibility: accessibilityScore, performance: performanceScore, seo: seoScore, mobile: mobileScore }, issues, metrics: { loadTime: result.loadTime || 0, contentSize: result.contentWeight?.totalSize || 0, resourceCount: result.contentWeight?.totalResources || 0 } }; } async generateReports(results, sitemapUrl) { const dateString = new Date().toISOString().split('T')[0]; const domain = new URL(sitemapUrl).hostname.replace(/^www\./, ''); const reports = {}; try { // Ensure output directory exists if (!fs.existsSync(this.config.outputDir)) { fs.mkdirSync(this.config.outputDir, { recursive: true }); } if (this.config.outputFormat === 'html' || this.config.outputFormat === 'both') { const htmlFilename = `${this.config.reportPrefix}-${domain}-${dateString}.html`; const htmlPath = path.join(this.config.outputDir, htmlFilename); // Generate HTML report using existing generator const htmlGenerator = new html_generator_1.HTMLGenerator(); const summary = this.calculateSummary(results, Date.now()); const auditResult = { summary, pages: results, metadata: { version: '2.0.0-alpha.2', timestamp: new Date().toISOString(), sitemapUrl: sitemapUrl, totalPages: summary.totalPages, testedPages: summary.testedPages, duration: summary.totalDuration } }; const htmlContent = await htmlGenerator.generate(auditResult); fs.writeFileSync(htmlPath, htmlContent, 'utf8'); reports.html = htmlPath; } if (this.config.outputFormat === 'markdown' || this.config.outputFormat === 'both') { const mdFilename = `${this.config.reportPrefix}-${domain}-${dateString}.md`; const mdPath = path.join(this.config.outputDir, mdFilename); // Generate Markdown report const markdown = this.generateMarkdownReport(results, sitemapUrl); fs.writeFileSync(mdPath, markdown, 'utf8'); reports.markdown = mdPath; } return reports; } catch (error) { this.reportError('SYSTEM_ERROR', 'Failed to generate reports', undefined, error, true); return {}; } } generateMarkdownReport(results, sitemapUrl) { const summary = this.calculateSummary(results, Date.now()); const domain = new URL(sitemapUrl).hostname; let markdown = `# Website Audit Report\n\n`; markdown += `**Domain:** ${domain} \n`; markdown += `**Date:** ${new Date().toLocaleDateString()} \n`; markdown += `**Pages Tested:** ${summary.testedPages} \n`; markdown += `**Success Rate:** ${summary.successRate.toFixed(1)}% \n\n`; markdown += `## Summary\n\n`; markdown += `| Metric | Value |\n`; markdown += `|--------|-------|\n`; markdown += `| Total Pages | ${summary.totalPages} |\n`; markdown += `| Tested Pages | ${summary.testedPages} |\n`; markdown += `| Passed Pages | ${summary.passedPages} |\n`; markdown += `| Failed Pages | ${summary.failedPages} |\n`; markdown += `| Crashed Pages | ${summary.crashedPages} |\n`; markdown += `| Total Duration | ${(summary.totalDuration / 1000).toFixed(1)}s |\n\n`; markdown += `## Page Results\n\n`; for (const result of results) { const status = result.crashed ? '💥 CRASHED' : result.passed ? '✅ PASSED' : '❌ FAILED'; markdown += `### ${result.title}\n`; markdown += `**URL:** ${result.url} \n`; markdown += `**Status:** ${status} \n`; markdown += `**Duration:** ${result.duration}ms \n\n`; markdown += `**Scores:**\n`; markdown += `- Accessibility: ${result.scores.accessibility}/100\n`; markdown += `- Performance: ${result.scores.performance}/100\n`; markdown += `- SEO: ${result.scores.seo}/100\n`; markdown += `- Mobile: ${result.scores.mobile}/100\n\n`; if (result.issues.errors.length > 0) { markdown += `**Errors (${result.issues.errors.length}):**\n`; result.issues.errors.slice(0, 5).forEach(error => { markdown += `- ${error.message}\n`; }); if (result.issues.errors.length > 5) { markdown += `- ... and ${result.issues.errors.length - 5} more\n`; } markdown += '\n'; } } return markdown; } calculateSummary(results, startTime) { const totalDuration = Date.now() - startTime; const testedPages = results.length; const passedPages = results.filter(r => r.passed).length; const failedPages = results.filter(r => !r.passed && !r.crashed).length; const crashedPages = results.filter(r => r.crashed).length; const totalErrors = results.reduce((sum, r) => sum + r.issues.errors.length, 0); const totalWarnings = results.reduce((sum, r) => sum + r.issues.warnings.length, 0); return { totalPages: this.config.maxPages, testedPages, passedPages, failedPages, crashedPages, successRate: testedPages > 0 ? (passedPages / testedPages) * 100 : 0, totalDuration, averagePageTime: testedPages > 0 ? totalDuration / testedPages : 0, totalErrors, totalWarnings }; } calculatePerformanceMetrics(results) { if (results.length === 0) { return { avgLoadTime: 0, avgAccessibilityScore: 0, avgPerformanceScore: 0, avgSeoScore: 0 }; } const totals = results.reduce((acc, result) => ({ loadTime: acc.loadTime + result.metrics.loadTime, accessibility: acc.accessibility + result.scores.accessibility, performance: acc.performance + result.scores.performance, seo: acc.seo + result.scores.seo }), { loadTime: 0, accessibility: 0, performance: 0, seo: 0 }); return { avgLoadTime: totals.loadTime / results.length, avgAccessibilityScore: totals.accessibility / results.length, avgPerformanceScore: totals.performance / results.length, avgSeoScore: totals.seo / results.length }; } reportProgress(phase, progress, completed, total, message) { if (this.progressCallback) { this.progressCallback({ phase, progress: Math.min(100, Math.max(0, progress)), completed, total, message }); } } reportError(code, message, url, details, recoverable = false) { const error = { code, message, url, details, recoverable }; if (this.errorCallback) { this.errorCallback(error); } else if (!recoverable) { console.error(`🚨 ${code}: ${message}`, details); } else { console.warn(`⚠️ ${code}: ${message}`, details); } } } exports.StableAuditor = StableAuditor; // Export default factory function for convenience function createStableAuditor(config) { return new StableAuditor(config); } //# sourceMappingURL=stable-audit-interface.js.map