@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
JavaScript
;
/**
* 🎯 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