export interface Theme { bg: string; fg: string; primary?: string; h1?: string; } export const defaultTheme: Theme = { bg: "#ffffff", fg: "#050505", primary: "#2e7dab", }; export function stringifyTheme(theme: Theme) { return [ ":root {", "--bg: " + theme.bg + ";", "--fg: " + theme.fg + ";", theme.primary ? "--primary: " + theme.primary + ";" : null, "}", theme.h1 ? "h1 { color: " + theme.h1 + "}" : null, ].filter(Boolean).join("\n"); } export function preprocess(css: string, theme: Theme): string { const keys = Object.keys(theme); const regex = new RegExp( `([{};\\n][^{};\\n]*var\\(--(${keys.join("|")})\\).*?(?=[;{}\\n]))`, "gs", ); const regex2 = new RegExp(`var\\(--(${keys.join("|")})\\)`); return css.replace( regex, (_, line) => line.replace( regex2, (_: string, varName: string) => theme[varName as keyof Theme], ) + ";" + line.slice(1), ); } export interface Output { text: string; sources: string[]; } export function styleKey( cssImports: string[], theme: Theme, ) { cssImports = cssImports .map((file) => (path.isAbsolute(file) ? path.relative(hot.projectSrc, file) : file) .replaceAll("\\", "/") ) .sort(); return cssImports.join(":") + ":" + Object.entries(theme).map(([k, v]) => `${k}=${v}`); } export async function bundleCssFiles( cssImports: string[], theme: Theme, dev: boolean = false, ): Promise { cssImports = cssImports.map((file) => path.resolve(hot.projectSrc, file)); const plugin = { name: "clover css", setup(b) { b.onLoad( { filter: /\.css$/ }, async ({ path: file }) => ({ loader: "css", contents: preprocess(await fs.readFile(file, "utf-8"), theme), }), ); }, } satisfies esbuild.Plugin; const build = await esbuild.build({ bundle: true, entryPoints: ["$input$"], external: ["*.woff2", "*.ttf", "*.png", "*.jpeg"], metafile: true, minify: !dev, plugins: [ virtualFiles({ "$input$": { contents: cssImports.map((path) => `@import url(${JSON.stringify(path)});` ) .join("\n") + stringifyTheme(theme), loader: "css", }, }), plugin, ], target: ["ie11"], write: false, }); const { errors, warnings, outputFiles, metafile } = build; if (errors.length > 0) { throw new AggregateError(errors, "CSS Build Failed"); } if (warnings.length > 0) { throw new AggregateError(warnings, "CSS Build Failed"); } if (outputFiles.length > 1) throw new Error("Too many output files"); return { text: outputFiles[0].text, sources: Object.keys(metafile.outputs["$input$.css"].inputs) .filter((x) => !x.startsWith("vfs:")), }; } import * as esbuild from "esbuild"; import * as fs from "#sitegen/fs"; import * as hot from "./hot.ts"; import * as path from "node:path"; import { virtualFiles } from "./esbuild-support.ts";