#!/usr/bin/env node /** * Theme Class Migration Script * * This script helps identify and replace hardcoded color classes with theme-aware classes. */ const fs = require('fs'); const path = require('path'); // Define replacement mappings const CLASS_REPLACEMENTS = { // Background colors 'bg-gray-50 dark:bg-gray-900': 'theme-bg-page', 'bg-gray-50 dark:bg-gray-800': 'theme-bg-secondary', 'bg-white dark:bg-gray-900': 'theme-bg', 'bg-white dark:bg-gray-800': 'theme-bg', 'bg-gray-100 dark:bg-gray-800': 'theme-bg-secondary', 'bg-gray-200 dark:bg-gray-700': 'theme-bg-secondary', // Text colors 'text-gray-900 dark:text-gray-100': 'theme-text', 'text-gray-900 dark:text-white': 'theme-text', 'text-gray-700 dark:text-gray-200': 'theme-text', 'text-gray-600 dark:text-gray-400': 'theme-text-secondary', 'text-gray-500 dark:text-gray-400': 'theme-text-muted', 'text-gray-500': 'theme-text-muted', // Border colors 'border-gray-200 dark:border-gray-700': 'theme-border', 'border-gray-300 dark:border-gray-600': 'theme-border', 'border-stone-200 dark:border-gray-700': 'theme-border', // Common patterns 'bg-gray-50 dark:bg-gray-800 rounded-lg p-6': 'theme-card', 'text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4': 'theme-text-heading', 'text-gray-600 dark:text-gray-400 mb-4': 'theme-text-body', 'text-sm text-gray-500': 'theme-text-caption', }; // Individual class mappings (for cases where full context isn't available) const INDIVIDUAL_REPLACEMENTS = { 'text-gray-900': 'theme-text', 'text-gray-700': 'theme-text', 'text-gray-600': 'theme-text-secondary', 'text-gray-500': 'theme-text-muted', 'text-gray-400': 'theme-text-muted', 'bg-gray-50': 'theme-bg-secondary', 'bg-gray-100': 'theme-bg-secondary', 'bg-gray-800': 'theme-bg-secondary', 'bg-gray-900': 'theme-bg-page', 'border-gray-200': 'theme-border', 'border-gray-300': 'theme-border', }; function findRustFiles(dir) { const files = []; function walk(currentPath) { const items = fs.readdirSync(currentPath); for (const item of items) { const fullPath = path.join(currentPath, item); const stat = fs.statSync(fullPath); if (stat.isDirectory() && !item.startsWith('.') && item !== 'target') { walk(fullPath); } else if (item.endsWith('.rs')) { files.push(fullPath); } } } walk(dir); return files; } function analyzeFile(filePath) { const content = fs.readFileSync(filePath, 'utf8'); const issues = []; // Find hardcoded gray/stone classes const grayClassRegex = /(text|bg|border)-(gray|stone)-\d+/g; const darkModeRegex = /(text|bg|border)-(gray|stone)-\d+\s+dark:(text|bg|border)-(gray|stone)-\d+/g; let match; while ((match = grayClassRegex.exec(content)) !== null) { issues.push({ type: 'hardcoded-class', class: match[0], line: content.substring(0, match.index).split('\n').length, suggestion: INDIVIDUAL_REPLACEMENTS[match[0]] || 'theme-*' }); } // Check for complex patterns for (const [pattern, replacement] of Object.entries(CLASS_REPLACEMENTS)) { if (content.includes(pattern)) { issues.push({ type: 'complex-pattern', pattern, replacement, line: content.substring(0, content.indexOf(pattern)).split('\n').length }); } } return issues; } function generateReport(clientDir) { console.log('šŸ” Analyzing theme class usage...\n'); const files = findRustFiles(clientDir); let totalIssues = 0; for (const file of files) { const issues = analyzeFile(file); if (issues.length > 0) { console.log(`šŸ“„ ${path.relative(process.cwd(), file)}:`); for (const issue of issues) { totalIssues++; if (issue.type === 'hardcoded-class') { console.log(` Line ${issue.line}: ${issue.class} → ${issue.suggestion}`); } else { console.log(` Line ${issue.line}: Complex pattern found`); console.log(` Replace: ${issue.pattern}`); console.log(` With: ${issue.replacement}`); } } console.log(''); } } console.log(`šŸ“Š Summary: Found ${totalIssues} hardcoded classes to migrate`); if (totalIssues > 0) { console.log('\nšŸ’” Migration Strategy:'); console.log('1. Use theme-aware classes: theme-text, theme-bg, theme-border'); console.log('2. For complex patterns, use composite classes: theme-card, theme-text-heading'); console.log('3. Test with different themes: npm run theme:build purple'); console.log('4. Verify colors change when switching themes'); } } function migrateSingleFile(filePath, dryRun = true) { console.log(`šŸ”§ ${dryRun ? 'Analyzing' : 'Migrating'}: ${filePath}`); let content = fs.readFileSync(filePath, 'utf8'); let changes = 0; // Apply complex pattern replacements first for (const [pattern, replacement] of Object.entries(CLASS_REPLACEMENTS)) { if (content.includes(pattern)) { console.log(` Replacing: ${pattern} → ${replacement}`); content = content.replace(new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), replacement); changes++; } } if (!dryRun && changes > 0) { fs.writeFileSync(filePath, content); console.log(`āœ… Applied ${changes} changes to ${filePath}`); } else if (changes > 0) { console.log(`šŸ“‹ Would apply ${changes} changes (use --apply to execute)`); } else { console.log(`✨ No changes needed`); } return changes; } // CLI handling if (require.main === module) { const args = process.argv.slice(2); const command = args[0]; if (command === 'analyze' || !command) { const clientDir = path.join(__dirname, '..', 'crates', 'client'); generateReport(clientDir); } else if (command === 'migrate') { const filePath = args[1]; const dryRun = !args.includes('--apply'); if (!filePath) { console.error('Usage: migrate [--apply]'); process.exit(1); } migrateSingleFile(filePath, dryRun); } else { console.log('Usage:'); console.log(' node migrate-theme-classes.js analyze'); console.log(' node migrate-theme-classes.js migrate [--apply]'); } } module.exports = { generateReport, migrateSingleFile, CLASS_REPLACEMENTS };