Files
WRNexusJSDoc/app/pages/packages/styles.wrn
T

570 lines
37 KiB
Plaintext

page wrnexusstyles {
seo {
title = "@wrnexus/styles"
description = "CSS pipeline, themes, fonts, profiles, and application config."
}
view {
<div class="docs-shell">
<a class="skip-link" href="#main">Skip to content</a>
<header class="topbar">
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
<nav aria-label="Primary"><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
<div class="topbar-actions"><a class="preview-pill" href="/access">Private preview · v0.2.19</a><button data-wire-theme-toggle class="theme-button" aria-label="Toggle color theme" title="Toggle color theme">◐</button></div>
</header>
<div class="mobile-doc-nav"><details><summary>Browse documentation</summary><nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a><a href="/tutorial">Tutorial</a><a href="/guides/project-structure">Guides</a><a href="/examples">Examples</a><a href="/search">Search</a></nav></details></div>
<main class="page package-page">
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">Frontend</span><h2>@wrnexus/styles</h2><p>CSS pipeline, themes, fonts, profiles, and application config.</p><span class="status status-beta">Private preview · 0.2.19</span><nav><a href="#access">Access</a><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
<article id="main" class="documentation"><section class="doc-intro"><span class="eyebrow">Frontend · Preview</span><h1>@wrnexus/styles</h1><p>CSS pipeline, themes, fonts, profiles, and application config.</p><section id="access" class="access-callout"><h2>Private registry access required</h2><p>This package is not available from the public npm registry. After WorkRoot approves access and supplies private registry instructions, install the release-aligned package:</p><pre><code>bun add @wrnexus/styles@0.2.19</code><button type="button" class="copy-button" aria-label="Copy installation command">Copy</button></pre><p><a href="/access">Request preview access</a>. Never put registry tokens in source control.</p></section></section><section id="guide" class="prose"><blockquote>Global CSS bundling, the <code>--wire-*</code> design-token theme system, and the <code>wrnexus.config.ts</code> app-config loader for WRNexusJS apps.</blockquote>
<p>Part of the <strong>WRNexusJS</strong> framework — an SSR-first, Bun-native full-stack web framework.</p>
<h3 id="overview">Overview</h3>
<p>This package owns three server-side concerns that shape every page a WRNexusJS app renders:</p>
<p>1. <strong>Global stylesheet pipeline</strong> — finds <code>app/styles/global.css</code> (or aggregates <code>app/styles/*.css</code>), bundles it with Bun's CSS bundler (which resolves <code>@import</code>, including from <code>node_modules</code>), and produces one stylesheet that is <code>&lt;link&gt;</code>ed into every page's <code>&lt;head&gt;</code>. Because it is a plain global sheet, it styles server-rendered markup and hydrated client islands identically. A custom <code>process</code> hook lets you swap in Tailwind / PostCSS / Sass. 2. <strong>Theme system</strong> — design tokens exposed as CSS custom properties (<code>--wire-&lt;key&gt;</code>), with built-in <code>light</code>/<code>dark</code> sets, deep-merged user overrides, an SSR <code>&lt;html data-theme&gt;</code> render (no flash), and a tiny client runtime to toggle/persist the choice. 3. <strong>App config</strong> — loads <code>wrnexus.config.ts</code> (the <code>AppConfig</code> type), applies named profile overrides, and loads the <code>.env</code> cascade.</p>
<p>It runs server-side / at build time. Reach for it when configuring an app, defining themes, or customising how global CSS is produced.</p>
<pre data-language="bash"><code>bun add @wrnexus/styles</code></pre>
<blockquote>Private package — the machine must be authenticated to the <code>wrnexus</code> npm org</blockquote>
<blockquote>(a read token in <code>~/.npmrc</code>). Requires <strong>Bun</strong> (Node is not supported).</blockquote>
<h3 id="api">API</h3>
<p>Everything is exported from the package root (<code>@wrnexus/styles</code>).</p>
<h4 id="config-loading">Config loading</h4>
<div class="table-wrap"><table>
<thead><tr><th>Export</th><th>Signature</th><th>Purpose</th></tr></thead>
<tbody><tr><td><code>loadAppConfig</code></td><td><code>(appRoot: string, profile?: string) =&gt; Promise&lt;AppConfig&gt;</code></td><td>Load <code>wrnexus.config.*</code> with the active profile deep-merged in (<code>profiles</code> stripped from the result).</td></tr><tr><td><code>loadRawConfig</code></td><td><code>(appRoot: string) =&gt; Promise&lt;AppConfig&gt;</code></td><td>Load the raw config with the <code>profiles</code> map intact; returns <code>&#123;&#125;</code> if no config file exists.</td></tr><tr><td><code>resolveProfile</code></td><td><code>(options?: &#123; explicit?; mode? &#125;) =&gt; string</code></td><td>Resolve the active profile: explicit arg &gt; <code>WRNEXUS_PROFILE</code> env var &gt; mode-based default (<code>production</code> in prod, else <code>development</code>).</td></tr><tr><td><code>loadEnv</code></td><td><code>(appRoot: string, profile: string) =&gt; Record&lt;string, string&gt;</code></td><td>Load the <code>.env</code> cascade for a profile into <code>process.env</code> without clobbering real env vars. Returns what it loaded.</td></tr><tr><td><code>headToString</code></td><td>`(head?: string \</td><td>string[]) =&gt; string`</td><td>Flatten <code>AppConfig.head</code> into a single HTML string.</td></tr></tbody></table></div>
<p>Config file names probed, in order: <code>wrnexus.config.ts</code>, <code>wrnexus.config.js</code>, <code>wrnexus.config.mjs</code>.</p>
<p><code>.env</code> cascade precedence (low → high): <code>.env</code> &lt; <code>.env.&lt;profile&gt;</code> &lt; <code>.env.local</code> &lt; <code>.env.&lt;profile&gt;.local</code>. Variables already present in the real environment always win.</p>
<h4 id="appconfig"><code>AppConfig</code></h4>
<p>The type of the object your <code>wrnexus.config.ts</code> default-exports. Every field is optional.</p>
<div class="table-wrap"><table>
<thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead>
<tbody><tr><td><code>head</code></td><td>`string \</td><td>string[]`</td><td>Raw HTML appended to every page's <code>&lt;head&gt;</code> (e.g. CDN stylesheet/script links).</td></tr><tr><td><code>seo</code></td><td><code>SeoConfig</code></td><td>Global SEO defaults, merged with each page's exported <code>meta</code>. (from <code>@wrnexus/core</code>)</td></tr><tr><td><code>security</code></td><td><code>SecurityConfig</code></td><td>Framework security headers and optional CORS policy. (from <code>@wrnexus/core</code>)</td></tr><tr><td><code>styles</code></td><td><code>StylesConfig</code></td><td>Global stylesheet pipeline config (see below).</td></tr><tr><td><code>theme</code></td><td><code>ThemeConfig</code></td><td>Design-token themes, deep-merged over the built-in light/dark.</td></tr><tr><td><code>i18n</code></td><td><code>&#123; default?: string; locales?: string[] &#125;</code></td><td>Default language + supported locales (strings live in <code>app/locales/*.json</code>).</td></tr><tr><td><code>db</code></td><td>`&#123; driver: &quot;sqlite&quot; \</td><td>&quot;postgres&quot; \</td><td>&quot;mysql&quot; \</td><td>&quot;mongo&quot;; url: string &#125;`</td><td>Default database connection; reached with <code>getDb()</code>.</td></tr><tr><td><code>databases</code></td><td><code>Record&lt;string, &#123; driver; url &#125;&gt;</code></td><td>Additional named databases, reached with <code>getDb(&quot;&lt;name&gt;&quot;)</code>; each has its own <code>app/db/&lt;name&gt;/</code> migrations/queries.</td></tr><tr><td><code>realtime</code></td><td><code>&#123; scale?: boolean; redisUrl?: string &#125;</code></td><td>When <code>scale</code> is true (or <code>redisUrl</code> is set), room broadcasts bridge over Redis pub/sub so they reach clients on every app process.</td></tr><tr><td><code>port</code></td><td><code>number</code></td><td>Default server port.</td></tr><tr><td><code>profiles</code></td><td><code>Record&lt;string, Partial&lt;Omit&lt;AppConfig, &quot;profiles&quot;&gt;&gt;&gt;</code></td><td>Named profiles (dev, prod, uat, test, …). The active profile's overrides are deep-merged over the base config. Selected via <code>--profile=&lt;name&gt;</code> or <code>WRNEXUS_PROFILE</code>.</td></tr></tbody></table></div>
<h4 id="styles-pipeline">Styles pipeline</h4>
<div class="table-wrap"><table>
<thead><tr><th>Export</th><th>Signature</th><th>Purpose</th></tr></thead>
<tbody><tr><td><code>findStyleEntry</code></td><td>`(appDir, appRoot, override?) =&gt; string \</td><td>null`</td><td>Resolve the CSS entry: <code>override</code> (relative to <code>appRoot</code>) → <code>app/styles/global.css</code> → an aggregate of all <code>app/styles/*.css</code> (written to <code>app/.wrnexus/styles-entry.css</code>). <code>null</code> if the app has no styles.</td></tr><tr><td><code>bundleCss</code></td><td><code>(entryPath: string, mode: Mode) =&gt; Promise&lt;string&gt;</code></td><td>Bundle an entry with <code>Bun.build</code> (CSS bundler). Resolves <code>@import</code> (local + node_modules), handles nesting, minifies when <code>mode === &quot;production&quot;</code>.</td></tr><tr><td><code>renderStyles</code></td><td><code>(ctx: StyleProcessContext, styles?: StylesConfig) =&gt; Promise&lt;string&gt;</code></td><td>Produce final CSS: runs <code>styles.process(ctx)</code> if provided, else <code>bundleCss</code>. Returns <code>&quot;&quot;</code> when <code>ctx.entryPath</code> is null.</td></tr></tbody></table></div>
<p><code>StylesConfig</code>:</p>
<pre data-language="ts"><code>interface StylesConfig &#123;
/** CSS entry path relative to the app root. Default: app/styles/global.css */
entry?: string;
/** Custom processor — return the final CSS string (Tailwind/PostCSS/Sass). */
process?: (ctx: StyleProcessContext) =&gt; string | Promise&lt;string&gt;;
&#125;
interface StyleProcessContext &#123;
entryPath: string | null; // resolved absolute CSS entry, or null
appDir: string;
appRoot: string;
mode: Mode; // &quot;development&quot; | &quot;production&quot;
&#125;</code></pre>
<h4 id="theme-system">Theme system</h4>
<div class="table-wrap"><table>
<thead><tr><th>Export</th><th>Type / Signature</th><th>Purpose</th></tr></thead>
<tbody><tr><td><code>DEFAULT_THEMES</code></td><td><code>Record&lt;string, ThemeTokens&gt;</code></td><td>Built-in <code>light</code> and <code>dark</code> token maps.</td></tr><tr><td><code>THEME_COOKIE</code></td><td><code>&quot;wire-theme&quot;</code></td><td>Cookie the resolved theme is read from / persisted to.</td></tr><tr><td><code>THEME_CSS_HREF</code></td><td><code>&quot;/__wrnexus/theme.css&quot;</code></td><td>URL the generated theme stylesheet is served at.</td></tr><tr><td><code>THEME_JS_HREF</code></td><td><code>&quot;/__wrnexus/theme.js&quot;</code></td><td>URL the client theme runtime is served at.</td></tr><tr><td><code>resolveThemeConfig</code></td><td><code>(config?: ThemeConfig) =&gt; ResolvedTheme</code></td><td>Deep-merge the user's <code>theme</code> config over the defaults; pick the default theme (config's <code>default</code> if valid, else <code>dark</code>, else the first).</td></tr><tr><td><code>resolveThemeName</code></td><td>`(cookieValue: string \</td><td>undefined, theme: ResolvedTheme) =&gt; string`</td><td>Pick a valid theme name from a cookie, falling back to <code>theme.default</code>.</td></tr><tr><td><code>renderThemeCss</code></td><td><code>(theme: ResolvedTheme) =&gt; string</code></td><td>Generate the theme stylesheet: a <code>:root&#123;…&#125;</code> default plus one <code>[data-theme=&quot;&lt;name&gt;&quot;]&#123;…&#125;</code> block per theme.</td></tr><tr><td><code>renderThemeRuntime</code></td><td><code>(theme: ResolvedTheme) =&gt; string</code></td><td>Generate the client runtime (see below).</td></tr></tbody></table></div>
<p>Tokens are emitted as <code>--wire-&lt;key&gt;</code> custom properties, <strong>except</strong> the reserved key <code>color-scheme</code>, which is emitted as the native <code>color-scheme</code> CSS property so form controls and scrollbars match the theme.</p>
<p><code>ThemeConfig</code> / <code>ThemeTokens</code> / <code>ResolvedTheme</code>:</p>
<pre data-language="ts"><code>type ThemeTokens = Record&lt;string, string&gt;;
interface ThemeConfig &#123;
default?: string; // theme used when no cookie is present
themes?: Record&lt;string, ThemeTokens&gt;; // deep-merged over built-in light/dark
&#125;
interface ResolvedTheme &#123;
default: string;
names: string[];
themes: Record&lt;string, ThemeTokens&gt;;
&#125;</code></pre>
<p>Built-in token keys (both <code>light</code> and <code>dark</code>): <code>color-scheme</code>, <code>color-bg</code>, <code>color-surface</code>, <code>color-surface-2</code>, <code>color-text</code>, <code>color-muted</code>, <code>color-border</code>, <code>color-primary</code>, <code>color-primary-hover</code>, <code>color-primary-contrast</code>, <code>color-danger</code>, <code>color-success</code>, <code>color-warning</code>, <code>radius</code>, <code>radius-sm</code>, <code>font-sans</code>, <code>shadow-1</code>.</p>
<p>The client runtime (<code>renderThemeRuntime</code>) exposes <code>window.wireTheme</code> with <code>&#123; get, set, toggle, bind, themes &#125;</code>, wires up any <code>[data-wire-theme-toggle]</code> and <code>[data-wire-theme-set]</code> elements on load, and persists the choice to the <code>wire-theme</code> cookie (<code>max-age</code> 1 year, <code>samesite=lax</code>). <code>toggle()</code> cycles through the configured theme names in order.</p>
<h3 id="usage">Usage</h3>
<h4 id="wrnexus-config-ts"><code>wrnexus.config.ts</code></h4>
<pre data-language="ts"><code>import type &#123; AppConfig &#125; from &quot;@wrnexus/styles&quot;;
export default &#123;
head: [
'&lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdn.jsdelivr.net/npm/bootstrap@5/dist/css/bootstrap.min.css&quot;&gt;',
],
port: 3000,
db: &#123; driver: &quot;sqlite&quot;, url: &quot;app.db&quot; &#125;,
theme: &#123;
default: &quot;dark&quot;,
themes: &#123;
light: &#123; &quot;color-primary&quot;: &quot;#7c3aed&quot; &#125;, // override one token; rest inherited
brand: &#123;
// add a whole new theme
&quot;color-scheme&quot;: &quot;dark&quot;,
&quot;color-bg&quot;: &quot;#0a0a0a&quot;,
&quot;color-primary&quot;: &quot;#22d3ee&quot;,
&#125;,
&#125;,
&#125;,
styles: &#123;
entry: &quot;app/styles/main.css&quot;,
&#125;,
profiles: &#123;
production: &#123;
db: &#123; driver: &quot;postgres&quot;, url: process.env.DATABASE_URL! &#125;,
&#125;,
&#125;,
&#125; satisfies AppConfig;</code></pre>
<h4 id="loading-config-producing-css">Loading config + producing CSS</h4>
<pre data-language="ts"><code>import &#123;
loadAppConfig,
resolveProfile,
loadEnv,
findStyleEntry,
renderStyles,
&#125; from &quot;@wrnexus/styles&quot;;
const appRoot = process.cwd();
const mode = &quot;production&quot; as const;
const profile = resolveProfile(&#123; mode &#125;);
loadEnv(appRoot, profile);
const config = await loadAppConfig(appRoot, profile);
const appDir = `$&#123;appRoot&#125;/app`;
const entryPath = findStyleEntry(appDir, appRoot, config.styles?.entry);
const css = await renderStyles(&#123; entryPath, appDir, appRoot, mode &#125;, config.styles);</code></pre>
<h4 id="rendering-the-theme">Rendering the theme</h4>
<pre data-language="ts"><code>import &#123;
resolveThemeConfig,
resolveThemeName,
renderThemeCss,
renderThemeRuntime,
THEME_COOKIE,
&#125; from &quot;@wrnexus/styles&quot;;
const theme = resolveThemeConfig(config.theme);
// Server: pick the active theme from the request cookie (no flash).
const active = resolveThemeName(cookies[THEME_COOKIE], theme);
// → render &lt;html data-theme=&#123;active&#125;&gt;
const themeCss = renderThemeCss(theme); // served at THEME_CSS_HREF
const themeJs = renderThemeRuntime(theme); // served at THEME_JS_HREF</code></pre>
<p>In templates, consume tokens via the custom properties:</p>
<pre data-language="css"><code>.card &#123;
background: var(--wire-color-surface);
color: var(--wire-color-text);
border: 1px solid var(--wire-color-border);
border-radius: var(--wire-radius);
box-shadow: var(--wire-shadow-1);
&#125;</code></pre>
<pre data-language="html"><code>&lt;button data-wire-theme-toggle&gt;Toggle theme&lt;/button&gt;
&lt;button data-wire-theme-set=&quot;brand&quot;&gt;Brand theme&lt;/button&gt;</code></pre>
<h3 id="requirements-notes">Requirements / Notes</h3>
<ul>
<li><strong>Bun-only.</strong> <code>bundleCss</code> uses <code>Bun.build</code>'s CSS bundler for <code>@import</code> resolution, nesting, and minification. Node is not supported.</li>
<li>Config and env loading use <code>node:fs</code> / <code>node:path</code> / <code>node:url</code> and read from <code>process.env</code>.</li>
<li>Peer package: <code>@wrnexus/core</code> supplies the <code>SeoConfig</code> and <code>SecurityConfig</code> types referenced by <code>AppConfig</code>.</li>
<li>The bundled global stylesheet, the theme stylesheet (<code>THEME_CSS_HREF</code>), and the theme runtime (<code>THEME_JS_HREF</code>) are wired into pages by the framework's server; this package only produces their contents.</li>
</ul></section><section id="api" class="prose api"><h2>Complete TypeScript API</h2><p>This declaration comes from the exact installed package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>import &#123; SeoConfig, SecurityConfig &#125; from '@wrnexus/core';
import &#123; StorageConfig &#125; from '@wrnexus/uploader';
/**
* Theme system — design tokens that work SSR and client-side.
*
* Tokens are plain CSS custom properties (`--wire-&lt;key&gt;`) so they cascade and
* can be overridden by user CSS. Each theme is a flat token map; the framework
* ships default `light`/`dark` sets and the user's config deep-merges over them.
*
* The server renders `&lt;html data-theme=&quot;…&quot;&gt;` from the `wire-theme` cookie (no
* flash), and a tiny client runtime toggles/persists it. The reserved token key
* `color-scheme` is emitted as the native CSS property (not a variable) so form
* controls and scrollbars match the theme.
*/
type ThemeTokens = Record&lt;string, string&gt;;
interface ThemeConfig &#123;
/** Name of the theme used when no `wire-theme` cookie is present. */
default?: string;
/** Named token maps. Deep-merged over the framework's built-in light/dark. */
themes?: Record&lt;string, ThemeTokens&gt;;
&#125;
interface ResolvedTheme &#123;
default: string;
names: string[];
themes: Record&lt;string, ThemeTokens&gt;;
&#125;
/** Cookie the resolved theme is read from / persisted to. */
declare const THEME_COOKIE = &quot;wire-theme&quot;;
declare const THEME_CSS_HREF = &quot;/__wrnexus/theme.css&quot;;
declare const THEME_JS_HREF = &quot;/__wrnexus/theme.js&quot;;
/** Built-in themes so components have tokens out of the box. */
declare const DEFAULT_THEMES: Record&lt;string, ThemeTokens&gt;;
/** Merge the user's theme config over the built-in defaults. */
declare function resolveThemeConfig(config?: ThemeConfig): ResolvedTheme;
/** Pick a valid theme name from a cookie value, falling back to the default. */
declare function resolveThemeName(cookieValue: string | undefined, theme: ResolvedTheme): string;
/** Generate the theme stylesheet: a `:root` default plus one block per theme. */
declare function renderThemeCss(theme: ResolvedTheme): string;
/**
* Generate the client theme runtime. It exposes `window.wireTheme` and binds
* `[data-wire-theme-toggle]` / `[data-wire-theme-set]` elements. The configured
* theme names are baked in so `toggle()` cycles through them in order.
*/
declare function renderThemeRuntime(theme: ResolvedTheme): string;
/**
* Font configuration.
*
* Declare fonts in `wrnexus.config.ts` under `fonts` and the framework emits
* optimized `&lt;head&gt;` markup for you:
* - Google Fonts: `preconnect` hints + a single subsetted stylesheet request
* (only the weights you list) with `font-display`. The CSP is auto-extended
* so the fonts load under the default security policy (see loadAppConfig).
* - Self-hosted fonts: generated `@font-face` rules + optional `&lt;link rel=preload&gt;`
* for above-the-fold text (the fastest, no-third-party option).
* - Family stacks: `sans`/`mono`/`serif` become `--wrn-font-*` CSS variables,
* and `sans` is applied to `body`.
*/
type FontDisplay = &quot;auto&quot; | &quot;block&quot; | &quot;swap&quot; | &quot;fallback&quot; | &quot;optional&quot;;
interface GoogleFont &#123;
/** Family name as it appears on fonts.google.com, e.g. &quot;Inter&quot;. */
family: string;
/** Weights to load — ONLY these are fetched. Default: [400]. */
weights?: (number | string)[];
/** Also load italic styles for each weight. */
italic?: boolean;
/** Per-font `font-display` override (else the config default). */
display?: FontDisplay;
&#125;
interface LocalFontFace &#123;
/** `font-family` name this face defines. */
family: string;
/** URL to the font file, typically served from `public/` (e.g. &quot;/fonts/inter.woff2&quot;). */
src: string;
/** e.g. 400, &quot;700&quot;, or &quot;100 900&quot; for a variable font. Default: 400. */
weight?: number | string;
style?: &quot;normal&quot; | &quot;italic&quot;;
/** CSS `src` format; inferred from the file extension when omitted. */
format?: string;
display?: FontDisplay;
/** Emit `&lt;link rel=&quot;preload&quot; as=&quot;font&quot;&gt;` — use for the primary above-the-fold face. */
preload?: boolean;
/** Optional `unicode-range` subset. */
unicodeRange?: string;
&#125;
interface FontConfig &#123;
/** Google Fonts, loaded with preconnect + weight subsetting + `font-display`. */
google?: GoogleFont[];
/** Self-hosted `@font-face` definitions (files served from `public/`). */
local?: LocalFontFace[];
/** Default `font-display` for faces that don't set their own. Default: &quot;swap&quot;. */
display?: FontDisplay;
/** Body / default family stack → `--wrn-font-sans` + `body &#123; font-family &#125;`. */
sans?: string;
/** Monospace family stack → `--wrn-font-mono`. */
mono?: string;
/** Serif family stack → `--wrn-font-serif`. */
serif?: string;
&#125;
/**
* Render all `&lt;head&gt;` markup for a font config. Returns &quot;&quot; when nothing is
* configured. The output is trusted, framework-controlled HTML.
*/
declare function renderFontHead(fonts?: FontConfig): string;
/**
* Production variant that inlines the small Google Fonts stylesheet at build
* time. This removes a render-blocking CSS round trip while retaining the same
* font files, `font-display`, CSP sources, and offline-safe fallback markup.
*/
declare function renderProductionFontHead(fonts?: FontConfig, fetcher?: (input: string, init?: RequestInit) =&gt; Promise&lt;Response&gt;): Promise&lt;string&gt;;
/**
* CSP source hosts required by the configured fonts, so the policy can be
* auto-extended (Google Fonts need their CSS + static hosts allow-listed).
*/
declare function fontCspSources(fonts?: FontConfig): &#123;
style: string[];
font: string[];
&#125;;
/**
* App configuration loader (`wrnexus.config.ts`).
*
* The config is optional. It lets an app inject arbitrary `&lt;head&gt;` HTML (ideal
* for CDN-delivered CSS frameworks like Bootstrap or the Tailwind Play CDN) and
* customise the global stylesheet pipeline (entry file or a custom processor for
* Tailwind / PostCSS / Sass).
*/
type Mode = &quot;development&quot; | &quot;production&quot;;
interface StyleProcessContext &#123;
/** Resolved absolute path to the CSS entry, or null if there is none. */
entryPath: string | null;
appDir: string;
appRoot: string;
mode: Mode;
&#125;
interface StylesConfig &#123;
/** Path to the CSS entry, relative to the app root. Default: app/styles/global.css */
entry?: string;
/**
* Optional custom processor. Return the final CSS string. Use this to run
* Tailwind, PostCSS, Sass, etc. When omitted, the built-in Bun CSS bundler is
* used (which already resolves `@import`, including from node_modules).
*/
process?: (ctx: StyleProcessContext) =&gt; string | Promise&lt;string&gt;;
&#125;
interface MobileConfig &#123;
enabled?: boolean;
/** Mobile renderer. `webview` uses Capacitor; `native` scaffolds an Expo/React Native app. */
mode?: &quot;webview&quot; | &quot;native&quot;;
appId?: string;
appName?: string;
serverUrl?: string;
userAgent?: string;
layout?: string;
backgroundColor?: string;
icon?: string;
errorTitle?: string;
errorMessage?: string;
/** Base URL used by a fully native client for WRNexusJS API and realtime requests. */
apiUrl?: string;
/** URL scheme used for native deep links (defaults to a slug of appName). */
scheme?: string;
/** Advanced Expo app config fields merged into generated app.config.ts. */
expo?: Record&lt;string, unknown&gt;;
/** Advanced CapacitorConfig fields merged into generated capacitor.config.ts. */
capacitor?: Record&lt;string, unknown&gt;;
&#125;
interface PwaScreenshot &#123;
src: string;
sizes: string;
type?: string;
formFactor?: &quot;wide&quot; | &quot;narrow&quot;;
label?: string;
&#125;
interface PwaShortcut &#123;
name: string;
shortName?: string;
description?: string;
url: string;
icons?: Array&lt;&#123;
src: string;
sizes: string;
type?: string;
purpose?: string;
&#125;&gt;;
&#125;
interface PwaConfig &#123;
enabled?: boolean;
id?: string;
name?: string;
shortName?: string;
description?: string;
startUrl?: string;
scope?: string;
lang?: string;
display?: &quot;standalone&quot; | &quot;fullscreen&quot; | &quot;minimal-ui&quot; | &quot;browser&quot;;
orientation?: &quot;any&quot; | &quot;natural&quot; | &quot;landscape&quot; | &quot;landscape-primary&quot; | &quot;landscape-secondary&quot; | &quot;portrait&quot; | &quot;portrait-primary&quot; | &quot;portrait-secondary&quot;;
themeColor?: string;
backgroundColor?: string;
icons?: Array&lt;&#123;
src: string;
sizes: string;
type?: string;
purpose?: string;
&#125;&gt;;
categories?: string[];
screenshots?: PwaScreenshot[];
shortcuts?: PwaShortcut[];
/** Disable service-worker registration while keeping the web manifest. */
serviceWorker?: boolean;
/** Navigation shown when both the network and requested page cache are unavailable. */
offlineUrl?: string;
/** Additional same-origin URLs precached during service-worker installation. */
cacheUrls?: string[];
/** Service-worker cache key. Change it to invalidate existing PWA caches. */
cacheName?: string;
&#125;
interface AppConfig &#123;
/** Raw HTML appended to every page's `&lt;head&gt;` (e.g. CDN stylesheet links). */
head?: string | string[];
/** Global SEO defaults merged with every page's exported `meta`. */
seo?: SeoConfig;
/** Framework security headers and optional CORS policy. */
security?: SecurityConfig;
styles?: StylesConfig;
/** Capacitor/native shell defaults and mobile-only page rendering. */
mobile?: MobileConfig;
/** Progressive Web App metadata. Enabled by default unless set to false. */
pwa?: PwaConfig | false;
/**
* Fonts. Declare Google Fonts (subsetted + preconnect + `font-display`) and/or
* self-hosted `@font-face` (with preload), and set `sans`/`mono`/`serif` family
* stacks. Google Fonts auto-extend the CSP so they load under the default policy.
*/
fonts?: FontConfig;
/** Design-token themes (deep-merged over the built-in light/dark). */
theme?: ThemeConfig;
/** i18n: default language + supported locales (strings live in app/locales/*.json). */
i18n?: &#123;
default?: string;
locales?: string[];
&#125;;
/** Default database connection (driver + url); reached with `getDb()`. */
db?: &#123;
driver: &quot;sqlite&quot; | &quot;postgres&quot; | &quot;mysql&quot; | &quot;mongo&quot;;
url: string;
&#125;;
/**
* File-upload storage. Declare named stores (local dir or S3-compatible),
* upload with `handleUpload`/`upload` from `@wrnexus/uploader`, and serve
* files back. Each store is `access: &quot;public&quot; | &quot;private&quot;`.
*/
storage?: StorageConfig;
/**
* Additional named databases, reached with `getDb(&quot;&lt;name&gt;&quot;)`. Each has its own
* migrations/queries under `app/db/&lt;name&gt;/`. Connect to as many as you like and
* read/write to any of them per request.
*
* databases: &#123; analytics: &#123; driver: &quot;postgres&quot;, url: &quot;…&quot; &#125; &#125;
*/
databases?: Record&lt;string, &#123;
driver: &quot;sqlite&quot; | &quot;postgres&quot; | &quot;mysql&quot; | &quot;mongo&quot;;
url: string;
&#125;&gt;;
/**
* Realtime scaling. When `scale` is true (or `redisUrl` is set), room
* broadcasts are bridged over Redis pub/sub so they reach clients on **every**
* app process/instance — realtime that works with multiple running apps.
*/
realtime?: &#123;
scale?: boolean;
redisUrl?: string;
&#125;;
/** Default server port. */
port?: number;
/**
* Named config profiles (dev, prod, uat, test, …). When a profile is active
* its overrides are DEEP-MERGED over the base config. Select with
* `--profile=&lt;name&gt;` or the `WRNEXUS_PROFILE` env var.
*/
profiles?: Record&lt;string, Partial&lt;Omit&lt;AppConfig, &quot;profiles&quot;&gt;&gt;&gt;;
&#125;
/**
* Resolve the active profile name: explicit argument &gt; `WRNEXUS_PROFILE` env var
* &gt; a mode-based default (&quot;production&quot; in prod, else &quot;development&quot;).
*/
declare function resolveProfile(options?: &#123;
explicit?: string;
mode?: Mode;
&#125;): string;
/** Load the raw `wrnexus.config.*` (with the `profiles` map intact), or `&#123;&#125;`. */
declare function loadRawConfig(appRoot: string): Promise&lt;AppConfig&gt;;
/** Load `wrnexus.config.*`, applying the active profile's overrides. */
declare function loadAppConfig(appRoot: string, profile?: string): Promise&lt;AppConfig&gt;;
/**
* Load the `.env` cascade for a profile into `process.env`, WITHOUT clobbering
* variables already set in the real environment (which always win). Order, low
* → high precedence: `.env` &lt; `.env.&lt;profile&gt;` &lt; `.env.local` &lt; `.env.&lt;profile&gt;.local`.
* Returns the variables it loaded.
*/
declare function loadEnv(appRoot: string, profile: string): Record&lt;string, string&gt;;
/** Flatten a head config into a single HTML string. */
declare function headToString(head?: string | string[]): string;
/**
* 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.
*/
/**
* 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.
*/
declare function findStyleEntry(appDir: string, appRoot: string, override?: string): string | 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.
*/
declare function bundleCss(entryPath: string, mode: Mode): Promise&lt;string&gt;;
/**
* @wrnexus/styles — global stylesheet pipeline + app config.
*
* Works for SSR and CSR: the bundled stylesheet is `&lt;link&gt;`ed into every page's
* `&lt;head&gt;`, 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.
*/
/**
* 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.
*
* If a custom processor throws (e.g. Tailwind can't resolve `tailwindcss`
* because deps aren't installed), we DON'T crash every request — we log a clear,
* actionable message and fall back to best-effort CSS so the app keeps serving.
*/
declare function renderStyles(ctx: StyleProcessContext, styles?: StylesConfig): Promise&lt;string&gt;;
export &#123; type AppConfig, DEFAULT_THEMES, type FontConfig, type FontDisplay, type GoogleFont, type LocalFontFace, type MobileConfig, type Mode, type PwaConfig, type ResolvedTheme, type StyleProcessContext, type StylesConfig, type Mode as StylesMode, THEME_COOKIE, THEME_CSS_HREF, THEME_JS_HREF, type ThemeConfig, type ThemeTokens, bundleCss, findStyleEntry, fontCspSources, headToString, loadAppConfig, loadEnv, loadRawConfig, renderFontHead, renderProductionFontHead, renderStyles, renderThemeCss, renderThemeRuntime, resolveProfile, resolveThemeConfig, resolveThemeName &#125;;
</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Examples are taken from this package's installed documentation and must be evaluated with its requirements and stability notes.</p><div class="example-grid"><article class="example-card"><h3>Example 1</h3><pre data-language="bash"><code>bun add @wrnexus/styles</code></pre></article><article class="example-card"><h3>Example 2</h3><pre data-language="ts"><code>interface StylesConfig &#123;
/** CSS entry path relative to the app root. Default: app/styles/global.css */
entry?: string;
/** Custom processor — return the final CSS string (Tailwind/PostCSS/Sass). */
process?: (ctx: StyleProcessContext) =&gt; string | Promise&lt;string&gt;;
&#125;
interface StyleProcessContext &#123;
entryPath: string | null; // resolved absolute CSS entry, or null
appDir: string;
appRoot: string;
mode: Mode; // &quot;development&quot; | &quot;production&quot;
&#125;</code></pre></article><article class="example-card"><h3>Example 3</h3><pre data-language="ts"><code>type ThemeTokens = Record&lt;string, string&gt;;
interface ThemeConfig &#123;
default?: string; // theme used when no cookie is present
themes?: Record&lt;string, ThemeTokens&gt;; // deep-merged over built-in light/dark
&#125;
interface ResolvedTheme &#123;
default: string;
names: string[];
themes: Record&lt;string, ThemeTokens&gt;;
&#125;</code></pre></article><article class="example-card"><h3>Example 4</h3><pre data-language="ts"><code>import type &#123; AppConfig &#125; from &quot;@wrnexus/styles&quot;;
export default &#123;
head: [
'&lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdn.jsdelivr.net/npm/bootstrap@5/dist/css/bootstrap.min.css&quot;&gt;',
],
port: 3000,
db: &#123; driver: &quot;sqlite&quot;, url: &quot;app.db&quot; &#125;,
theme: &#123;
default: &quot;dark&quot;,
themes: &#123;
light: &#123; &quot;color-primary&quot;: &quot;#7c3aed&quot; &#125;, // override one token; rest inherited
brand: &#123;
// add a whole new theme
&quot;color-scheme&quot;: &quot;dark&quot;,
&quot;color-bg&quot;: &quot;#0a0a0a&quot;,
&quot;color-primary&quot;: &quot;#22d3ee&quot;,
&#125;,
&#125;,
&#125;,
styles: &#123;
entry: &quot;app/styles/main.css&quot;,
&#125;,
profiles: &#123;
production: &#123;
db: &#123; driver: &quot;postgres&quot;, url: process.env.DATABASE_URL! &#125;,
&#125;,
&#125;,
&#125; satisfies AppConfig;</code></pre></article></div></section></article>
<aside class="on-this-page"><h2>On this page</h2><nav><a class="toc-level-2" href="#guide">Guide</a><a class="toc-level-3" href="#overview">Overview</a><a class="toc-level-3" href="#api">API</a><a class="toc-level-4" href="#config-loading">Config loading</a><a class="toc-level-4" href="#appconfig">AppConfig</a><a class="toc-level-4" href="#styles-pipeline">Styles pipeline</a><a class="toc-level-4" href="#theme-system">Theme system</a><a class="toc-level-3" href="#usage">Usage</a><a class="toc-level-4" href="#wrnexus-config-ts">wrnexus.config.ts</a><a class="toc-level-4" href="#loading-config-producing-css">Loading config + producing CSS</a><a class="toc-level-4" href="#rendering-the-theme">Rendering the theme</a><a class="toc-level-3" href="#requirements-notes">Requirements / Notes</a><a class="toc-level-2" href="#api">Complete API</a><a class="toc-level-2" href="#examples">Examples</a></nav></aside>
</main>
<footer>WRNexusJS 0.2.19 · Private Developer Preview · Bun-native · Documentation generated from installed package APIs.</footer>
</div>
}
}