UNPKG

a11yanalyze

Version:

A command-line tool for developers and QA engineers to test web pages and websites for WCAG 2.2 AA accessibility compliance

600 lines 21.3 kB
"use strict"; /** * Configuration Management System * Provides progressive configuration with sensible defaults, file-based config, * environment variables, and CLI argument overrides */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ConfigManager = void 0; const fs_1 = require("fs"); const path_1 = require("path"); const os_1 = require("os"); const js_yaml_1 = __importDefault(require("js-yaml")); const ajv_1 = __importDefault(require("ajv")); // Define JSON Schema for A11yAnalyzeConfig (partial, for demonstration; expand as needed) // NOTE: Use 'any' for now to allow extensibility; replace with full JSONSchemaType<A11yAnalyzeConfig> for strict typing const configSchema = { type: 'object', properties: { scanning: { type: 'object', properties: { wcagLevel: { type: 'string', enum: ['A', 'AA', 'AAA', 'ARIA'] }, includeAAA: { type: 'boolean' }, includeARIA: { type: 'boolean' }, includeWarnings: { type: 'boolean' }, timeout: { type: 'integer', minimum: 1 }, retries: { type: 'integer', minimum: 0 }, retryDelay: { type: 'integer', minimum: 0 }, waitForNetworkIdle: { type: 'boolean' }, captureScreenshots: { type: 'boolean' }, screenshotOnFailure: { type: 'boolean' }, customRules: { type: 'array', items: { type: 'string' } }, disabledRules: { type: 'array', items: { type: 'string' } }, }, required: [ 'wcagLevel', 'includeAAA', 'includeARIA', 'includeWarnings', 'timeout', 'retries', 'retryDelay', 'waitForNetworkIdle', 'captureScreenshots', 'screenshotOnFailure', 'customRules', 'disabledRules' ], additionalProperties: true }, browser: { type: 'object', additionalProperties: true }, crawling: { type: 'object', additionalProperties: true }, output: { type: 'object', additionalProperties: true }, scoring: { type: 'object', additionalProperties: true }, issues: { type: 'object', additionalProperties: true }, performance: { type: 'object', additionalProperties: true }, advanced: { type: 'object', additionalProperties: true }, vpat: { type: 'object', additionalProperties: true }, storybook: { type: 'object', additionalProperties: true }, reporting: { type: 'object', additionalProperties: true }, }, required: [ 'scanning', 'browser', 'crawling', 'output', 'scoring', 'issues', 'performance', 'advanced', 'vpat', 'storybook', 'reporting' ], additionalProperties: false }; const ajv = new ajv_1.default({ allErrors: true }); const validateConfigSchema = ajv.compile(configSchema); /** * Configuration Manager * Handles loading, merging, and validation of configuration from multiple sources */ class ConfigManager { constructor() { this.configSources = []; } /** * Load configuration from all sources with proper priority */ async loadConfig(searchPaths = [], cliOptions = {}) { const warnings = []; const errors = []; this.configSources = []; try { // 1. Start with default configuration let config = this.deepClone(ConfigManager.DEFAULT_CONFIG); this.addConfigSource('defaults', 0, true); // 2. Load from configuration files const fileConfig = await this.loadFromFiles(searchPaths); if (fileConfig.config) { config = this.mergeConfigs(config, fileConfig.config); this.addConfigSource('file', 1, true, fileConfig.path); } warnings.push(...fileConfig.warnings); errors.push(...fileConfig.errors); // 3. Load from environment variables const envConfig = this.loadFromEnvironment(); if (envConfig.config) { config = this.mergeConfigs(config, envConfig.config); this.addConfigSource('environment', 2, true); } warnings.push(...envConfig.warnings); // 4. Apply CLI options (highest priority) const cliConfig = this.convertCliToConfig(cliOptions); if (cliConfig.config) { config = this.mergeConfigs(config, cliConfig.config); this.addConfigSource('cli', 3, true); } // 5. Validate final configuration const validation = this.validateConfig(config); warnings.push(...validation.warnings); errors.push(...validation.errors); // Store loaded configuration this.loadedConfig = config; return { config, sources: this.configSources, warnings, errors, }; } catch (error) { errors.push(`Configuration loading failed: ${error instanceof Error ? error.message : String(error)}`); // Return default config on failure return { config: this.deepClone(ConfigManager.DEFAULT_CONFIG), sources: [{ type: 'defaults', priority: 0, loaded: true }], warnings, errors, }; } } /** * Get the currently loaded configuration */ getConfig() { return this.loadedConfig || null; } /** * Get configuration sources information */ getConfigSources() { return [...this.configSources]; } /** * Generate a sample configuration file */ generateSampleConfig(format = 'json') { const config = ConfigManager.DEFAULT_CONFIG; switch (format) { case 'json': return JSON.stringify(config, null, 2); case 'js': return `module.exports = ${JSON.stringify(config, null, 2)};`; case 'yaml': // Simple YAML generation - in production, use a proper YAML library return this.convertToYaml(config); default: throw new Error(`Unsupported config format: ${format}`); } } /** * Save configuration to file */ async saveConfig(config, path, format = 'json') { try { const fullConfig = this.mergeConfigs(ConfigManager.DEFAULT_CONFIG, config); const content = this.generateSampleConfig(format); (0, fs_1.writeFileSync)(path, content, 'utf8'); } catch (error) { throw new Error(`Failed to save configuration: ${error instanceof Error ? error.message : String(error)}`); } } /** * Get default configuration */ static getDefaultConfig() { return this.prototype.deepClone(ConfigManager.DEFAULT_CONFIG); } /** * Load configuration from files * @private */ async loadFromFiles(searchPaths) { const warnings = []; const errors = []; // Build search paths const paths = [ ...searchPaths, process.cwd(), (0, os_1.homedir)(), ]; for (const basePath of paths) { for (const filename of ConfigManager.CONFIG_FILENAMES) { const configPath = (0, path_1.resolve)(basePath, filename); if ((0, fs_1.existsSync)(configPath)) { try { const config = await this.loadConfigFile(configPath); return { config, path: configPath, warnings, errors }; } catch (error) { errors.push(`Failed to load config from ${configPath}: ${error instanceof Error ? error.message : String(error)}`); } } } } return { warnings, errors }; } /** * Load a specific configuration file * @private */ async loadConfigFile(path) { const content = (0, fs_1.readFileSync)(path, 'utf8'); if (path.endsWith('.json')) { return JSON.parse(content); } else if (path.endsWith('.js')) { // For .js files, use dynamic import or require delete require.cache[require.resolve(path)]; return require(path); } else if (path.endsWith('.yaml') || path.endsWith('.yml')) { return js_yaml_1.default.load(content); } else if (path.includes('package.json')) { const pkg = JSON.parse(content); return pkg.a11yanalyze || {}; } throw new Error(`Unsupported config file format: ${path}`); } /** * Load configuration from environment variables * @private */ loadFromEnvironment() { const warnings = []; const config = {}; // Map environment variables to config structure const envMappings = { 'A11Y_WCAG_LEVEL': 'scanning.wcagLevel', 'A11Y_INCLUDE_AAA': 'scanning.includeAAA', 'A11Y_INCLUDE_ARIA': 'scanning.includeARIA', 'A11Y_TIMEOUT': 'scanning.timeout', 'A11Y_HEADLESS': 'browser.headless', 'A11Y_VIEWPORT_WIDTH': 'browser.viewport.width', 'A11Y_VIEWPORT_HEIGHT': 'browser.viewport.height', 'A11Y_MAX_DEPTH': 'crawling.maxDepth', 'A11Y_MAX_PAGES': 'crawling.maxPages', 'A11Y_CONCURRENCY': 'crawling.maxConcurrency', 'A11Y_OUTPUT_FORMAT': 'output.format', 'A11Y_VERBOSE': 'output.verbose', 'A11Y_QUIET': 'output.quiet', 'A11Y_DEBUG': 'output.debug', 'A11Y_SCORING_PROFILE': 'scoring.profile', 'A11Y_MIN_SEVERITY': 'scoring.minSeverity', }; for (const [envVar, configPath] of Object.entries(envMappings)) { const value = process.env[envVar]; if (value !== undefined && value !== null) { try { this.setNestedValue(config, configPath, this.parseEnvValue(value)); } catch (error) { warnings.push(`Invalid environment variable ${envVar}: ${error instanceof Error ? error.message : String(error)}`); } } } return { config: Object.keys(config).length > 0 ? config : undefined, warnings }; } /** * Convert CLI options to configuration structure * @private */ convertCliToConfig(cliOptions) { const config = {}; // Map CLI options to config structure const mappings = { 'wcagLevel': 'scanning.wcagLevel', 'includeAaa': 'scanning.includeAAA', 'includeAria': 'scanning.includeARIA', 'timeout': 'scanning.timeout', 'viewport': 'browser.viewport', 'depth': 'crawling.maxDepth', 'maxPages': 'crawling.maxPages', 'concurrency': 'crawling.maxConcurrency', 'format': 'output.format', 'verbose': 'output.verbose', 'quiet': 'output.quiet', 'debug': 'output.debug', 'output': 'output.outputPath', 'exportErrors': 'output.exportErrors', 'profile': 'scoring.profile', 'minSeverity': 'scoring.minSeverity', 'screenshot': 'scanning.captureScreenshots', }; for (const [cliKey, configPath] of Object.entries(mappings)) { const value = cliOptions[cliKey]; if (value !== undefined) { if (cliKey === 'viewport' && typeof value === 'string') { // Parse viewport string like "1280x720" const match = value.match(/^(\d+)x(\d+)$/); if (match && match[1] && match[2]) { this.setNestedValue(config, 'browser.viewport.width', parseInt(match[1], 10)); this.setNestedValue(config, 'browser.viewport.height', parseInt(match[2], 10)); } } else { this.setNestedValue(config, configPath, value); } } } return { config: Object.keys(config).length > 0 ? config : undefined }; } /** * Validate configuration * @private */ validateConfig(config) { const warnings = []; const errors = []; // JSON Schema validation const valid = validateConfigSchema(config); if (!valid) { for (const err of validateConfigSchema.errors || []) { errors.push(`Config schema error: ${err.instancePath} ${err.message}`); } } // Validate WCAG level const validWcagLevels = ['A', 'AA', 'AAA', 'ARIA']; if (!validWcagLevels.includes(config.scanning.wcagLevel)) { errors.push(`Invalid WCAG level: ${config.scanning.wcagLevel}. Must be one of: ${validWcagLevels.join(', ')}`); } // Validate scoring profile const validProfiles = ['strict', 'balanced', 'lenient', 'enterprise', 'custom']; if (!validProfiles.includes(config.scoring.profile)) { errors.push(`Invalid scoring profile: ${config.scoring.profile}. Must be one of: ${validProfiles.join(', ')}`); } // Validate timeout values if (config.scanning.timeout <= 0) { errors.push('Scanning timeout must be greater than 0'); } // Validate viewport if (config.browser.viewport.width <= 0 || config.browser.viewport.height <= 0) { errors.push('Browser viewport dimensions must be greater than 0'); } // Validate crawling limits if (config.crawling.maxDepth < 0) { errors.push('Max crawl depth cannot be negative'); } if (config.crawling.maxPages <= 0) { warnings.push('Max pages is 0 or negative, this may prevent scanning'); } // Validate concurrency if (config.crawling.maxConcurrency <= 0) { errors.push('Max concurrency must be greater than 0'); } return { warnings, errors }; } /** * Merge two configuration objects deeply * @private */ mergeConfigs(base, override) { const result = this.deepClone(base); return this.deepMerge(result, override); } /** * Deep clone an object * @private */ deepClone(obj) { if (obj === null || typeof obj !== 'object') return obj; if (obj instanceof Date) return new Date(obj.getTime()); if (obj instanceof Array) return obj.map(item => this.deepClone(item)); const cloned = {}; for (const key in obj) { if (obj.hasOwnProperty(key)) { cloned[key] = this.deepClone(obj[key]); } } return cloned; } /** * Deep merge objects * @private */ deepMerge(target, source) { for (const key in source) { if (source.hasOwnProperty(key)) { if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { if (!target[key] || typeof target[key] !== 'object') { target[key] = {}; } this.deepMerge(target[key], source[key]); } else { target[key] = source[key]; } } } return target; } /** * Set nested object value using dot notation * @private */ setNestedValue(obj, path, value) { const keys = path.split('.'); if (keys.length === 0) return; let current = obj; for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; if (!key) continue; if (!current[key] || typeof current[key] !== 'object') { current[key] = {}; } current = current[key]; } const finalKey = keys[keys.length - 1]; if (finalKey) { current[finalKey] = value; } } /** * Parse environment variable value to appropriate type * @private */ parseEnvValue(value) { // Boolean values if (value.toLowerCase() === 'true') return true; if (value.toLowerCase() === 'false') return false; // Number values if (/^\d+$/.test(value)) return parseInt(value, 10); if (/^\d+\.\d+$/.test(value)) return parseFloat(value); // Array values (comma-separated) if (value.includes(',')) { return value.split(',').map(v => v.trim()); } // String value return value; } /** * Convert configuration to YAML format (simplified) * @private */ convertToYaml(obj, indent = 0) { const spaces = ' '.repeat(indent); let yaml = ''; for (const [key, value] of Object.entries(obj)) { if (value && typeof value === 'object' && !Array.isArray(value)) { yaml += `${spaces}${key}:\n${this.convertToYaml(value, indent + 1)}`; } else if (Array.isArray(value)) { yaml += `${spaces}${key}:\n`; for (const item of value) { yaml += `${spaces} - ${JSON.stringify(item)}\n`; } } else { yaml += `${spaces}${key}: ${JSON.stringify(value)}\n`; } } return yaml; } /** * Add configuration source to tracking * @private */ addConfigSource(type, priority, loaded, path, errors) { this.configSources.push({ type, priority, loaded, path, errors, }); } } exports.ConfigManager = ConfigManager; ConfigManager.CONFIG_FILENAMES = [ '.a11yanalyzerc.json', '.a11yanalyzerc.js', 'a11yanalyze.config.json', 'a11yanalyze.config.js', 'package.json', // Look for a11yanalyze key ]; ConfigManager.DEFAULT_CONFIG = { scanning: { wcagLevel: 'AA', includeAAA: false, includeARIA: true, includeWarnings: true, timeout: 30000, retries: 2, retryDelay: 1000, waitForNetworkIdle: true, captureScreenshots: false, screenshotOnFailure: true, customRules: [], disabledRules: [], }, browser: { headless: true, viewport: { width: 1280, height: 720, }, userAgent: 'A11yAnalyze/1.0.0 (Accessibility Testing Tool)', enableJavaScript: true, allowInsecure: false, locale: 'en-US', timezone: 'UTC', }, crawling: { maxDepth: 2, maxPages: 50, maxConcurrency: 3, requestDelay: 1000, respectRobotsTxt: false, followRedirects: true, allowedDomains: [], excludedDomains: [], excludedPaths: [], includedPaths: [], useSitemaps: false, discoverExternalLinks: false, maxExternalDepth: 1, }, output: { format: 'console', verbose: false, quiet: false, debug: false, colors: true, timestamps: false, progressBars: true, exportErrors: false, }, scoring: { profile: 'balanced', minScore: 0, maxScore: 100, enableBonuses: true, enablePenalties: true, minSeverity: 'moderate', }, issues: { groupSimilar: true, includeRemediation: true, includeCodeExamples: true, includeTestingGuidance: true, contextAware: true, maxCodeExamples: 3, priorityMode: 'impact', }, performance: { enableCircuitBreaker: true, circuitBreakerThreshold: 5, maxRetries: 3, baseRetryDelay: 1000, timeoutStrategy: 'adaptive', memoryLimit: 512, // MB resourceCleanup: true, }, advanced: { experimentalFeatures: false, cacheResults: false, cacheTTL: 3600, // 1 hour telemetry: false, updateCheck: true, configVersion: '1.0.0', }, vpat: { enabled: false, mode: 'component', outputFormat: 'json', section508: true, remarks: true, jiraIntegration: false, }, storybook: { enabled: false, url: '', iframeSelector: '#storybook-preview-iframe', componentIsolation: true, autoDiscover: true, }, reporting: { format: 'json', templatePath: undefined, jiraIntegration: false, includeScreenshots: true, includeSummary: true, }, }; //# sourceMappingURL=config-manager.js.map