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
43export function styleKey(
44 cssImports: string[],
45 theme: Theme,
46) {
47 cssImports = cssImports
48 .map((file) =>
49 (path.isAbsolute(file) ? path.relative(hot.projectSrc, file) : file)
50 .replaceAll("\\", "/")
51 )
52 .sort();
53 return cssImports.join(":") + ":"
54 + Object.entries(theme).map(([k, v]) => `${k}=${v}`);
55}
56
57export async function bundleCssFiles(
58 io: Io,
59 { cssImports, theme, dev }: {
60 cssImports: string[];
61 theme: Theme;
62 dev: boolean;
63 },
64) {
65 using _ = io.start(styleKey(cssImports, theme));
66 cssImports = await Promise.all(
67 cssImports.map((file) => io.trackFile("src/" + file)),
68 );
69 const plugin = {
70 name: "clover css",
71 setup(b) {
72 b.onLoad(
73 { filter: /\.css$/ },
74 async ({ path: file }) => ({
75 loader: "css",
76 contents: preprocess(await fs.readFile(file, "utf-8"), theme),
77 }),
78 );
79 },
80 } satisfies esbuild.Plugin;
81 const build = await esbuild.build({
82 bundle: true,
83 entryPoints: ["$input$"],
84 external: ["*.woff2", "*.eot", "*.eot?#iefix", "*.ttf", "*.png", "*.jpeg"],
85 metafile: true,
86 minify: !dev,
87 plugins: [
88 virtualFiles({
89 "$input$": {
90 contents: cssImports.map((path) => `@import url(${JSON.stringify(path)});`)
91 .join("\n") + stringifyTheme(theme),
92 loader: "css",
93 },
94 }),
95 plugin,
96 ],
97 target: ["ie11"],
98 write: false,
99 });
100 const { errors, warnings, outputFiles, metafile } = build;
101 if (errors.length > 0) {
102 throw new AggregateError(errors, "CSS Build Failed");
103 }
104 if (warnings.length > 0) {
105 throw new AggregateError(warnings, "CSS Build Failed");
106 }
107 if (outputFiles.length > 1) throw new Error("Too many output files");
108 return UNWRAP(outputFiles[0]).text;
109}
110
111import * as fs from "#sitegen/fs";
112import { UNWRAP } from "@clo/lib/assert";
113import * as esbuild from "esbuild";
114import * as path from "node:path";
115import { virtualFiles } from "./esbuild-support.ts";
116import * as hot from "./hot.ts";
117import type { Io } from "./incremental.ts";