202 lines
6.2 KiB
JavaScript
202 lines
6.2 KiB
JavaScript
|
|
#!/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 <file-path> [--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 <file> [--apply]');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
module.exports = { generateReport, migrateSingleFile, CLASS_REPLACEMENTS };
|