Files
WRNexusJS/packages/styles
Clintchiz fed1d5d3f4
Quality / quality (ubuntu-latest) (push) Failing after 22s
Quality / quality (windows-latest) (push) Canceled after 0s
perf: omit unused UI CSS and minify final bundles
2026-08-03 01:03:33 +05:30
..
2026-08-02 23:18:51 +05:30
2026-08-02 23:18:51 +05:30

@wrnexus/styles

Reusable layers and presets

Compose local or package foundations in order; later layers override earlier ones and the application has final base-config precedence:

export default defineConfig({
  extends: ["@workroot/wrnexus-enterprise", "./layers/company"],
  profiles: { production: { port: 8080 } },
});

A directory layer exports wrnexus.layer.ts (JavaScript/MJS are supported). A package can provide that conventional file or declare wrnexus.layer in its package.json. Layers may extend other layers and carry the complete app configuration, including plugins that contribute layouts, components, routes, middleware, and migrations. plugins and head compose; other arrays intentionally replace earlier values. Cycles and missing/invalid entries fail with stable WRN-CONFIG-LAYER-* diagnostics. wrnexus config --explain lists every resolved layer source.

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 {
  palette?: ThemePaletteName | CustomThemePalette;
  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 palettes are blue, indigo, violet, emerald, cyan, rose, amber, and slate. Each supplies primary, secondary, info, success, warning, danger/error, hover, and contrast colors to every light/dark theme. Built-in token keys also include surfaces, text, borders, radii, fonts, and shadows.

A custom palette is intentionally complete, so components never fall back to an unrelated blue status or action color:

theme: {
  palette: {
    primary: "#7c3aed",
    primaryHover: "#6d28d9",
    primaryContrast: "#ffffff",
    secondary: "#db2777",
    secondaryHover: "#be185d",
    secondaryContrast: "#ffffff",
    info: "#2563eb",
    success: "#059669",
    warning: "#d97706",
    danger: "#dc2626",
  },
}

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: {
    palette: "violet",
    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.