Skip to content

Instantly share code, notes, and snippets.

@ahuggins
Last active April 21, 2026 17:57
Show Gist options
  • Select an option

  • Save ahuggins/196e90ebe9a00acd48378d083cc936be to your computer and use it in GitHub Desktop.

Select an option

Save ahuggins/196e90ebe9a00acd48378d083cc936be to your computer and use it in GitHub Desktop.
alli-tailwind-color-upgrade-dry-run
#!/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