#!/usr/bin/env node /** * Rewrites legacy pixel typography / icon sizes in shell surfaces to token utilities. * Run: node scripts/codemod-ui-scale.mjs */ import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; import { join } from "node:path"; const ROOT = new URL("../apps/web/src", import.meta.url).pathname; const TEXT_REPLACEMENTS = [ ["text-[9px]", "text-ui-caption"], ["text-[10px]", "text-ui-caption"], ["text-[11px]", "text-ui-label"], ["text-[11.5px]", "text-ui-label"], ["text-[12px]", "text-ui-body-sm"], ["text-[12.5px]", "text-ui-body-sm"], ["text-[13px]", "text-ui-body-sm"], ["text-[13.5px]", "text-ui-body"], ["text-[14px]", "text-ui-body"], ["text-[15px]", "text-ui-body"], ["text-[15.5px]", "text-ui-body"], ["text-[16px]", "text-ui-body"], ]; const SIZE_REPLACEMENTS = [ ["size={9}", 'className="size-icon-sm"'], ["size={10}", 'className="size-icon-sm"'], ["size={11}", 'className="size-icon-sm"'], ["size={12}", 'className="size-icon-sm"'], ["size={13}", 'className="size-icon-sm"'], ["size={14}", 'className="size-icon-md"'], ["size={15}", 'className="size-icon-md"'], ["size={16}", 'className="size-icon-md"'], ["size={18}", 'className="size-icon-lg"'], ["size={19}", 'className="size-icon-lg"'], ]; const CONTROL_REPLACEMENTS = [ ["grid h-8 w-8", "grid size-control-lg"], ["h-8 w-8 rounded", "size-control-md rounded"], ["h-8 w-8 shrink-0", "size-control-lg shrink-0"], ["h-8 w-8 place-items-center", "size-control-lg place-items-center"], ["h-7 w-7", "size-control-sm"], ]; function walk(dir, out = []) { for (const entry of readdirSync(dir)) { const path = join(dir, entry); const stat = statSync(path); if (stat.isDirectory()) { walk(path, out); } else if (/\.tsx?$/.test(entry) && !/\.test\.tsx?$/.test(entry)) { out.push(path); } } return out; } function mergeIconClassName(content) { return content.replace( /<(\w+)\s+([^>]*?)size=\{(9|10|11|12|13|14|15|16|18|19)\}([^>]*?)\/>/g, (_match, tag, before, size, after) => { const iconClass = Number(size) <= 13 ? "size-icon-sm" : Number(size) <= 16 ? "size-icon-md" : "size-icon-lg"; const combined = `${before}${after}`; const classMatch = combined.match(/className="([^"]*)"/); if (classMatch) { const merged = `${classMatch[1]} ${iconClass}`.trim(); const next = combined.replace(/className="[^"]*"/, `className="${merged}"`); return `<${tag} ${next.trim()} />`; } return `<${tag} ${before}${after} className="${iconClass}" />`.replace(/\s+/g, " "); }, ); } function transform(content) { let next = content; for (const [from, to] of TEXT_REPLACEMENTS) { next = next.replaceAll(from, to); } for (const [from, to] of CONTROL_REPLACEMENTS) { next = next.replaceAll(from, to); } next = mergeIconClassName(next); for (const [from, to] of SIZE_REPLACEMENTS) { next = next.replaceAll(from, to); } next = next.replace(/className="([^"]*)"\s+className="([^"]*)"/g, (_, a, b) => { return `className="${`${a} ${b}`.trim()}"`; }); return next; } const files = walk(ROOT); let changed = 0; for (const file of files) { const original = readFileSync(file, "utf8"); const updated = transform(original); if (updated !== original) { writeFileSync(file, updated); changed += 1; console.log(`updated ${file.replace(ROOT, "")}`); } } console.log(`\nDone. ${changed} files updated.`);