evOo-local/scripts/codemod-ui-scale.mjs
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

110 lines
3.4 KiB
JavaScript

#!/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.`);