Last active
April 21, 2026 17:57
-
-
Save ahuggins/196e90ebe9a00acd48378d083cc936be to your computer and use it in GitHub Desktop.
alli-tailwind-color-upgrade-dry-run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const args = process.argv.slice(2); | |
| function getArg(name, fallback) { | |
| const idx = args.indexOf(name); | |
| if (idx === -1) return fallback; | |
| return args[idx + 1] || fallback; | |
| } | |
| function hasArg(name) { | |
| return args.includes(name); | |
| } | |
| if (hasArg('--help')) { | |
| console.log('Usage: node tailwind-class-map-dry-run.js --project /absolute/path/to/repo [--scan src]'); | |
| console.log('Example: node tailwind-class-map-dry-run.js --project /Users/me/my-app --scan src'); | |
| process.exit(0); | |
| } | |
| const projectDir = getArg('--project', process.cwd()); | |
| const scanDirArg = getArg('--scan', 'src'); | |
| const scanRoot = path.isAbsolute(scanDirArg) ? scanDirArg : path.join(projectDir, scanDirArg); | |
| const exts = new Set(['.ts', '.tsx', '.js', '.jsx', '.mdx', '.css']); | |
| const mapping = { | |
| 'text-gray-700': 'text-gray-500', | |
| 'border-gray-400': 'border-gray-150', | |
| 'text-gray-600': 'text-gray-300', | |
| 'ring-gray-400': 'ring-gray-150', | |
| 'border-gray-500': 'border-gray-200', | |
| 'bg-gray-300': 'bg-gray-50', | |
| 'bg-gray-100': 'bg-gray-25', | |
| 'text-gray-750': 'text-gray-600', | |
| 'divide-gray-400': 'divide-gray-150', | |
| 'bg-gray-400': 'bg-gray-150', | |
| 'bg-gray-600': 'bg-gray-300', | |
| 'border-gray-700': 'border-gray-500', | |
| 'bg-gray-700': 'bg-gray-500', | |
| 'border-gray-600': 'border-gray-300', | |
| 'divide-gray-700': 'divide-gray-500', | |
| 'placeholder-gray-600': 'placeholder-gray-300', | |
| 'placeholder-gray-700': 'placeholder-gray-500', | |
| 'ring-gray-300': 'ring-gray-50', | |
| 'text-gray-400': 'text-gray-150', | |
| 'text-red-400': 'text-red-500' | |
| }; | |
| if (!fs.existsSync(scanRoot)) { | |
| console.error('Scan path not found: ' + scanRoot); | |
| process.exit(1); | |
| } | |
| const keys = Object.keys(mapping) | |
| .sort((a, b) => b.length - a.length) | |
| .map(k => k.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); | |
| const tokenRegex = new RegExp( | |
| '\\b((?:[a-z-]+:)*)((?:alli-)?)((' + keys.join('|') + '))\\b', | |
| 'g' | |
| ); | |
| const perMapCounts = new Map(); | |
| const perFileCounts = new Map(); | |
| const sampleChanges = []; | |
| const MAX_SAMPLES = 150; | |
| function walk(dir) { | |
| const entries = fs.readdirSync(dir, { withFileTypes: true }); | |
| for (const ent of entries) { | |
| const full = path.join(dir, ent.name); | |
| if (ent.isDirectory()) { | |
| walk(full); | |
| continue; | |
| } | |
| if (exts.has(path.extname(ent.name))) { | |
| processFile(full); | |
| } | |
| } | |
| } | |
| function processFile(filePath) { | |
| const rel = path.relative(projectDir, filePath); | |
| const text = fs.readFileSync(filePath, 'utf8'); | |
| const lines = text.split(/\r?\n/); | |
| let fileHits = 0; | |
| for (let i = 0; i < lines.length; i++) { | |
| const line = lines[i]; | |
| tokenRegex.lastIndex = 0; | |
| let match; | |
| while ((match = tokenRegex.exec(line)) !== null) { | |
| const variant = match[1] || ''; | |
| const prefix = match[2] || ''; | |
| const base = match[3]; | |
| const fromToken = variant + prefix + base; | |
| const toToken = variant + prefix + mapping[base]; | |
| const mapKey = fromToken + ' -> ' + toToken; | |
| perMapCounts.set(mapKey, (perMapCounts.get(mapKey) || 0) + 1); | |
| fileHits++; | |
| if (sampleChanges.length < MAX_SAMPLES) { | |
| sampleChanges.push(rel + ':' + String(i + 1) + ' | ' + fromToken + ' -> ' + toToken); | |
| } | |
| } | |
| } | |
| if (fileHits > 0) { | |
| perFileCounts.set(rel, fileHits); | |
| } | |
| } | |
| walk(scanRoot); | |
| const totalHits = Array.from(perMapCounts.values()).reduce((a, b) => a + b, 0); | |
| console.log('DRY RUN ONLY (no files modified)'); | |
| console.log('Project: ' + projectDir); | |
| console.log('Scan root: ' + scanRoot); | |
| console.log('Total matches: ' + totalHits); | |
| console.log('Files affected: ' + perFileCounts.size); | |
| console.log(''); | |
| console.log('Mappings by count:'); | |
| Array.from(perMapCounts.entries()) | |
| .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])) | |
| .forEach(([k, v]) => { | |
| console.log(String(v).padStart(4, ' ') + ' ' + k); | |
| }); | |
| console.log(''); | |
| console.log('Top files by hit count:'); | |
| Array.from(perFileCounts.entries()) | |
| .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])) | |
| .slice(0, 50) | |
| .forEach(([f, c]) => { | |
| console.log(String(c).padStart(4, ' ') + ' ' + f); | |
| }); | |
| console.log(''); | |
| console.log('Sample replacements (first ' + MAX_SAMPLES + '):'); | |
| for (const s of sampleChanges) { | |
| console.log(s); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment