1export interface Theme {
2 bg: string;
3 fg: string;
4 primary?: string;
5 h1?: string;
6}
7
8export const defaultTheme: Theme = {
9 bg: "#ffffff",
10 fg: "#050505",
11 primary: "#2e7dab",
12};
13
14export function stringifyTheme(theme: Theme) {
15 return [
16 ":root {",
17 "--bg: " + theme.bg + ";",
18 "--fg: " + theme.fg + ";",
19 theme.primary ? "--primary: " + theme.primary + ";" : null,
20 "}",
21 theme.h1 ? "h1 { color: " + theme.h1 + "}" : null,
22 ].filter(Boolean).join("\n");
23}
24
25export function preprocess(css: string, theme: Theme): string {
26 const keys = Object.keys(theme);
27 const regex = new RegExp(
28 `([{};\\n][^{};\\n]*var\\(--(${keys.join("|")})\\).*?(?=[;{}\\n]))`,
29 "gs",
30 );
31 const regex2 = new RegExp(`var\\(--(${keys.join("|")})\\)`);
32 return css.replace(
33 regex,
34 (_, line) =>
35 line.replace(
36 regex2,
37 (_: string, varName: string) => theme[varName as keyof Theme],
38 )
39 + ";" + line.slice(1),
40 );
41}
42
43/** names the stylesheet of every page with the same files and theme */
44export function styleKey(cssImports: string[], theme: Theme) {
45 return cssImports.toSorted().join(":") + ":" + Object.entries(theme).map(([k, v]) => `${k}=${v}`);
46}
47
48/** one minified stylesheet from files given relative to the graph's root, in order */
49export async function bundleCssFiles(
50 b: incremental.Build,
51 { cssImports, theme }: { cssImports: string[]; theme: Theme },
52) {
53 using _ = b.start(styleKey(cssImports, theme));
54 const plugin = {
55 name: "clover css",
56 setup(build) {
57 // reading through the step tracks what the entry files import too
58 build.onLoad(
59 { filter: /\.css$/ },
60 async ({ path: file }) => ({
61 loader: "css",
62 contents: preprocess(await b.readFile(file).text(), theme),
63 }),
64 );
65 },
66 } satisfies esbuild.Plugin;
67 const { errors, warnings, outputFiles } = await esbuild.build({
68 bundle: true,
69 entryPoints: ["$input$"],
70 external: ["*.woff2", "*.eot", "*.eot?#iefix", "*.ttf", "*.png", "*.jpeg"],
71 minify: true,
72 plugins: [
73 virtualFiles({
74 "$input$": {
75 contents: cssImports.map((file) => `@import url(${JSON.stringify("./" + file)});`)
76 .join("\n") + stringifyTheme(theme),
77 loader: "css",
78 },
79 }),
80 plugin,
81 ],
82 target: ["ie11"],
83 write: false,
84 });
85 if (errors.length > 0) {
86 throw new AggregateError(errors, "CSS Build Failed");
87 }
88 if (warnings.length > 0) {
89 throw new AggregateError(warnings, "CSS Build Failed");
90 }
91 if (outputFiles.length > 1) throw new Error("Too many output files");
92 return UNWRAP(outputFiles[0]).text;
93}
94
95import { UNWRAP } from "@clo/lib/assert";
96import type * as incremental from "@clo/lib/incremental";
97import * as esbuild from "esbuild";
98import { virtualFiles } from "./esbuild-support.ts";