import { writeFileSync } from "node:fs" import { definitions, read, repositoryRoot, validateDocument } from "./theme-contract.mjs" // themes/default.json is the authored COLOR source. Templates own aliases/geometry. const theme = validateDocument(JSON.parse(read("themes/default.json")), true) const templates = ["primitives", "default"].map(name => ({ name, source: read(`scripts/theme-templates/${name}.css`) })) const graph = new Map(templates.flatMap(({ source }) => [...source.replace(/\/\*[\s\S]*?\*\//g, "").matchAll(/(--[\w-]+)\s*:\s*([^;]+);/g)].map(([, key, value]) => [key, value.trim()]))) const publicProperties = new Map(definitions.filter(def => def.type === "color").map(def => [def.property, theme.tokens[def.key].toLowerCase()])) function resolve(value, stack = []) { const placeholder = /^\{\{([^|}]+)(?:\|([^}]+))?\}\}$/.exec(value) if (placeholder) return theme.tokens[placeholder[1]].toLowerCase() const alias = /^var\((--[\w-]+)\)$/.exec(value) if (!alias) return value const property = alias[1] if (stack.includes(property)) throw Error(`Circular fallback alias: ${property}`) if (publicProperties.has(property)) return publicProperties.get(property) if (!graph.has(property)) throw Error(`Missing fallback alias: ${property}`) return resolve(graph.get(property), [...stack, property]) } function render(source) { return source.replace(/\{\{([^|}]+)(?:\|([^}]+))?\}\}/g, (_, key, alias) => { if (!Object.hasOwn(theme.tokens, key)) throw Error(`Missing template color: ${key}`) const value = theme.tokens[key].toLowerCase() // Preserve existing palette aliases when they match the canonical Default. // A separately configured role always wins if its JSON value differs. return alias && resolve(alias) === value ? alias : value }) } const defaults = Object.fromEntries(definitions.map(def => [def.property, def.type === "color" ? theme.tokens[def.key].toLowerCase() : resolve(graph.get(def.property))])) for (const def of definitions) if (!(def.type === "color" ? /^#[\da-f]{6}$/i : /^(?:[2-9]|1[0-2])px$/).test(defaults[def.property])) throw Error(`Unresolved fallback: ${def.property}`) const outputs = templates.map(({name,source}) => [`packages/ui/src/theme/${name}.css`, `/* Generated from themes/default.json + scripts/theme-templates/${name}.css. Do not edit. */\n${render(source)}`]) outputs.push(["packages/ui/src/theme/default-values.generated.ts", `// Generated by npm run generate:theme-defaults. Do not edit.\n// Colors: themes/default.json; geometry/aliases: scripts/theme-templates.\nexport const runtimeThemeDefaults = ${JSON.stringify(defaults, null, 2)} as const\n`]) for (const [path, content] of outputs) { if (process.argv.includes("--check")) { if (read(path) !== content) throw Error(`Stale ${path}. Run npm run generate:theme-defaults.`) } else writeFileSync(new URL(path, repositoryRoot), content) } console.log(`Theme defaults: ${Object.keys(theme.tokens).length} JSON colors / ${definitions.length} contract defaults; ${process.argv.includes("--check") ? "generated outputs match" : "generated CSS + data"}`)