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
+9 -1
View File
@@ -66,6 +66,12 @@ interface RunningServer {
In development, `startServer` also connects `app/db/migrations` (and `app/db/<name>/migrations`) and auto-applies migrations, then starts an in-process file watcher. CSS edits hot-swap live. Page, component, layout, API, middleware, realtime, schema, locale, and public-asset edits invalidate only their cached modules, rescan routes where necessary, and morph fresh HTML through the existing HMR WebSocket. The server process and active gateway stay running.
`getWrnCompileMetrics()` exposes cumulative content-addressed compiler cache
`hits`, `misses`, successful `compilations`, `errors`, `totalDurationMs`, and
`lastDurationMs` for the DevToolbar or custom diagnostics. Tests and embedded
servers can call `resetWrnCompileMetrics()` to establish a fresh measurement
window.
### `createHandlers(deps)`
The core runtime shared by dev and prod. It handles CORS preflight, `/healthz` and `/__wrnexus/health`, request-body size limits (413), HMR socket upgrades (`/__wrnexus/hmr`), realtime WebSocket upgrades (`defineRoom` default export or a raw `websocket` export), the middleware pipeline, API routes (`/api/*`), framework assets (`/__wrnexus/*`), public assets, and full SSR page rendering (component mounts, layouts, slots, i18n markers, per-page script selection, ETag/304, gzip).
@@ -296,7 +302,9 @@ Pages get only the scripts they use: `nav.js` always, `reactive.js` when a page
- **Bun-only.** Uses `Bun.serve` (HTTP + WebSocket), `Bun.file`, and `Bun.gzipSync`. The full app also relies on `bun:sqlite` / `Bun.SQL` via `@wrnexus/db`.
- Orchestrates the whole framework: `@wrnexus/core` (context, security, realtime registry), `@wrnexus/router`, `@wrnexus/ssr` (`renderDocument`), `@wrnexus/csr` (client runtimes), `@wrnexus/compiler` (`.wrn` → TS), `@wrnexus/styles`, `@wrnexus/ui`, `@wrnexus/validation`, `@wrnexus/i18n`, `@wrnexus/db`, and `@wrnexus/pubsub` (Redis-backed cross-process realtime).
- `.wrn` files are compiled to TypeScript into a hidden sibling `.wrnexus/` cache dir and dynamically imported; the module cache means each edited server module needs a fresh process (dev) — hence the restart-on-change model.
- `.wrn` files compile into a content-addressed hidden `.wrnexus/` cache. Targeted
invalidation gives changed modules a fresh import identity without restarting
the development server.
- Responses are gzipped when the client accepts it and the body is a buffered, compressible payload ≥ 1 KB; streaming/SSE responses opt out via `Cache-Control: no-transform`.
</content>
+4 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@wrnexus/dev-server",
"version": "0.7.0",
"version": "0.8.0",
"type": "module",
"main": "src/index.ts",
"exports": {
@@ -24,6 +24,8 @@
"@wrnexus/plugin": "workspace:*",
"@wrnexus/store": "workspace:*",
"@wrnexus/security": "workspace:*",
"@wrnexus/observability": "workspace:*"
"@wrnexus/observability": "workspace:*",
"@wrnexus/cache": "workspace:*",
"@wrnexus/pwa": "workspace:*"
}
}
+7 -1
View File
@@ -10,7 +10,12 @@
* invalidated in-process by the file watcher so edits show without a restart.
*/
import { getReactiveRuntime, getNavRuntime, getRealtimeRuntime } from "@wrnexus/csr";
import {
getActionRuntime,
getReactiveRuntime,
getNavRuntime,
getRealtimeRuntime,
} from "@wrnexus/csr";
import {
renderStyles,
renderThemeCss,
@@ -91,6 +96,7 @@ export function createDevAssetServer(
if (pathname === "/__wrnexus/reactive.js") return jsResponse(getReactiveRuntime());
if (pathname === "/__wrnexus/nav.js") return jsResponse(getNavRuntime());
if (pathname === "/__wrnexus/realtime.js") return jsResponse(getRealtimeRuntime());
if (pathname === "/__wrnexus/actions.js") return jsResponse(getActionRuntime());
if (pathname === "/__wrnexus/validate.js") return jsResponse(VALIDATE_RUNTIME);
if (pathname === "/__wrnexus/i18n.js") return jsResponse(I18N_RUNTIME);
if (pathname === UPLOAD_JS_HREF) return jsResponse(UPLOAD_RUNTIME);
+103 -39
View File
@@ -6,7 +6,7 @@
* the running process while the HMR socket morphs fresh HTML into the browser.
*/
import { readFileSync } from "node:fs";
import { mkdirSync, readFileSync, writeFileSync } 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";
@@ -28,6 +28,7 @@ import {
setDb,
registerDb,
registerLazyDb,
getDbPerformanceSnapshot,
} from "@wrnexus/db";
import { connectFromConfig } from "@wrnexus/db/connect";
import { configureStorage, type StorageConfig } from "@wrnexus/uploader";
@@ -38,6 +39,7 @@ import {
loadWrnServerModule,
setCompileCacheDir,
setCompileImportOptions,
setDevCompilerPipeline,
wrnBrowserArtifactUrl,
} from "./pipeline.ts";
import { createRpcHandler } from "@wrnexus/ssr/rpc";
@@ -48,18 +50,27 @@ import { resolvePackageMigrations } from "./plugin-migrations.ts";
import { HmrHub } from "./hmr.ts";
import { startWatcher } from "./watch.ts";
export { RESTART_EXIT_CODE } from "./restart.ts";
export { expandStaticComponents, precomputePartialStaticShell } from "./partial-build.ts";
export { getWrnCompileMetrics, resetWrnCompileMetrics } from "./pipeline.ts";
export type { WrnCompileMetrics } from "./pipeline.ts";
import { resetDevCache } from "./cache.ts";
import type { DevToolbarConfig } from "@wrnexus/dev-toolbar/types";
import { createPluginRunner, discoverPlugins, type PluginInput } from "@wrnexus/plugin";
import type { ObservabilityConfig, TenancyConfig } from "@wrnexus/styles";
import { createDevToolbarCollector, type DevToolbarCollector } from "@wrnexus/dev-toolbar/server";
import {
builtinDevToolbarPanels,
createDevToolbarCollector,
type DevToolbarCollector,
} from "@wrnexus/dev-toolbar/server";
export interface ServeOptions {
appDir: string;
port?: number;
hostname?: string;
/** Development TLS material. Production TLS is normally terminated by the deployment proxy. */
tls?: { cert: string; key: string };
mode?: Mode;
/** Inject the live-reload client (defaults to true in development). */
hmr?: boolean;
@@ -193,6 +204,12 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
includeDevDependencies: true,
strict: true,
warn: (message) => console.warn(`[wrnexus:plugin] ${message}`),
enforcePermissions: (opts.appConfig?.pluginPermissions as { enforce?: boolean } | undefined)
?.enforce,
grantedPermissions: (
opts.appConfig?.pluginPermissions as
{ grants?: Record<string, import("@wrnexus/plugin").PluginPermission[]> } | undefined
)?.grants,
});
const pluginRunner = createPluginRunner(discoveredPlugins, {
@@ -252,6 +269,30 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
);
const pluginContributions = await pluginRunner.contributions();
const virtualModules = new Map<string, string>();
const virtualDir = join(appRoot, ".wrnexus", "virtual");
mkdirSync(virtualDir, { recursive: true });
for (const [index, module] of pluginContributions.virtualModules.entries()) {
const output = join(virtualDir, `plugin-${index}.ts`);
writeFileSync(
output,
await module.load({
root: appRoot,
mode,
command: "dev",
profile: process.env.WRNEXUS_PROFILE,
metadata: new Map(),
warn: (message) => console.warn(`[wrnexus:plugin] ${message}`),
}),
"utf8",
);
virtualModules.set(module.id, output);
}
setDevCompilerPipeline({
transformAst: (ast, file) => pluginRunner.transformAst(ast, file),
transformCode: (code, file) => pluginRunner.transformCode(code, file),
virtualModules,
});
console.log(
`[wrnexus:plugin] discovered: ${
@@ -296,7 +337,7 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
const schemasJs = await schemaRuntime(router);
// i18n is opt-in by the presence of app/locales/*.json.
const localeMessages = loadLocales(join(appDir, "locales"));
const localeMessages = loadLocales(join(appDir, "locales"), { strict: opts.i18n?.strict });
const i18n = Object.keys(localeMessages).length
? resolveI18n(localeMessages, opts.i18n)
: undefined;
@@ -369,6 +410,34 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
})
: undefined;
const middleware = middlewareLoader(router);
const toolbarPlatform = {
wrnexus060: {
clientFunctions: true,
serverFunctions: true,
sharedFunctions: true,
typedOutputs: true,
requestScopedStores: true,
},
plugins: pluginRunner.plugins.map((plugin) => ({ name: plugin.name, version: plugin.version })),
runtimes: pluginContributions.clientRuntimes.map((runtime) => ({
id: runtime.id,
publicPath: runtime.publicPath,
type: runtime.type,
load: runtime.load,
})),
assets: pluginContributions.assets.map((asset) => ({
id: asset.id,
publicPath: asset.publicPath,
contentType: asset.contentType,
})),
componentDirs,
styles: pluginContributions.styles,
routes: {
pages: router.pages.length,
api: router.api.length,
realtime: router.realtime.length,
},
};
const runtimeDeps = {
mode,
@@ -392,13 +461,14 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
clientRuntimes: pluginContributions.clientRuntimes,
hub,
realtimeBus: realtimeBusFromConfig(opts.realtime),
renderHtml: (html: string) => pluginRunner.render(html),
devToolbar:
devToolbarConfig && devToolbarCollector
? {
config: devToolbarConfig,
collector: devToolbarCollector,
root: appRoot,
panels: [
panels: () => [
{
id: "runtime",
title: "Runtime",
@@ -415,39 +485,23 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
"Global/page stores, safe client state, computed values, actions, persistence, and hydration.",
order: 20,
},
{
id: "cache",
title: "Cache",
icon: "layers",
description:
"Request, data, component, and page cache entries plus hit/miss history.",
order: 30,
},
...builtinDevToolbarPanels({
root: appRoot,
platform: toolbarPlatform,
database: getDbPerformanceSnapshot(),
version: { current: "0.8.0" },
}),
...pluginToolbarPanels,
],
platform: {
wrnexus060: {
clientFunctions: true,
serverFunctions: true,
sharedFunctions: true,
typedOutputs: true,
requestScopedStores: true,
},
plugins: pluginRunner.plugins.map((plugin) => ({
name: plugin.name,
version: plugin.version,
})),
runtimes: pluginContributions.clientRuntimes.map((runtime) => ({
id: runtime.id,
publicPath: runtime.publicPath,
type: runtime.type,
load: runtime.load,
})),
assets: pluginContributions.assets.map((asset) => ({
id: asset.id,
publicPath: asset.publicPath,
contentType: asset.contentType,
})),
componentDirs,
styles: pluginContributions.styles,
routes: {
pages: router.pages.length,
api: router.api.length,
realtime: router.realtime.length,
},
},
platform: toolbarPlatform,
}
: undefined,
};
@@ -501,6 +555,7 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
hostname,
development: mode === "development",
maxRequestBodySize: 10 * 1024 * 1024,
...(opts.tls ? { tls: opts.tls } : {}),
fetch(request, server) {
if (new URL(request.url).pathname === "/__wrnexus/rpc") return rpcHandler(request);
return handlers.fetch(request, server);
@@ -551,13 +606,21 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
assets.updateSchemas(await schemaRuntime(router));
}
if (appFiles.some((file) => file === "locales" || file.startsWith("locales/"))) {
const messages = loadLocales(join(appDir, "locales"));
runtimeDeps.i18n = Object.keys(messages).length
? resolveI18n(messages, opts.i18n)
: undefined;
try {
const messages = loadLocales(join(appDir, "locales"), { strict: opts.i18n?.strict });
runtimeDeps.i18n = Object.keys(messages).length
? resolveI18n(messages, opts.i18n)
: undefined;
} catch (error) {
console.warn(
"[wrnexus] locale hot update was incomplete; keeping the last valid bundle",
error,
);
}
}
console.log(`[wrnexus] hot update — ${files.join(", ")}`);
await pluginRunner.hook("hmrUpdate", files);
const storeUpdates: Array<{ name: string; url: string; kind: string }> = [];
for (const changed of files) {
const absolute = isAbsolute(changed) ? changed : resolve(appDir, changed);
@@ -634,6 +697,7 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
watcher?.close();
unsubscribeDevToolbar?.();
server.stop();
void pluginRunner.hook("shutdown");
},
};
}
+73
View File
@@ -0,0 +1,73 @@
import { partialPrerender } from "@wrnexus/ssr";
import {
fillSlots,
normalizeComponentName,
parseComponentProps,
readElementBody,
} from "./runtime.ts";
export interface PartialBuildModule {
default?: unknown;
render?: (props?: Record<string, unknown>) => string | Promise<string>;
layout?: string | { name?: string; render?: (props?: Record<string, unknown>) => string };
__wrnexusBuildStaticShell?: (ctx?: Record<string, unknown>) => string | Promise<string>;
}
export interface PartialBuildEntry {
name: string;
mod: PartialBuildModule;
}
const MOUNT_OPEN_RE =
/<([A-Za-z][A-Za-z0-9-]*)\b([^>]*?\bdata-component="([A-Za-z0-9_-]+)"[^>]*?)(\/?)>/;
/** Expand compiler component mounts at build time using only their pure render exports. */
export async function expandStaticComponents(
html: string,
components: readonly PartialBuildEntry[],
depth = 0,
): Promise<string> {
if (depth > 15) throw new Error("WRN-PARTIAL-STATIC-DEPTH: component nesting exceeds 15");
if (!html.includes("data-component=")) return html;
let output = "";
let cursor = 0;
for (;;) {
const match = MOUNT_OPEN_RE.exec(html.slice(cursor));
if (!match) return output + html.slice(cursor);
const start = cursor + match.index;
output += html.slice(cursor, start);
const [open, tag, attributes, name, selfClosing] = match;
const openEnd = start + open.length;
const body =
selfClosing === "/" ? { inner: "", end: openEnd } : readElementBody(html, tag!, openEnd);
const component = components.find(
(entry) => normalizeComponentName(entry.name) === normalizeComponentName(name!),
);
if (!component || typeof component.mod.render !== "function") {
throw new Error(`WRN-PARTIAL-STATIC-COMPONENT: '${name}' has no build-time renderer`);
}
const rendered = await component.mod.render(parseComponentProps(attributes!));
output += await expandStaticComponents(
fillSlots(String(rendered), body.inner),
components,
depth + 1,
);
cursor = body.end;
}
}
/** Produce the body shell stored in dist; dynamic region bodies are never evaluated here. */
export async function precomputePartialStaticShell(
page: PartialBuildModule,
components: readonly PartialBuildEntry[],
): Promise<{ shell: string; regions: number }> {
if (typeof page.__wrnexusBuildStaticShell !== "function") {
throw new Error("WRN-PARTIAL-STATIC-EXPORT: compiler did not emit a static-shell renderer");
}
const body = await expandStaticComponents(
String(await page.__wrnexusBuildStaticShell({})),
components,
);
const result = partialPrerender(body);
return { shell: result.shell, regions: result.regions.length };
}
+248 -55
View File
@@ -15,7 +15,13 @@ import {
existsSync,
} from "node:fs";
import { dirname, join, basename, extname, resolve } from "node:path";
import { compile, generateTargets, resolveWrnImports, type PageAst } from "@wrnexus/compiler";
import {
compile,
generate,
generateTargets,
resolveWrnImports,
type PageAst,
} from "@wrnexus/compiler";
import type { Context, Middleware } from "@wrnexus/core";
/**
@@ -60,6 +66,18 @@ interface CompileImportOptions {
const compileImportOptions = new Map<string, CompileImportOptions>();
const warnedImportDiagnostics = new Set<string>();
interface DevCompilerPipeline {
transformAst(ast: PageAst, file: string): Promise<PageAst>;
transformCode(code: string, file: string): Promise<string>;
virtualModules: Map<string, string>;
}
let devCompilerPipeline: DevCompilerPipeline | null = null;
/** Install the configured plugin compiler pipeline for development compilation. */
export function setDevCompilerPipeline(pipeline: DevCompilerPipeline | null): void {
devCompilerPipeline = pipeline;
}
export function setCompileImportOptions(
appRoot: string,
options: { mode?: ImportMode; aliases?: Record<string, string>; autoImport?: boolean } = {},
@@ -142,34 +160,93 @@ function rewriteArtifactImports(
return output;
}
export function loadModule(file: string): Promise<Record<string, unknown>> {
async function rewriteArtifactImportsAsync(
code: string,
ast: PageAst,
importer: string,
target: "main" | "server" | "browser",
): Promise<string> {
let output = code;
for (const [id, replacement] of devCompilerPipeline?.virtualModules ?? []) {
const escaped = id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
output = output.replace(
new RegExp(`(["'])${escaped}\\1`, "g"),
JSON.stringify(pathToFileURL(replacement).href),
);
}
if (!ast.structuredImports.length) return output;
const root = projectRootForFile(importer);
const importOptions = compileImportOptions.get(resolve(root)) ?? {
mode: "compatible" as const,
aliases: { "@": "./app" },
autoImport: true,
};
const resolved = resolveWrnImports(ast.structuredImports, importer, {
appRoot: root,
mode: importOptions.mode,
aliases: importOptions.aliases,
});
for (const entry of resolved) {
if (entry.diagnostic?.severity === "error") throw new Error(entry.diagnostic.message);
if (!entry.resolved || !entry.declaration.source) continue;
if (!entry.declaration.source.startsWith(".") && !entry.declaration.source.startsWith("@/"))
continue;
let replacement = entry.resolved;
if (replacement.endsWith(".wrn")) {
const dependencySource = readFileSync(replacement, "utf8");
const isStore = /\b(?:global|page)\s+store\s+[A-Za-z_$][\w$]*\s*\{/.test(dependencySource);
const dependency = await compileWireArtifactsAsync(
replacement,
moduleVersions.get(replacement) ?? 0,
);
if (target === "browser") {
if (!isStore) {
output = output.replace(entry.declaration.raw, "");
continue;
}
replacement = `/__wrnexus/client/${basename(dependency.browser).replace(/\.client\.mjs$/, ".mjs")}`;
} else replacement = target === "server" ? dependency.server : dependency.main;
}
const escaped = entry.declaration.source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const specifier = replacement.startsWith("/") ? replacement : pathToFileURL(replacement).href;
output = output.replace(new RegExp(`(["'])${escaped}\\1`, "g"), JSON.stringify(specifier));
}
return output;
}
export async function loadModule(file: string): Promise<Record<string, unknown>> {
let mod = moduleCache.get(file);
if (!mod) {
const version = moduleVersions.get(file) ?? 0;
// `.wrn` files are compiled to TypeScript first, then imported.
let target = file.endsWith(".wrn") ? compileWireArtifacts(file, version).main : file;
let temporary = false;
// Bun intentionally caches local TS/JS modules by filesystem path and ignores
// URL query strings. A short-lived versioned sibling keeps relative imports
// correct while giving the changed module a genuinely new import identity.
if (version && !file.endsWith(".wrn")) {
const extension = extname(file);
const stem = basename(file, extension);
target = join(dirname(file), `${stem}.wrnexus-hmr-${version}${extension}`);
copyFileSync(file, target);
temporary = true;
}
// pathToFileURL handles Windows drive letters and spaces correctly.
mod = import(pathToFileURL(target).href) as Promise<Record<string, unknown>>;
if (temporary) {
mod = mod.finally(() => {
try {
unlinkSync(target);
} catch {
/* best-effort cleanup after Bun has loaded the module */
}
});
}
mod = (async () => {
const version = moduleVersions.get(file) ?? 0;
// `.wrn` files are compiled to TypeScript first, then imported.
let target = file.endsWith(".wrn")
? (await compileWireArtifactsAsync(file, version)).main
: file;
let temporary = false;
// Bun intentionally caches local TS/JS modules by filesystem path and ignores
// URL query strings. A short-lived versioned sibling keeps relative imports
// correct while giving the changed module a genuinely new import identity.
if (version && !file.endsWith(".wrn")) {
const extension = extname(file);
const stem = basename(file, extension);
target = join(dirname(file), `${stem}.wrnexus-hmr-${version}${extension}`);
copyFileSync(file, target);
temporary = true;
}
// pathToFileURL handles Windows drive letters and spaces correctly.
let imported = import(pathToFileURL(target).href) as Promise<Record<string, unknown>>;
if (temporary) {
imported = imported.finally(() => {
try {
unlinkSync(target);
} catch {
/* best-effort cleanup after Bun has loaded the module */
}
});
}
return imported;
})();
moduleCache.set(file, mod);
}
return mod;
@@ -228,7 +305,18 @@ function validateConfiguredImports(source: string, ast: PageAst, file: string):
const usedComponents = new Set(
Array.from(source.matchAll(/<([A-Z][A-Za-z0-9_$]*)\b/g), (match) => match[1]!),
);
const missing = [...usedComponents].filter((name) => !imported.has(name));
const compilerBuiltins = new Set([
"Async",
"Component",
"Error",
"Loading",
"Portal",
"Success",
"Transition",
]);
const missing = [...usedComponents].filter(
(name) => !compilerBuiltins.has(name) && !imported.has(name),
);
if (ast.layoutIsSymbol && ast.layout && !imported.has(ast.layout)) missing.push(ast.layout);
if (!missing.length) return;
const unique = [...new Set(missing)];
@@ -250,6 +338,98 @@ export interface WrnCompileArtifacts {
rpc: string;
}
export interface WrnCompileMetrics {
hits: number;
misses: number;
compilations: number;
errors: number;
totalDurationMs: number;
lastDurationMs: number;
}
const compileMetrics: WrnCompileMetrics = {
hits: 0,
misses: 0,
compilations: 0,
errors: 0,
totalDurationMs: 0,
lastDurationMs: 0,
};
const asyncCompileInProgress = new Map<string, Promise<WrnCompileArtifacts>>();
export function compileWireArtifactsAsync(file: string, version = 0): Promise<WrnCompileArtifacts> {
if (!devCompilerPipeline) return Promise.resolve(compileWireArtifacts(file, version));
const key = `${file}:${version}`;
const active = asyncCompileInProgress.get(key);
if (active) return active;
const task = (async () => {
const cacheDir = compileCacheDir ?? join(dirname(file), ".wrnexus");
const name = basename(file).replace(/\.wrn$/, "");
const suffix = version ? `-hmr-${version}` : "";
const source = readFileSync(file, "utf8");
// Plugin output affects the artifact, so use a separate cache generation.
const stem = `${name}-${WRN_COMPILE_CACHE_VERSION}-plugin-${hashPath(file)}-${hashPath(source)}${suffix}`;
const artifacts: WrnCompileArtifacts = {
main: join(cacheDir, `${stem}.wrn.ts`),
browser: join(cacheDir, `${stem}.client.mjs`),
server: join(cacheDir, `${stem}.server.ts`),
declarations: join(cacheDir, `${stem}.d.ts`),
contract: join(cacheDir, `${stem}.contract.json`),
rpc: join(cacheDir, `${stem}.rpc.json`),
};
const result = compile(source, file);
validateConfiguredImports(source, result.ast, file);
const ast = await devCompilerPipeline!.transformAst(result.ast, file);
const targets = generateTargets(ast);
mkdirSync(cacheDir, { recursive: true });
const browserPath = `/__wrnexus/client/${stem}.mjs`;
const outputs = {
main: `// compiled from .wrn\n${generate(ast)}`.replaceAll(
"__WRNEXUS_CLIENT_MODULE__",
browserPath,
),
browser: targets.browser,
server: targets.server,
declarations: targets.declarations,
};
for (const target of ["main", "browser", "server"] as const) {
const rewritten = await rewriteArtifactImportsAsync(outputs[target], ast, file, target);
writeFileSync(
artifacts[target],
await devCompilerPipeline!.transformCode(rewritten, file),
"utf8",
);
}
writeFileSync(
artifacts.declarations,
await devCompilerPipeline!.transformCode(outputs.declarations, file),
"utf8",
);
writeFileSync(artifacts.contract, JSON.stringify(targets.contract, null, 2) + "\n", "utf8");
writeFileSync(artifacts.rpc, JSON.stringify(targets.rpc, null, 2) + "\n", "utf8");
browserArtifactPaths.set(browserPath, artifacts.browser);
compileMetrics.compilations++;
return artifacts;
})().finally(() => asyncCompileInProgress.delete(key));
asyncCompileInProgress.set(key, task);
return task;
}
export function getWrnCompileMetrics(): Readonly<WrnCompileMetrics> {
return { ...compileMetrics };
}
export function resetWrnCompileMetrics(): void {
Object.assign(compileMetrics, {
hits: 0,
misses: 0,
compilations: 0,
errors: 0,
totalDurationMs: 0,
lastDurationMs: 0,
});
}
export function compileWireArtifacts(file: string, version = 0): WrnCompileArtifacts {
const active = compileInProgress.get(file);
if (active) return active;
@@ -270,39 +450,52 @@ export function compileWireArtifacts(file: string, version = 0): WrnCompileArtif
try {
try {
if (Object.values(artifacts).every((path) => statSync(path).isFile())) {
compileMetrics.hits++;
browserArtifactPaths.set(`/__wrnexus/client/${stem}.mjs`, artifacts.browser);
return artifacts;
}
} catch {
// Compile missing artifact set below.
}
const result = compile(source, file);
validateConfiguredImports(source, result.ast, file);
const targets = generateTargets(result.ast);
mkdirSync(cacheDir, { recursive: true });
const browserPath = `/__wrnexus/client/${stem}.mjs`;
const mainCode = rewriteArtifactImports(
result.code.replaceAll("__WRNEXUS_CLIENT_MODULE__", browserPath),
result.ast,
file,
"main",
);
writeFileSync(artifacts.main, mainCode, "utf8");
writeFileSync(
artifacts.browser,
rewriteArtifactImports(targets.browser, result.ast, file, "browser"),
"utf8",
);
browserArtifactPaths.set(browserPath, artifacts.browser);
writeFileSync(
artifacts.server,
rewriteArtifactImports(targets.server, result.ast, file, "server"),
"utf8",
);
writeFileSync(artifacts.declarations, targets.declarations, "utf8");
writeFileSync(artifacts.contract, JSON.stringify(targets.contract, null, 2) + "\n", "utf8");
writeFileSync(artifacts.rpc, JSON.stringify(targets.rpc, null, 2) + "\n", "utf8");
return artifacts;
compileMetrics.misses++;
const started = performance.now();
try {
const result = compile(source, file);
validateConfiguredImports(source, result.ast, file);
const targets = generateTargets(result.ast);
mkdirSync(cacheDir, { recursive: true });
const browserPath = `/__wrnexus/client/${stem}.mjs`;
const mainCode = rewriteArtifactImports(
result.code.replaceAll("__WRNEXUS_CLIENT_MODULE__", browserPath),
result.ast,
file,
"main",
);
writeFileSync(artifacts.main, mainCode, "utf8");
writeFileSync(
artifacts.browser,
rewriteArtifactImports(targets.browser, result.ast, file, "browser"),
"utf8",
);
browserArtifactPaths.set(browserPath, artifacts.browser);
writeFileSync(
artifacts.server,
rewriteArtifactImports(targets.server, result.ast, file, "server"),
"utf8",
);
writeFileSync(artifacts.declarations, targets.declarations, "utf8");
writeFileSync(artifacts.contract, JSON.stringify(targets.contract, null, 2) + "\n", "utf8");
writeFileSync(artifacts.rpc, JSON.stringify(targets.rpc, null, 2) + "\n", "utf8");
compileMetrics.compilations++;
return artifacts;
} catch (error) {
compileMetrics.errors++;
throw error;
} finally {
const duration = performance.now() - started;
compileMetrics.lastDurationMs = duration;
compileMetrics.totalDurationMs += duration;
}
} finally {
compileInProgress.delete(file);
}
+7
View File
@@ -153,5 +153,12 @@ export function mergePluginAssets(
routes: [],
middleware: [],
migrations: [],
directives: [],
cliCommands: [],
virtualModules: [],
deploymentAdapters: [],
configSchemas: [],
documentation: [],
typeDefinitions: [],
});
}
+22 -4
View File
@@ -5,7 +5,8 @@
* `wrnexus build` generates an entry that statically imports every route and
* component module and hands them here as a manifest. We rebuild the (cheap)
* route-matching tables from the raw patterns and run the exact same request
* runtime as dev — just with production error pages and no live-reload client.
* runtime as dev. Normal preview/deploy output has no live-reload client; the
* supervised `dev --production-runtime` mode can explicitly enable it.
*/
import type { Middleware, Mode, SecurityConfig, SeoConfig } from "@wrnexus/core";
@@ -16,7 +17,12 @@ import {
type Route,
type Router,
} from "@wrnexus/router";
import { getReactiveRuntime, getNavRuntime, getRealtimeRuntime } from "@wrnexus/csr";
import {
getActionRuntime,
getReactiveRuntime,
getNavRuntime,
getRealtimeRuntime,
} from "@wrnexus/csr";
import {
loadEnv,
resolveProfile,
@@ -43,6 +49,7 @@ import { realtimeBusFromConfig } from "./realtime-bus.ts";
import { createHandlers, type AssetServer, type WsData } from "./runtime.ts";
import { servePublicAsset } from "./public.ts";
import type { ClientRuntimeDefinition } from "@wrnexus/plugin";
import { HmrHub } from "./hmr.ts";
type RouteModule = Record<string, unknown>;
@@ -51,6 +58,8 @@ export interface ManifestRoute {
raw: string;
/** The statically-imported route module. */
mod: RouteModule;
/** Body shell precomputed by `wrnexus build` for a partial-static page. */
staticShell?: string;
}
export interface ProdManifest {
@@ -139,6 +148,8 @@ export interface ProdOptions {
port?: number;
hostname?: string;
maxBodyBytes?: number;
/** Enable only for the CLI's supervised exact-production development mode. */
developmentRuntime?: boolean;
}
const MODE: Mode = "production";
@@ -181,7 +192,10 @@ function buildProdRouter(manifest: ProdManifest): {
const routes = entries.map((e): Route => {
const { regex, paramNames } = compileRoutePattern(e.raw);
// Use the raw pattern as a stable module key.
modules.set(e.raw, e.mod);
modules.set(
e.raw,
e.staticShell === undefined ? e.mod : { ...e.mod, __wrnexusStaticShell: e.staticShell },
);
return { raw: e.raw, file: e.raw, regex, paramNames };
});
return sortRoutes(routes);
@@ -236,6 +250,8 @@ function createProdAssetServer(opts: ProdOptions): AssetServer {
return new Response(getNavRuntime(), { headers: JS_HEADERS });
if (pathname === "/__wrnexus/realtime.js")
return new Response(getRealtimeRuntime(), { headers: JS_HEADERS });
if (pathname === "/__wrnexus/actions.js")
return new Response(getActionRuntime(), { headers: JS_HEADERS });
if (pathname === "/__wrnexus/validate.js")
return new Response(VALIDATE_RUNTIME, { headers: JS_HEADERS });
if (pathname === "/__wrnexus/i18n.js")
@@ -308,9 +324,11 @@ export function createProductionHandlers(
return mod;
};
const productionHmr = opts.developmentRuntime === true;
const handlers = createHandlers({
mode: MODE,
hmr: false,
hmr: productionHmr,
hub: productionHmr ? new HmrHub() : undefined,
router,
loadModule,
getMiddleware,
+482 -58
View File
@@ -14,6 +14,8 @@ import {
createCorsPreflightResponse,
createRealtimeRegistry,
csrfToken,
verifyCsrf,
escapeHtml,
etag,
isRoomDefinition,
isWebSocketOriginAllowed,
@@ -25,7 +27,7 @@ import {
withSecurityHeaders,
resolveRequestUrl,
tenantMiddleware,
tracingMiddleware,
HealthRegistry,
type Context,
type Middleware,
type Mode,
@@ -38,13 +40,23 @@ import {
} from "@wrnexus/core";
import { requestHardening } from "@wrnexus/security";
import {
createLivenessHandler,
createOtlpTraceExporter,
createReadinessHandler,
createWebVitalsHandler,
defaultMetrics,
metricsMiddleware,
traceMiddleware,
webVitalsClient,
} from "@wrnexus/observability";
import type { Router } from "@wrnexus/router";
import { renderDocument, type RenderScript, type ScriptAsset } from "@wrnexus/ssr";
import {
partialPrerender,
renderDocument,
streamPartialDocument,
type RenderScript,
type ScriptAsset,
} from "@wrnexus/ssr";
import {
disposeRequestStores,
renderStoreHydration,
@@ -52,6 +64,8 @@ import {
} from "@wrnexus/ssr/store-context";
import type { StoreDefinition } from "@wrnexus/store";
import type { ClientRuntimeDefinition } from "@wrnexus/plugin";
import { CacheCoordinator } from "@wrnexus/cache";
import { generateServiceWorker } from "@wrnexus/pwa";
import { runtimeScriptsForMarkup } from "./plugin-assets.ts";
import {
ACCENT_COOKIE,
@@ -67,8 +81,8 @@ import {
type TenancyConfig,
} from "@wrnexus/styles";
import {
LANG_COOKIE,
I18N_JS_HREF,
renderI18nData,
makeT,
resolveLang,
translateHtml,
@@ -106,6 +120,12 @@ export type WsData =
type RouteModule = Record<string, unknown>;
type ApiRegistry = Record<string, unknown>;
interface ActionEntry {
run: (input: unknown, ctx: Context) => unknown | Promise<unknown>;
schema?: {
parse(input: unknown): { ok: boolean; value: unknown; errors: Record<string, string> };
};
}
interface CsrBinding {
id: string;
method?: string;
@@ -161,6 +181,8 @@ export interface RuntimeDeps {
security?: SecurityConfig;
/** Built-in request tracing and Server-Timing policy. */
observability?: ObservabilityConfig;
/** Dependency health checks used by `/readyz` and `/__wrnexus/ready`. */
health?: HealthRegistry;
/** Built-in tenant identity resolution. */
tenancy?: TenancyConfig;
/** Max request body size in bytes (413 above this). Default 10 MB. */
@@ -173,13 +195,17 @@ export interface RuntimeDeps {
* bus (use the Redis pub/sub driver). Enables realtime across multiple apps.
*/
realtimeBus?: RealtimeBus;
/** Shared first-class data/component/page caches. */
cache?: CacheCoordinator;
/** Final document transform supplied by the plugin render lifecycle. */
renderHtml?: (html: string) => string | Promise<string>;
devToolbar?: {
config: DevToolbarConfig;
collector: DevToolbarCollector;
root: string;
platform?: DevToolbarPlatformSnapshot;
panels?: DevToolbarPanel[];
panels?: DevToolbarPanel[] | (() => DevToolbarPanel[] | Promise<DevToolbarPanel[]>);
};
}
@@ -230,24 +256,34 @@ function frameworkMiddleware(deps: RuntimeDeps): Middleware[] {
if (deps.observability && deps.observability.enabled !== false) {
middleware.push(metricsMiddleware({ registry: defaultMetrics, includePath: false }));
const traceExporter =
deps.observability.exporter === "otlp" && deps.observability.endpoint
? createOtlpTraceExporter(deps.observability.endpoint, {
serviceName: deps.observability.serviceName,
})
: undefined;
middleware.push(
tracingMiddleware(undefined, {
traceMiddleware({
serviceName: deps.observability.serviceName,
sampleRate: deps.observability.sampleRate,
serverTiming: deps.observability.serverTiming,
onComplete:
exporter: traceExporter,
onSpan:
deps.observability.exporter === "console"
? (ctx, records) => {
const total = records.find((record) => record.name === "http.request")?.durationMs;
? (span) => {
console.log(
`[wrnexus:trace] ${ctx.req.method} ${ctx.url.pathname} ${total?.toFixed(2) ?? "0.00"}ms`,
`[wrnexus:trace] ${span.name} ${span.durationMs.toFixed(2)}ms trace=${span.traceId}`,
);
}
: undefined,
onExportError(error) {
console.error("[wrnexus:trace] export failed", error);
},
}),
);
}
if (deps.tenancy && deps.tenancy.mode !== "custom") {
if (deps.tenancy && Object.keys(deps.tenancy).length > 0 && deps.tenancy.mode !== "custom") {
middleware.push(
tenantMiddleware(tenantIdentityFromConfig(deps.tenancy), {
required: deps.tenancy.required,
@@ -277,30 +313,20 @@ export const PWA_CLIENT = `if ("serviceWorker" in navigator) {
addEventListener("load", () => navigator.serviceWorker.register(swUrl).catch(() => {}));
}`;
export const PWA_DEV_CLEANUP_CLIENT = `if ("serviceWorker" in navigator && !sessionStorage.getItem("wrnexus-pwa-dev-cleaned")) {
sessionStorage.setItem("wrnexus-pwa-dev-cleaned", "1");
navigator.serviceWorker.getRegistrations().then(function (registrations) {
return Promise.all(registrations.filter(function (registration) {
return new URL(registration.active?.scriptURL || registration.installing?.scriptURL || registration.waiting?.scriptURL || location.origin, location.origin).pathname === "/sw.js";
}).map(function (registration) { return registration.unregister(); }));
}).catch(function () {});
if (window.caches) caches.keys().then(function (keys) {
return Promise.all(keys.filter(function (key) { return key.indexOf("wrnexus-pwa-") === 0; }).map(function (key) { return caches.delete(key); }));
}).catch(function () {});
}`;
function renderPwaServiceWorker(pwa: PwaConfig): string {
const offlineUrl = pwa.offlineUrl ?? pwa.startUrl ?? "/";
const cacheUrls = [...new Set([offlineUrl, ...(pwa.cacheUrls ?? [])])];
return `const CACHE = ${JSON.stringify(pwa.cacheName ?? "wrnexus-pwa-v1")};
const OFFLINE_URL = ${JSON.stringify(offlineUrl)};
const PRECACHE_URLS = ${JSON.stringify(cacheUrls)};
self.addEventListener("install", event => {
event.waitUntil(caches.open(CACHE).then(cache => cache.addAll(PRECACHE_URLS)).catch(() => {}));
self.skipWaiting();
});
self.addEventListener("activate", event => event.waitUntil(
caches.keys().then(keys => Promise.all(keys.filter(key => key.startsWith("wrnexus-pwa-") && key !== CACHE).map(key => caches.delete(key))))
.then(() => self.clients.claim())
));
self.addEventListener("fetch", event => {
if (event.request.method !== "GET" || event.request.mode !== "navigate") return;
event.respondWith(fetch(event.request).then(response => {
if (response.ok) {
const copy = response.clone();
caches.open(CACHE).then(cache => cache.put(event.request, copy));
}
return response;
}).catch(() => caches.match(event.request).then(hit => hit || caches.match(OFFLINE_URL))));
});`;
return generateServiceWorker(pwa);
}
const DEFAULT_PWA_ICON = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
@@ -825,6 +851,20 @@ 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 cache =
deps.cache ??
new CacheCoordinator({
onEvent: (event) => {
if (mode === "development")
console.debug(
`[wrnexus:cache] ${event.layer} ${event.operation}${event.key ? ` ${event.key}` : ""}`,
);
},
});
const initializeRequestCache = (ctx: Context): void => {
ctx.locals.cache = cache;
ctx.locals.requestCache ??= cache.request();
};
const builtInMiddleware = frameworkMiddleware(deps);
const resolveMiddleware = async (): Promise<Middleware[]> => [
...builtInMiddleware,
@@ -867,11 +907,15 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
const extraHead = headParts.join("\n ") || undefined;
const pwaEnabled = deps.pwa !== false && deps.pwa?.enabled !== false;
const pwaConfig: PwaConfig = deps.pwa && typeof deps.pwa === "object" ? deps.pwa : {};
const pwaServiceWorkerEnabled = pwaEnabled && pwaConfig.serviceWorker !== false;
const pwaServiceWorkerEnabled =
mode === "production" && pwaEnabled && pwaConfig.serviceWorker !== false;
const pwaDevCleanupEnabled = mode === "development";
const webVitalsEnabled =
deps.observability?.enabled !== false && deps.observability?.webVitals === true;
const webVitalsEndpoint = deps.observability?.webVitalsEndpoint ?? "/__wrnexus/metrics/vitals";
const webVitalsHandler = createWebVitalsHandler({ registry: defaultMetrics });
const livenessHandler = createLivenessHandler();
const readinessHandler = createReadinessHandler(deps.health ?? new HealthRegistry());
const configuredPermissions = deps.security?.permissionsPolicy;
const runtimeSecurity: SecurityConfig | undefined =
@@ -898,7 +942,10 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
// Health check — unauthenticated, skips the middleware pipeline.
if (url.pathname === "/healthz" || url.pathname === "/__wrnexus/health") {
return secure(Response.json({ status: "ok" }));
return secure(await livenessHandler(req));
}
if (url.pathname === "/readyz" || url.pathname === "/__wrnexus/ready") {
return secure(await readinessHandler(req));
}
if (webVitalsEnabled && url.pathname === webVitalsEndpoint) {
return secure(await webVitalsHandler(req));
@@ -995,6 +1042,16 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
}),
);
}
if (url.pathname === "/__wrnexus/pwa-dev-cleanup.js" && pwaDevCleanupEnabled) {
return secure(
new Response(PWA_DEV_CLEANUP_CLIENT, {
headers: {
"content-type": "text/javascript; charset=utf-8",
"cache-control": "no-store",
},
}),
);
}
if (url.pathname === "/__wrnexus/mobile.js" && deps.mobile?.enabled !== false) {
return secure(
new Response(MOBILE_CLIENT, {
@@ -1081,13 +1138,26 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
try {
const ctx = createContext(req, url);
initializeRequestCache(ctx);
ctx.ip = server.requestIP?.(req)?.address ?? undefined;
ctx.locals.cspNonce = nonce; // available to pages for their own inline scripts
if (!["GET", "HEAD", "OPTIONS"].includes(req.method.toUpperCase())) {
const contentType = req.headers.get("content-type") ?? "";
if (contentType.includes("form")) {
try {
const form = await req.clone().formData();
const token = form.get("_csrf");
if (typeof token === "string") ctx.locals._csrf = token;
} catch {
// The endpoint will return its normal malformed-input response.
}
}
}
// Resolve the request language so both pages and API can translate.
if (deps.i18n) {
ctx.lang = resolveLang(
deps.i18n,
ctx.cookies.get(LANG_COOKIE),
ctx.cookies.get(deps.i18n.cookie.name),
req.headers.get("accept-language"),
);
ctx.t = makeT(deps.i18n, ctx.lang);
@@ -1125,10 +1195,18 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
async function dispatch(ctx: Context): Promise<Response> {
const { pathname } = ctx.url;
if (pathname === "/__wrnexus/cache") {
if (mode !== "development") return new Response("Not Found", { status: 404 });
return Response.json(cache.inspect(), { headers: { "cache-control": "no-store" } });
}
// Framework-owned assets (island chunks, reactive runtime, HMR stream).
if (pathname === "/__wrnexus/csr") {
return handleCsrBinding(ctx);
}
if (pathname === "/__wrnexus/client-load") {
return handleClientLoad(ctx);
}
if (pathname.startsWith("/__wrnexus/")) {
const res = await assets.serve(pathname);
@@ -1186,6 +1264,40 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
});
}
async function handleClientLoad(ctx: Context): Promise<Response> {
const routePath = ctx.url.searchParams.get("route") ?? "";
const name = ctx.url.searchParams.get("name") ?? "";
if (
!routePath.startsWith("/") ||
routePath.startsWith("/__wrnexus/") ||
!isSafeRequestPath(routePath) ||
!/^[A-Za-z_$][\w$]{0,63}$/.test(name)
) {
return new Response("Not Found", { status: 404 });
}
const page = router.matchPage(routePath);
if (!page) return new Response("Not Found", { status: 404 });
const pageModule = await loadModule(page.route.file);
const load = pageModule.__wrnexusClientLoad;
if (typeof load !== "function") return new Response("Not Found", { status: 404 });
try {
ctx.params = page.params;
const values = await load(ctx);
if (!values || typeof values !== "object" || !(name in values)) {
return new Response("Not Found", { status: 404 });
}
return Response.json(
{ data: (values as Record<string, unknown>)[name] },
{ headers: { "cache-control": "private, no-store" } },
);
} catch {
return Response.json(
{ error: { code: "CLIENT_LOAD_FAILED", message: "Client data loading failed." } },
{ status: 500, headers: { "cache-control": "private, no-store" } },
);
}
}
async function callApiFromContext(ctx: Context, path: string, method = "GET"): Promise<unknown> {
if (!isSafeApiPath(path)) {
throw new Error("Unsafe framework API path");
@@ -1283,6 +1395,7 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
async function renderComponents(
body: string,
translate: TFunction = (key) => key,
language?: string,
depth = 0,
): Promise<string> {
if (depth > 15 || router.components.length === 0 || !body.includes("data-component=")) {
@@ -1328,8 +1441,37 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
// Props may carry `{t:key}` i18n markers — resolve them for the active
// language before handing them to the component.
const props = resolveTProps(parseComponentProps(attrStr!), translate);
const rendered = fillSlots(String(render(props)), inner);
result += await renderComponents(rendered, translate, depth + 1);
if (normalizedName === "languageswitcher" && deps.i18n) {
props.locales ??= JSON.stringify(
deps.i18n.langs.map((locale) => ({
value: locale,
label: deps.i18n?.labels[locale] ?? locale.toUpperCase(),
shortLabel: locale.split("-")[0]!.toUpperCase(),
})),
);
props.current ??=
language && deps.i18n.langs.includes(language) ? language : deps.i18n.default;
}
const policy = (mod.__wrnexusCache ?? {}) as Record<string, string>;
const strategy = policy.strategy?.toLowerCase();
const renderComponent = () => fillSlots(String(render(props)), inner);
const rendered =
strategy && !["none", "no-store", "request"].includes(strategy)
? await cache.getOrLoad(
"component",
`${normalizedName}:${language}:${JSON.stringify(props)}:${inner}`,
renderComponent,
{
ttlMs: cacheDuration(policy.ttl, 60_000),
staleWhileRevalidateMs:
strategy === "stale-while-revalidate"
? cacheDuration(policy.stale ?? policy.ttl, 60_000)
: 0,
tags: cacheList(policy.tags),
},
)
: renderComponent();
result += await renderComponents(rendered, translate, language, depth + 1);
} catch (err) {
console.error(`[wrnexus] component '${name}' failed to render`, err);
deps.devToolbar?.collector.add(
@@ -1347,6 +1489,9 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
}
async function handlePage(ctx: Context): Promise<Response> {
// Page rendering is also reached by HMR synchronization and internal
// dispatches, so never rely exclusively on the public fetch initializer.
initializeRequestCache(ctx);
const isMobileRequest =
ctx.req.headers.get("x-wrnexus-mobile") === "1" ||
new RegExp(deps.mobile?.userAgent ?? "WrNexusMobile", "i").test(
@@ -1378,10 +1523,14 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
}
// Issue the CSRF token cookie so forms on this page can echo it back.
csrfToken(ctx);
const pageCsrf = csrfToken(ctx);
const storeContainer = requestStoreContainer(ctx.req, matched.route.raw);
const mod = await loadModule(matched.route.file);
if (ctx.req.method.toUpperCase() === "POST") {
const actionResponse = await handlePageAction(ctx, mod);
if (actionResponse) return actionResponse;
}
const component = mod.default;
if (typeof component !== "function") {
throw new Error(`Page ${matched.route.file} has no default export`);
@@ -1389,13 +1538,88 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
ctx.params = matched.params;
const meta = (mod.meta ?? {}) as PageMeta;
const pageNavigation = (mod.__wrnexusNavigation ?? {}) as { preserve?: string };
const pageCache = (mod.__wrnexusCache ?? {}) as Record<string, string>;
const fullPageEnabled = ["page", "full-page"].includes(pageCache.scope?.toLowerCase() ?? "");
const fullPageKey = `page:${matched.route.raw}:${ctx.url.search}:${cacheIdentity(ctx, [
"language",
`cookie:${THEME_COOKIE}`,
`cookie:${ACCENT_COOKIE}`,
...cacheList(pageCache.vary),
])}`;
if (fullPageEnabled && ["GET", "HEAD"].includes(ctx.req.method.toUpperCase())) {
const pageHit = cache.page.lookup(fullPageKey);
if (pageHit.state === "fresh") {
const cached = pageHit.entry.value as { html: string; etag: string; nonce: string };
const restoredHtml = cached.nonce
? cached.html.replaceAll(
`nonce="${cached.nonce}"`,
`nonce="${String(ctx.locals.cspNonce ?? "")}"`,
)
: cached.html;
await disposeRequestStores(ctx.req);
return new Response(ctx.req.method.toUpperCase() === "HEAD" ? null : restoredHtml, {
headers: {
"content-type": "text/html; charset=utf-8",
etag: cached.etag,
"cache-control": "public, max-age=0, must-revalidate",
"x-wrnexus-page-cache": "HIT",
},
});
}
}
let dataCacheState: "HIT" | "STALE" | "MISS" | "BYPASS" = "BYPASS";
const preserve = (pageNavigation.preserve ?? "")
.match(/(?:scroll|forms|tabs|expanded|filters|pagination|component|workflow)/g)
?.filter((value, index, values) => values.indexOf(value) === index)
.join(",");
const pageCtx = ctx as Context & {
__wrnexusCallApi?: (path: string, method?: string) => Promise<unknown>;
__wrnexusUseStore?: (definition: StoreDefinition<any, any, any>) => Promise<unknown>;
};
pageCtx.__wrnexusCallApi = (path, method = "GET") => callApiFromContext(ctx, path, method);
pageCtx.__wrnexusUseStore = (definition) => storeContainer.use(definition);
let body = await renderComponents(String(await component(pageCtx)), ctx.t);
const load = mod.__wrnexusLoad as ((ctx: Context) => Promise<unknown>) | undefined;
if (typeof load === "function") {
const strategy = pageCache.strategy?.toLowerCase();
const cacheEnabled = Boolean(strategy && !["none", "no-store", "request"].includes(strategy));
let data: unknown;
if (cacheEnabled && ["GET", "HEAD"].includes(ctx.req.method.toUpperCase())) {
const key = `route:${matched.route.raw}:${ctx.url.search}:${cacheIdentity(ctx, cacheList(pageCache.vary))}`;
const lookup = cache.data.lookup(key);
dataCacheState =
lookup.state === "fresh" ? "HIT" : lookup.state === "stale" ? "STALE" : "MISS";
data = await cache.getOrLoad("data", key, () => load(pageCtx), {
ttlMs: cacheDuration(pageCache.ttl, 60_000),
staleWhileRevalidateMs:
strategy === "stale-while-revalidate"
? cacheDuration(pageCache.stale ?? pageCache.ttl, 60_000)
: 0,
tags: cacheList(pageCache.tags),
});
} else {
data = await (
ctx.locals.requestCache as {
getOrLoad<V>(key: string, loader: () => Promise<V>): Promise<V>;
}
).getOrLoad(`route:${matched.route.raw}`, () => load(pageCtx));
}
(pageCtx as Context & { data?: unknown }).data = data;
if (data && typeof data === "object") Object.assign(pageCtx, data);
}
let body = await renderComponents(String(await component(pageCtx)), ctx.t, ctx.lang);
const partial = (mod as { __wrnexusRender?: string }).__wrnexusRender === "partial-static";
const precomputedShell = (mod as { __wrnexusStaticShell?: unknown }).__wrnexusStaticShell;
const pagePartial = partial ? partialPrerender(body) : undefined;
if (partial) {
body = typeof precomputedShell === "string" ? precomputedShell : (pagePartial?.shell ?? body);
}
if (body.includes("data-wrn-action=")) {
body = body.replace(
/(<form\b[^>]*\bdata-wrn-action=(?:"[^"]+"|'[^']+')[^>]*>)/gi,
`$1<input type="hidden" name="_csrf" value="${pageCsrf}">`,
);
}
const resolvedTheme = deps.theme
? resolveThemeName(ctx.cookies.get(THEME_COOKIE), deps.theme)
: "";
@@ -1421,14 +1645,18 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
? undefined
: router.layouts.find((l) => l.name === layoutName);
if (importedLayout?.render) {
body = await renderComponents(fillSlots(String(importedLayout.render({})), body), ctx.t);
body = await renderComponents(
fillSlots(String(importedLayout.render({})), body),
ctx.t,
ctx.lang,
);
} else if (layout) {
try {
const layoutMod = await loadModule(layout.file);
const layoutRender = (layoutMod as { render?: (p: Record<string, string>) => string })
.render;
if (typeof layoutRender === "function") {
body = await renderComponents(fillSlots(String(layoutRender({})), body), ctx.t);
body = await renderComponents(fillSlots(String(layoutRender({})), body), ctx.t, ctx.lang);
}
} catch (err) {
console.error(`[wrnexus] layout '${layoutName}' failed to render`, err);
@@ -1467,7 +1695,7 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
pathname: ctx.url.pathname,
}),
);
documentTemplate = await renderComponents(fillSlots(rendered, body), ctx.t);
documentTemplate = await renderComponents(fillSlots(rendered, body), ctx.t, ctx.lang);
body = documentTemplate;
}
} catch (err) {
@@ -1498,6 +1726,7 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
}
if (pwaServiceWorkerEnabled)
scripts.push(versionAssetUrl("/__wrnexus/pwa.js", deps.assetVersion));
if (pwaDevCleanupEnabled) scripts.push("/__wrnexus/pwa-dev-cleanup.js");
if (deps.mobile?.enabled !== false && usesMobileRuntime(body))
scripts.push(versionAssetUrl("/__wrnexus/mobile.js", deps.assetVersion));
@@ -1516,15 +1745,24 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
}
}
attrs.push(`lang="${safeLanguageTag(language)}"`);
if (deps.i18n) attrs.push(`dir="${deps.i18n.direction[language] ?? "ltr"}"`);
const htmlAttrs = attrs.length ? ` ${attrs.join(" ")}` : undefined;
const html = renderDocument({
const outerPartial = partial
? partialPrerender(body, pagePartial?.regions.length ?? 0)
: undefined;
const partialRegions = [...(pagePartial?.regions ?? []), ...(outerPartial?.regions ?? [])];
const renderedBody = outerPartial?.shell ?? body;
if (partial && documentTemplate) documentTemplate = renderedBody;
let html = renderDocument({
meta,
seo: deps.seo,
url: ctx.url,
body,
body: renderedBody,
scripts,
extraHead: [
preserve ? `<meta name="wrnexus-preserve" content="${preserve}" />` : "",
pwaEnabled ? `<link rel="manifest" href="/site.webmanifest" />` : "",
pwaEnabled ? `<meta name="mobile-web-app-capable" content="yes" />` : "",
pwaEnabled ? `<meta name="apple-mobile-web-app-capable" content="yes" />` : "",
@@ -1535,6 +1773,9 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
extraBody:
[
renderStoreHydration(storeContainer, (ctx.locals.cspNonce as string) ?? undefined),
deps.i18n
? `<script${ctx.locals.cspNonce ? ` nonce="${String(ctx.locals.cspNonce)}"` : ""}>${renderI18nData(deps.i18n, language)}</script>`
: "",
hmr ? hmrClientTag((ctx.locals.cspNonce as string) ?? "") : "",
shouldEnableDevToolbar(mode, deps) ? DEV_TOOLBAR_SCRIPT : "",
]
@@ -1544,10 +1785,29 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
documentTemplate,
styleNonce: (ctx.locals.cspNonce as string | undefined) ?? undefined,
});
if (deps.renderHtml) html = await deps.renderHtml(html);
// Conditional GET: hash the page CONTENT (`body`), not the assembled shell —
// the shell carries a per-request CSP nonce in dev, which would otherwise make
// the ETag change every request. Same content → same ETag → 304 on revalidate.
const tag = etag(`${htmlAttrs ?? ""}\n${JSON.stringify(scripts)}\n${body}`);
if (
fullPageEnabled &&
["GET", "HEAD"].includes(ctx.req.method.toUpperCase()) &&
!/name=["']_csrf["']/.test(html)
) {
cache.page.set(
fullPageKey,
{ html, etag: tag, nonce: String(ctx.locals.cspNonce ?? "") },
{
ttlMs: cacheDuration(pageCache.ttl, 60_000),
staleWhileRevalidateMs:
pageCache.strategy?.toLowerCase() === "stale-while-revalidate"
? cacheDuration(pageCache.stale ?? pageCache.ttl, 60_000)
: 0,
tags: cacheList(pageCache.tags),
},
);
}
await disposeRequestStores(ctx.req);
const method = ctx.req.method.toUpperCase();
if ((method === "GET" || method === "HEAD") && notModified(ctx.req, tag)) {
@@ -1556,18 +1816,132 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
headers: { etag: tag, "cache-control": "private, no-cache" },
});
}
return new Response(html, {
headers: {
"content-type": "text/html; charset=utf-8",
etag: tag,
"cache-control": "private, no-cache",
...(shouldEnableDevToolbar(mode, deps)
? {
"x-wrnexus-dev-toolbar": "enabled",
"x-wrnexus-route": matched.route.raw,
}
: {}),
return new Response(
partial && method !== "HEAD"
? streamPartialDocument(
{ shell: html, regions: partialRegions },
(ctx.locals.cspNonce as string | undefined) ?? undefined,
)
: html,
{
headers: {
"content-type": "text/html; charset=utf-8",
etag: tag,
"cache-control": "private, no-cache",
"x-wrnexus-data-cache": dataCacheState,
...(partial ? { "x-wrnexus-render": "partial-static" } : {}),
...(partial && typeof precomputedShell === "string"
? { "x-wrnexus-static-shell": "build" }
: {}),
...(fullPageEnabled ? { "x-wrnexus-page-cache": "MISS" } : {}),
...(shouldEnableDevToolbar(mode, deps)
? {
"x-wrnexus-dev-toolbar": "enabled",
"x-wrnexus-route": matched.route.raw,
}
: {}),
},
},
);
}
async function actionInput(request: Request): Promise<{ name?: string; input: unknown }> {
const contentType = request.headers.get("content-type") ?? "";
if (contentType.includes("application/json")) {
return {
name: request.headers.get("x-wrnexus-action") ?? undefined,
input: await request.json(),
};
}
const form = await request.formData();
const input: Record<string, unknown> = {};
for (const [key, value] of form) {
if (key === "_wrnexus_action" || key === "_csrf") continue;
if (!(key in input)) input[key] = value;
else
input[key] = Array.isArray(input[key])
? [...(input[key] as unknown[]), value]
: [input[key], value];
}
const submittedName = form.get("_wrnexus_action");
return {
name:
request.headers.get("x-wrnexus-action") ??
(typeof submittedName === "string" ? submittedName : undefined),
input,
};
}
async function handlePageAction(ctx: Context, mod: RouteModule): Promise<Response | null> {
const actions = mod.__wrnexusActions as Record<string, ActionEntry> | undefined;
if (!actions) return null;
let submitted: Awaited<ReturnType<typeof actionInput>>;
try {
submitted = await actionInput(ctx.req);
} catch {
return Response.json({ error: "Malformed action input" }, { status: 400 });
}
if (!submitted.name) return null;
if (!/^[A-Za-z_$][\w$]*$/.test(submitted.name) || !actions[submitted.name]) {
return Response.json({ error: "Unknown server action" }, { status: 404 });
}
const security = (mod.__wrnexusSecurity ?? {}) as Record<string, string>;
if (/^(?:required|true)$/i.test(security.auth ?? "") && !ctx.user) {
return Response.json({ error: "Authentication required" }, { status: 401 });
}
if (security.permission) {
const permissions = ctx.locals.permissions;
const allowed =
typeof permissions === "function"
? await permissions(security.permission, ctx)
: Array.isArray(permissions) && permissions.includes(security.permission);
if (!allowed) return Response.json({ error: "Permission denied" }, { status: 403 });
}
if (security.csrf !== "false" && !verifyCsrf(ctx)) {
return Response.json({ error: "Invalid CSRF token" }, { status: 403 });
}
const action = actions[submitted.name]!;
let input = submitted.input;
if (action.schema) {
const parsed = action.schema.parse(input);
if (!parsed.ok) {
const acceptsJson = (ctx.req.headers.get("accept") ?? "").includes("application/json");
if (acceptsJson)
return Response.json(
{ error: "Validation failed", errors: parsed.errors },
{ status: 422 },
);
const errors = Object.entries(parsed.errors)
.map(
([field, message]) =>
`<li><strong>${escapeHtml(field)}</strong>: ${escapeHtml(message)}</li>`,
)
.join("");
return new Response(
`<!doctype html><title>Validation failed</title><h1>Validation failed</h1><ul>${errors}</ul><a href="${escapeHtml(ctx.url.pathname)}">Go back</a>`,
{
status: 422,
headers: { "content-type": "text/html; charset=utf-8" },
},
);
}
input = parsed.value;
}
const data = await action.run(input, ctx);
const invalidated = [
...new Set(
Array.isArray(ctx.locals.__wrnexusInvalidatedTags)
? (ctx.locals.__wrnexusInvalidatedTags as string[])
: [],
),
];
if (invalidated.length) cache.invalidateTags(invalidated);
if ((ctx.req.headers.get("accept") ?? "").includes("application/json")) {
return Response.json({ ok: true, data, invalidated });
}
return new Response(null, {
status: 303,
headers: { location: ctx.url.pathname + ctx.url.search },
});
}
@@ -1605,11 +1979,12 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
headers.set("x-wrnexus-hmr", "1");
const req = new Request(url, { headers });
const ctx = createContext(req, url);
initializeRequestCache(ctx);
ctx.locals.cspNonce = randomNonce();
if (deps.i18n) {
ctx.lang = resolveLang(
deps.i18n,
ctx.cookies.get(LANG_COOKIE),
ctx.cookies.get(deps.i18n.cookie.name),
req.headers.get("accept-language"),
);
ctx.t = makeT(deps.i18n, ctx.lang);
@@ -1908,9 +2283,15 @@ export function collectScripts(
navigation: { mode?: "auto" | "client" | "document" } = {},
): RenderScript[] {
const scripts: RenderScript[] = [];
if (/\bdata-scope=/.test(body) || /\bdata-wrnexus-csr=/.test(body)) {
if (
/\bdata-scope=/.test(body) ||
/\bdata-wrnexus-csr=/.test(body) ||
/\bdata-wrn-client-template=/.test(body) ||
/\bdata-wrn-async=/.test(body)
) {
scripts.push("/__wrnexus/reactive.js");
}
if (/\bdata-wrn-action=/.test(body)) scripts.push("/__wrnexus/actions.js");
// The theme runtime is only needed when the page can switch themes.
if (
/\bdata-wire-theme-(toggle|set)\b/.test(body) ||
@@ -1961,6 +2342,49 @@ function safeLanguageTag(value: string): string {
return /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/.test(value) ? value : "en";
}
function cacheDuration(value: string | undefined, fallback: number): number {
if (!value) return fallback;
const match = /^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)?$/i.exec(value.trim());
if (!match) return fallback;
const scale = { ms: 1, s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 }[
(match[2]?.toLowerCase() ?? "ms") as "ms" | "s" | "m" | "h" | "d"
];
return Math.max(0, Number(match[1]) * scale);
}
function cacheList(value: string | undefined): string[] {
if (!value) return [];
try {
const parsed = JSON.parse(value) as unknown;
if (Array.isArray(parsed))
return parsed.filter((item): item is string => typeof item === "string");
} catch {
// Fall through to a convenient comma-separated form.
}
return value
.split(",")
.map((item) => item.trim())
.filter(Boolean);
}
function cacheIdentity(ctx: Context, vary: string[]): string {
const user = ctx.user as { id?: unknown } | undefined;
const values = new Map<string, string>();
if (ctx.tenant?.id) values.set("tenant", String(ctx.tenant.id));
if (user?.id !== undefined) values.set("user", String(user.id));
for (const item of vary) {
if (item === "tenant") values.set(item, String(ctx.tenant?.id ?? ""));
else if (item === "user") values.set(item, String(user?.id ?? ""));
else if (item === "language") values.set(item, ctx.lang);
else if (item.startsWith("cookie:")) values.set(item, ctx.cookies.get(item.slice(7)) ?? "");
else values.set(`header:${item}`, ctx.req.headers.get(item) ?? "");
}
return [...values.entries()]
.sort()
.map(([key, value]) => `${key}=${value}`)
.join("|");
}
function versionAssetUrl(src: string, version?: string): string {
if (!version) return src;
return `${src}${src.includes("?") ? "&" : "?"}v=${encodeURIComponent(version)}`;
+6 -1
View File
@@ -8,12 +8,13 @@
* the supervisor delivers live reload of edited server code.
*/
import { readFileSync } from "node:fs";
import { dirname } from "node:path";
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 [appDir, portStr, modeStr, hostname, hmrStr, certFile, keyFile] = process.argv.slice(2);
const mode = (modeStr as Mode) || "development";
@@ -38,6 +39,10 @@ const server = await startServer({
port,
hostname,
tls:
certFile && keyFile
? { cert: readFileSync(certFile, "utf8"), key: readFileSync(keyFile, "utf8") }
: undefined,
mode,
hmr: hmrStr === undefined ? undefined : hmrStr === "true",
@@ -0,0 +1,166 @@
import { afterEach, expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { buildRouter } from "@wrnexus/router";
import { v } from "@wrnexus/validation";
import { createHandlers, type RuntimeDeps } from "../src/runtime.ts";
const roots: string[] = [];
afterEach(() => roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true })));
const server = { upgrade: () => false };
test("server actions validate, enforce CSRF, invalidate, and progressively enhance forms", async () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-action-runtime-"));
roots.push(root);
const app = join(root, "app");
mkdirSync(join(app, "pages"), { recursive: true });
writeFileSync(join(app, "pages", "users.ts"), "export default () => '';");
const router = buildRouter(app);
const schema = v.object({ name: v.string().min(2) });
const security: Record<string, string> = {};
let authenticated = false;
let granted = false;
const handlers = createHandlers({
mode: "production",
hmr: false,
router,
loadModule: async () => ({
default: () => `<form method="post" data-wrn-action="createUser"><input name="name"></form>`,
__wrnexusActions: {
createUser: {
schema,
run: (input: { name: string }, ctx: { locals: Record<string, unknown> }) => {
ctx.locals.__wrnexusInvalidatedTags = ["users", "users"];
return { id: `user-${input.name}` };
},
},
},
__wrnexusSecurity: security,
}),
getMiddleware: async () => [
(ctx, next) => {
if (authenticated) ctx.user = { id: "operator" };
ctx.locals.permissions = granted ? ["users.create"] : [];
return next();
},
],
assets: { serve: async () => null },
} satisfies RuntimeDeps);
const page = await handlers.fetch(new Request("https://example.test/users"), server);
const html = await page!.text();
expect(html).toContain('name="_csrf"');
expect(html).toContain("/__wrnexus/actions.js");
const cookie = page!.headers.get("set-cookie")!;
const token = /wire-csrf=([^;]+)/.exec(cookie)?.[1];
if (!token) throw new Error("expected CSRF cookie");
const invalid = await handlers.fetch(
new Request("https://example.test/users", {
method: "POST",
headers: {
"content-type": "application/json",
accept: "application/json",
cookie,
"x-wrnexus-action": "createUser",
"x-csrf-token": token,
},
body: JSON.stringify({ name: "x" }),
}),
server,
);
expect(invalid?.status).toBe(422);
expect(await invalid?.json()).toMatchObject({ errors: { name: expect.any(String) } });
const noCsrf = await handlers.fetch(
new Request("https://example.test/users", {
method: "POST",
headers: {
"content-type": "application/json",
accept: "application/json",
cookie,
"x-wrnexus-action": "createUser",
},
body: JSON.stringify({ name: "Ada" }),
}),
server,
);
expect(noCsrf?.status).toBe(403);
const success = await handlers.fetch(
new Request("https://example.test/users", {
method: "POST",
headers: {
"content-type": "application/json",
accept: "application/json",
cookie,
"x-wrnexus-action": "createUser",
"x-csrf-token": token,
},
body: JSON.stringify({ name: "Ada" }),
}),
server,
);
expect(await success?.json()).toEqual({
ok: true,
data: { id: "user-Ada" },
invalidated: ["users"],
});
security.auth = "required";
expect(
(
await handlers.fetch(
new Request("https://example.test/users", {
method: "POST",
headers: {
"content-type": "application/json",
accept: "application/json",
cookie,
"x-wrnexus-action": "createUser",
"x-csrf-token": token,
},
body: JSON.stringify({ name: "Ada" }),
}),
server,
)
)?.status,
).toBe(401);
authenticated = true;
security.permission = "users.create";
expect(
(
await handlers.fetch(
new Request("https://example.test/users", {
method: "POST",
headers: {
"content-type": "application/json",
accept: "application/json",
cookie,
"x-wrnexus-action": "createUser",
"x-csrf-token": token,
},
body: JSON.stringify({ name: "Ada" }),
}),
server,
)
)?.status,
).toBe(403);
granted = true;
const form = new FormData();
form.set("_wrnexus_action", "createUser");
form.set("_csrf", token);
form.set("name", "Grace");
const progressive = await handlers.fetch(
new Request("https://example.test/users", {
method: "POST",
headers: { cookie, origin: "https://example.test" },
body: form,
}),
server,
);
expect(progressive?.status).toBe(303);
expect(progressive?.headers.get("location")).toBe("/users");
});
@@ -0,0 +1,110 @@
import { afterEach, expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { CacheCoordinator } from "@wrnexus/cache";
import { buildRouter } from "@wrnexus/router";
import { createHandlers, type RuntimeDeps } from "../src/runtime.ts";
const roots: string[] = [];
afterEach(() => roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true })));
test("declarative route policies cache loader data and expose inspection", async () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-cache-runtime-"));
roots.push(root);
const app = join(root, "app");
mkdirSync(join(app, "pages"), { recursive: true });
writeFileSync(join(app, "pages/dashboard.ts"), "export default () => '';\n");
let loads = 0;
const cache = new CacheCoordinator();
const handlers = createHandlers({
mode: "development",
hmr: false,
router: buildRouter(app),
cache,
loadModule: async () => ({
__wrnexusCache: {
strategy: "stale-while-revalidate",
ttl: "5m",
tags: '["dashboard"]',
vary: '["language"]',
},
__wrnexusLoad: async () => ({ count: ++loads }),
default: (ctx: { count: number }) => `<p>${ctx.count}</p>`,
}),
getMiddleware: async () => [],
assets: { serve: async () => null },
} satisfies RuntimeDeps);
const server = { upgrade: () => false };
const first = await handlers.fetch(new Request("https://example.test/dashboard"), server);
const second = await handlers.fetch(new Request("https://example.test/dashboard"), server);
expect(first?.headers.get("x-wrnexus-data-cache")).toBe("MISS");
expect(second?.headers.get("x-wrnexus-data-cache")).toBe("HIT");
expect(loads).toBe(1);
const inspection = await handlers.fetch(
new Request("https://example.test/__wrnexus/cache"),
server,
);
expect((await inspection?.json())?.layers.data).toHaveLength(1);
});
test("safe full-page policies reuse static documents", async () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-page-cache-runtime-"));
roots.push(root);
const app = join(root, "app");
mkdirSync(join(app, "pages"), { recursive: true });
writeFileSync(join(app, "pages/marketing.ts"), "export default () => '';\n");
let renders = 0;
const pageCache = new CacheCoordinator();
const handlers = createHandlers({
mode: "production",
hmr: false,
router: buildRouter(app),
cache: pageCache,
loadModule: async () => ({
__wrnexusCache: { scope: "page", strategy: "fresh", ttl: "5m", tags: '["marketing"]' },
default: () => `<h1>Render ${++renders}</h1>`,
}),
getMiddleware: async () => [],
assets: { serve: async () => null },
} satisfies RuntimeDeps);
const server = { upgrade: () => false };
const first = await handlers.fetch(new Request("https://example.test/marketing"), server);
const second = await handlers.fetch(new Request("https://example.test/marketing"), server);
expect(first?.headers.get("x-wrnexus-page-cache")).toBe("MISS");
expect(pageCache.inspect().layers.page).toHaveLength(1);
expect(second?.headers.get("x-wrnexus-page-cache")).toBe("HIT");
const firstHtml = await first!.text();
const secondHtml = await second!.text();
expect(secondHtml).toContain("Render 1");
expect(/nonce="([^"]+)"/.exec(firstHtml)?.[1]).not.toBe(/nonce="([^"]+)"/.exec(secondHtml)?.[1]);
expect(renders).toBe(1);
});
test("full-page cache refuses CSRF-bearing documents", async () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-page-cache-csrf-"));
roots.push(root);
const app = join(root, "app");
mkdirSync(join(app, "pages"), { recursive: true });
writeFileSync(join(app, "pages/account.ts"), "export default () => '';\n");
let renders = 0;
const cache = new CacheCoordinator();
const handlers = createHandlers({
mode: "production",
hmr: false,
router: buildRouter(app),
cache,
loadModule: async () => ({
__wrnexusCache: { scope: "page", strategy: "fresh", ttl: "5m" },
default: () =>
`<form method="post" data-wrn-action="save"><button>${++renders}</button></form>`,
}),
getMiddleware: async () => [],
assets: { serve: async () => null },
} satisfies RuntimeDeps);
const server = { upgrade: () => false };
await handlers.fetch(new Request("https://example.test/account"), server);
await handlers.fetch(new Request("https://example.test/account"), server);
expect(renders).toBe(2);
expect(cache.inspect().layers.page).toEqual([]);
});
@@ -53,3 +53,20 @@ test("legacy, compatible, and explicit import modes are enforced from app config
console.warn = originalWarn;
}
});
test("compiler-native reactive elements do not require application imports", () => {
const { root, page } = fixture();
writeFileSync(
page,
`page Home {
state active = "Admin"
view {
<Transition name="fade"><Component is={active}><div data-component-case="Admin">Admin</div></Component></Transition>
<Portal to="body"><p>Notice</p></Portal>
<Async source="profile"><Loading>Loading</Loading><Success data="profile">Ready</Success><Error error="error">Failed</Error></Async>
}
}`,
);
setCompileImportOptions(root, { mode: "explicit" });
expect(() => compileWireArtifacts(page, 5)).not.toThrow();
});
@@ -0,0 +1,72 @@
import { afterEach, expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { buildRouter } from "@wrnexus/router";
import { createHandlers, type RuntimeDeps } from "../src/runtime.ts";
const roots: string[] = [];
afterEach(() => roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true })));
test("server loader data is available to page rendering", async () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-load-runtime-"));
roots.push(root);
const app = join(root, "app");
mkdirSync(join(app, "pages"), { recursive: true });
writeFileSync(join(app, "pages/users.ts"), "export default () => '';\n");
const handlers = createHandlers({
mode: "production",
hmr: false,
router: buildRouter(app),
loadModule: async () => ({
__wrnexusLoad: async () => ({ users: ["Ada", "Lin"] }),
default: (ctx: { users: string[]; data: { users: string[] } }) =>
`<p>${ctx.users.join(",")} / ${ctx.data.users.length}</p>`,
}),
getMiddleware: async () => [],
assets: { serve: async () => null },
} satisfies RuntimeDeps);
const response = await handlers.fetch(new Request("https://example.test/users"), {
upgrade: () => false,
});
expect(await response!.text()).toContain("Ada,Lin / 2");
});
test("HMR page synchronization initializes request-scoped loader caching", async () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-hmr-load-runtime-"));
roots.push(root);
const app = join(root, "app");
mkdirSync(join(app, "pages"), { recursive: true });
writeFileSync(join(app, "pages/async.ts"), "export default () => '';\n");
const handlers = createHandlers({
mode: "development",
hmr: true,
router: buildRouter(app),
loadModule: async () => ({
__wrnexusLoad: async () => ({ message: "Loaded through HMR" }),
default: (ctx: { message: string }) => `<p>${ctx.message}</p>`,
}),
getMiddleware: async () => [],
assets: { serve: async () => null },
} satisfies RuntimeDeps);
const html = await new Promise<string>((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error("HMR response timed out")), 1_000);
handlers.websocket.message(
{
data: { kind: "hmr", baseUrl: "http://localhost", headers: [] },
send(value) {
clearTimeout(timeout);
resolve(String(value));
},
close() {},
},
JSON.stringify({ type: "sync", path: "/async" }),
);
});
const message = JSON.parse(html) as { type: string; html?: string; message?: string };
expect(message.type).toBe("html");
expect(message.message).toBeUndefined();
expect(message.html).toContain("Loaded through HMR");
});
@@ -0,0 +1,55 @@
import { expect, test } from "bun:test";
import { HealthRegistry } from "@wrnexus/core";
import type { Router } from "@wrnexus/router";
import { createHandlers, type RuntimeDeps } from "../src/runtime.ts";
function runtime(health: HealthRegistry) {
const router: Router = {
pages: [],
api: [],
realtime: [],
middlewareFiles: [],
components: [],
layouts: [],
stores: [],
schemas: [],
matchPage: () => null,
matchApi: () => null,
matchRealtime: () => null,
};
return createHandlers({
mode: "production",
hmr: false,
router,
loadModule: async () => ({}),
getMiddleware: async () => [],
assets: { serve: async () => null },
health,
observability: { enabled: true, sampleRate: 1, exporter: "none" },
} satisfies RuntimeDeps);
}
const server = { upgrade: () => false };
test("runtime exposes separate liveness and dependency readiness probes", async () => {
const health = new HealthRegistry();
health.register("database", () => ({ status: "down", message: "offline" }));
const handlers = runtime(health);
const live = await handlers.fetch(new Request("https://example.test/healthz"), server);
const ready = await handlers.fetch(new Request("https://example.test/readyz"), server);
expect(live?.status).toBe(200);
expect(await live?.json()).toEqual({ status: "up" });
expect(ready?.status).toBe(503);
expect(await ready?.json()).toEqual({ status: "down" });
});
test("built production responses carry the framework security-header baseline", async () => {
const handlers = runtime(new HealthRegistry());
const response = await handlers.fetch(new Request("https://example.test/healthz"), server);
expect(response?.headers.get("strict-transport-security")).toContain("max-age=");
expect(response?.headers.get("content-security-policy")).toContain("default-src 'self'");
expect(response?.headers.get("x-content-type-options")).toBe("nosniff");
expect(response?.headers.get("referrer-policy")).toBeTruthy();
});
@@ -0,0 +1,21 @@
import { expect, test } from "bun:test";
import { precomputePartialStaticShell } from "../src/partial-build.ts";
test("precomputes nested page components while erasing dynamic region bodies", async () => {
const result = await precomputePartialStaticShell(
{
__wrnexusBuildStaticShell: () =>
'<div data-component="Card" title="Docs"></div><wrn-dynamic-region data-wrn-dynamic="true"></wrn-dynamic-region>',
},
[
{
name: "Card",
mod: { render: (props) => `<article>${props?.title}</article>` },
},
],
);
expect(result.regions).toBe(1);
expect(result.shell).toContain("<article>Docs</article>");
expect(result.shell).toContain('data-wrn-dynamic-placeholder="wrn-region-0"');
expect(result.shell).not.toContain("wrn-dynamic-region");
});
@@ -1,5 +1,5 @@
import { expect, test } from "bun:test";
import { PWA_CLIENT, usesMobileRuntime } from "../src/runtime.ts";
import { PWA_CLIENT, PWA_DEV_CLEANUP_CLIENT, usesMobileRuntime } from "../src/runtime.ts";
test("PWA registration is valid JavaScript and Trusted Types compatible", () => {
expect(() => new Bun.Transpiler({ loader: "js" }).transformSync(PWA_CLIENT)).not.toThrow();
@@ -7,6 +7,14 @@ test("PWA registration is valid JavaScript and Trusted Types compatible", () =>
expect(PWA_CLIENT).toContain("createScriptURL(swUrl)");
});
test("development PWA cleanup removes stale WRNexus service workers and caches", () => {
expect(() =>
new Bun.Transpiler({ loader: "js" }).transformSync(PWA_DEV_CLEANUP_CLIENT),
).not.toThrow();
expect(PWA_DEV_CLEANUP_CLIENT).toContain("registration.unregister()");
expect(PWA_DEV_CLEANUP_CLIENT).toContain("wrnexus-pwa-");
});
test("mobile runtime is shipped only for pages using mobile or native directives", () => {
expect(usesMobileRuntime('<main class="page">Docs</main>')).toBe(false);
expect(usesMobileRuntime('<button data-native-mobile="share">Share</button>')).toBe(true);
+61 -1
View File
@@ -2,7 +2,67 @@ import { expect, test } from "bun:test";
import { mkdtempSync, rmSync, utimesSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { invalidateModule, loadModule, setCompileCacheDir } from "../src/pipeline.ts";
import {
compileWireArtifacts,
compileWireArtifactsAsync,
getWrnCompileMetrics,
invalidateModule,
loadModule,
resetWrnCompileMetrics,
setCompileCacheDir,
setDevCompilerPipeline,
} from "../src/pipeline.ts";
test("development compilation awaits plugin AST and code transforms", async () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-plugin-pipeline-"));
const file = join(root, "page.wrn");
let astTransformed = false;
setCompileCacheDir(join(root, ".wrnexus"));
setDevCompilerPipeline({
async transformAst(ast) {
await Promise.resolve();
astTransformed = true;
return ast;
},
async transformCode(code) {
await Promise.resolve();
return `${code}\nexport const pluginTransformed = true;\n`;
},
virtualModules: new Map(),
});
try {
writeFileSync(file, "page Home { view { <h1>Plugin</h1> } }\n");
const artifact = await compileWireArtifactsAsync(file);
expect(astTransformed).toBeTrue();
expect((await import(artifact.main)).pluginTransformed).toBeTrue();
} finally {
setDevCompilerPipeline(null);
rmSync(root, { recursive: true, force: true });
}
});
test("WRN compilation exposes cache hit, miss, timing, and error metrics", () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-cache-metrics-"));
const file = join(root, "page.wrn");
setCompileCacheDir(join(root, ".wrnexus"));
resetWrnCompileMetrics();
try {
writeFileSync(file, "page Home { view { <h1>Metrics</h1> } }\n");
compileWireArtifacts(file);
compileWireArtifacts(file);
writeFileSync(file, "page Broken { view { <h1> } }\n");
expect(() => compileWireArtifacts(file)).toThrow();
expect(getWrnCompileMetrics()).toMatchObject({
hits: 1,
misses: 2,
compilations: 1,
errors: 1,
});
expect(getWrnCompileMetrics().totalDurationMs).toBeGreaterThanOrEqual(0);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("invalidateModule loads changed server modules without restarting the process", async () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-hmr-"));
@@ -0,0 +1,30 @@
import { afterEach, expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { buildRouter } from "@wrnexus/router";
import { createHandlers, type RuntimeDeps } from "../src/runtime.ts";
const roots: string[] = [];
afterEach(() => roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true })));
test("plugin render lifecycle transforms final documents", async () => {
const root = mkdtempSync(join(tmpdir(), "wrnexus-plugin-render-"));
roots.push(root);
const app = join(root, "app");
mkdirSync(join(app, "pages"), { recursive: true });
writeFileSync(join(app, "pages/index.ts"), "export default () => '';\n");
const handlers = createHandlers({
mode: "development",
hmr: false,
router: buildRouter(app),
loadModule: async () => ({ default: () => "<h1>Home</h1>" }),
renderHtml: (html) => html.replace("</body>", "<!-- plugin-render --></body>"),
getMiddleware: async () => [],
assets: { serve: async () => null },
} satisfies RuntimeDeps);
const response = await handlers.fetch(new Request("https://example.test/"), {
upgrade: () => false,
});
expect(await response?.text()).toContain("<!-- plugin-render -->");
});
@@ -0,0 +1,74 @@
import { expect, test } from "bun:test";
import { createProductionHandlers, type ProdManifest } from "../src/prod.ts";
const manifest: ProdManifest = {
pages: [
{
raw: "/",
mod: { default: () => "<main>Production artifact</main>", meta: { title: "Prod" } },
},
{
raw: "/partial",
staticShell:
'<main>Build shell<template data-wrn-dynamic-placeholder="wrn-region-0"></template></main>',
mod: {
default: () =>
'<main>Request shell<wrn-dynamic-region data-wrn-dynamic="true"><strong>User 42</strong></wrn-dynamic-region></main>',
meta: { title: "Partial" },
__wrnexusRender: "partial-static",
},
},
{
raw: "/async",
mod: {
default: () => "<main>Async page</main>",
meta: { title: "Async" },
__wrnexusClientLoad: async () => ({ users: [{ id: 1, name: "Ada" }] }),
},
},
],
api: [],
realtime: [],
middleware: [],
components: [],
layouts: [],
};
const server = { upgrade: () => false } as never;
test("supervised production runtime injects reconnecting DOM-morph support", async () => {
const handlers = createProductionHandlers(manifest, { developmentRuntime: true });
const response = await handlers.fetch(new Request("http://localhost/"), server);
expect(response).toBeInstanceOf(Response);
expect(await (response as Response).text()).toContain("/__wrnexus/hmr");
});
test("production streams request regions into the build-time static shell", async () => {
const handlers = createProductionHandlers(manifest, {});
const response = (await handlers.fetch(
new Request("http://localhost/partial"),
server,
)) as Response;
const html = await response.text();
expect(response.headers.get("x-wrnexus-static-shell")).toBe("build");
expect(html).toContain("Build shell");
expect(html).not.toContain("Request shell");
expect(html).toContain("User 42");
});
test("normal production output remains free of development HMR", async () => {
const handlers = createProductionHandlers(manifest, {});
const response = await handlers.fetch(new Request("http://localhost/"), server);
expect(await (response as Response).text()).not.toContain("/__wrnexus/hmr");
});
test("production client-load endpoint returns only the requested named result", async () => {
const handlers = createProductionHandlers(manifest, {});
const response = (await handlers.fetch(
new Request("http://localhost/__wrnexus/client-load?route=%2Fasync&name=users"),
server,
)) as Response;
expect(response.status).toBe(200);
expect(response.headers.get("cache-control")).toBe("private, no-store");
expect(await response.json()).toEqual({ data: [{ id: 1, name: "Ada" }] });
});