paperclip/scripts/codemod-extract-misc.mjs

600 lines
28 KiB
JavaScript

#!/usr/bin/env node
/**
* codemod-extract-misc.mjs
*
* Phase 2 (extraction), Batch 4/4 (final sweep) of the design-token audit
* (branch design/token-extraction). Replaces the remaining value-bearing
* arbitrary Tailwind bracket utilities in `ui/src/components/**` and
* `ui/src/pages/**` (including their *.test.tsx companions) with references
* to CSS custom-property tokens defined in `ui/src/index.css`.
*
* Patterns covered this batch:
* grid-cols-[...] / grid-rows-[...] -> --gtc-<n> / --gtr-<n> (track lists,
* deduped on exact string match)
* transition-[...] -> --tp-<slug> (property lists,
* deduped on exact string match)
* z-[...] -> --z-<n> (bare z-index scale values)
* scale-[...] -> --s-<slug>
* ease-[cubic-bezier(...)] -> --e-<slug>
* align-[...] -> --va-<slug> (vertical-align)
* stroke-[...] -> --sw-<slug> (SVG stroke-WIDTH,
* requires the `length:` hint)
* blur-[...] -> --blur-<slug>
* drop-shadow-[...] -> --drop-shadow-extract-<n> (its own
* family, separate from Batch 3's
* --shadow-extract-* box-shadow
* family — filter vs. box-shadow are
* different CSS properties, see
* TOKEN-AUDIT.md Batch 3 "Needs human
* decision")
* bg-[linear|radial|conic-gradient(...)] -> --gradient-extract-<n>,
* CONTINUING Batch 1's counter (not
* restarting it) - 2 sites that use
* hsl(var(...))/color-mix(...)
* CSS-native color functions rather
* than raw hex/rgb literals, so
* Batch 1's hand-audited color table
* did not catch them even though
* they are the same gradient-bracket
* shape.
* bg|text|border-[var(--x)] -> bare paren passthrough, no new
* (no fallback) token minted (same rule as Batch
* 3's var()-only case)
* bg|text|border-[var(--x,fallback)] -> mints a --*-resolved wrapper token
* (fallback form) whose value is `var(--x, fallback)`
* verbatim (paren-with-fallback-comma
* does not parse in Tailwind v4 —
* confirmed in the Step 0 spike --
* so the fallback expression itself
* must live in the CSS token, not in
* the utility). theme(colors.a.b)
* fallbacks are resolved to their
* BUILT-CSS equivalent var(...) form
* first (inspected from
* storybook-static output before
* writing this codemod - theme() is
* a Tailwind build-time function).
*
* NOT rewritten (documented, not a bug):
* - Selector/variant brackets (data-[...], group-data-[...], has-[...],
* aria-[...], supports-[...], etc.) are CSS selector conditions, not
* visual values - out of scope for this codemod BY DEFINITION (see
* check-token-gates.mjs header for the same distinction, gate 2).
* - tw-animate/animate-plugin arbitrary utilities (zoom-in-[0.97],
* zoom-out-[0.97], slide-in-from-top-[1%], slide-out-to-top-[1%]) plus
* their siblings (animate-in, animate-out, fade-in-0, fade-out-0):
* confirmed via the Step 0 spike that NONE of these compile to any CSS
* at all in this repo's build (no tw-animate-css plugin is installed,
* and no @utility overrides exist in index.css) - grep of the built
* storybook-static CSS shows zero occurrences of "animate-in", "zoom",
* "slide-in-from-top", etc. These are dead, no-op class names with zero
* rendered visual value today, so there is nothing to tokenize; touching
* them would not preserve OR change any pixel. Left untouched and
* allowlisted (third-party plugin syntax, retained for whenever
* tw-animate-css is actually installed) rather than silently deleted,
* since deleting dead classes is itself a (no-op but non-mechanical)
* edit outside this batch's mandate.
* - max-[480px] / min-[420px] breakpoint variants and rounded-[inherit]:
* documented allowlist entries, not code the codemod touches (variant
* position cannot reference a CSS custom property; `inherit` is a
* keyword). See ALLOWLIST block in index.css.
*
* Idempotent: the FIND regexes only match the ORIGINAL bracket-literal form;
* once rewritten the pattern no longer matches, so re-running is a no-op.
*
* Usage: node scripts/codemod-extract-misc.mjs [--check]
* --check Report what WOULD change without writing files (dry run).
*/
import { readFileSync, writeFileSync, readdirSync } from "node:fs";
import { resolve, dirname, join, relative } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = resolve(__dirname, "..");
const UI_SRC = resolve(REPO_ROOT, "ui/src");
const SCAN_DIRS = ["components", "pages"];
const DRY_RUN = process.argv.includes("--check");
// ── Helpers ────────────────────────────────────────────────────────────
function walk(dir, out) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const p = join(dir, entry.name);
if (entry.isDirectory()) walk(p, out);
else if (/\.(tsx?|jsx?)$/.test(entry.name)) out.push(p);
}
}
// Tailwind bracket-escaping uses `_` for literal spaces; reverse it before
// writing into a real CSS custom property value (Batch 1's gradient gotcha,
// generalized in Batch 3, applies again here for grid track lists / calc
// expressions embedded in transition/ease values).
function unescapeSpaces(value) {
return value.replace(/_/g, " ");
}
// "320" "0.7rem" "-50" "auto_minmax(0,1fr)" etc -> safe token-name suffix.
// Dots become underscores, spaces/commas/parens/percent become hyphens,
// collapsed and trimmed (matches Batch 1-3's `--fs-0_7rem`-style convention
// for numeric suffixes; grid/transition/ease values use a slug instead since
// their content isn't a single number).
function slugify(value) {
return value
.trim()
.replace(/_/g, " ")
.replace(/\./g, "_")
.replace(/[^a-zA-Z0-9_]+/g, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "")
.toLowerCase();
}
// ── Token registries ────────────────────────────────────────────────────
const gtcTokens = new Map(); // --gtc-<n> grid-template-columns
const gtrTokens = new Map(); // --gtr-<n> grid-template-rows
const tpTokens = new Map(); // --tp-<slug> transition-property lists
const zTokens = new Map(); // --z-<n>
const sTokens = new Map(); // --s-<slug> scale
const eTokens = new Map(); // --e-<slug> ease / timing-function
const vaTokens = new Map(); // --va-<slug> vertical-align
const swTokens = new Map(); // --sw-<slug> stroke-width
const blurTokens = new Map(); // --blur-<slug>
const dropShadowTokens = new Map(); // --drop-shadow-extract-<n>
const resolvedVarTokens = new Map(); // --*-resolved wrapper tokens (fallback var() forms)
// --gradient-extract-<n> continuing Batch 1's family/counter (these 2 sites
// use hsl(var(...))/color-mix(...) CSS-native color functions rather than
// raw hex/rgb literals, which is why Batch 1's hand-audited color-literal
// table did not catch them - they're still a value-bearing gradient bracket,
// in scope for this batch's "remaining bracket utilities" sweep).
const gradientTokens = new Map();
let gtcCounter = 0;
let gtrCounter = 0;
let dropShadowCounter = 0;
// Batch 1 minted --gradient-extract-1 through --gradient-extract-24; this
// batch's new gradient sites continue that numbering, not restart it.
let gradientCounter = 24;
const gtcByValue = new Map();
function registerGtcToken(unescapedValue, sourceNote) {
if (gtcByValue.has(unescapedValue)) return gtcByValue.get(unescapedValue);
gtcCounter += 1;
const name = `gtc-${gtcCounter}`;
gtcTokens.set(name, { value: unescapedValue, comment: sourceNote });
gtcByValue.set(unescapedValue, name);
return name;
}
const gtrByValue = new Map();
function registerGtrToken(unescapedValue, sourceNote) {
if (gtrByValue.has(unescapedValue)) return gtrByValue.get(unescapedValue);
gtrCounter += 1;
const name = `gtr-${gtrCounter}`;
gtrTokens.set(name, { value: unescapedValue, comment: sourceNote });
gtrByValue.set(unescapedValue, name);
return name;
}
function registerTpToken(unescapedValue, sourceNote) {
const slug = slugify(unescapedValue.replace(/,/g, " "));
const name = `tp-${slug}`;
if (!tpTokens.has(name)) tpTokens.set(name, { value: unescapedValue, comment: sourceNote });
return name;
}
function registerZToken(rawNum, sourceNote) {
const name = `z-${rawNum}`;
if (!zTokens.has(name)) zTokens.set(name, { value: rawNum, comment: sourceNote });
return name;
}
function registerSToken(unescapedValue, sourceNote) {
const slug = slugify(unescapedValue);
const name = `s-${slug}`;
if (!sTokens.has(name)) sTokens.set(name, { value: unescapedValue, comment: sourceNote });
return name;
}
function registerEToken(unescapedValue, sourceNote) {
const slug = slugify(unescapedValue);
const name = `e-${slug}`;
if (!eTokens.has(name)) eTokens.set(name, { value: unescapedValue, comment: sourceNote });
return name;
}
function registerVaToken(unescapedValue, sourceNote) {
const slug = slugify(unescapedValue);
const name = `va-${slug}`;
if (!vaTokens.has(name)) vaTokens.set(name, { value: unescapedValue, comment: sourceNote });
return name;
}
function registerSwToken(unescapedValue, sourceNote) {
const slug = slugify(unescapedValue);
const name = `sw-${slug}`;
if (!swTokens.has(name)) swTokens.set(name, { value: unescapedValue, comment: sourceNote });
return name;
}
function registerBlurToken(unescapedValue, sourceNote) {
const slug = slugify(unescapedValue);
const name = `blur-${slug}`;
if (!blurTokens.has(name)) blurTokens.set(name, { value: unescapedValue, comment: sourceNote });
return name;
}
const gradientByValue = new Map();
function registerGradientToken(unescapedValue, sourceNote) {
if (gradientByValue.has(unescapedValue)) return gradientByValue.get(unescapedValue);
gradientCounter += 1;
const name = `gradient-extract-${gradientCounter}`;
gradientTokens.set(name, { value: unescapedValue, comment: sourceNote });
gradientByValue.set(unescapedValue, name);
return name;
}
const dropShadowByValue = new Map();
function registerDropShadowToken(unescapedValue, sourceNote) {
if (dropShadowByValue.has(unescapedValue)) return dropShadowByValue.get(unescapedValue);
dropShadowCounter += 1;
const name = `drop-shadow-extract-${dropShadowCounter}`;
dropShadowTokens.set(name, { value: unescapedValue, comment: sourceNote });
dropShadowByValue.set(unescapedValue, name);
return name;
}
// Wrapper tokens for the var(--x, fallback) forms that can't use the
// paren-with-fallback-comma shorthand (confirmed unsupported in the Step 0
// spike). theme(colors.a.b) fallbacks are resolved to the equivalent
// var(--token) form the Tailwind build already resolves them to today
// (inspected in ui/storybook-static/assets/*.css before writing this
// codemod - theme() is a Tailwind build-time function and cannot appear
// inside a runtime custom property).
const THEME_COLOR_MAP = {
"theme(colors.muted.DEFAULT)": "var(--muted)",
"theme(colors.muted.foreground)": "var(--muted-foreground)",
};
function resolveThemeColor(raw) {
let out = raw;
for (const [from, to] of Object.entries(THEME_COLOR_MAP)) {
out = out.split(from).join(to);
}
return out;
}
function registerResolvedVarToken(varName, fallbackRaw, sourceNote) {
// varName like "--paperclip-code-highlight-bg" -> token name
// "code-highlight-bg-resolved" (strip the leading "--paperclip-" prefix
// for readability, matching the mission's suggested name for the first
// site; other vars in the same family follow the same convention).
const bare = varName.replace(/^--paperclip-/, "").replace(/^--/, "");
const name = `${bare}-resolved`;
const fallback = resolveThemeColor(fallbackRaw);
const value = `var(${varName}, ${fallback})`;
if (!resolvedVarTokens.has(name)) resolvedVarTokens.set(name, { value, comment: sourceNote });
return name;
}
// ── Regexes ──────────────────────────────────────────────────────────────
// Every regex requires the utility to start at a genuine class-token
// boundary (preceded by whitespace, a quote/backtick, template-literal `${`,
// or start-of-string) - see Batch 3's BOUNDARY GOTCHA. `:` is included for
// variant prefixes (`data-[state=open]:`, `sm:`, etc.).
const BOUNDARY = String.raw`(?<=^|[\s"'\`{:])`;
const GRID_COLS_RE = new RegExp(`${BOUNDARY}(!?)grid-cols-\\[([^\\]]+)\\]`, "g");
const GRID_ROWS_RE = new RegExp(`${BOUNDARY}(!?)grid-rows-\\[([^\\]]+)\\]`, "g");
const TRANSITION_RE = new RegExp(`${BOUNDARY}(!?)transition-\\[([^\\]]+)\\]`, "g");
const Z_RE = new RegExp(`${BOUNDARY}(!?)z-\\[([0-9]+)\\]`, "g");
const SCALE_RE = new RegExp(`${BOUNDARY}(!?)scale-\\[([^\\]]+)\\]`, "g");
const EASE_RE = new RegExp(`${BOUNDARY}(!?)ease-\\[([^\\]]+)\\]`, "g");
const ALIGN_RE = new RegExp(`${BOUNDARY}(!?)align-\\[([^\\]]+)\\]`, "g");
const STROKE_RE = new RegExp(`${BOUNDARY}(!?)stroke-\\[([^\\]]+)\\]`, "g");
// NOTE: negative lookbehind isn't reliably portable across regex engines at
// the boundary position used elsewhere, so drop-shadow is matched with its
// own explicit prefix (drop-shadow-) which never collides with bare blur-.
// Matches both bare `blur-[...]` and `backdrop-blur-[...]` (the latter's
// `backdrop-` prefix means `blur` doesn't start at a class-token BOUNDARY,
// so it needs its own alternative rather than relying on the shared
// boundary-anchored pattern).
const BLUR_RE = new RegExp(`${BOUNDARY}(!?)(backdrop-blur|blur)-\\[([^\\]]+)\\]`, "g");
const DROP_SHADOW_RE = new RegExp(`${BOUNDARY}(!?)drop-shadow-\\[([^\\]]+)\\]`, "g");
// bg-[linear-gradient(...)] / bg-[radial-gradient(...)] / bg-[conic-gradient(...)]
// - continuing Batch 1's --gradient-extract-* family (see registerGradientToken).
// Matched separately from the generic VAR_* regexes below since these are not
// var() passthrough; they need the `image:` paren hint (Batch 1 convention).
const GRADIENT_RE = new RegExp(
`${BOUNDARY}(!?)bg-\\[((?:linear|radial|conic)-gradient\\([^\\]]*\\))\\]`,
"g",
);
// bg|text|border-[var(--x)] (no fallback) and bg|text|border-[var(--x,fallback)]
// (with fallback). The fallback variant's raw capture includes everything up
// to the matching `)]` - since none of these fallback values contain nested
// brackets, a simple `[^\]]+` capture is safe (verified by inspection of all
// matching sites in this batch).
const VAR_NOFALLBACK_RE = new RegExp(
`${BOUNDARY}(!?)(bg|text|border)-\\[var\\((--[a-zA-Z0-9-]+)\\)\\]`,
"g",
);
const VAR_FALLBACK_RE = new RegExp(
`${BOUNDARY}(!?)(bg|text|border)-\\[var\\((--[a-zA-Z0-9-]+),([^\\]]+)\\)\\]`,
"g",
);
function rewriteFile(filePath, relPath) {
const original = readFileSync(filePath, "utf8");
let content = original;
let siteCount = 0;
content = content.replace(GRID_COLS_RE, (match, bang, raw) => {
const unescaped = unescapeSpaces(raw);
const sourceNote = `Extracted from ${relPath} (grid-cols-[${raw}]).`;
const name = registerGtcToken(unescaped, sourceNote);
siteCount++;
return `${bang}grid-cols-(--${name})`;
});
content = content.replace(GRID_ROWS_RE, (match, bang, raw) => {
const unescaped = unescapeSpaces(raw);
const sourceNote = `Extracted from ${relPath} (grid-rows-[${raw}]).`;
const name = registerGtrToken(unescaped, sourceNote);
siteCount++;
return `${bang}grid-rows-(--${name})`;
});
content = content.replace(TRANSITION_RE, (match, bang, raw) => {
const unescaped = unescapeSpaces(raw);
const sourceNote = `Extracted from ${relPath} (transition-[${raw}]).`;
const name = registerTpToken(unescaped, sourceNote);
siteCount++;
return `${bang}transition-(--${name})`;
});
content = content.replace(Z_RE, (match, bang, raw) => {
const sourceNote = `Extracted from ${relPath} (z-[${raw}]).`;
const name = registerZToken(raw, sourceNote);
siteCount++;
return `${bang}z-(--${name})`;
});
content = content.replace(SCALE_RE, (match, bang, raw) => {
const unescaped = unescapeSpaces(raw);
const sourceNote = `Extracted from ${relPath} (scale-[${raw}]).`;
const name = registerSToken(unescaped, sourceNote);
siteCount++;
return `${bang}scale-(--${name})`;
});
content = content.replace(EASE_RE, (match, bang, raw) => {
const unescaped = unescapeSpaces(raw);
const sourceNote = `Extracted from ${relPath} (ease-[${raw}]).`;
const name = registerEToken(unescaped, sourceNote);
siteCount++;
return `${bang}ease-(--${name})`;
});
content = content.replace(ALIGN_RE, (match, bang, raw) => {
const unescaped = unescapeSpaces(raw);
const sourceNote = `Extracted from ${relPath} (align-[${raw}]).`;
const name = registerVaToken(unescaped, sourceNote);
siteCount++;
return `${bang}align-(--${name})`;
});
content = content.replace(STROKE_RE, (match, bang, raw) => {
const unescaped = unescapeSpaces(raw);
const sourceNote = `Extracted from ${relPath} (stroke-[${raw}]).`;
const name = registerSwToken(unescaped, sourceNote);
siteCount++;
// stroke-WIDTH requires the `length:` hint (bare stroke-(--x) is
// ambiguous with the stroke-COLOR utility) - confirmed in Step 0 spike.
return `${bang}stroke-(length:--${name})`;
});
content = content.replace(DROP_SHADOW_RE, (match, bang, raw) => {
const unescaped = unescapeSpaces(raw);
const sourceNote = `Extracted from ${relPath} (drop-shadow-[${raw}]).`;
const name = registerDropShadowToken(unescaped, sourceNote);
siteCount++;
return `${bang}drop-shadow-(--${name})`;
});
// Gradient bg-[...] brackets must be rewritten BEFORE the generic var()
// passthrough regexes below (a gradient value can itself contain
// `var(--x)` sub-expressions, e.g. hsl(var(--primary)), which the generic
// VAR_NOFALLBACK_RE must not also try to match against the outer bg-[...]).
content = content.replace(GRADIENT_RE, (match, bang, raw) => {
const unescaped = unescapeSpaces(raw);
const sourceNote = `Extracted from ${relPath} (bg-[${raw}]).`;
const name = registerGradientToken(unescaped, sourceNote);
siteCount++;
return `${bang}bg-(image:--${name})`;
});
content = content.replace(BLUR_RE, (match, bang, util, raw) => {
const unescaped = unescapeSpaces(raw);
const sourceNote = `Extracted from ${relPath} (${util}-[${raw}]).`;
const name = registerBlurToken(unescaped, sourceNote);
siteCount++;
return `${bang}${util}-(--${name})`;
});
// var(--x, fallback) forms MUST be rewritten before the no-fallback form
// (which would otherwise partially match the `var(--x` prefix of a
// fallback expression and corrupt it - the fallback regex requires a
// literal comma so there's no real ambiguity, but ordering fallback-first
// keeps the intent explicit and avoids relying on regex engine match order).
content = content.replace(VAR_FALLBACK_RE, (match, bang, util, varName, fallbackRaw) => {
const sourceNote = `Extracted from ${relPath} (${util}-[var(${varName},${fallbackRaw})]).`;
const name = registerResolvedVarToken(varName, fallbackRaw, sourceNote);
siteCount++;
return `${bang}${util}-(--${name})`;
});
content = content.replace(VAR_NOFALLBACK_RE, (match, bang, util, varName) => {
siteCount++;
// Bare var() passthrough - no new token minted, per DESIGN.md/Batch 3's
// special case for runtime library/component variables. Here the vars
// already exist as first-class design tokens in index.css (--chip-match-*),
// so this is a pure syntax modernization, not a token mint.
return `${bang}${util}-(${varName})`;
});
if (content !== original && !DRY_RUN) {
writeFileSync(filePath, content, "utf8");
}
return { changed: content !== original, siteCount };
}
function main() {
const files = [];
for (const dir of SCAN_DIRS) walk(resolve(UI_SRC, dir), files);
files.sort();
let totalSites = 0;
let filesChanged = 0;
const changedFiles = [];
for (const filePath of files) {
const relPath = "ui/src/" + relative(UI_SRC, filePath);
const { changed, siteCount } = rewriteFile(filePath, relPath);
if (changed) {
filesChanged++;
changedFiles.push(relPath);
}
totalSites += siteCount;
}
// ── index.css token block ──────────────────────────────────────────
const cssPath = resolve(UI_SRC, "index.css");
const cssOriginal = readFileSync(cssPath, "utf8");
const marker = "/* ── Extracted verbatim MISC tokens (Phase 2 Batch 4, design/token-extraction) ── */";
let cssNext = cssOriginal;
let cssChanged = false;
const anyTokens =
gtcTokens.size ||
gtrTokens.size ||
tpTokens.size ||
zTokens.size ||
sTokens.size ||
eTokens.size ||
vaTokens.size ||
swTokens.size ||
blurTokens.size ||
dropShadowTokens.size ||
resolvedVarTokens.size ||
gradientTokens.size;
if (!cssOriginal.includes(marker) && anyTokens) {
const lines = [];
lines.push(marker);
lines.push("/* Batch 4/4 (final sweep): grid track lists, transition-property");
lines.push(" lists, z-index, scale, easing, vertical-align, stroke-width, blur,");
lines.push(" drop-shadow, and half-migrated var(x, fallback) color forms,");
lines.push(" verbatim (no normalizing - the human scale-collapse decision comes");
lines.push(" later per DESIGN.md/TOKEN-AUDIT.md). --gtc-* and --gtr-* and --tp-* are");
lines.push(" sequentially numbered / slugged since their content (track lists,");
lines.push(" property lists) is not safely nameable by value alone.");
lines.push("");
lines.push(" drop-shadow (a CSS filter function) gets its own");
lines.push(" --drop-shadow-extract-* family, kept separate from Batch 3's");
lines.push(" --shadow-extract-* box-shadow family per that batch's logged");
lines.push(" human-decision note.");
lines.push("");
lines.push(" *-resolved wrapper tokens hold a verbatim var(--x, fallback)");
lines.push(" expression for sites where Tailwind v4's paren-with-fallback-comma");
lines.push(" shorthand does not parse (confirmed unsupported in this batch's");
lines.push(" Step 0 syntax spike). theme(colors.a.b) fallbacks were resolved to");
lines.push(" the equivalent var(--token) form the Tailwind build already");
lines.push(" compiles them to today (inspected byte-for-byte from the built");
lines.push(" storybook-static CSS before this codemod ran: theme(colors.muted.DEFAULT)");
lines.push(" -> var(--muted), theme(colors.muted.foreground) -> var(--muted-foreground)).");
lines.push("");
lines.push(" --gradient-extract-* here CONTINUES Batch 1's counter (Batch 1 minted");
lines.push(" 1 through 24) rather than restarting it - these 2 sites use");
lines.push(" hsl(var(...))/color-mix(...) CSS-native color functions rather than raw");
lines.push(" hex/rgb literals, which is why Batch 1's hand-audited color-literal");
lines.push(" table did not catch them, but they are still value-bearing gradient");
lines.push(" brackets in scope for this batch's final sweep.");
lines.push("");
lines.push(" Allowlist (sites intentionally left as-is - see ALLOWLIST doc-comment");
lines.push(" at the end of this file for the machine-readable list consumed by");
lines.push(" scripts/check-token-gates.mjs):");
lines.push(" allow ui/src/components/ui/dialog.tsx — tw-animate-css plugin utilities (zoom-in-[0.97] etc.) are dead/no-op classes today (plugin not installed, verified via built-CSS grep); nothing to tokenize without visually changing a currently-inert class");
lines.push(" allow ui/src/components/ui/alert-dialog.tsx — same tw-animate-css dead-class situation as dialog.tsx");
lines.push("*/");
lines.push(":root {");
for (const [name, { value, comment }] of gtcTokens) {
lines.push(` --${name}: ${value}; /* ${comment} */`);
}
for (const [name, { value, comment }] of gtrTokens) {
lines.push(` --${name}: ${value}; /* ${comment} */`);
}
for (const [name, { value, comment }] of tpTokens) {
lines.push(` --${name}: ${value}; /* ${comment} */`);
}
for (const [name, { value, comment }] of zTokens) {
lines.push(` --${name}: ${value}; /* ${comment} */`);
}
for (const [name, { value, comment }] of sTokens) {
lines.push(` --${name}: ${value}; /* ${comment} */`);
}
for (const [name, { value, comment }] of eTokens) {
lines.push(` --${name}: ${value}; /* ${comment} */`);
}
for (const [name, { value, comment }] of vaTokens) {
lines.push(` --${name}: ${value}; /* ${comment} */`);
}
for (const [name, { value, comment }] of swTokens) {
lines.push(` --${name}: ${value}; /* ${comment} */`);
}
for (const [name, { value, comment }] of blurTokens) {
lines.push(` --${name}: ${value}; /* ${comment} */`);
}
for (const [name, { value, comment }] of dropShadowTokens) {
lines.push(` --${name}: ${value}; /* ${comment} */`);
}
for (const [name, { value, comment }] of resolvedVarTokens) {
lines.push(` --${name}: ${value}; /* ${comment} */`);
}
for (const [name, { value, comment }] of gradientTokens) {
lines.push(` --${name}: ${value}; /* ${comment} */`);
}
lines.push("}");
const block = "\n" + lines.join("\n") + "\n";
cssNext = cssOriginal + block;
cssChanged = true;
}
if (cssChanged && !DRY_RUN) writeFileSync(cssPath, cssNext, "utf8");
// ── Summary ─────────────────────────────────────────────────────────
console.log(`\n${DRY_RUN ? "[DRY RUN] " : ""}codemod-extract-misc summary`);
console.log(` Sites rewritten: ${totalSites}`);
console.log(` Files changed: ${filesChanged}`);
console.log(` New --gtc-*: ${gtcTokens.size}`);
console.log(` New --gtr-*: ${gtrTokens.size}`);
console.log(` New --tp-*: ${tpTokens.size}`);
console.log(` New --z-*: ${zTokens.size}`);
console.log(` New --s-*: ${sTokens.size}`);
console.log(` New --e-*: ${eTokens.size}`);
console.log(` New --va-*: ${vaTokens.size}`);
console.log(` New --sw-*: ${swTokens.size}`);
console.log(` New --blur-*: ${blurTokens.size}`);
console.log(` New --drop-shadow-extract-*: ${dropShadowTokens.size}`);
console.log(` New *-resolved wrapper: ${resolvedVarTokens.size}`);
console.log(` New --gradient-extract-* (continuing Batch 1): ${gradientTokens.size}`);
console.log(` index.css token block: ${cssChanged ? "added" : "already present or nothing to add (idempotent no-op)"}`);
if (changedFiles.length) {
console.log(`\n Changed files:`);
for (const f of changedFiles) console.log(` - ${f}`);
}
}
main();