release: WRNexusJS 0.5.0

This commit is contained in:
2026-07-29 12:51:10 +05:30
parent 76c768099d
commit 6afe32f63f
456 changed files with 40879 additions and 8850 deletions
+5 -2
View File
@@ -65,7 +65,7 @@ export function createDevAssetServer(
mode: Mode,
styles?: DevStyles,
theme?: ResolvedTheme,
uiCss?: string,
uiCss?: string | (() => string),
schemasJs?: string,
pluginAssets: readonly ServedPluginAsset[] = [],
): DevAssetServer {
@@ -96,7 +96,10 @@ export function createDevAssetServer(
if (pathname === "/__wrnexus/schemas.js") return jsResponse(schemasCode);
if (pathname === "/__wrnexus/ui.css") {
return uiCss ? cssResponse(uiCss) : new Response("Not Found", { status: 404 });
const currentUiCss = typeof uiCss === "function" ? uiCss() : uiCss;
return currentUiCss
? cssResponse(currentUiCss)
: new Response("Not Found", { status: 404 });
}
if (pathname === "/__wrnexus/theme.css") {
+65 -6
View File
@@ -6,6 +6,7 @@
* the running process while the HMR socket morphs fresh HTML into the browser.
*/
import { readFileSync } from "node:fs";
import { resolve, dirname, isAbsolute, join } from "node:path";
import type { Middleware, Mode, SecurityConfig, SeoConfig } from "@wrnexus/core";
import { buildRouter, type Router } from "@wrnexus/router";
@@ -16,7 +17,7 @@ import {
type MobileConfig,
type PwaConfig,
} from "@wrnexus/styles";
import { uiComponentsDir, uiCss } from "@wrnexus/ui";
import { uiComponentsDir, uiCssPath } from "@wrnexus/ui";
import { renderSchemasScript, type ObjectSchema, type SchemaDescriptor } from "@wrnexus/validation";
import { loadLocales, resolveI18n, type I18nConfig } from "@wrnexus/i18n";
import { applyMigrations, migrate, setDb, registerDb } from "@wrnexus/db";
@@ -46,6 +47,7 @@ export interface ServeOptions {
mode?: Mode;
/** Inject the live-reload client (defaults to true in development). */
hmr?: boolean;
appConfig?: Record<string, unknown>;
/** Resolved absolute path to the global CSS entry, or null. */
styleEntry?: string | null;
/** Custom styles config (e.g. a Tailwind/PostCSS processor). */
@@ -162,11 +164,14 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
const appDir = resolve(opts.appDir);
const appRoot = dirname(appDir);
const mode: Mode = opts.mode ?? "development";
const discoveredPlugins = await discoverPlugins(appRoot, opts.plugins, {
const configuredPlugins = opts.plugins ?? (opts.appConfig?.plugins as PluginInput | undefined);
const discoveredPlugins = await discoverPlugins(appRoot, configuredPlugins, {
includeDevDependencies: true,
strict: true,
warn: (message) => console.warn(`[wrnexus:plugin] ${message}`),
});
const pluginRunner = createPluginRunner(discoveredPlugins, {
root: appRoot,
mode,
@@ -174,11 +179,64 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
metadata: new Map(),
warn: (message) => console.warn(`[wrnexus:plugin] ${message}`),
});
await pluginRunner.configure(opts as unknown as Record<string, unknown>);
/**
* Plugins must receive the complete
* wrnexus.config.ts object.
*
* ServeOptions only contains framework-known
* properties. Package-specific configuration,
* such as `auth`, is stored in appConfig.
*/
const pluginConfig: Record<string, unknown> = {
...(opts.appConfig ?? {}),
// Resolved runtime values take priority.
appDir,
port: opts.port,
hostname: opts.hostname,
mode,
hmr: opts.hmr,
styleEntry: opts.styleEntry,
styles: opts.stylesConfig ?? (opts.appConfig?.styles as StylesConfig | undefined),
head: opts.head,
seo: opts.seo,
security: opts.security,
theme: opts.theme,
i18n: opts.i18n,
db: opts.db,
databases: opts.databases,
realtime: opts.realtime,
storage: opts.storage,
mobile: opts.mobile,
pwa: opts.pwa,
devToolbar: opts.devToolbar,
plugins: configuredPlugins,
observability: opts.observability,
tenancy: opts.tenancy,
};
await pluginRunner.configure(pluginConfig);
await pluginRunner.configResolved(
Object.freeze({ ...opts }) as Readonly<Record<string, unknown>>,
Object.freeze({
...pluginConfig,
}),
);
const pluginContributions = await pluginRunner.contributions();
console.log(
`[wrnexus:plugin] discovered: ${
pluginRunner.plugins.map((plugin) => plugin.name).join(", ") || "none"
}`,
);
console.log(`[wrnexus:plugin] contributed routes: ${pluginContributions.routes.length}`);
const pluginToolbarPanels = await pluginRunner.devToolbarPanels();
const componentDirs = [uiComponentsDir(), ...pluginContributions.componentDirs];
@@ -209,7 +267,7 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
// `.wrnexus/` next to each source file (and inside node_modules UI dirs).
setCompileCacheDir(join(appRoot, ".wrnexus"));
const theme = resolveThemeConfig(opts.theme);
const uiStyles = uiCss();
const uiStylesPath = uiCssPath();
const schemasJs = await schemaRuntime(router);
@@ -268,7 +326,7 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
entries: pluginContributions.styles.flatMap((style) => (style.entry ? [style.entry] : [])),
},
theme,
uiStyles,
() => readFileSync(uiStylesPath, "utf8"),
schemasJs,
pluginAssetsFromContributions(pluginContributions),
);
@@ -407,6 +465,7 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
});
};
const packageWatchDirs = [
dirname(uiStylesPath),
...componentDirs,
...pluginContributions.clientRuntimes.flatMap((runtime) =>
runtime.entry ? [dirname(runtime.entry)] : [],
+5 -1
View File
@@ -87,6 +87,7 @@ export function loadModule(file: string): Promise<Record<string, unknown>> {
* When unset, compilation falls back to a sibling `.wrnexus/` next to each file.
*/
let compileCacheDir: string | null = null;
const WRN_COMPILE_CACHE_VERSION = "v2";
/**
* Point all `.wrn` compilation at ONE cache dir (typically `<appRoot>/.wrnexus`)
@@ -124,7 +125,10 @@ function compileWireToTs(file: string, version = 0): string {
// checkouts, archive extraction, and linked dependencies can all replace a
// file while preserving (or moving backwards) its mtime. An mtime-only cache
// then serves an older compiled component even across a clean build.
const out = join(cacheDir, `${name}-${hashPath(file)}-${hashPath(source)}${suffix}.wrn.ts`);
const out = join(
cacheDir,
`${name}-${WRN_COMPILE_CACHE_VERSION}-${hashPath(file)}-${hashPath(source)}${suffix}.wrn.ts`,
);
// The content hash makes this safe even when source timestamps are preserved.
try {
+21 -4
View File
@@ -599,10 +599,20 @@ export const HMR_CLIENT_JS = `
// Minimal index-based DOM morph: preserve matching nodes (keeps state/focus),
// patch text and attributes, clone genuinely new nodes, drop removed ones.
// Hydrated subtrees (reactive scopes) are CLIENT-OWNED and left untouched,
// so live state (e.g. a counter at 5) is never reset to the SSR 0.
// Preserve a hydrated subtree only while its server hydration signature and
// behavior are unchanged. Component edits must replace and re-hydrate the
// old subtree or HMR will keep stale markup indefinitely.
function morph(from, to) {
if (from.__wrnexusHydrated) return;
if (from.__wrnexusHydrated) {
var sameHydration =
from.getAttribute("data-wrn-hydration") === to.getAttribute("data-wrn-hydration") &&
from.getAttribute("data-wrn-behavior") === to.getAttribute("data-wrn-behavior") &&
from.getAttribute("data-scope") === to.getAttribute("data-scope");
if (sameHydration) return;
if (window.__wrnexusDisposeBehaviors) window.__wrnexusDisposeBehaviors(from);
from.replaceWith(to.cloneNode(true));
return;
}
syncAttrs(from, to);
var fc = from.childNodes, tc = to.childNodes, i;
for (i = 0; i < tc.length; i++) {
@@ -1070,6 +1080,14 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
const matched = router.matchApi(ctx.url.pathname);
if (!matched) return Response.json({ error: "Not Found" }, { status: 404 });
// Expose the canonical matched route to package dispatchers. A package may
// contribute several URL paths from one module, and request URLs can be
// rewritten by gateways or internal framework calls. The router match is
// the authoritative route identity.
ctx.params = matched.params;
ctx.locals.__wrnexusRoute = matched.route.raw;
ctx.locals.__wrnexusRouteKind = "api";
const mod = await loadModule(matched.route.file);
const method = ctx.req.method.toUpperCase();
const embeddedApi = mod.__wrnexusApi as ApiRegistry | undefined;
@@ -1087,7 +1105,6 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
});
}
ctx.params = matched.params;
return (await handler(ctx)) as Response;
}
+56 -18
View File
@@ -9,29 +9,43 @@
*/
import { dirname } from "node:path";
import { startServer } from "./index.ts";
import { loadAppConfig, headToString, findStyleEntry, renderFontHead } from "@wrnexus/styles";
import type { Mode } from "@wrnexus/core";
import { findStyleEntry, headToString, loadAppConfig, renderFontHead } from "@wrnexus/styles";
import { startServer } from "./index.ts";
const [appDir, portStr, modeStr, hostname, hmrStr] = process.argv.slice(2);
const mode = (modeStr as Mode) || "development";
const port = Number(portStr) || 3000;
// Load optional wrnexus.config.ts (sits next to the app/ dir) + resolve styles.
const appRoot = dirname(appDir!);
if (!appDir) {
throw new Error("WRN-DEV-APP-DIR: app directory argument is required.");
}
// Load optional wrnexus.config.ts next to the app directory.
const appRoot = dirname(appDir);
const config = await loadAppConfig(appRoot);
const styleEntry = findStyleEntry(appDir!, appRoot, config.styles?.entry);
const styleEntry = findStyleEntry(appDir, appRoot, config.styles?.entry);
const server = await startServer({
appDir: appDir!,
appConfig: {
...config,
},
port,
hostname,
mode,
hmr: hmrStr === undefined ? undefined : hmrStr === "true",
styleEntry,
stylesConfig: config.styles,
head: [renderFontHead(config.fonts), headToString(config.head)].filter(Boolean).join("\n "),
seo: config.seo,
security: config.security,
theme: config.theme,
@@ -47,20 +61,44 @@ const server = await startServer({
observability: config.observability,
tenancy: config.tenancy,
});
const r = server.router;
const group = (label: string, items: { raw: string }[]) => {
if (!items.length) return;
console.log(` ${label}`);
for (const it of items) console.log(` ${it.raw}`);
};
const router = server.router;
interface PrintableRoute {
raw: string;
}
function printRouteGroup(label: string, items: readonly PrintableRoute[]): void {
const routes = Array.from(new Set(items.map((item) => item.raw.trim()).filter(Boolean))).sort(
(left, right) => left.localeCompare(right),
);
console.log(` ${label}: ${routes.length}`);
if (routes.length === 0) {
console.warn(` ⚠ No ${label.toLowerCase()} registered`);
return;
}
for (const route of routes) {
console.log(` ${route}`);
}
}
console.log(`\n ⚡ WrNexus — ${server.url}\n`);
group("Pages", r.pages);
group("API", r.api);
group("Realtime", r.realtime);
if (r.components.length) {
console.log(" Components");
for (const c of r.components) console.log(` ${c.name}`);
}
printRouteGroup("Pages", router.pages);
console.log("");
printRouteGroup("API Routes", router.api);
if (router.realtime.length > 0) {
console.log("");
printRouteGroup("Realtime Routes", router.realtime);
}
console.log(`\n Components: ${router.components.length}`);
console.log("");