/** * Validate SVG source files for the icon system * * Checks: * - Filename follows kebab-case convention * - viewBox present (warns if missing — ensureViewBox in generate-icons handles it) * - viewBox is square (warns if not) * - No embedded raster images ( or data:image) * - File size < 50KB * * Usage: pnpm tsx scripts/validate-svgs.ts [--dir=logos|general] */ import fs from 'fs/promises' import path from 'path' type SourceDir = 'providers' | 'models' | 'general' function parseArgs(): { dir: SourceDir } { const dirArg = process.argv.find((a) => a.startsWith('--dir=')) const dir = (dirArg?.split('=')[1] as SourceDir) || 'providers' return { dir } } interface ValidationResult { file: string errors: string[] warnings: string[] } const MAX_FILE_SIZE = 50 * 1024 // 50KB /** * Validate that a name follows kebab-case convention. * Allows: lowercase letters, digits, hyphens. Must start with letter or digit. * Examples: "openai", "aws-bedrock", "302ai" */ const KEBAB_CASE_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/ function isKebabCase(name: string): boolean { return KEBAB_CASE_RE.test(name) } /** * Suggest a kebab-case version of a non-conforming name. */ function suggestKebabCase(name: string): string { return ( name // Insert hyphen before uppercase letters: "aiOnly" -> "ai-Only" .replace(/([a-z0-9])([A-Z])/g, '$1-$2') // Handle uppercase sequences: "DMXAPI" -> "DMX-API" -> handled below .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2') .toLowerCase() // Collapse multiple hyphens .replace(/-+/g, '-') // Remove leading/trailing hyphens .replace(/^-|-$/g, '') ) } async function collectSvgFiles(dir: SourceDir): Promise { const baseDir = path.join(__dirname, '../icons', dir) const files = await fs.readdir(baseDir) return files.filter((f) => f.endsWith('.svg')).map((f) => path.join(baseDir, f)) } async function validateSvg(filePath: string): Promise { const file = path.relative(path.join(__dirname, '..'), filePath) const errors: string[] = [] const warnings: string[] = [] // Naming convention check const baseName = path.basename(filePath, '.svg') if (!isKebabCase(baseName)) { warnings.push(`Filename "${baseName}.svg" is not kebab-case. Suggested: "${suggestKebabCase(baseName)}.svg"`) } const stat = await fs.stat(filePath) if (stat.size > MAX_FILE_SIZE) { warnings.push(`File size ${(stat.size / 1024).toFixed(1)}KB exceeds ${MAX_FILE_SIZE / 1024}KB limit`) } const content = await fs.readFile(filePath, 'utf-8') // Check for embedded raster images if (content.includes(' 0.01) { warnings.push(`viewBox is not square: ${w}x${h} (mono icons may look distorted)`) } } } // Check for tag if (!/ tag found') } return { file, errors, warnings } } async function main() { const { dir } = parseArgs() console.log(`Validating SVG files (source: ${dir})...\n`) const files = await collectSvgFiles(dir) if (files.length === 0) { console.log('No SVG files found.') return } console.log(`Found ${files.length} SVG files\n`) const results = await Promise.all(files.map(validateSvg)) let errorCount = 0 let warningCount = 0 for (const result of results) { if (result.errors.length === 0 && result.warnings.length === 0) continue console.log(`${result.file}:`) for (const error of result.errors) { console.log(` ERROR: ${error}`) errorCount++ } for (const warning of result.warnings) { console.log(` WARN: ${warning}`) warningCount++ } console.log() } console.log(`\nValidation complete: ${files.length} files, ${errorCount} errors, ${warningCount} warnings`) if (errorCount > 0) { process.exit(1) } } void main()