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