#!/usr/bin/env node /** * Codemod: replace text-[10px] and text-[9px] with text-xs (design system). * Usage: node scripts/codemod-typography-arbitrary.mjs [--dry-run] [--dir src/components] * Excludes: avatar.tsx (exception for size xs), badge.tsx (doc only). */ import fs from 'fs'; import path from 'path'; const DEFAULT_DIRS = ['src/components', 'src/features']; const IGNORE_DIRS = ['node_modules', 'dist', '.storybook', 'dist_verification']; const EXCLUDE_FILES = ['avatar.tsx', 'badge.tsx']; const EXTENSIONS = new Set(['.tsx', '.jsx', '.ts', '.js']); function parseArgs() { const args = process.argv.slice(2); let dirs = DEFAULT_DIRS; let dryRun = false; for (let i = 0; i < args.length; i++) { if (args[i] === '--dry-run') dryRun = true; else if (args[i] === '--dir' && args[i + 1]) dirs = [args[++i]]; } return { dirs, dryRun }; } function* walkFiles(dir, base = dir) { if (!fs.existsSync(dir)) return; const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const e of entries) { const full = path.join(dir, e.name); const rel = path.relative(base, full); if (e.isDirectory()) { if (!IGNORE_DIRS.includes(e.name)) yield* walkFiles(full, base); } else if (EXTENSIONS.has(path.extname(e.name)) && !EXCLUDE_FILES.includes(e.name)) { yield { full, rel }; } } } function transform(content) { let out = content; const replacements = []; const r10 = /text-\[10px\]/g; const r9 = /text-\[9px\]/g; let m; while ((m = r10.exec(content)) !== null) replacements.push({ index: m.index, from: 'text-[10px]', to: 'text-xs' }); while ((m = r9.exec(content)) !== null) replacements.push({ index: m.index, from: 'text-[9px]', to: 'text-xs' }); if (replacements.length === 0) return { content: out, count: 0 }; replacements.sort((a, b) => a.index - b.index); let count = 0; out = out.replace(/text-\[10px\]/g, () => { count++; return 'text-xs'; }); out = out.replace(/text-\[9px\]/g, () => { count++; return 'text-xs'; }); return { content: out, count }; } function main() { const { dirs, dryRun } = parseArgs(); const cwd = process.cwd(); const modified = []; for (const dir of dirs) { const absDir = path.isAbsolute(dir) ? dir : path.join(cwd, dir); for (const { full, rel } of walkFiles(absDir)) { const content = fs.readFileSync(full, 'utf8'); const { content: newContent, count } = transform(content); if (count > 0) { modified.push({ rel, count }); if (!dryRun) fs.writeFileSync(full, newContent, 'utf8'); } } } if (dryRun) { console.log('# [DRY RUN] Would replace text-[10px]/text-[9px] → text-xs in:\n'); } else { console.log('# Replaced text-[10px]/text-[9px] → text-xs in:\n'); } for (const { rel, count } of modified) { console.log(` ${rel} (${count} replacement(s))`); } console.log(`\nTotal: ${modified.length} files, ${modified.reduce((s, r) => s + r.count, 0)} replacements.`); if (dryRun && modified.length > 0) console.log('\nRun without --dry-run to apply.'); } main();