#!/usr/bin/env node /** * Token validation script — checks for hardcoded colors in source files * and reports token coverage per product. * * Usage: node scripts/validate-tokens.js [product-path] */ const { readFileSync, readdirSync, statSync } = require('fs'); const { join, resolve } = require('path'); const HARD_COLOR_REGEX = /#[0-9A-Fa-f]{3,8}\b|rgb\([^)]+\)|rgba\([^)]+\)|hsl\([^)]+\)/g; const EXCLUDED_DIRS = ['node_modules', 'dist', 'build', '.git', 'generated', '__mocks__']; const INCLUDED_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.swift', '.kt']; function findFiles(dir, files = []) { try { const items = readdirSync(dir); for (const item of items) { const fullPath = join(dir, item); if (EXCLUDED_DIRS.some(ex => fullPath.includes(ex))) continue; const stat = statSync(fullPath); if (stat.isDirectory()) { findFiles(fullPath, files); } else if (INCLUDED_EXTENSIONS.some(ext => item.endsWith(ext))) { files.push(fullPath); } } } catch { // Directory might not exist or be accessible } return files; } function analyzeFile(filePath) { const content = readFileSync(filePath, 'utf-8'); const lines = content.split('\n'); const issues = []; lines.forEach((line, index) => { // Skip comments if (line.trim().startsWith('//') || line.trim().startsWith('*') || line.trim().startsWith('/*')) return; const matches = line.match(HARD_COLOR_REGEX); if (matches) { // Filter out legitimate uses (like transparency values) const suspicious = matches.filter(m => { if (m.startsWith('#') && (m.length === 9 || m.length === 5)) return false; // Skip alpha hex if (m.includes('0.0') || m.includes('1.0')) return false; // Skip clear/opaque // Skip CSS-variable token usage (already tokenized) if ((m.startsWith('hsl(') || m.startsWith('rgb(') || m.startsWith('rgba(')) && m.includes('var(--')) return false; return true; }); if (suspicious.length > 0) { issues.push({ line: index + 1, colors: suspicious, content: line.trim().slice(0, 80), }); } } }); return issues; } function main() { const targetPath = process.argv[2] || '.'; const absolutePath = resolve(targetPath); console.log(`šŸ” Scanning ${absolutePath} for hardcoded colors...\n`); const files = findFiles(absolutePath); let totalIssues = 0; let filesWithIssues = 0; for (const file of files) { const issues = analyzeFile(file); if (issues.length > 0) { filesWithIssues++; totalIssues += issues.length; const relativePath = file.replace(absolutePath, '').slice(1); console.log(`\nšŸ“„ ${relativePath}`); issues.forEach(issue => { console.log(` Line ${issue.line}: ${issue.colors.join(', ')}`); console.log(` ${issue.content}`); }); } } console.log(`\n${'='.repeat(60)}`); console.log(`šŸ“Š Summary:`); console.log(` Files scanned: ${files.length}`); console.log(` Files with hardcoded colors: ${filesWithIssues}`); console.log(` Total hardcoded colors found: ${totalIssues}`); if (totalIssues > 0) { console.log(`\nāš ļø Consider replacing hardcoded colors with design tokens:`); console.log(` Web: var(--ml-) from @bytelyst/design-tokens`); console.log(` iOS: MindLystColors.dark / MindLystColors.light`); console.log(` KMP: MindLystTokens.Dark. / MindLystTokens.Light.`); process.exit(1); } else { console.log(`\nāœ… No hardcoded colors found! All colors use design tokens.`); process.exit(0); } } main();