Files
WRNexusJS/packages/styles/src/styles.ts
T
2026-07-27 12:42:18 +05:30

70 lines
2.5 KiB
TypeScript

/**
* Global stylesheet pipeline.
*
* Convention: `app/styles/global.css` is the entry. If it is absent but other
* `app/styles/*.css` files exist, they are aggregated into one entry. The entry
* is bundled by Bun's CSS bundler, which resolves `@import` — including from
* node_modules — so any npm CSS framework (Bootstrap, etc.) works by importing
* it. A custom `process` hook can replace the bundler for Tailwind/PostCSS/Sass.
*/
import { existsSync, readdirSync, mkdirSync, writeFileSync } from "node:fs";
import { isAbsolute, join, relative } from "node:path";
import type { Mode } from "./config.ts";
const fwd = (p: string) => p.replace(/\\/g, "/");
/**
* Resolve the CSS entry for an app.
* - `override` (from config.styles.entry) is resolved relative to `appRoot`.
* - otherwise prefer `app/styles/global.css`.
* - otherwise aggregate all `app/styles/*.css` into a generated entry.
* Returns null when the app has no styles.
*/
export function findStyleEntry(appDir: string, appRoot: string, override?: string): string | null {
if (override) {
const p = isAbsolute(override) ? override : join(appRoot, override);
return existsSync(p) ? p : null;
}
const stylesDir = join(appDir, "styles");
const globalCss = join(stylesDir, "global.css");
if (existsSync(globalCss)) return globalCss;
if (existsSync(stylesDir)) {
const cssFiles = readdirSync(stylesDir)
.filter((f) => f.endsWith(".css"))
.sort();
if (cssFiles.length > 0) {
const cacheDir = join(appDir, ".wrnexus");
mkdirSync(cacheDir, { recursive: true });
const entry = join(cacheDir, "styles-entry.css");
const imports = cssFiles
.map((f) => `@import "${fwd(relative(cacheDir, join(stylesDir, f)))}";`)
.join("\n");
writeFileSync(entry, imports + "\n", "utf8");
return entry;
}
}
return null;
}
/**
* Bundle a CSS entry into a single stylesheet string using Bun's CSS bundler.
* Resolves `@import` (local and node_modules), handles nesting, minifies in prod.
*/
export async function bundleCss(entryPath: string, mode: Mode): Promise<string> {
const result = await Bun.build({
entrypoints: [entryPath],
minify: mode === "production",
});
if (!result.success) {
throw new Error("CSS bundle failed:\n" + result.logs.map(String).join("\n"));
}
const cssOutput =
result.outputs.find((output) => output.path?.endsWith(".css")) ?? result.outputs[0];
if (!cssOutput) throw new Error("CSS bundle produced no output.");
return await cssOutput.text();
}