release: WRNexusJS 0.3.0

This commit is contained in:
2026-07-22 17:29:08 +05:30
parent 13dfa31d19
commit 07d8fb59d6
145 changed files with 9664 additions and 3881 deletions
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dev-server",
"version": "0.2.79",
"version": "0.3.0",
"type": "module",
"main": "src/index.ts",
"exports": {
@@ -20,6 +20,7 @@
"@wrnexus/i18n": "workspace:*",
"@wrnexus/db": "workspace:*",
"@wrnexus/pubsub": "workspace:*",
"@wrnexus/uploader": "workspace:*"
"@wrnexus/uploader": "workspace:*",
"@wrnexus/plugin": "workspace:*"
}
}
+33 -1
View File
@@ -32,6 +32,8 @@ export { RESTART_EXIT_CODE } from "./restart.ts";
import { resetDevCache } from "./cache.ts";
import type { DevToolbarConfig } from "@wrnexus/dev-toolbar/types";
import { createPluginRunner, type PluginInput } from "@wrnexus/plugin";
import type { ObservabilityConfig, TenancyConfig } from "@wrnexus/styles";
import { createDevToolbarCollector, type DevToolbarCollector } from "@wrnexus/dev-toolbar/server";
@@ -67,6 +69,9 @@ export interface ServeOptions {
mobile?: MobileConfig;
pwa?: PwaConfig | false;
devToolbar?: boolean | DevToolbarConfig;
plugins?: PluginInput;
observability?: ObservabilityConfig;
tenancy?: TenancyConfig;
}
export interface RunningServer {
@@ -153,7 +158,20 @@ function resolveDevToolbarConfig(
export async function startServer(opts: ServeOptions): Promise<RunningServer> {
const appDir = resolve(opts.appDir);
const appRoot = dirname(appDir);
const mode: Mode = opts.mode ?? "development";
const pluginRunner = createPluginRunner(opts.plugins, {
root: appRoot,
mode,
command: "dev",
metadata: new Map(),
warn: (message) => console.warn(`[wrnexus:plugin] ${message}`),
});
await pluginRunner.configure(opts as unknown as Record<string, unknown>);
await pluginRunner.configResolved(
Object.freeze({ ...opts }) as Readonly<Record<string, unknown>>,
);
const hmr = opts.hmr ?? mode === "development";
const port = opts.port ?? 3000;
const hostname = opts.hostname ?? "::";
@@ -161,7 +179,6 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
const router = buildRouter(appDir, { componentDirs: [uiComponentsDir()] });
const styleEntry = opts.styleEntry ?? null;
const appRoot = dirname(appDir);
const devToolbarConfig = resolveDevToolbarConfig(mode, opts.devToolbar);
@@ -256,6 +273,8 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
mobile: opts.mobile,
pwa: opts.pwa,
security: opts.security,
observability: opts.observability,
tenancy: opts.tenancy,
hub,
realtimeBus: realtimeBusFromConfig(opts.realtime),
devToolbar:
@@ -278,6 +297,19 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
websocket: handlers.websocket,
});
try {
await pluginRunner.hook("configureServer", {
server,
router,
handlers,
assets,
devToolbarCollector,
});
} catch (error) {
server.stop();
throw error;
}
let watcher: ReturnType<typeof startWatcher>;
// In-process HMR: keep the server and socket alive, invalidate only changed
+8
View File
@@ -23,6 +23,8 @@ import {
type ResolvedTheme,
type MobileConfig,
type PwaConfig,
type ObservabilityConfig,
type TenancyConfig,
} from "@wrnexus/styles";
import { VALIDATE_RUNTIME } from "@wrnexus/validation";
import { I18N_RUNTIME, type ResolvedI18n } from "@wrnexus/i18n";
@@ -116,6 +118,10 @@ export interface ProdOptions {
pwa?: PwaConfig | false;
/** Framework security headers and CORS policy. */
security?: SecurityConfig;
/** Built-in request tracing and Server-Timing policy. */
observability?: ObservabilityConfig;
/** Built-in tenant identity resolution. */
tenancy?: TenancyConfig;
port?: number;
hostname?: string;
maxBodyBytes?: number;
@@ -305,6 +311,8 @@ export function createProductionHandlers(
mobile: opts.mobile,
pwa: opts.pwa,
security: opts.security,
observability: opts.observability,
tenancy: opts.tenancy,
maxBodyBytes: opts.maxBodyBytes,
realtimeBus: realtimeBusFromConfig(opts.realtime),
});
+74 -2
View File
@@ -23,6 +23,8 @@ import {
withContextHeaders,
withSecurityHeaders,
resolveRequestUrl,
tenantMiddleware,
tracingMiddleware,
type Context,
type Middleware,
type Mode,
@@ -43,6 +45,8 @@ import {
type ResolvedTheme,
type MobileConfig,
type PwaConfig,
type ObservabilityConfig,
type TenancyConfig,
} from "@wrnexus/styles";
import {
LANG_COOKIE,
@@ -129,6 +133,10 @@ export interface RuntimeDeps {
pwa?: PwaConfig | false;
/** Framework security headers and CORS policy. */
security?: SecurityConfig;
/** Built-in request tracing and Server-Timing policy. */
observability?: ObservabilityConfig;
/** Built-in tenant identity resolution. */
tenancy?: TenancyConfig;
/** Max request body size in bytes (413 above this). Default 10 MB. */
maxBodyBytes?: number;
/** HMR hub for browser live-update sockets (dev only). */
@@ -158,6 +166,65 @@ function shouldEnableDevToolbar(mode: string, deps: RuntimeDeps): boolean {
);
}
function tenantIdentityFromConfig(
config: TenancyConfig,
): (ctx: Context) => Promise<{ id: string; slug?: string } | null> {
return async (ctx) => {
const host = ctx.url.hostname.toLowerCase();
if (config.mode === "domain") return host ? { id: host, slug: host } : null;
if (config.mode === "path") {
const segments = ctx.url.pathname.split("/").filter(Boolean);
const prefix = config.pathPrefix?.replace(/^\/+|\/+$/g, "");
const slug = prefix ? (segments[0] === prefix ? segments[1] : undefined) : segments[0];
return slug ? { id: slug, slug } : null;
}
if (config.mode === "subdomain" || config.mode === undefined) {
const roots = config.rootDomains?.map((domain) => domain.toLowerCase()) ?? [];
const root = roots.find((domain) => host === domain || host.endsWith(`.${domain}`));
const slug = root ? host.slice(0, -(root.length + 1)) : host.split(".")[0];
if (!slug || slug === host || slug === "www" || slug === "localhost") return null;
return { id: slug, slug };
}
return null;
};
}
function frameworkMiddleware(deps: RuntimeDeps): Middleware[] {
const middleware: Middleware[] = [];
if (deps.observability && deps.observability.enabled !== false) {
middleware.push(
tracingMiddleware(undefined, {
sampleRate: deps.observability.sampleRate,
serverTiming: deps.observability.serverTiming,
onComplete:
deps.observability.exporter === "console"
? (ctx, records) => {
const total = records.find((record) => record.name === "http.request")?.durationMs;
console.log(
`[wrnexus:trace] ${ctx.req.method} ${ctx.url.pathname} ${total?.toFixed(2) ?? "0.00"}ms`,
);
}
: undefined,
}),
);
}
if (deps.tenancy && deps.tenancy.mode !== "custom") {
middleware.push(
tenantMiddleware(tenantIdentityFromConfig(deps.tenancy), {
required: deps.tenancy.required,
}),
);
}
return middleware;
}
function shouldReportNotFound(pathname: string): boolean {
return !(
pathname.startsWith("/__wrnexus/") ||
@@ -597,6 +664,11 @@ export interface Handlers {
/** Build the fetch + websocket handlers from a set of dependencies. */
export function createHandlers(deps: RuntimeDeps): Handlers {
const { mode, hmr, router, loadModule, getMiddleware, assets } = deps;
const builtInMiddleware = frameworkMiddleware(deps);
const resolveMiddleware = async (): Promise<Middleware[]> => [
...builtInMiddleware,
...(await getMiddleware()),
];
const maxBodyBytes = deps.maxBodyBytes ?? 10 * 1024 * 1024; // 10 MB default
// Server-side realtime room manager (shared by every `defineRoom` connection).
@@ -832,7 +904,7 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
);
ctx.t = makeT(deps.i18n, ctx.lang);
}
const mws = await getMiddleware();
const mws = await resolveMiddleware();
const res = secure(
withContextHeaders(ctx, await runMiddleware(mws, ctx, () => dispatch(ctx))),
);
@@ -1271,7 +1343,7 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
}
const res = withContextHeaders(
ctx,
await runMiddleware(await getMiddleware(), ctx, () => dispatch(ctx)),
await runMiddleware(await resolveMiddleware(), ctx, () => dispatch(ctx)),
);
const html = await res.text();
ws.send(JSON.stringify({ type: "html", html }));
+3
View File
@@ -43,6 +43,9 @@ const server = await startServer({
mobile: config.mobile,
pwa: config.pwa,
devToolbar: config.devToolbar,
plugins: config.plugins,
observability: config.observability,
tenancy: config.tenancy,
});
const r = server.router;