first commit
This commit is contained in:
@@ -0,0 +1,562 @@
|
||||
page wrnexusstyles {
|
||||
seo {
|
||||
title = "@wrnexus/styles"
|
||||
description = "CSS pipeline, themes, fonts, profiles, and application config."
|
||||
}
|
||||
|
||||
view {
|
||||
<div class="docs-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="/"><span>W</span> WRNexusJS</a>
|
||||
<nav><a href="/getting-started">Get started</a><a href="/packages">Packages</a><a href="/language">Language</a><a href="/architecture">Architecture</a></nav>
|
||||
<button data-wire-theme-toggle class="theme-button" aria-label="Toggle theme">Theme</button>
|
||||
</header>
|
||||
<main class="page package-page">
|
||||
<aside class="sidebar"><a href="/packages">← All packages</a><span class="category">Frontend</span><h1>@wrnexus/styles</h1><p>CSS pipeline, themes, fonts, profiles, and application config.</p><code>bun add @wrnexus/styles@0.2.12</code><nav><a href="#guide">Guide</a><a href="#api">Complete API</a></nav></aside>
|
||||
<article class="documentation"><section class="doc-intro"><span class="eyebrow">Frontend</span><h1>@wrnexus/styles</h1><p>CSS pipeline, themes, fonts, profiles, and application config.</p><pre><code>bun add @wrnexus/styles@0.2.12</code></pre></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><link></code>ed into every page's <code><head></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-<key></code>), with built-in <code>light</code>/<code>dark</code> sets, deep-merged user overrides, an SSR <code><html data-theme></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>
|
||||
<h3 id="installation">Installation</h3>
|
||||
<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) => Promise<AppConfig></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) => Promise<AppConfig></code></td><td>Load the raw config with the <code>profiles</code> map intact; returns <code>{}</code> if no config file exists.</td></tr><tr><td><code>resolveProfile</code></td><td><code>(options?: { explicit?; mode? }) => string</code></td><td>Resolve the active profile: explicit arg > <code>WRNEXUS_PROFILE</code> env var > 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) => Record<string, string></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[]) => 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> < <code>.env.<profile></code> < <code>.env.local</code> < <code>.env.<profile>.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><head></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>{ default?: string; locales?: string[] }</code></td><td>Default language + supported locales (strings live in <code>app/locales/*.json</code>).</td></tr><tr><td><code>db</code></td><td>`{ driver: "sqlite" \</td><td>"postgres" \</td><td>"mysql" \</td><td>"mongo"; url: string }`</td><td>Default database connection; reached with <code>getDb()</code>.</td></tr><tr><td><code>databases</code></td><td><code>Record<string, { driver; url }></code></td><td>Additional named databases, reached with <code>getDb("<name>")</code>; each has its own <code>app/db/<name>/</code> migrations/queries.</td></tr><tr><td><code>realtime</code></td><td><code>{ scale?: boolean; redisUrl?: string }</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<string, Partial<Omit<AppConfig, "profiles">>></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=<name></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?) => 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) => Promise<string></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 === "production"</code>.</td></tr><tr><td><code>renderStyles</code></td><td><code>(ctx: StyleProcessContext, styles?: StylesConfig) => Promise<string></code></td><td>Produce final CSS: runs <code>styles.process(ctx)</code> if provided, else <code>bundleCss</code>. Returns <code>""</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 {
|
||||
/** 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) => string | Promise<string>;
|
||||
}
|
||||
|
||||
interface StyleProcessContext {
|
||||
entryPath: string | null; // resolved absolute CSS entry, or null
|
||||
appDir: string;
|
||||
appRoot: string;
|
||||
mode: Mode; // "development" | "production"
|
||||
}</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<string, ThemeTokens></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>"wire-theme"</code></td><td>Cookie the resolved theme is read from / persisted to.</td></tr><tr><td><code>THEME_CSS_HREF</code></td><td><code>"/__wrnexus/theme.css"</code></td><td>URL the generated theme stylesheet is served at.</td></tr><tr><td><code>THEME_JS_HREF</code></td><td><code>"/__wrnexus/theme.js"</code></td><td>URL the client theme runtime is served at.</td></tr><tr><td><code>resolveThemeConfig</code></td><td><code>(config?: ThemeConfig) => 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) => 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) => string</code></td><td>Generate the theme stylesheet: a <code>:root{…}</code> default plus one <code>[data-theme="<name>"]{…}</code> block per theme.</td></tr><tr><td><code>renderThemeRuntime</code></td><td><code>(theme: ResolvedTheme) => string</code></td><td>Generate the client runtime (see below).</td></tr></tbody></table></div>
|
||||
<p>Tokens are emitted as <code>--wire-<key></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<string, string>;
|
||||
|
||||
interface ThemeConfig {
|
||||
default?: string; // theme used when no cookie is present
|
||||
themes?: Record<string, ThemeTokens>; // deep-merged over built-in light/dark
|
||||
}
|
||||
|
||||
interface ResolvedTheme {
|
||||
default: string;
|
||||
names: string[];
|
||||
themes: Record<string, ThemeTokens>;
|
||||
}</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>{ get, set, toggle, bind, themes }</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 { AppConfig } from "@wrnexus/styles";
|
||||
|
||||
export default {
|
||||
head: [
|
||||
'<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5/dist/css/bootstrap.min.css">',
|
||||
],
|
||||
port: 3000,
|
||||
db: { driver: "sqlite", url: "app.db" },
|
||||
theme: {
|
||||
default: "dark",
|
||||
themes: {
|
||||
light: { "color-primary": "#7c3aed" }, // override one token; rest inherited
|
||||
brand: {
|
||||
// add a whole new theme
|
||||
"color-scheme": "dark",
|
||||
"color-bg": "#0a0a0a",
|
||||
"color-primary": "#22d3ee",
|
||||
},
|
||||
},
|
||||
},
|
||||
styles: {
|
||||
entry: "app/styles/main.css",
|
||||
},
|
||||
profiles: {
|
||||
production: {
|
||||
db: { driver: "postgres", url: process.env.DATABASE_URL! },
|
||||
},
|
||||
},
|
||||
} satisfies AppConfig;</code></pre>
|
||||
<h4 id="loading-config-producing-css">Loading config + producing CSS</h4>
|
||||
<pre data-language="ts"><code>import {
|
||||
loadAppConfig,
|
||||
resolveProfile,
|
||||
loadEnv,
|
||||
findStyleEntry,
|
||||
renderStyles,
|
||||
} from "@wrnexus/styles";
|
||||
|
||||
const appRoot = process.cwd();
|
||||
const mode = "production" as const;
|
||||
|
||||
const profile = resolveProfile({ mode });
|
||||
loadEnv(appRoot, profile);
|
||||
|
||||
const config = await loadAppConfig(appRoot, profile);
|
||||
|
||||
const appDir = `${appRoot}/app`;
|
||||
const entryPath = findStyleEntry(appDir, appRoot, config.styles?.entry);
|
||||
const css = await renderStyles({ entryPath, appDir, appRoot, mode }, config.styles);</code></pre>
|
||||
<h4 id="rendering-the-theme">Rendering the theme</h4>
|
||||
<pre data-language="ts"><code>import {
|
||||
resolveThemeConfig,
|
||||
resolveThemeName,
|
||||
renderThemeCss,
|
||||
renderThemeRuntime,
|
||||
THEME_COOKIE,
|
||||
} from "@wrnexus/styles";
|
||||
|
||||
const theme = resolveThemeConfig(config.theme);
|
||||
|
||||
// Server: pick the active theme from the request cookie (no flash).
|
||||
const active = resolveThemeName(cookies[THEME_COOKIE], theme);
|
||||
// → render <html data-theme={active}>
|
||||
|
||||
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 {
|
||||
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);
|
||||
}</code></pre>
|
||||
<pre data-language="html"><code><button data-wire-theme-toggle>Toggle theme</button>
|
||||
<button data-wire-theme-set="brand">Brand theme</button></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 is generated from the exact published package and lists its exported functions, classes, interfaces, and types.</p><pre data-language="typescript"><code>import { SeoConfig, SecurityConfig } from '@wrnexus/core';
|
||||
import { StorageConfig } from '@wrnexus/uploader';
|
||||
|
||||
/**
|
||||
* Theme system — design tokens that work SSR and client-side.
|
||||
*
|
||||
* Tokens are plain CSS custom properties (`--wire-<key>`) 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 `<html data-theme="…">` 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<string, string>;
|
||||
interface ThemeConfig {
|
||||
/** 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<string, ThemeTokens>;
|
||||
}
|
||||
interface ResolvedTheme {
|
||||
default: string;
|
||||
names: string[];
|
||||
themes: Record<string, ThemeTokens>;
|
||||
}
|
||||
/** Cookie the resolved theme is read from / persisted to. */
|
||||
declare const THEME_COOKIE = "wire-theme";
|
||||
declare const THEME_CSS_HREF = "/__wrnexus/theme.css";
|
||||
declare const THEME_JS_HREF = "/__wrnexus/theme.js";
|
||||
/** Built-in themes so components have tokens out of the box. */
|
||||
declare const DEFAULT_THEMES: Record<string, ThemeTokens>;
|
||||
/** 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 `<head>` 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 `<link rel=preload>`
|
||||
* 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 = "auto" | "block" | "swap" | "fallback" | "optional";
|
||||
interface GoogleFont {
|
||||
/** Family name as it appears on fonts.google.com, e.g. "Inter". */
|
||||
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;
|
||||
}
|
||||
interface LocalFontFace {
|
||||
/** `font-family` name this face defines. */
|
||||
family: string;
|
||||
/** URL to the font file, typically served from `public/` (e.g. "/fonts/inter.woff2"). */
|
||||
src: string;
|
||||
/** e.g. 400, "700", or "100 900" for a variable font. Default: 400. */
|
||||
weight?: number | string;
|
||||
style?: "normal" | "italic";
|
||||
/** CSS `src` format; inferred from the file extension when omitted. */
|
||||
format?: string;
|
||||
display?: FontDisplay;
|
||||
/** Emit `<link rel="preload" as="font">` — use for the primary above-the-fold face. */
|
||||
preload?: boolean;
|
||||
/** Optional `unicode-range` subset. */
|
||||
unicodeRange?: string;
|
||||
}
|
||||
interface FontConfig {
|
||||
/** 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: "swap". */
|
||||
display?: FontDisplay;
|
||||
/** Body / default family stack → `--wrn-font-sans` + `body { font-family }`. */
|
||||
sans?: string;
|
||||
/** Monospace family stack → `--wrn-font-mono`. */
|
||||
mono?: string;
|
||||
/** Serif family stack → `--wrn-font-serif`. */
|
||||
serif?: string;
|
||||
}
|
||||
/**
|
||||
* Render all `<head>` markup for a font config. Returns "" when nothing is
|
||||
* configured. The output is trusted, framework-controlled HTML.
|
||||
*/
|
||||
declare function renderFontHead(fonts?: FontConfig): string;
|
||||
/**
|
||||
* 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): {
|
||||
style: string[];
|
||||
font: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* App configuration loader (`wrnexus.config.ts`).
|
||||
*
|
||||
* The config is optional. It lets an app inject arbitrary `<head>` 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 = "development" | "production";
|
||||
interface StyleProcessContext {
|
||||
/** Resolved absolute path to the CSS entry, or null if there is none. */
|
||||
entryPath: string | null;
|
||||
appDir: string;
|
||||
appRoot: string;
|
||||
mode: Mode;
|
||||
}
|
||||
interface StylesConfig {
|
||||
/** 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) => string | Promise<string>;
|
||||
}
|
||||
interface MobileConfig {
|
||||
enabled?: boolean;
|
||||
/** Mobile renderer. `webview` uses Capacitor; `native` scaffolds an Expo/React Native app. */
|
||||
mode?: "webview" | "native";
|
||||
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<string, unknown>;
|
||||
/** Advanced CapacitorConfig fields merged into generated capacitor.config.ts. */
|
||||
capacitor?: Record<string, unknown>;
|
||||
}
|
||||
interface PwaScreenshot {
|
||||
src: string;
|
||||
sizes: string;
|
||||
type?: string;
|
||||
formFactor?: "wide" | "narrow";
|
||||
label?: string;
|
||||
}
|
||||
interface PwaShortcut {
|
||||
name: string;
|
||||
shortName?: string;
|
||||
description?: string;
|
||||
url: string;
|
||||
icons?: Array<{
|
||||
src: string;
|
||||
sizes: string;
|
||||
type?: string;
|
||||
purpose?: string;
|
||||
}>;
|
||||
}
|
||||
interface PwaConfig {
|
||||
enabled?: boolean;
|
||||
id?: string;
|
||||
name?: string;
|
||||
shortName?: string;
|
||||
description?: string;
|
||||
startUrl?: string;
|
||||
scope?: string;
|
||||
lang?: string;
|
||||
display?: "standalone" | "fullscreen" | "minimal-ui" | "browser";
|
||||
orientation?: "any" | "natural" | "landscape" | "landscape-primary" | "landscape-secondary" | "portrait" | "portrait-primary" | "portrait-secondary";
|
||||
themeColor?: string;
|
||||
backgroundColor?: string;
|
||||
icons?: Array<{
|
||||
src: string;
|
||||
sizes: string;
|
||||
type?: string;
|
||||
purpose?: string;
|
||||
}>;
|
||||
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;
|
||||
}
|
||||
interface AppConfig {
|
||||
/** Raw HTML appended to every page's `<head>` (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?: {
|
||||
default?: string;
|
||||
locales?: string[];
|
||||
};
|
||||
/** Default database connection (driver + url); reached with `getDb()`. */
|
||||
db?: {
|
||||
driver: "sqlite" | "postgres" | "mysql" | "mongo";
|
||||
url: string;
|
||||
};
|
||||
/**
|
||||
* 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: "public" | "private"`.
|
||||
*/
|
||||
storage?: StorageConfig;
|
||||
/**
|
||||
* Additional named databases, reached with `getDb("<name>")`. Each has its own
|
||||
* migrations/queries under `app/db/<name>/`. Connect to as many as you like and
|
||||
* read/write to any of them per request.
|
||||
*
|
||||
* databases: { analytics: { driver: "postgres", url: "…" } }
|
||||
*/
|
||||
databases?: Record<string, {
|
||||
driver: "sqlite" | "postgres" | "mysql" | "mongo";
|
||||
url: string;
|
||||
}>;
|
||||
/**
|
||||
* 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?: {
|
||||
scale?: boolean;
|
||||
redisUrl?: string;
|
||||
};
|
||||
/** 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=<name>` or the `WRNEXUS_PROFILE` env var.
|
||||
*/
|
||||
profiles?: Record<string, Partial<Omit<AppConfig, "profiles">>>;
|
||||
}
|
||||
/**
|
||||
* Resolve the active profile name: explicit argument > `WRNEXUS_PROFILE` env var
|
||||
* > a mode-based default ("production" in prod, else "development").
|
||||
*/
|
||||
declare function resolveProfile(options?: {
|
||||
explicit?: string;
|
||||
mode?: Mode;
|
||||
}): string;
|
||||
/** Load the raw `wrnexus.config.*` (with the `profiles` map intact), or `{}`. */
|
||||
declare function loadRawConfig(appRoot: string): Promise<AppConfig>;
|
||||
/** Load `wrnexus.config.*`, applying the active profile's overrides. */
|
||||
declare function loadAppConfig(appRoot: string, profile?: string): Promise<AppConfig>;
|
||||
/**
|
||||
* 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` < `.env.<profile>` < `.env.local` < `.env.<profile>.local`.
|
||||
* Returns the variables it loaded.
|
||||
*/
|
||||
declare function loadEnv(appRoot: string, profile: string): Record<string, string>;
|
||||
/** 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<string>;
|
||||
|
||||
/**
|
||||
* @wrnexus/styles — global stylesheet pipeline + app config.
|
||||
*
|
||||
* Works for SSR and CSR: the bundled stylesheet is `<link>`ed into every page's
|
||||
* `<head>`, 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<string>;
|
||||
|
||||
export { 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, renderStyles, renderThemeCss, renderThemeRuntime, resolveProfile, resolveThemeConfig, resolveThemeName };
|
||||
</code></pre></section><section id="examples" class="prose examples"><h2>Examples</h2><p>Copy-ready examples taken from this package's published documentation.</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 {
|
||||
/** 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) => string | Promise<string>;
|
||||
}
|
||||
|
||||
interface StyleProcessContext {
|
||||
entryPath: string | null; // resolved absolute CSS entry, or null
|
||||
appDir: string;
|
||||
appRoot: string;
|
||||
mode: Mode; // "development" | "production"
|
||||
}</code></pre></article><article class="example-card"><h3>Example 3</h3><pre data-language="ts"><code>type ThemeTokens = Record<string, string>;
|
||||
|
||||
interface ThemeConfig {
|
||||
default?: string; // theme used when no cookie is present
|
||||
themes?: Record<string, ThemeTokens>; // deep-merged over built-in light/dark
|
||||
}
|
||||
|
||||
interface ResolvedTheme {
|
||||
default: string;
|
||||
names: string[];
|
||||
themes: Record<string, ThemeTokens>;
|
||||
}</code></pre></article><article class="example-card"><h3>Example 4</h3><pre data-language="ts"><code>import type { AppConfig } from "@wrnexus/styles";
|
||||
|
||||
export default {
|
||||
head: [
|
||||
'<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5/dist/css/bootstrap.min.css">',
|
||||
],
|
||||
port: 3000,
|
||||
db: { driver: "sqlite", url: "app.db" },
|
||||
theme: {
|
||||
default: "dark",
|
||||
themes: {
|
||||
light: { "color-primary": "#7c3aed" }, // override one token; rest inherited
|
||||
brand: {
|
||||
// add a whole new theme
|
||||
"color-scheme": "dark",
|
||||
"color-bg": "#0a0a0a",
|
||||
"color-primary": "#22d3ee",
|
||||
},
|
||||
},
|
||||
},
|
||||
styles: {
|
||||
entry: "app/styles/main.css",
|
||||
},
|
||||
profiles: {
|
||||
production: {
|
||||
db: { driver: "postgres", url: process.env.DATABASE_URL! },
|
||||
},
|
||||
},
|
||||
} 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="#installation">Installation</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.12 · SSR-first · Bun-native · Documentation generated from published package APIs.</footer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user