Files
WRNexusJS/.publish/styles
2026-07-15 18:54:29 +05:30
..
2026-07-15 18:54:29 +05:30
2026-07-12 15:55:18 +05:30

@wrnexus/styles

Global CSS bundling, the --wire-* design-token theme system, and the wrnexus.config.ts app-config loader for WrNexus apps.

Part of the WrNexus framework — an SSR-first, Bun-native full-stack web framework.

Overview

This package owns three server-side concerns that shape every page a WrNexus app renders:

  1. Global stylesheet pipeline — finds app/styles/global.css (or aggregates app/styles/*.css), bundles it with Bun's CSS bundler (which resolves @import, including from node_modules), and produces one stylesheet that is <link>ed into every page's <head>. Because it is a plain global sheet, it styles server-rendered markup and hydrated client islands identically. A custom process hook lets you swap in Tailwind / PostCSS / Sass.
  2. Theme system — design tokens exposed as CSS custom properties (--wire-<key>), with built-in light/dark sets, deep-merged user overrides, an SSR <html data-theme> render (no flash), and a tiny client runtime to toggle/persist the choice.
  3. App config — loads wrnexus.config.ts (the AppConfig type), applies named profile overrides, and loads the .env cascade.

It runs server-side / at build time. Reach for it when configuring an app, defining themes, or customising how global CSS is produced.

Installation

bun add @wrnexus/styles

Private package — the machine must be authenticated to the wrnexus npm org (a read token in ~/.npmrc). Requires Bun (Node is not supported).

API

Everything is exported from the package root (@wrnexus/styles).

Config loading

Export Signature Purpose
loadAppConfig (appRoot: string, profile?: string) => Promise<AppConfig> Load wrnexus.config.* with the active profile deep-merged in (profiles stripped from the result).
loadRawConfig (appRoot: string) => Promise<AppConfig> Load the raw config with the profiles map intact; returns {} if no config file exists.
resolveProfile (options?: { explicit?; mode? }) => string Resolve the active profile: explicit arg > WRNEXUS_PROFILE env var > mode-based default (production in prod, else development).
loadEnv (appRoot: string, profile: string) => Record<string, string> Load the .env cascade for a profile into process.env without clobbering real env vars. Returns what it loaded.
headToString (head?: string | string[]) => string Flatten AppConfig.head into a single HTML string.

Config file names probed, in order: wrnexus.config.ts, wrnexus.config.js, wrnexus.config.mjs.

.env cascade precedence (low → high): .env < .env.<profile> < .env.local < .env.<profile>.local. Variables already present in the real environment always win.

AppConfig

The type of the object your wrnexus.config.ts default-exports. Every field is optional.

Field Type Description
head string | string[] Raw HTML appended to every page's <head> (e.g. CDN stylesheet/script links).
seo SeoConfig Global SEO defaults, merged with each page's exported meta. (from @wrnexus/core)
security SecurityConfig Framework security headers and optional CORS policy. (from @wrnexus/core)
styles StylesConfig Global stylesheet pipeline config (see below).
theme ThemeConfig Design-token themes, deep-merged over the built-in light/dark.
i18n { default?: string; locales?: string[] } Default language + supported locales (strings live in app/locales/*.json).
db { driver: "sqlite" | "postgres" | "mysql" | "mongo"; url: string } Default database connection; reached with getDb().
databases Record<string, { driver; url }> Additional named databases, reached with getDb("<name>"); each has its own app/db/<name>/ migrations/queries.
realtime { scale?: boolean; redisUrl?: string } When scale is true (or redisUrl is set), room broadcasts bridge over Redis pub/sub so they reach clients on every app process.
port number Default server port.
profiles Record<string, Partial<Omit<AppConfig, "profiles">>> Named profiles (dev, prod, uat, test, …). The active profile's overrides are deep-merged over the base config. Selected via --profile=<name> or WRNEXUS_PROFILE.

Styles pipeline

Export Signature Purpose
findStyleEntry (appDir, appRoot, override?) => string | null Resolve the CSS entry: override (relative to appRoot) → app/styles/global.css → an aggregate of all app/styles/*.css (written to app/.wrnexus/styles-entry.css). null if the app has no styles.
bundleCss (entryPath: string, mode: Mode) => Promise<string> Bundle an entry with Bun.build (CSS bundler). Resolves @import (local + node_modules), handles nesting, minifies when mode === "production".
renderStyles (ctx: StyleProcessContext, styles?: StylesConfig) => Promise<string> Produce final CSS: runs styles.process(ctx) if provided, else bundleCss. Returns "" when ctx.entryPath is null.

StylesConfig:

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"
}

Theme system

Export Type / Signature Purpose
DEFAULT_THEMES Record<string, ThemeTokens> Built-in light and dark token maps.
THEME_COOKIE "wire-theme" Cookie the resolved theme is read from / persisted to.
THEME_CSS_HREF "/__wrnexus/theme.css" URL the generated theme stylesheet is served at.
THEME_JS_HREF "/__wrnexus/theme.js" URL the client theme runtime is served at.
resolveThemeConfig (config?: ThemeConfig) => ResolvedTheme Deep-merge the user's theme config over the defaults; pick the default theme (config's default if valid, else dark, else the first).
resolveThemeName (cookieValue: string | undefined, theme: ResolvedTheme) => string Pick a valid theme name from a cookie, falling back to theme.default.
renderThemeCss (theme: ResolvedTheme) => string Generate the theme stylesheet: a :root{…} default plus one [data-theme="<name>"]{…} block per theme.
renderThemeRuntime (theme: ResolvedTheme) => string Generate the client runtime (see below).

Tokens are emitted as --wire-<key> custom properties, except the reserved key color-scheme, which is emitted as the native color-scheme CSS property so form controls and scrollbars match the theme.

ThemeConfig / ThemeTokens / ResolvedTheme:

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>;
}

Built-in token keys (both light and dark): color-scheme, color-bg, color-surface, color-surface-2, color-text, color-muted, color-border, color-primary, color-primary-hover, color-primary-contrast, color-danger, color-success, color-warning, radius, radius-sm, font-sans, shadow-1.

The client runtime (renderThemeRuntime) exposes window.wireTheme with { get, set, toggle, bind, themes }, wires up any [data-wire-theme-toggle] and [data-wire-theme-set] elements on load, and persists the choice to the wire-theme cookie (max-age 1 year, samesite=lax). toggle() cycles through the configured theme names in order.

Usage

wrnexus.config.ts

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;

Loading config + producing CSS

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);

Rendering the theme

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

In templates, consume tokens via the custom properties:

.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);
}
<button data-wire-theme-toggle>Toggle theme</button>
<button data-wire-theme-set="brand">Brand theme</button>

Requirements / Notes

  • Bun-only. bundleCss uses Bun.build's CSS bundler for @import resolution, nesting, and minification. Node is not supported.
  • Config and env loading use node:fs / node:path / node:url and read from process.env.
  • Peer package: @wrnexus/core supplies the SeoConfig and SecurityConfig types referenced by AppConfig.
  • The bundled global stylesheet, the theme stylesheet (THEME_CSS_HREF), and the theme runtime (THEME_JS_HREF) are wired into pages by the framework's server; this package only produces their contents.