/** * 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 { 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(); }