#!/usr/bin/env bun import { Schema, SchemaAST } from "effect" import { format } from "prettier" import { ThemeDefinition, ThemeDocument } from "../../tui/src/theme/v2/schema" const target = import.meta.dir + "/../snippets/generated/theme-tokens.mdx" const root = requireObject(ThemeDefinition.ast) const hue = requireObject(requireField(root, "hue").type) const hueNames = hue.propertySignatures.map((field) => String(field.name)) const hueSteps = requireObject(requireField(hue, hueNames[0]).type).propertySignatures.map((field) => String(field.name), ) const contexts = root.propertySignatures .map((field) => String(field.name)) .filter((name) => name.startsWith("@context:")) const tokens = root.propertySignatures .filter((field) => { const name = String(field.name) return name !== "hue" && name !== "categorical" && !name.startsWith("@context:") }) .flatMap((field) => tokenPaths(field.type, String(field.name))) const groups = Map.groupBy(tokens, (token) => token .split(".") .slice(0, token.split(".").length > 2 ? 2 : 1) .join("."), ) const table = [...groups] .map(([group, values]) => `| \`${group}\` | ${values.map((value) => `\`${value}\``).join("
")} |`) .join("\n") const example = { version: 2, light: { hue: { accent: "$hue.purple", interactive: "$hue.purple", }, text: { default: "$hue.neutral.900", }, background: { default: "#fafafa", }, }, dark: { mergeMode: true, text: { default: "$hue.neutral.100", }, background: { default: "#101014", }, }, } satisfies ThemeDocument Schema.decodeUnknownSync(ThemeDocument)(example) const output = await format( `{/* Generated by packages/docs/script/generate-theme-tokens.ts. Do not edit. */} \`\`\`json title="my-theme.json" ${JSON.stringify(example, null, 2)} \`\`\` ## Token reference This reference is generated from the Effect schema in \`packages/tui/src/theme/v2/schema.ts\`. Changes to the runtime schema update this section through \`bun run generate\`. ### Hue tokens Every hue is a ${hueSteps.length}-step scale. Define a scale with all of these steps, or alias it to another hue with a value such as \`$hue.blue\`. | | Values | | --- | --- | | Hues | ${hueNames.map((name) => `\`${name}\``).join(", ")} | | Steps | ${hueSteps.map((step) => `\`${step}\``).join(", ")} | Reference a hue color as \`$hue..\`, for example \`$hue.interactive.500\`. ### Semantic tokens Semantic values can reference another token by prefixing its path with \`$\`, for example \`$text.default\`. Stateful tokens inherit their \`default\` value when a state is omitted. | Group | Tokens | | --- | --- | ${table} ### Contexts ${contexts.map((context) => `\`${context}\``).join(" and ")} accept partial overrides of the semantic tokens above. Components apply these contexts to surfaces that need different contrast without changing the base theme. `, { parser: "mdx", printWidth: 120, semi: false }, ) if (process.argv.includes("--check")) { const current = await Bun.file(target).text() if (current === output) process.exit(0) console.error("Generated theme token documentation is stale. Run `bun run generate` from packages/docs.") process.exit(1) } await Bun.write(target, output) function requireObject(ast: SchemaAST.AST): SchemaAST.Objects { if (SchemaAST.isObjects(ast)) return ast if (SchemaAST.isUnion(ast)) { const object = ast.types.map(findObject).find((value) => value !== undefined) if (object) return object } throw new Error(`Expected an object schema, received ${ast._tag}`) } function findObject(ast: SchemaAST.AST): SchemaAST.Objects | undefined { if (SchemaAST.isObjects(ast)) return ast if (SchemaAST.isUnion(ast)) return ast.types.map(findObject).find((value) => value !== undefined) if (SchemaAST.isSuspend(ast)) return findObject(ast.thunk()) } function requireField(ast: SchemaAST.Objects, name: string) { const field = ast.propertySignatures.find((field) => String(field.name) === name) if (field) return field throw new Error(`Theme schema field not found: ${name}`) } function tokenPaths(ast: SchemaAST.AST, prefix: string): string[] { const object = findObject(ast) if (!object || object.propertySignatures.length === 0) return [prefix] return object.propertySignatures.flatMap((field) => tokenPaths(field.type, `${prefix}.${String(field.name)}`)) }