import { cp, readdir, mkdir, rm, unlink, writeFile } from "node:fs/promises"; import { watch } from "node:fs"; import { join, resolve } from "node:path"; import { pathToFileURL } from "node:url"; import { renderTheme, type Theme } from "src/core"; import { getCollectedCss } from "src/types/vars"; import "styles"; const root = resolve(import.meta.dir, ".."); const outputDir = join(root, ".gitea-data", "gitea", "public", "assets", "css"); const templatesDir = join(root, "templates"); const templatesOutputDir = join(root, ".gitea-data", "gitea", "templates"); const watchDirs = [join(root, "src"), join(root, "styles"), join(root, "themes"), templatesDir]; const themeEntries = [ { name: "modern-light", source: join(root, "themes", "modern-light.css.ts"), output: "theme-banana-modern-light.css", displayName: "banana Modern Light", }, { name: "modern-dark", source: join(root, "themes", "modern-dark.css.ts"), output: "theme-banana-modern-dark.css", displayName: "banana Modern Dark", }, ] as const; const autoTheme = { output: "theme-banana-modern-auto.css", displayName: "banana Modern Auto", lightOutput: "theme-banana-modern-light.css", darkOutput: "theme-banana-modern-dark.css", } as const; const expectedOutputs = new Set([...themeEntries.map(theme => theme.output), autoTheme.output]); const args = new Set(Bun.argv.slice(2)); const isWatch = args.has("--watch"); const isMinify = args.has("--minify"); function compileStyles(): string { const css = getCollectedCss().replace(/(^|[^:])\/\/.*$/gm, "$1"); if (!isMinify) return css; return css.replace(/\s+/g, " ").replace(/\s*([{}:;,>+~])\s*/g, "$1").trim(); } async function cleanOldThemes(): Promise { await mkdir(outputDir, { recursive: true }); const existingFiles = await readdir(outputDir, { withFileTypes: true }); for (const file of existingFiles) { if (file.isFile() && file.name.startsWith("theme-") && file.name.endsWith(".css") && !expectedOutputs.has(file.name)) { await unlink(join(outputDir, file.name)); } } } async function copyTemplates(): Promise { await rm(templatesOutputDir, { recursive: true, force: true }); await cp(templatesDir, templatesOutputDir, { recursive: true }); console.log(`copied ${templatesOutputDir}`); } function meta(displayName: string): string { return `gitea-theme-meta-info{--theme-display-name:"${displayName}";}`; } async function loadTheme(source: string): Promise { const module = await import(`${pathToFileURL(source).href}?t=${Date.now()}`); return module.default as Theme; } async function buildTheme() { await cleanOldThemes(); await copyTemplates(); const styles = compileStyles(); const buildVersion = Date.now(); for (const theme of themeEntries) { const themeCss = renderTheme(await loadTheme(theme.source)); const outputFile = join(outputDir, theme.output); await writeFile(outputFile, `${meta(theme.displayName)}${themeCss}\n${styles}`, "utf8"); console.log(`built ${outputFile}`); } const autoCss = [ `@import "./${autoTheme.lightOutput}?v=${buildVersion}" (prefers-color-scheme: light);`, `@import "./${autoTheme.darkOutput}?v=${buildVersion}" (prefers-color-scheme: dark);`, meta(autoTheme.displayName), ].join("\n"); const autoOutputFile = join(outputDir, autoTheme.output); await writeFile(autoOutputFile, autoCss, "utf8"); console.log(`built ${autoOutputFile}`); } let queuedBuild: Timer | undefined; function queueBuild() { if (queuedBuild) { clearTimeout(queuedBuild); } queuedBuild = setTimeout(() => { buildTheme().catch(error => { console.error(error); }); }, 80); } await buildTheme(); if (isWatch) { console.log(`watching ${watchDirs.join(", ")}`); for (const watchDir of watchDirs) { watch(watchDir, { recursive: true }, queueBuild); } await new Promise(() => {}); }