Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
AI-powered design system generator that produces complete, tailored design systems from project requirements.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/validate-tokens.cjs
1#!/usr/bin/env node2/**3* Validate token usage in codebase4* Finds hardcoded values that should use design tokens5*6* Usage:7* node validate-tokens.cjs --dir src/8* node validate-tokens.cjs --dir src/ --fix9*/1011const fs = require('fs');12const path = require('path');1314/**15* Parse command line arguments16*/17function parseArgs() {18const args = process.argv.slice(2);19const options = {20dir: null,21fix: false,22ignore: ['node_modules', '.git', 'dist', 'build', '.next']23};2425for (let i = 0; i < args.length; i++) {26if (args[i] === '--dir' || args[i] === '-d') {27options.dir = args[++i];28} else if (args[i] === '--fix') {29options.fix = true;30} else if (args[i] === '--ignore' || args[i] === '-i') {31options.ignore.push(args[++i]);32} else if (args[i] === '--help' || args[i] === '-h') {33console.log(`34Usage: node validate-tokens.cjs [options]3536Options:37-d, --dir <path> Directory to scan (required)38--fix Show suggested fixes (no auto-fix)39-i, --ignore <dir> Additional directories to ignore40-h, --help Show this help4142Checks for:43- Hardcoded hex colors (#RGB, #RRGGBB)44- Hardcoded pixel values (except 0, 1px)45- Hardcoded rem values in CSS46`);47process.exit(0);48}49}5051return options;52}5354/**55* Patterns to detect hardcoded values56*/57const patterns = {58hexColor: {59regex: /#([0-9A-Fa-f]{3}){1,2}\b/g,60message: 'Hardcoded hex color',61suggestion: 'Use var(--color-*) token'62},63rgbColor: {64regex: /rgb\s*\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)/gi,65message: 'Hardcoded RGB color',66suggestion: 'Use var(--color-*) token'67},68pixelValue: {69regex: /:\s*(\d{2,})px/g, // 2+ digit px values70message: 'Hardcoded pixel value',71suggestion: 'Use var(--space-*) or var(--radius-*) token'72},73remValue: {74regex: /:\s*\d+\.?\d*rem(?![^{]*\$value)/g, // rem not in token definition75message: 'Hardcoded rem value',76suggestion: 'Use var(--space-*) or var(--font-size-*) token'77}78};7980/**81* File extensions to scan82*/83const extensions = ['.css', '.scss', '.tsx', '.jsx', '.ts', '.js', '.vue', '.svelte'];8485/**86* Files/patterns to skip87*/88const skipPatterns = [89/\.min\.(css|js)$/,90/tailwind\.config/,91/globals\.css/, // Token definitions92/tokens\.(css|json)/93];9495/**96* Get all files recursively97*/98function getFiles(dir, ignore, files = []) {99const entries = fs.readdirSync(dir, { withFileTypes: true });100101for (const entry of entries) {102const fullPath = path.join(dir, entry.name);103104if (entry.isDirectory()) {105if (!ignore.includes(entry.name)) {106getFiles(fullPath, ignore, files);107}108} else if (entry.isFile()) {109const ext = path.extname(entry.name);110if (extensions.includes(ext)) {111files.push(fullPath);112}113}114}115116return files;117}118119/**120* Check if file should be skipped121*/122function shouldSkip(filePath) {123return skipPatterns.some(pattern => pattern.test(filePath));124}125126/**127* Scan file for violations128*/129function scanFile(filePath) {130const content = fs.readFileSync(filePath, 'utf-8');131const lines = content.split('\n');132const violations = [];133134lines.forEach((line, index) => {135// Skip comments136if (line.trim().startsWith('//') || line.trim().startsWith('/*')) {137return;138}139140// Skip lines that already use CSS variables141if (line.includes('var(--')) {142return;143}144145for (const [name, pattern] of Object.entries(patterns)) {146const matches = line.match(pattern.regex);147if (matches) {148matches.forEach(match => {149// Skip common exceptions150if (name === 'hexColor' && ['#000', '#fff', '#FFF', '#000000', '#FFFFFF'].includes(match.toUpperCase())) {151return; // Skip black/white, often intentional152}153154violations.push({155file: filePath,156line: index + 1,157column: line.indexOf(match) + 1,158value: match,159type: name,160message: pattern.message,161suggestion: pattern.suggestion,162context: line.trim().substring(0, 80)163});164});165}166}167});168169return violations;170}171172/**173* Format violation report174*/175function formatReport(violations) {176if (violations.length === 0) {177return 'โ No token violations found';178}179180let report = `โ ๏ธ Found ${violations.length} potential token violations:\n\n`;181182// Group by file183const byFile = {};184violations.forEach(v => {185if (!byFile[v.file]) byFile[v.file] = [];186byFile[v.file].push(v);187});188189for (const [file, fileViolations] of Object.entries(byFile)) {190report += `๐ ${file}\n`;191fileViolations.forEach(v => {192report += ` Line ${v.line}: ${v.message}\n`;193report += ` Found: ${v.value}\n`;194report += ` Suggestion: ${v.suggestion}\n`;195report += ` Context: ${v.context}\n\n`;196});197}198199// Summary200const byType = {};201violations.forEach(v => {202byType[v.type] = (byType[v.type] || 0) + 1;203});204205report += `\n๐ Summary:\n`;206for (const [type, count] of Object.entries(byType)) {207report += ` ${patterns[type].message}: ${count}\n`;208}209210return report;211}212213/**214* Main215*/216function main() {217const options = parseArgs();218219if (!options.dir) {220console.error('Error: --dir is required');221process.exit(1);222}223224const dirPath = path.resolve(process.cwd(), options.dir);225226if (!fs.existsSync(dirPath)) {227console.error(`Error: Directory not found: ${dirPath}`);228process.exit(1);229}230231console.log(`Scanning ${dirPath} for token violations...\n`);232233const files = getFiles(dirPath, options.ignore);234const allViolations = [];235236for (const file of files) {237if (shouldSkip(file)) continue;238239const violations = scanFile(file);240allViolations.push(...violations);241}242243console.log(formatReport(allViolations));244245// Exit with error code if violations found246if (allViolations.length > 0) {247process.exit(1);248}249}250251main();252