#!/usr/bin/env node // ============================================================= // adapters/lit/adapt.mjs — the Lit reference adapter (WHYNOT-WP-0002) // // Projects the technology-neutral IR (ir/) onto the Lit stack. Per // adapters/ADAPTER_CONTRACT.md this is scaffold + drift-detect, never a rewrite: // // • tokens (T06) → FULLY generated, deterministic (this file, today) // • new component → stub (T07) // • changed component → drift report (T07), never an overwrite // // Exit codes: 0 ok · 2 usage/config · 3 drift · 4 parity · 5 internal. // // Run via `make adapt-lit`. Tokens regenerate the :root block of // src/styles/colors_and_type.css between generated markers; the hand-authored // type/utility CSS after it is preserved untouched. Re-running with an unchanged // ir/tokens.json is a no-op (byte-identical output). // ============================================================= import { readFileSync, writeFileSync, existsSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; const REPO = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); const TOKENS_JSON = join(REPO, "ir", "tokens.json"); const TOKEN_CSS = join(REPO, "src", "styles", "colors_and_type.css"); const BEGIN = "/* @generated tokens — regenerated by `make adapt-lit` from ir/tokens.json. DO NOT EDIT. */"; const END = "/* @end generated tokens */"; // ---------- tokens: ir/tokens.json (DTCG) → CSS custom properties ---------- function refToVar(value) { // {group.key} alias → var(--key). Literals pass through unchanged. const m = /^\{[A-Za-z0-9]+\.([A-Za-z0-9-]+)\}$/.exec(String(value).trim()); return m ? `var(--${m[1]})` : value; } function renderRootBlock(tokens) { const lines = [BEGIN, ":root {"]; for (const [group, entries] of Object.entries(tokens)) { lines.push(` /* ${group} */`); for (const [key, tok] of Object.entries(entries)) { if (key === "$type") continue; lines.push(` --${key}: ${refToVar(tok.$value)};`); } } lines.push("}", END); return lines.join("\n"); } // Replace the current :root token region with the freshly generated block. // First run (no markers): replace the first `:root { … }` via brace matching. // Later runs: replace strictly between the @generated markers. function spliceTokenBlock(css, block) { const b = css.indexOf(BEGIN); if (b !== -1) { const e = css.indexOf(END, b); if (e === -1) throw new Error("Found @generated marker without its @end — refusing to guess."); return css.slice(0, b) + block + css.slice(e + END.length); } const rootAt = css.indexOf(":root"); if (rootAt === -1) throw new Error("No :root block in colors_and_type.css to replace."); let i = css.indexOf("{", rootAt), depth = 0; for (; i < css.length; i++) { if (css[i] === "{") depth++; else if (css[i] === "}" && --depth === 0) break; } return css.slice(0, rootAt) + block + css.slice(i + 1); } function generateTokens() { if (!existsSync(TOKENS_JSON)) { console.error("No ir/tokens.json — run `make ir` first."); process.exit(2); } const tokens = JSON.parse(readFileSync(TOKENS_JSON, "utf8")); const block = renderRootBlock(tokens); const before = existsSync(TOKEN_CSS) ? readFileSync(TOKEN_CSS, "utf8") : `${block}\n`; const after = existsSync(TOKEN_CSS) ? spliceTokenBlock(before, block) : `${block}\n`; const count = block.split("\n").filter((l) => l.trim().startsWith("--")).length; if (after === before) { console.log(`tokens: up to date (${count} custom properties, no change).`); return; } writeFileSync(TOKEN_CSS, after); console.log(`tokens: regenerated ${count} custom properties → src/styles/colors_and_type.css`); } function main() { generateTokens(); // T07 will add: component stub scaffolding + drift reports here. console.log("adapt-lit: tokens done. (component scaffold + drift: T07)"); } main();