Files

119 lines
3.8 KiB
TypeScript

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<string>([...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<void> {
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<void> {
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<Theme> {
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(() => {});
}