/** * @wrnexus/styles — global stylesheet pipeline + app config. * * Works for SSR and CSR: the bundled stylesheet is ``ed into every page's * ``, so it styles server-rendered markup and hydrated client islands * alike. Use any CSS framework via `@import` in global.css (npm) or via a CDN * link in `wrnexus.config.ts`'s `head` field. */ export type { AppConfig, AppConfigInput, ResolvedAppConfig, BuildConfig, ConfigIssue, DevToolbarConfig, ExplainedConfig, ExperimentalConfig, MobileConfig, NavigationConfig, ObservabilityConfig, PerformanceConfig, TenancyConfig, PwaConfig, StylesConfig, StyleProcessContext, Mode, ResolvedConfigLayers, } from "./config.ts"; export { defineConfig, explainAppConfig, headToString, loadAppConfig, loadEnv, loadRawConfig, resolveProfile, resolveConfigLayers, validateAppConfig, } from "./config.ts"; export { findStyleEntry, bundleCss } from "./styles.ts"; export type { FontConfig, GoogleFont, LocalFontFace, FontDisplay } from "./fonts.ts"; export { renderFontHead, renderProductionFontHead, fontCspSources } from "./fonts.ts"; export type { ThemeConfig, ThemeTokens, ResolvedTheme, ThemePaletteName, CustomThemePalette, ThemeToken, ThemeSemanticColor, BrowserCookieOptions, BrowserCookiesConfig, BrowserCookiePreference, BrowserCookieApi, } from "./theme.ts"; export { DEFAULT_THEMES, THEME_PALETTES, THEME_PALETTE_NAMES, THEME_COOKIE, THEME_CSS_HREF, THEME_CSS_PREFIX, THEME_JS_HREF, activeThemeCssHref, resolveThemeConfig, resolveThemeName, renderThemeCss, renderActiveThemeCss, renderThemeRuntime, defineThemeTokens, themeVar, resolveBrowserCookieOptions, } from "./theme.ts"; import type { Mode, StyleProcessContext, StylesConfig } from "./config.ts"; import { bundleCss } from "./styles.ts"; import { mkdir, writeFile } from "node:fs/promises"; import { isAbsolute, join, relative } from "node:path"; /** * Produce the final CSS for an entry: run the config's custom processor if one * is provided (Tailwind/PostCSS/Sass), otherwise use the built-in Bun bundler. * * Development can fall back to best-effort CSS. Production throws by default so * a deployment cannot silently ship unprocessed Tailwind/PostCSS directives. */ export async function renderStyles( ctx: StyleProcessContext, styles?: StylesConfig, ): Promise { if (!ctx.entryPath && !ctx.entries?.length) return ""; const processContext = styles?.includePackageSources === false ? ctx : await packageAwareStyleContext(ctx); if (!processContext.entryPath) return ""; if (styles?.process) { try { return String(await styles.process(processContext)); } catch (err) { const msg = err instanceof Error ? err.message : String(err); const details = `[wrnexus] Stylesheet processing failed.\n` + ` ${msg.split("\n")[0]}\n` + " If you use Tailwind, run `bun install` in the application workspace so the CLI and plugins are available."; const failureMode = styles.failureMode ?? (ctx.mode === "production" ? "throw" : "fallback"); if (failureMode === "throw") throw new Error(details, { cause: err }); console.error(`\n${details}\n Serving best-effort unprocessed CSS in development.\n`); return await fallbackCss(processContext); } } return bundleCss(processContext.entryPath!, processContext.mode); } /** * Best-effort CSS when a custom processor fails: try the built-in bundler; if that * also fails (e.g. `@import "tailwindcss"` can't resolve), serve the raw entry with * the tool-only directives stripped so the page still renders. */ async function fallbackCss(ctx: StyleProcessContext): Promise { try { return await bundleCss(ctx.entryPath!, ctx.mode); } catch { try { const raw = await Bun.file(ctx.entryPath!).text(); return raw.replace(/@import\s+["']tailwindcss["'];?/g, "").replace(/@source[^;\n]*;?/g, ""); } catch { return ""; } } } async function packageAwareStyleContext(ctx: StyleProcessContext): Promise { const sources = [...new Set((ctx.sources ?? []).filter(Boolean))]; const entries = [...new Set((ctx.entries ?? []).filter(Boolean))]; if (sources.length === 0 && entries.length === 0) return ctx; if (!ctx.entryPath && entries.length === 0) return ctx; const outputDir = join(ctx.appRoot, ".wrnexus", "styles"); const wrapper = join(outputDir, "global.with-package-sources.css"); await mkdir(outputDir, { recursive: true }); const localPath = (value: string): string => { const path = isAbsolute(value) ? relative(outputDir, value) : value; const normalized = path.replace(/\\/g, "/"); return normalized.startsWith(".") ? normalized : `./${normalized}`; }; const quote = (value: string) => localPath(value).replace(/"/g, '\\"'); const imports = [...(ctx.entryPath ? [ctx.entryPath] : []), ...entries] .map((value) => `@import "${quote(value)}";`) .join("\n"); const directives = sources.map((value) => `@source "${quote(value)}";`).join("\n"); await writeFile( wrapper, `${imports}\n\n/* WRNexus package scan sources */\n${directives}\n`, "utf8", ); return { ...ctx, originalEntryPath: ctx.entryPath, entryPath: wrapper, sources, entries, }; } export type { Mode as StylesMode }; export { auditWrnTokens, normalizeStyleSources, tailwindSourceDirectives, contrast, auditCssPerformance, } from "./audit.ts"; export type { CssTokenAudit, StyleSource, ContrastResult, CssPerformanceAuditIssue, } from "./audit.ts"; export * from "./theme.ts";