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
+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],
};
}