release: WRNexusJS 0.8.0
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-02 23:18:51 +05:30
parent 87507edf59
commit 586a6db8ff
625 changed files with 243608 additions and 11210 deletions
+21
View File
@@ -1,5 +1,26 @@
# @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:
```ts
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.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/styles",
"version": "0.7.0",
"version": "0.8.0",
"type": "module",
"main": "src/index.ts",
"exports": {
+59
View File
@@ -0,0 +1,59 @@
export const CURRENT_COMPATIBILITY_DATE = "2026-08-02";
export const CURRENT_FRAMEWORK_BEHAVIOUR = 1;
export interface CompatibilityPolicy {
compatibilityDate?: string;
frameworkBehaviour?: number;
}
export interface CompatibilityReport {
configuredDate?: string;
effectiveDate: string;
currentDate: string;
configuredBehaviour?: number;
effectiveBehaviour: number;
currentBehaviour: number;
needsUpgrade: boolean;
future: boolean;
messages: string[];
}
export function isCompatibilityDate(value: string): boolean {
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
const date = new Date(`${value}T00:00:00.000Z`);
return Number.isFinite(date.getTime()) && date.toISOString().slice(0, 10) === value;
}
export function resolveCompatibility(policy: CompatibilityPolicy): CompatibilityReport {
const configuredDate = policy.compatibilityDate;
const configuredBehaviour = policy.frameworkBehaviour;
const effectiveDate = configuredDate ?? "1970-01-01";
const effectiveBehaviour = configuredBehaviour ?? 0;
const future =
(configuredDate !== undefined && configuredDate > CURRENT_COMPATIBILITY_DATE) ||
(configuredBehaviour !== undefined && configuredBehaviour > CURRENT_FRAMEWORK_BEHAVIOUR);
const needsUpgrade =
!future &&
(effectiveDate < CURRENT_COMPATIBILITY_DATE ||
effectiveBehaviour < CURRENT_FRAMEWORK_BEHAVIOUR);
const messages: string[] = [];
if (!configuredDate) messages.push("compatibilityDate is not configured; legacy defaults apply.");
if (!configuredBehaviour)
messages.push("frameworkBehaviour is not configured; behaviour version 0 applies.");
if (future)
messages.push("Configuration targets framework behavior newer than this CLI supports.");
else if (needsUpgrade)
messages.push("A newer compatibility policy is available; review it before upgrading.");
else messages.push("Compatibility policy matches the current framework behavior.");
return {
configuredDate,
effectiveDate,
currentDate: CURRENT_COMPATIBILITY_DATE,
configuredBehaviour,
effectiveBehaviour,
currentBehaviour: CURRENT_FRAMEWORK_BEHAVIOUR,
needsUpgrade,
future,
messages,
};
}
+203 -10
View File
@@ -8,14 +8,20 @@
*/
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { dirname, extname, join, resolve } from "node:path";
import { createRequire } from "node:module";
import { pathToFileURL } from "node:url";
import type { PerformanceBudgets, SecurityConfig, SeoConfig } from "@wrnexus/core";
import type { PluginInput } from "@wrnexus/plugin";
import type { PluginInput, PluginPermission } from "@wrnexus/plugin";
import type { StorageConfig } from "@wrnexus/uploader";
import type { ThemeConfig } from "./theme.ts";
import type { FontConfig } from "./fonts.ts";
import { fontCspSources } from "./fonts.ts";
import {
isCompatibilityDate,
resolveCompatibility,
type CompatibilityPolicy,
} from "./compatibility.ts";
export type Mode = "development" | "production";
@@ -120,6 +126,15 @@ export interface PwaConfig {
cacheUrls?: string[];
/** Service-worker cache key. Change it to invalidate existing PWA caches. */
cacheName?: string;
/** Ordered URL rules for runtime caching. Patterns are regular-expression source strings. */
runtimeCaching?: Array<{
pattern: string;
strategy: "network-first" | "cache-first" | "stale-while-revalidate";
cacheName?: string;
methods?: string[];
}>;
/** Background Sync tag used by the offline mutation queue. */
backgroundSyncTag?: string;
}
export type DevToolbarPosition = "bottom-center" | "bottom-left" | "bottom-right";
@@ -229,9 +244,16 @@ export interface CompatibilityConfig {
stringLayouts?: boolean;
}
export interface AppConfig {
export interface AppConfig extends CompatibilityPolicy {
/** Ordered reusable configuration layers; the application always has final precedence. */
extends?: string | string[];
/** Compiler/dev/build plugins, resolved in deterministic pre/normal/post order. */
plugins?: PluginInput;
/** Optional least-privilege enforcement for automatically discovered packages. */
pluginPermissions?: {
enforce?: boolean;
grants?: Record<string, PluginPermission[]>;
};
/** WRN v0.6 explicit import and compatibility resolution. */
imports?: ImportsConfig;
/** TypeScript-backed .wrn type checking and declaration generation. */
@@ -276,7 +298,21 @@ export interface AppConfig {
/** 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[] };
i18n?: {
default?: string;
locales?: string[];
labels?: Record<string, string>;
fallbacks?: Record<string, string[]>;
direction?: Record<string, "ltr" | "rtl">;
cookie?: {
name?: string;
maxAge?: number;
path?: string;
sameSite?: "Strict" | "Lax" | "None";
secure?: boolean;
};
strict?: boolean;
};
/** Default database connection (driver + url); reached with `getDb()`. */
db?: { driver: "sqlite" | "postgres" | "mysql" | "mongo"; url: string };
/**
@@ -330,15 +366,116 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
}
/** Deep-merge `override` onto `base` (objects merge; arrays/scalars replace). */
function deepMerge<T>(base: T, override: unknown): T {
function deepMerge<T>(base: T, override: unknown, path = ""): T {
if (!isPlainObject(base) || !isPlainObject(override)) return (override ?? base) as T;
const out: Record<string, unknown> = { ...base };
for (const [key, value] of Object.entries(override)) {
out[key] = key in out ? deepMerge(out[key], value) : value;
const currentPath = path ? `${path}.${key}` : key;
if (
key in out &&
["plugins", "head"].includes(currentPath) &&
(Array.isArray(out[key]) || Array.isArray(value))
) {
const before = Array.isArray(out[key]) ? out[key] : out[key] == null ? [] : [out[key]];
const after = Array.isArray(value) ? value : value == null ? [] : [value];
out[key] = [...before, ...after];
} else {
out[key] = key in out ? deepMerge(out[key], value, currentPath) : value;
}
}
return out as T;
}
const LAYER_CONFIG_NAMES = [
"wrnexus.layer.ts",
"wrnexus.layer.js",
"wrnexus.layer.mjs",
...CONFIG_NAMES,
];
function layerSpecifiers(config: AppConfig): string[] {
return config.extends ? (Array.isArray(config.extends) ? config.extends : [config.extends]) : [];
}
function resolveLayerFile(specifier: string, declaringRoot: string, appRoot: string): string {
const local =
specifier.startsWith(".") || specifier.startsWith("/") || /^[A-Za-z]:[\\/]/.test(specifier);
if (local) {
const candidate = resolve(declaringRoot, specifier);
if (extname(candidate) && existsSync(candidate)) return candidate;
for (const name of LAYER_CONFIG_NAMES) {
const file = join(candidate, name);
if (existsSync(file)) return file;
}
throw new Error(`WRN-CONFIG-LAYER-NOT-FOUND: ${specifier} from ${declaringRoot}.`);
}
const require = createRequire(join(appRoot, "package.json"));
try {
const packageFile = require.resolve(`${specifier}/package.json`);
const packageRoot = dirname(packageFile);
const pkg = JSON.parse(readFileSync(packageFile, "utf8")) as {
wrnexus?: { layer?: string };
};
if (pkg.wrnexus?.layer) {
const file = resolve(packageRoot, pkg.wrnexus.layer);
if (existsSync(file)) return file;
}
for (const name of LAYER_CONFIG_NAMES) {
const file = join(packageRoot, name);
if (existsSync(file)) return file;
}
} catch (error) {
throw new Error(`WRN-CONFIG-LAYER-NOT-FOUND: package ${specifier} from ${appRoot}.`, {
cause: error,
});
}
throw new Error(`WRN-CONFIG-LAYER-ENTRY: ${specifier} has no wrnexus layer entry.`);
}
async function importConfigFile(file: string): Promise<AppConfig> {
const mod = (await import(pathToFileURL(file).href)) as { default?: AppConfig };
if (!mod.default || !isPlainObject(mod.default)) {
throw new Error(`WRN-CONFIG-LAYER-SHAPE: ${file} must export a configuration object.`);
}
return mod.default;
}
export interface ResolvedConfigLayers {
config: AppConfig;
sources: string[];
}
export async function resolveConfigLayers(
appRoot: string,
application: AppConfig,
): Promise<ResolvedConfigLayers> {
const sources: string[] = [];
const visiting: string[] = [];
const resolvedFiles = new Set<string>();
const visit = async (config: AppConfig, declaringRoot: string): Promise<AppConfig> => {
let merged: AppConfig = {};
for (const specifier of layerSpecifiers(config)) {
const file = resolveLayerFile(specifier, declaringRoot, appRoot);
if (visiting.includes(file)) {
throw new Error(`WRN-CONFIG-LAYER-CYCLE: ${[...visiting, file].join(" -> ")}`);
}
visiting.push(file);
const layer = await importConfigFile(file);
const resolvedLayer = await visit(layer, dirname(file));
visiting.pop();
merged = deepMerge(merged, resolvedLayer);
if (!resolvedFiles.has(file)) {
resolvedFiles.add(file);
sources.push(file);
}
}
const own = { ...config };
delete own.extends;
return deepMerge(merged, own);
};
return { config: await visit(application, appRoot), sources };
}
/** Load the raw `wrnexus.config.*` (with the `profiles` map intact), or `{}`. */
export async function loadRawConfig(appRoot: string): Promise<AppConfig> {
for (const name of CONFIG_NAMES) {
@@ -358,10 +495,12 @@ export async function loadAppConfig(appRoot: string, profile?: string): Promise<
// module evaluation. Load the profile cascade before importing the module so
// development and production behave consistently.
loadEnv(appRoot, active);
const base = await loadRawConfig(appRoot);
const raw = await loadRawConfig(appRoot);
const { config: base } = await resolveConfigLayers(appRoot, raw);
const override = base.profiles?.[active];
const merged: AppConfig = override ? deepMerge(base, override) : { ...base };
delete merged.profiles;
delete merged.extends;
applyFontCsp(merged);
const issues = validateAppConfig(merged);
const errors = issues.filter((issue) => issue.severity === "error");
@@ -451,14 +590,66 @@ export function defineConfig(config: AppConfig): AppConfig {
export function validateAppConfig(config: AppConfig): ConfigIssue[] {
const issues: ConfigIssue[] = [];
if (config.compatibilityDate !== undefined && !isCompatibilityDate(config.compatibilityDate)) {
issues.push({
path: "compatibilityDate",
severity: "error",
message: "must be a real ISO calendar date in YYYY-MM-DD format",
});
}
if (
config.frameworkBehaviour !== undefined &&
(!Number.isInteger(config.frameworkBehaviour) || config.frameworkBehaviour < 1)
) {
issues.push({
path: "frameworkBehaviour",
severity: "error",
message: "must be a positive integer",
});
}
const compatibility = resolveCompatibility(config);
if (compatibility.future) {
issues.push({
path: "compatibilityDate",
severity: "error",
message: "targets framework behavior newer than this version supports",
});
}
const sampleRate = config.observability?.sampleRate;
if (sampleRate !== undefined && (sampleRate < 0 || sampleRate > 1)) {
if (
sampleRate !== undefined &&
(!Number.isFinite(sampleRate) || sampleRate < 0 || sampleRate > 1)
) {
issues.push({
path: "observability.sampleRate",
severity: "error",
message: "must be between 0 and 1",
});
}
if (config.observability?.exporter === "otlp") {
const endpoint = config.observability.endpoint;
const validEndpoint = (() => {
try {
return Boolean(endpoint && ["http:", "https:"].includes(new URL(endpoint).protocol));
} catch {
return false;
}
})();
if (!validEndpoint) {
issues.push({
path: "observability.endpoint",
severity: "error",
message: "must be an absolute HTTP(S) URL when exporter is otlp",
});
}
}
if (config.observability?.serviceName !== undefined && !config.observability.serviceName.trim()) {
issues.push({
path: "observability.serviceName",
severity: "error",
message: "must not be empty",
});
}
const budgets = config.performance?.budgets;
if (budgets) {
for (const [name, value] of Object.entries(budgets)) {
@@ -494,7 +685,9 @@ export async function explainAppConfig(
): Promise<ExplainedConfig> {
const active = profile ?? resolveProfile();
const config = await loadAppConfig(appRoot, active);
const sources = CONFIG_NAMES.filter((name) => existsSync(join(appRoot, name)));
const appSources = CONFIG_NAMES.filter((name) => existsSync(join(appRoot, name)));
const raw = await loadRawConfig(appRoot);
const layerSources = (await resolveConfigLayers(appRoot, raw)).sources;
const envSources = [".env", ".env.local", `.env.${active}`, `.env.${active}.local`].filter(
(name) => existsSync(join(appRoot, name)),
);
@@ -502,7 +695,7 @@ export async function explainAppConfig(
profile: active,
config,
issues: validateAppConfig(config),
sources: [...sources, ...envSources],
sources: [...layerSources, ...appSources, ...envSources],
};
}
+13
View File
@@ -23,6 +23,7 @@ export type {
StylesConfig,
StyleProcessContext,
Mode,
ResolvedConfigLayers,
} from "./config.ts";
export {
defineConfig,
@@ -32,8 +33,16 @@ export {
loadEnv,
loadRawConfig,
resolveProfile,
resolveConfigLayers,
validateAppConfig,
} from "./config.ts";
export {
CURRENT_COMPATIBILITY_DATE,
CURRENT_FRAMEWORK_BEHAVIOUR,
isCompatibilityDate,
resolveCompatibility,
} from "./compatibility.ts";
export type { CompatibilityPolicy, CompatibilityReport } from "./compatibility.ts";
export { findStyleEntry, bundleCss } from "./styles.ts";
export type { FontConfig, GoogleFont, LocalFontFace, FontDisplay } from "./fonts.ts";
export { renderFontHead, renderProductionFontHead, fontCspSources } from "./fonts.ts";
@@ -43,6 +52,8 @@ export type {
ResolvedTheme,
ThemePaletteName,
CustomThemePalette,
ThemeToken,
ThemeSemanticColor,
} from "./theme.ts";
export {
DEFAULT_THEMES,
@@ -55,6 +66,8 @@ export {
resolveThemeName,
renderThemeCss,
renderThemeRuntime,
defineThemeTokens,
themeVar,
} from "./theme.ts";
import type { Mode, StyleProcessContext, StylesConfig } from "./config.ts";
+46 -1
View File
@@ -11,7 +11,52 @@
* emitted as the native CSS property instead of a custom property.
*/
export type ThemeTokens = Record<string, string>;
export type ThemeSemanticColor =
"primary" | "secondary" | "info" | "success" | "warning" | "danger" | "error";
export type ThemeToken =
| "color-scheme"
| "color-bg"
| "color-background"
| "color-foreground"
| "color-surface"
| "color-surface-2"
| "color-surface-raised"
| "color-surface-muted"
| "color-text"
| "color-text-muted"
| "color-text-subtle"
| "color-muted"
| "color-border"
| "color-border-strong"
| "color-code-background"
| "color-code-surface"
| "color-code-text"
| "color-code-muted"
| "color-code-border"
| `color-${ThemeSemanticColor}`
| `color-${ThemeSemanticColor}-${"hover" | "active" | "contrast" | "soft" | "muted" | "text"}`
| `color-on-${"primary" | "secondary"}`
| "radius"
| "radius-sm"
| "shadow-1"
| "shadow-sm"
| "shadow-md"
| "shadow-lg"
| "space-section"
| "space-section-sm"
| "container-max"
| "font-sans";
/** Known tokens get autocomplete while applications may add namespaced custom tokens. */
export type ThemeTokens = Partial<Record<ThemeToken, string>> & Record<string, string>;
export function defineThemeTokens<T extends ThemeTokens>(tokens: T): T {
return tokens;
}
export function themeVar(token: ThemeToken, fallback?: string): string {
return `var(--wire-${token}${fallback ? `, ${fallback}` : ""})`;
}
export const THEME_PALETTE_NAMES = [
"blue",
+110 -1
View File
@@ -2,12 +2,50 @@ import { test, expect, afterEach } from "bun:test";
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { loadAppConfig, resolveProfile, loadEnv, renderStyles } from "../src/index.ts";
import {
loadAppConfig,
resolveProfile,
loadEnv,
renderStyles,
resolveCompatibility,
validateAppConfig,
explainAppConfig,
} from "../src/index.ts";
afterEach(() => {
delete process.env.WRNEXUS_PROFILE;
});
test("compatibility dates pin behavior and reject invalid or future policies", () => {
expect(
resolveCompatibility({ compatibilityDate: "2026-08-02", frameworkBehaviour: 1 }),
).toMatchObject({ needsUpgrade: false, future: false, effectiveBehaviour: 1 });
expect(validateAppConfig({ compatibilityDate: "2026-02-31" })).toContainEqual(
expect.objectContaining({ path: "compatibilityDate", severity: "error" }),
);
expect(validateAppConfig({ frameworkBehaviour: 2 })).toContainEqual(
expect.objectContaining({ severity: "error" }),
);
});
test("observability config requires bounded sampling and a valid OTLP endpoint", () => {
expect(validateAppConfig({ observability: { sampleRate: Number.NaN } })).toContainEqual(
expect.objectContaining({ path: "observability.sampleRate", severity: "error" }),
);
expect(
validateAppConfig({ observability: { exporter: "otlp", endpoint: "collector:4318" } }),
).toContainEqual(expect.objectContaining({ path: "observability.endpoint", severity: "error" }));
expect(
validateAppConfig({
observability: {
exporter: "otlp",
endpoint: "https://collector.example/v1/traces",
serviceName: "api",
},
}),
).toEqual([]);
});
test("resolveProfile: explicit > WRNEXUS_PROFILE > mode default", () => {
delete process.env.WRNEXUS_PROFILE;
expect(resolveProfile({ mode: "development" })).toBe("development");
@@ -47,6 +85,77 @@ test("loadAppConfig deep-merges the active profile and strips `profiles`", async
expect(uat.db!.driver).toBe("sqlite");
});
test("config layers compose recursively with deterministic application precedence", async () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-layers-"));
try {
mkdirSync(join(root, "layers", "base"), { recursive: true });
mkdirSync(join(root, "layers", "company"), { recursive: true });
writeFileSync(
join(root, "layers", "base", "wrnexus.layer.mjs"),
`export default {
port: 1000, head: ["<meta name='base'>"], seo: { siteName: "Foundation", title: "Base" },
profiles: { production: { port: 7000 } }
}`,
);
writeFileSync(
join(root, "layers", "company", "wrnexus.layer.mjs"),
`export default {
extends: ["../base"], head: "<meta name='company'>", seo: { title: "Company" }
}`,
);
writeFileSync(
join(root, "wrnexus.config.mjs"),
`export default {
extends: ["./layers/company"], port: 3000, head: ["<meta name='app'>"], seo: { title: "App" },
profiles: { production: { port: 8000 } }
}`,
);
const config = await loadAppConfig(root, "production");
expect(config.port).toBe(8000);
expect((await loadAppConfig(root, "development")).port).toBe(3000);
expect(config.seo).toEqual({ siteName: "Foundation", title: "App" });
expect(config.head).toEqual([
"<meta name='base'>",
"<meta name='company'>",
"<meta name='app'>",
]);
expect(config.extends).toBeUndefined();
const explained = await explainAppConfig(root, "production");
expect(explained.sources.filter((source) => source.includes("wrnexus.layer")).length).toBe(2);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("config layers resolve packages and reject dependency cycles", async () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-package-layer-"));
try {
const pkg = join(root, "node_modules", "company-layer");
mkdirSync(pkg, { recursive: true });
writeFileSync(join(root, "package.json"), JSON.stringify({ name: "app" }));
writeFileSync(
join(pkg, "package.json"),
JSON.stringify({
name: "company-layer",
wrnexus: { layer: "./foundation.mjs" },
}),
);
writeFileSync(join(pkg, "foundation.mjs"), `export default { port: 4400 }`);
writeFileSync(join(root, "wrnexus.config.mjs"), `export default { extends: "company-layer" }`);
expect((await loadAppConfig(root)).port).toBe(4400);
const cycle = join(root, "cycle");
mkdirSync(cycle);
writeFileSync(join(cycle, "a.mjs"), `export default { extends: "./b.mjs" }`);
writeFileSync(join(cycle, "b.mjs"), `export default { extends: "./a.mjs" }`);
writeFileSync(join(cycle, "wrnexus.config.mjs"), `export default { extends: "./a.mjs" }`);
await expect(loadAppConfig(cycle)).rejects.toThrow("WRN-CONFIG-LAYER-CYCLE");
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("loadEnv layers .env files by precedence; real env always wins", () => {
const dir = mkdtempSync(join(tmpdir(), "wire-env-"));
writeFileSync(join(dir, ".env"), 'BASE=1\nSHARED=base\n# a comment\nQUOTED="hi there"\n');
+12
View File
@@ -0,0 +1,12 @@
import { expect, test } from "bun:test";
import { defineThemeTokens, themeVar } from "../src/index.ts";
test("typed theme token helpers preserve custom tokens and emit CSS variables", () => {
const tokens = defineThemeTokens({
"color-primary": "#2563eb",
"space-product-card": "1rem",
});
expect(tokens["space-product-card"]).toBe("1rem");
expect(themeVar("color-primary")).toBe("var(--wire-color-primary)");
expect(themeVar("color-text", "#111")).toBe("var(--wire-color-text, #111)");
});