export interface Theme { bg: string; fg: string; primary?: string; h1?: string; } 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]) + ";" + line.slice(1), ); } export async function bundleCssFiles( cssImports: string[], theme: Theme, dev: boolean = false, ): Promise { const plugin = { name: "clover", setup(b) { b.onResolve( { filter: /^\$input\$$/ }, () => ({ path: ".", namespace: "input" }), ); b.onLoad( { filter: /./, namespace: "input" }, () => ({ loader: "css", contents: cssImports.map((path) => `@import url(${JSON.stringify(path)});`) .join("\n") + stringifyTheme(theme), resolveDir: ".", }), ); b.onLoad( { filter: /\.css$/ }, async ({ path: file }) => ({ loader: "css", contents: preprocess(await fs.readFile(file, "utf-8"), theme), }), ); }, } satisfies Plugin; const build = await esbuild.build({ bundle: true, entryPoints: ["$input$"], write: false, external: ["*.woff2"], target: ["ie11"], plugins: [plugin], minify: !dev, }); const { errors, warnings, outputFiles } = 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 outputFiles[0].text; } import type { Plugin } from "esbuild"; import * as esbuild from "esbuild"; import * as fs from "./fs.ts";