#!/usr/bin/env node /** * SLDS Quality Analyzer * * Analyzes CSS and HTML files for SLDS quality issues beyond what the linter catches. * * Usage: node analyze-quality.cjs [--hooks-index ] * * Output: JSON with findings categorized by severity */ const fs = require('fs'); const path = require('path'); function resolveHooksIndexPath(args) { const idx = args.indexOf('--hooks-index'); if (idx !== -1 && idx + 1 < args.length) { return path.resolve(args[idx + 1]); } return null; } let HOOKS_INDEX_PATH = null; // Severity levels const CRITICAL = 'critical'; const WARNING = 'warning'; const INFO = 'info'; // Detection patterns // NOTE: Checks that the SLDS linter already handles are excluded here to avoid // double-counting. The linter covers: slds/class-override (L001), // slds/lwc-token-to-slds-hook (L002), slds/no-hardcoded-values (L003). // This script focuses on what the linter does NOT catch. const PATTERNS = { // CSS patterns (linter-complementary only) css: { missingFallback: { pattern: /var\(--slds-g-[^,)]+\)/g, severity: CRITICAL, id: 'T002', message: 'SLDS hook without fallback value', recommendation: 'Add fallback: var(--slds-g-color-surface-1, #fff)' }, important: { pattern: /!important/g, severity: WARNING, id: 'Q001', message: '!important declaration found', recommendation: 'Remove !important, use proper specificity' }, magicPixels: { pattern: /\b(?:margin(?:-[a-z-]+)?|padding(?:-[a-z-]+)?|gap|row-gap|column-gap)\s*:\s*(\d+)px\b(?![^;]*var\()/g, severity: WARNING, id: 'T021', message: 'Magic pixel value not using spacing hook', recommendation: 'Use var(--slds-g-spacing-*) or utility class' }, highZindex: { pattern: /z-index\s*:\s*(\d{3,})/g, severity: WARNING, id: 'Q021', message: 'High z-index value', recommendation: 'Use defined z-index scale' }, outlineNone: { pattern: /outline\s*:\s*none/g, severity: WARNING, id: 'A021', message: 'Focus outline removed without alternative', recommendation: 'Provide alternative focus indicator' } }, // JS patterns (inline styles and dynamic class manipulation) js: { inlineStyleJS: { pattern: /\.style\.\w+\s*=/g, severity: WARNING, id: 'Q025', message: 'Inline style manipulation in JavaScript', recommendation: 'Use CSS classes instead of direct style property assignment' }, classListManipulation: { pattern: /\.classList\.(add|remove|toggle)\(\s*['"]slds-/g, severity: WARNING, id: 'Q012', message: 'Dynamic SLDS class manipulation in JavaScript', recommendation: 'Prefer declarative class bindings; avoid manipulating slds-* classes directly' } }, // HTML patterns html: { inlineStyle: { pattern: /style\s*=\s*["'][^"']+["']/gi, severity: WARNING, id: 'Q002', message: 'Inline style attribute', recommendation: 'Move styles to CSS file' }, lightningInputNoLabel: { pattern: /]*\blabel\b)[^>]*>/gi, severity: CRITICAL, id: 'A001', message: 'Lightning input without label attribute', recommendation: 'Add label attribute to lightning-input' }, iconNoAlt: { pattern: /]*alternative-text)[^>]*>/gi, severity: CRITICAL, id: 'A004', message: 'Icon without alternative-text', recommendation: 'Add alternative-text (or empty string for decorative)' }, imgNoAlt: { pattern: /]*\balt\b)[^>]*>/gi, severity: CRITICAL, id: 'A005', message: 'Image without alt attribute', recommendation: 'Add alt attribute' }, positiveTabindex: { pattern: /tabindex\s*=\s*["']([1-9]\d*)["']/gi, severity: WARNING, id: 'A020', message: 'Positive tabindex value', recommendation: 'Use tabindex="0" or "-1" only' }, clickableDiv: { pattern: /]*onclick[^>]*>/gi, severity: WARNING, id: 'A022', message: 'Div with click handler instead of button', recommendation: 'Use