#!/usr/bin/env node // ============================================================= // adapters/plain-css/adapt.mjs — second-adapter SMOKE (WHYNOT-WP-0002 · T10) // // NOT a finished stack. This exists only to prove the IR/adapter *boundary*: // a non-Lit adapter consuming the very same ir/ and honouring the same // adapters/ADAPTER_CONTRACT.md (token full-gen + stub/drift + exit codes). // The deliverable is confidence in the seam, not a usable plain-CSS kit. // // It does two contract things from ir/ and nothing Lit-specific: // • tokens → fully generated CSS custom properties (deterministic no-op re-run) // • components → write-once class stubs + a drift roll-up in the contract shape // (every component is "new" here — plain-CSS has no existing source to drift). // // Exit codes (shared contract): 0 ok · 2 usage · 3 drift/new · 5 internal. // Run: `make adapt-plain-css`. // ============================================================= import { readFileSync, readdirSync, writeFileSync, existsSync, mkdirSync, rmSync } from "node:fs"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; const REPO = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); const IR = join(REPO, "ir"); const HERE = join(REPO, "adapters", "plain-css"); const STUBS = join(HERE, "stubs"); const refToVar = (v) => { const m = /^\{[A-Za-z0-9]+\.([A-Za-z0-9-]+)\}$/.exec(String(v).trim()); return m ? `var(--${m[1]})` : v; }; function generateTokens() { const tokens = JSON.parse(readFileSync(join(IR, "tokens.json"), "utf8")); const lines = ["/* @generated by adapters/plain-css (WHYNOT-WP-0002 T10) from ir/tokens.json — DO NOT EDIT. */", ":root {"]; let n = 0; 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)};`); n++; } } lines.push("}", ""); const out = join(HERE, "tokens.css"); const body = lines.join("\n"); const before = existsSync(out) ? readFileSync(out, "utf8") : null; if (before === body) { console.log(`tokens: up to date (${n} custom properties, no change).`); return; } writeFileSync(out, body); console.log(`tokens: regenerated ${n} custom properties → adapters/plain-css/tokens.css`); } function renderClassStub(c) { const base = c.tag; // reuse the contract tag as the base class name const lines = [ `/* @generated STUB by adapters/plain-css (T10) from ir/components/${c.name}.json — write-once. */`, `/* ${c.name}: ${c.description} */`, `.${base} { /* TODO: base appearance per ir/exemplars/${c.name}.html */ }`, ]; for (const v of c.variants || []) { for (const val of v.values) lines.push(`.${base}--${val} { /* ${v.axis}=${val} */ }`); } return lines.join("\n") + "\n"; } function scaffold() { const contracts = readdirSync(join(IR, "components")).filter((f) => f.endsWith(".json")) .map((f) => JSON.parse(readFileSync(join(IR, "components", f), "utf8"))) .sort((a, b) => a.name.localeCompare(b.name)); // Plain-CSS has no hand-authored source, so every IR component is "new". const components = contracts.map((c) => ({ name: c.name, status: "new", issues: [{ kind: "no-counterpart", detail: `no .${c.tag} class — stub generated.` }] })); mkdirSync(STUBS, { recursive: true }); let stubbed = 0; for (const c of contracts) { const out = join(STUBS, `${c.tag}.css`); if (!existsSync(out)) { writeFileSync(out, renderClassStub(c)); stubbed++; } } // Drift roll-up — same shape as the Lit adapter's, proving the contract is portable. const reportPath = join(HERE, "_report.json"); let generatedAt = new Date().toISOString(); if (existsSync(reportPath)) { try { const prev = JSON.parse(readFileSync(reportPath, "utf8")); if (JSON.stringify(prev.components) === JSON.stringify(components) && prev.generatedAt) generatedAt = prev.generatedAt; } catch { /* fresh */ } } writeFileSync(reportPath, JSON.stringify({ stack: "plain-css", generatedAt, components }, null, 2) + "\n"); console.log(`scaffold: ${components.length} components, all new (${stubbed} stub${stubbed === 1 ? "" : "s"} written) → adapters/plain-css/stubs/`); return components.length > 0; } function main() { if (!existsSync(join(IR, "tokens.json"))) { console.error("No ir/ — run `make ir` first."); process.exit(2); } generateTokens(); const isNew = scaffold(); console.log("\nadapt-plain-css: SMOKE only — proves a non-Lit adapter consumes the same ir/ and"); console.log("emits the same contract shapes. Not a finished stack (see adapters/plain-css/README.md)."); if (isNew) process.exit(3); // new components present (contract: 3) — expected for a fresh stack } main();