learning_ai_common_plat/packages/design-tokens/scripts/validate-tokens.cjs
saravanakumardb1 2f7e3ad9b6 refactor(design-tokens): improve token validator
- ignore hsl(var(--...)) / rgb(var(--...))

- export generated/tokens entry
2026-03-04 18:13:13 -08:00

114 lines
3.6 KiB
JavaScript
Executable File

#!/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 (e) {
// 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-<token>) from @bytelyst/design-tokens`);
console.log(` iOS: MindLystColors.dark<Token> / MindLystColors.light<Token>`);
console.log(` KMP: MindLystTokens.Dark.<TOKEN> / MindLystTokens.Light.<TOKEN>`);
process.exit(1);
} else {
console.log(`\n✅ No hardcoded colors found! All colors use design tokens.`);
process.exit(0);
}
}
main();