#!/usr/bin/env node /** * DSS Rules Validator * * CLI tool for validating files against DSS rules. * Used by CI pipelines and pre-commit hooks. */ const fs = require('fs'); const path = require('path'); const rules = require('./index'); // ANSI colors for output const colors = { red: '\x1b[31m', yellow: '\x1b[33m', green: '\x1b[32m', blue: '\x1b[34m', reset: '\x1b[0m', bold: '\x1b[1m' }; /** * Validate a single file against all applicable rules * @param {string} filePath - Path to file to validate * @param {Object} options - Validation options * @returns {Object} Validation results */ function validateFile(filePath, options = {}) { const results = { file: filePath, errors: [], warnings: [], info: [], passed: true }; if (!fs.existsSync(filePath)) { results.errors.push({ message: `File not found: ${filePath}` }); results.passed = false; return results; } const content = fs.readFileSync(filePath, 'utf-8'); const ext = path.extname(filePath).toLowerCase(); // Determine which rule categories apply based on file type const applicableCategories = getApplicableCategories(ext); for (const category of applicableCategories) { const ruleSet = rules.getRulesByCategory(category); if (!ruleSet?.rules) continue; for (const rule of ruleSet.rules) { // Skip if file matches exception patterns if (rule.exceptions?.some(exc => filePath.includes(exc.replace('*', '')))) { continue; } // Check forbidden patterns if (rule.patterns?.forbidden) { for (const pattern of rule.patterns.forbidden) { try { const regex = new RegExp(pattern, 'gm'); let match; while ((match = regex.exec(content)) !== null) { const lineNumber = content.substring(0, match.index).split('\n').length; const violation = { rule: `${category}/${rule.id}`, name: rule.name, line: lineNumber, match: match[0], message: rule.description || `Violation of ${rule.name}` }; const severity = rule.severity || ruleSet.severity || 'warning'; if (severity === 'error') { results.errors.push(violation); results.passed = false; } else if (severity === 'warning') { results.warnings.push(violation); } else { results.info.push(violation); } } } catch (e) { // Invalid regex, skip } } } } } return results; } /** * Determine which rule categories apply to a file type * @param {string} ext - File extension * @returns {string[]} Applicable category names */ function getApplicableCategories(ext) { const cssTypes = ['.css', '.scss', '.sass', '.less', '.styl']; const jsTypes = ['.js', '.jsx', '.ts', '.tsx', '.vue', '.svelte']; const htmlTypes = ['.html', '.htm', '.vue', '.svelte', '.jsx', '.tsx']; const categories = []; if (cssTypes.includes(ext)) { categories.push('colors', 'spacing', 'typography'); } if (jsTypes.includes(ext)) { categories.push('colors', 'spacing', 'components'); } if (htmlTypes.includes(ext)) { categories.push('accessibility', 'components'); } return categories; } /** * Validate multiple files and aggregate results * @param {string[]} files - Array of file paths * @param {Object} options - Validation options * @returns {Object} Aggregated results */ function validateFiles(files, options = {}) { const results = { totalFiles: files.length, passedFiles: 0, failedFiles: 0, totalErrors: 0, totalWarnings: 0, fileResults: [] }; for (const file of files) { const fileResult = validateFile(file, options); results.fileResults.push(fileResult); if (fileResult.passed) { results.passedFiles++; } else { results.failedFiles++; } results.totalErrors += fileResult.errors.length; results.totalWarnings += fileResult.warnings.length; } return results; } /** * Print validation results to console * @param {Object} results - Validation results */ function printResults(results) { console.log('\n' + colors.bold + '=== DSS Rules Validation ===' + colors.reset); console.log(`Rules version: ${rules.getVersion()}\n`); for (const fileResult of results.fileResults) { const statusIcon = fileResult.passed ? colors.green + '✓' : colors.red + '✗'; console.log(`${statusIcon}${colors.reset} ${fileResult.file}`); for (const error of fileResult.errors) { console.log(` ${colors.red}ERROR${colors.reset} [${error.rule}] Line ${error.line}: ${error.message}`); console.log(` Found: ${colors.yellow}${error.match}${colors.reset}`); } for (const warning of fileResult.warnings) { console.log(` ${colors.yellow}WARN${colors.reset} [${warning.rule}] Line ${warning.line}: ${warning.message}`); } } console.log('\n' + colors.bold + '=== Summary ===' + colors.reset); console.log(`Files: ${results.passedFiles}/${results.totalFiles} passed`); console.log(`Errors: ${colors.red}${results.totalErrors}${colors.reset}`); console.log(`Warnings: ${colors.yellow}${results.totalWarnings}${colors.reset}`); if (results.totalErrors > 0) { console.log(`\n${colors.red}${colors.bold}Validation failed!${colors.reset}`); } else { console.log(`\n${colors.green}${colors.bold}Validation passed!${colors.reset}`); } } /** * Self-test to verify rules package is correctly structured */ function selfTest() { console.log('Running @dss/rules self-test...\n'); const allRules = rules.loadRules(); let passed = true; for (const [category, ruleSet] of Object.entries(allRules)) { if (!ruleSet) { console.log(`${colors.red}✗${colors.reset} ${category}: Failed to load`); passed = false; continue; } const ruleCount = ruleSet.rules?.length || 0; console.log(`${colors.green}✓${colors.reset} ${category}: ${ruleCount} rules, version ${ruleSet.version}`); } const ciConfig = rules.getCIConfig(); console.log(`\nCI Config: ${ciConfig.blockingRules.length} blocking rules, ${ciConfig.advisoryRules.length} advisory rules`); if (passed) { console.log(`\n${colors.green}Self-test passed!${colors.reset}`); process.exit(0); } else { console.log(`\n${colors.red}Self-test failed!${colors.reset}`); process.exit(1); } } // CLI entry point if (require.main === module) { const args = process.argv.slice(2); if (args.includes('--self-test')) { selfTest(); } else if (args.includes('--help') || args.length === 0) { console.log(` @dss/rules validator Usage: node validate.js [options] Options: --self-test Run self-test to verify rules package --json Output results as JSON --help Show this help message Examples: node validate.js src/**/*.tsx node validate.js --self-test node validate.js --json src/components/*.tsx `); } else { const jsonOutput = args.includes('--json'); const files = args.filter(a => !a.startsWith('--')); const results = validateFiles(files); if (jsonOutput) { console.log(JSON.stringify(results, null, 2)); } else { printResults(results); } process.exit(results.totalErrors > 0 ? 1 : 0); } } module.exports = { validateFile, validateFiles, getApplicableCategories };