39 lines
3.0 KiB
JavaScript
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"}`)
|