sitegen/framework/css.ts
chloe caruso 25f01d878c rewrite incremental.ts (#21)
the problems with the original implementation was mostly around error handling. sources had to be tracked manually and provided to each incremental output. the `hasArtifact` check was frequently forgotten. this has been re-abstracted through `incr.work()`, which is given an `io` object. all fs reads and module loads go through this interface, which allows the sources to be properly tracked, even if it throws.

closes #12
2025-08-02 17:31:58 -07:00

114 lines
2.9 KiB
TypeScript

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 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(
io: Io,
{ cssImports, theme, dev }: {
cssImports: string[],
theme: Theme,
dev: boolean,
}
) {
cssImports = await Promise.all(cssImports.map((file) => io.trackFile('src/' + 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 outputFiles[0].text;
}
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";import type { Io } from "./incremental.ts";