| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160 |
- import type { Plugin } from "vite"
- import { readdir, readFile, writeFile } from "fs/promises"
- import { join, resolve } from "path"
- interface ThemeDefinition {
- $schema?: string
- defs?: Record<string, string>
- theme: Record<string, any>
- }
- interface ResolvedThemeColor {
- dark: string
- light: string
- }
- class ColorResolver {
- private colors: Map<string, any> = new Map()
- private visited: Set<string> = new Set()
- constructor(defs: Record<string, string> = {}, theme: Record<string, any> = {}) {
- Object.entries(defs).forEach(([key, value]) => {
- this.colors.set(key, value)
- })
- Object.entries(theme).forEach(([key, value]) => {
- this.colors.set(key, value)
- })
- }
- resolveColor(key: string, value: any): ResolvedThemeColor {
- if (this.visited.has(key)) {
- throw new Error(`Circular reference detected for color ${key}`)
- }
- this.visited.add(key)
- try {
- if (typeof value === "string") {
- if (value.startsWith("#") || value === "none") {
- return { dark: value, light: value }
- }
- const resolved = this.resolveReference(value)
- return { dark: resolved, light: resolved }
- }
- if (typeof value === "object" && value !== null) {
- const dark = this.resolveColorValue(value.dark || value.light || "#000000")
- const light = this.resolveColorValue(value.light || value.dark || "#ffffff")
- return { dark, light }
- }
- return { dark: "#000000", light: "#ffffff" }
- } finally {
- this.visited.delete(key)
- }
- }
- private resolveColorValue(value: any): string {
- if (typeof value === "string") {
- if (value.startsWith("#") || value === "none") {
- return value
- }
- return this.resolveReference(value)
- }
- return value
- }
- private resolveReference(ref: string): string {
- const colorValue = this.colors.get(ref)
- if (colorValue === undefined) {
- throw new Error(`Color reference '${ref}' not found`)
- }
- if (typeof colorValue === "string") {
- if (colorValue.startsWith("#") || colorValue === "none") {
- return colorValue
- }
- return this.resolveReference(colorValue)
- }
- return colorValue
- }
- }
- function kebabCase(str: string): string {
- return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase()
- }
- function parseTheme(themeData: ThemeDefinition): Record<string, ResolvedThemeColor> {
- const resolver = new ColorResolver(themeData.defs, themeData.theme)
- const colors: Record<string, ResolvedThemeColor> = {}
- Object.entries(themeData.theme).forEach(([key, value]) => {
- colors[key] = resolver.resolveColor(key, value)
- })
- return colors
- }
- async function loadThemes(): Promise<Record<string, Record<string, ResolvedThemeColor>>> {
- const themesDir = resolve(__dirname, "../../tui/internal/theme/themes")
- const files = await readdir(themesDir)
- const themes: Record<string, Record<string, ResolvedThemeColor>> = {}
- for (const file of files) {
- if (!file.endsWith(".json")) continue
- const themeName = file.replace(".json", "")
- const themeData: ThemeDefinition = JSON.parse(await readFile(join(themesDir, file), "utf-8"))
- themes[themeName] = parseTheme(themeData)
- }
- return themes
- }
- function generateCSS(themes: Record<string, Record<string, ResolvedThemeColor>>): string {
- let css = `/* Auto-generated theme CSS - Do not edit manually */\n:root {\n`
- const defaultTheme = themes["opencode"] || Object.values(themes)[0]
- if (defaultTheme) {
- Object.entries(defaultTheme).forEach(([key, color]) => {
- const cssVar = `--theme-${kebabCase(key)}`
- css += ` ${cssVar}: ${color.light};\n`
- })
- }
- css += `}\n\n`
- Object.entries(themes).forEach(([themeName, colors]) => {
- css += `[data-theme="${themeName}"][data-dark="false"] {\n`
- Object.entries(colors).forEach(([key, color]) => {
- const cssVar = `--theme-${kebabCase(key)}`
- css += ` ${cssVar}: ${color.light};\n`
- })
- css += `}\n\n`
- css += `[data-theme="${themeName}"][data-dark="true"] {\n`
- Object.entries(colors).forEach(([key, color]) => {
- const cssVar = `--theme-${kebabCase(key)}`
- css += ` ${cssVar}: ${color.dark};\n`
- })
- css += `}\n\n`
- })
- return css
- }
- export function generateThemeCSS(): Plugin {
- return {
- name: "generate-theme-css",
- async buildStart() {
- try {
- console.log("Generating theme CSS...")
- const themes = await loadThemes()
- const css = generateCSS(themes)
- const outputPath = resolve(__dirname, "../src/assets/theme.css")
- await writeFile(outputPath, css)
- console.log(`✅ Generated theme CSS with ${Object.keys(themes).length} themes`)
- console.log(` Output: ${outputPath}`)
- } catch (error) {
- throw new Error(`Theme CSS generation failed: ${error}`)
- }
- },
- }
- }
|