Files
HMDesign/scripts/theme-defaults.mjs

39 lines
3.0 KiB
JavaScript

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"}`)