complete performance and reliability follow-ups

This commit is contained in:
2026-08-09 23:04:36 +05:30
parent 8f19a5eb2b
commit 232d8e6734
31 changed files with 525 additions and 726 deletions
@@ -0,0 +1,71 @@
import { describe, expect, test } from "bun:test";
import { createContext } from "@wrnexus/core";
import { v } from "@wrnexus/validation";
import {
captchaPlugin,
defineCaptchaProvider,
parseWithCaptcha,
type CaptchaProvider,
} from "../src/index.ts";
import type { PluginContext, TransformContext } from "@wrnexus/plugin";
describe("CAPTCHA validation and development audit", () => {
test("combines schema and action-bound provider verification failures", async () => {
const requests: unknown[] = [];
const provider: CaptchaProvider = defineCaptchaProvider({
name: "fixture",
client: { responseField: "captcha-response" },
async verify(input) {
requests.push(input);
return { success: false, provider: "fixture", action: input.action, message: "Try again" };
},
});
const ctx = createContext(
new Request("https://app.test/signup"),
new URL("https://app.test/signup"),
);
const result = await parseWithCaptcha(
v.object({ email: v.string().email() }),
{ email: "invalid", "captcha-response": "token" },
ctx,
{ action: "signup", provider, bindIp: true },
);
expect(result.ok).toBe(false);
expect(result.errors.email).toBeDefined();
expect(result.errors["captcha-response"]).toBe("Try again");
expect(requests[0]).toEqual(
expect.objectContaining({ action: "signup", hostname: "app.test", providerToken: "token" }),
);
});
test("rejects incomplete custom providers", () => {
expect(() =>
defineCaptchaProvider({ name: "", client: { responseField: "x" } } as never),
).toThrow("stable name");
expect(() =>
defineCaptchaProvider({ name: "x", client: { responseField: "" } } as never),
).toThrow("responseField");
expect(() =>
defineCaptchaProvider({ name: "x", client: { responseField: "token" } } as never),
).toThrow("verify()");
});
test("flags exposed secrets, missing provider keys, and inaccessible hard challenges", async () => {
const metadata = new Map<string, unknown>();
const plugin = captchaPlugin();
const context = {
mode: "development",
file: "signup.wrn",
metadata,
} as TransformContext;
await plugin.transformCode?.(
`<Captcha provider="turnstile" secretKey="leaked" action="signup" disturbance="90" showAudio="false" />`,
context,
);
const panels = await plugin.devToolbarPanels?.({ metadata } as PluginContext);
const ids = panels?.[0]?.issues?.map((issue) => String((issue as { id: string }).id)) ?? [];
expect(ids.some((id) => id.startsWith("client-secret:"))).toBe(true);
expect(ids.some((id) => id.startsWith("missing-site-key:"))).toBe(true);
expect(ids.some((id) => id.startsWith("hard-without-audio:"))).toBe(true);
});
});
+15
View File
@@ -44,6 +44,7 @@ import {
bundleCss,
renderStyles,
resolveThemeConfig,
renderActiveThemeCss,
renderThemeCss,
renderThemeRuntime,
} from "@wrnexus/styles";
@@ -497,6 +498,19 @@ export async function runBuild(appRoot: string): Promise<void> {
const themeJs = renderThemeRuntime(theme);
writeFileSync(join(distDir, "theme.css"), themeCss, "utf8");
writeFileSync(join(distDir, "theme.js"), themeJs, "utf8");
const themeAssetsDir = join(distDir, "theme");
for (const themeName of theme.names) {
const themeDir = join(themeAssetsDir, encodeURIComponent(themeName));
mkdirSync(themeDir, { recursive: true });
writeFileSync(join(themeDir, "_.css"), renderActiveThemeCss(theme, themeName), "utf8");
for (const accentName of theme.accentNames) {
writeFileSync(
join(themeDir, `${encodeURIComponent(accentName)}.css`),
renderActiveThemeCss(theme, themeName, accentName),
"utf8",
);
}
}
assetHash.update(themeCss);
assetHash.update(themeJs);
console.log(`✓ Theme: ${theme.names.length} themes (default: ${theme.default})`);
@@ -709,6 +723,7 @@ await createProductionServer(
controllersPath: join(import.meta.dir, "controllers.js"),
clientModulesDir: join(import.meta.dir, "client"),
themePath: join(import.meta.dir, "theme.css"),
themeAssetsDir: join(import.meta.dir, "theme"),
themeJsPath: join(import.meta.dir, "theme.js"),
theme: ${JSON.stringify(theme)},
uiCssPath: join(import.meta.dir, "ui.css"),
+37 -32
View File
@@ -882,7 +882,12 @@ function __wrnexusEscapeHtml(value: unknown): string {
return String(value).replace(/[&<>"']/g, (ch) => __wrnexusHtmlEscapes[ch] ?? ch);
}
function __wrnexusEvalData(data: unknown, body: string, helpers = "", ctx: any): unknown {
type __WrnexusContext = import("@wrnexus/core").Context & {
__wrnexusCallApi?: (path: string, method: string) => Promise<unknown>;
localStorage?: unknown;
};
function __wrnexusEvalData(data: unknown, body: string, helpers = "", ctx: __WrnexusContext): unknown {
const adapters = {
cookies: ctx.cookies,
session: ctx.session,
@@ -913,7 +918,7 @@ function __wrnexusPropAttr(
);
}
async function __wrnexusCallApi(path: string, method: string, ctx: any): Promise<unknown> {
async function __wrnexusCallApi(path: string, method: string, ctx: __WrnexusContext): Promise<unknown> {
if (typeof ctx.__wrnexusCallApi === "function") {
return await ctx.__wrnexusCallApi(path, method);
}
@@ -928,7 +933,7 @@ async function __wrnexusCallApi(path: string, method: string, ctx: any): Promise
return type.includes("application/json") ? await res.json() : await res.text();
}
async function __wrnexusRenderSsrBindings(html: string, ctx: any): Promise<string> {
async function __wrnexusRenderSsrBindings(html: string, ctx: __WrnexusContext): Promise<string> {
for (const binding of __wrnexusSsrBindings) {
const data = await __wrnexusCallApi(binding.path, binding.method, ctx);
const value = __wrnexusEvalData(data, binding.body, binding.helpers, ctx);
@@ -1438,14 +1443,14 @@ export function generate(ast: PageAst): string {
out.push(`const __wrnexusSsrBindings = ${JSON.stringify(ssrBindings, null, 2)};`);
const decls = loopConsts.length > 0 ? loopConsts.join("\n") + "\n" : "";
out.push(
`export default async function ${ast.name}(ctx: any) {
`export default async function ${ast.name}(ctx: __WrnexusContext) {
${storeDeclarations}
${serverLoadAliases}
${decls}
const __state: ${stateType} = ${dynamicStateInitializer};
${ssrStateAliases}
${ssrComputedAliases}
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[key]]));
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, Reflect.get(__state, key)]));
const __scopeValue = Object.entries(__hydrationState)
.map(([key, value]) => {
let encoded: string;
@@ -1472,13 +1477,13 @@ export function generate(ast: PageAst): string {
);
} else {
out.push(
`export default ${storeBindings.length > 0 ? "async " : ""}function ${ast.name}(ctx: any) {
`export default ${storeBindings.length > 0 ? "async " : ""}function ${ast.name}(ctx: import("@wrnexus/core").Context) {
${storeDeclarations}
${serverLoadAliases}
const __state: ${stateType} = ${dynamicStateInitializer};
${ssrStateAliases}
${ssrComputedAliases}
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[key]]));
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, Reflect.get(__state, key)]));
const __scopeValue = Object.entries(__hydrationState)
.map(([key, value]) => {
let encoded: string;
@@ -1505,14 +1510,14 @@ export function generate(ast: PageAst): string {
if (staticShellBody !== undefined) {
out.push(
`export async function __wrnexusBuildStaticShell(ctx: any = {}) {
`export async function __wrnexusBuildStaticShell(ctx: import("@wrnexus/core").Context = {} as import("@wrnexus/core").Context) {
${storeDeclarations}
${serverLoadAliases}
${loopConsts.length > 0 ? loopConsts.join("\n") : ""}
const __state: ${stateType} = ${dynamicStateInitializer};
${ssrStateAliases}
${ssrComputedAliases}
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, (__state as any)[key]]));
const __hydrationState = Object.fromEntries(${hydrationStateNames}.map((key) => [key, Reflect.get(__state, key)]));
const __scopeValue = Object.entries(__hydrationState)
.map(([key, value]) => {
let encoded: string;
@@ -1572,7 +1577,7 @@ export function generate(ast: PageAst): string {
})
.join("\n");
const visible = exposed.filter((entry) => entry.name);
return `export async function ${exportName}(ctx: any) {
return `export async function ${exportName}(ctx: import("@wrnexus/core").Context) {
${exposed
.filter((entry) => !entry.name)
.map((entry) => entry.body)
@@ -1599,7 +1604,7 @@ ${
);
continue;
}
out.push(`export async function ${action.name}(input: any, ctx: any) {
out.push(`export async function ${action.name}(input: InferSchema<typeof ${action.schema}>, ctx: import("@wrnexus/core").Context) {
const invalidate = (...tags: string[]) => {
const bucket = (ctx.locals.__wrnexusInvalidatedTags ??= []);
bucket.push(...tags.flat());
@@ -1630,7 +1635,7 @@ ${ast.actions
ast.apis.forEach((api, index) => {
const name = `__wrnexusApi_${api.method}_${index}`;
out.push(`// ${api.method} ${apiRoutePath(api.path)}
const ${name} = async (ctx: any) => {${api.body}};`);
const ${name} = async (ctx: import("@wrnexus/core").Context) => {${api.body}};`);
});
const entries = ast.apis.map(
@@ -1652,7 +1657,7 @@ const ${name} = async (ctx: any) => {${api.body}};`);
const handlers = ast.realtimes.flatMap((rt) =>
rt.handlers.map((h) => {
const params = ["ws", ...h.args].join(", ");
return ` ${h.event}(${params}: any) {${h.body}},`;
return ` ${h.event}(${params}: Event) {${h.body}},`;
}),
);
out.push(`export const websocket = {\n${handlers.join("\n")}\n};`);
@@ -2413,7 +2418,7 @@ function generateComponent(ast: PageAst): string {
);
}
decls.push(
` const ${nameRefs.get(prop.name)}: ${prop.valueType ?? "any"} = __coerce(__p[${JSON.stringify(prop.name)}], (${resolveExpr(prop.default)}), ${JSON.stringify(runtimeTypeOf(prop.valueType))}, ${JSON.stringify(prop.name)});`,
` const ${nameRefs.get(prop.name)}: ${prop.valueType ?? "unknown"} = __coerce(__p[${JSON.stringify(prop.name)}], (${resolveExpr(prop.default)}), ${JSON.stringify(runtimeTypeOf(prop.valueType))}, ${JSON.stringify(prop.name)}) as ${prop.valueType ?? "unknown"};`,
);
}
if (!effectiveProps.some((prop) => prop.name === "attrs")) {
@@ -2503,7 +2508,7 @@ function generateComponent(ast: PageAst): string {
);
}
out.push(`function __coerce(v: any, def: any, declared: string = "unknown", propName: string = "prop"): any {
out.push(`function __coerce(v: unknown, def: unknown, declared: string = "unknown", propName: string = "prop"): unknown {
if (v === undefined || v === null) {
return def;
}
@@ -2563,7 +2568,7 @@ function generateComponent(ast: PageAst): string {
throw new TypeError("Expected an object prop '" + propName + "'");
}
if (declared === "bigint") return BigInt(v);
if (declared === "bigint") return BigInt(v as string | number | bigint | boolean);
if (declared === "function" && typeof v !== "function") {
throw new TypeError("Expected a function prop");
}
@@ -2571,15 +2576,15 @@ function generateComponent(ast: PageAst): string {
}
function __restProps(
props: Record<string, any>,
props: Record<string, unknown>,
declared: Set<string>,
): Record<string, any> {
): Record<string, unknown> {
return Object.fromEntries(
Object.entries(props).filter(([name]) => !declared.has(name)),
);
}
function __wireHtml(v: any): string {
function __wireHtml(v: unknown): string {
return String(v == null ? "" : v).replace(
/[&<>]/g,
(c) =>
@@ -2591,7 +2596,7 @@ function __wireHtml(v: any): string {
);
}
function __wireAttr(v: any): string {
function __wireAttr(v: unknown): string {
return String(v == null ? "" : v).replace(
/[&<>"]/g,
(c) =>
@@ -2605,7 +2610,7 @@ function __wireAttr(v: any): string {
);
}
function __wireBooleanAttr(name: string, value: any): string {
function __wireBooleanAttr(name: string, value: unknown): string {
return value === true ||
value === "true" ||
value === "" ||
@@ -2616,7 +2621,7 @@ function __wireBooleanAttr(name: string, value: any): string {
: "";
}
function __wireSpreadAttrs(value: any): string {
function __wireSpreadAttrs(value: unknown): string {
if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
const booleanAttributes = new Set(${JSON.stringify([...HTML_BOOLEAN_ATTRIBUTES])});
@@ -2652,7 +2657,7 @@ function __wireSpreadAttrs(value: any): string {
return attributes.join("");
}
function __wireProp(v: any): string {
function __wireProp(v: unknown): string {
const value =
v !== null && typeof v === "object"
? JSON.stringify(v)
@@ -2661,18 +2666,18 @@ function __wireProp(v: any): string {
return __wireAttr(value);
}
function __wireRaw(v: any): string {
function __wireRaw(v: unknown): string {
return String(v == null ? "" : v);
}`);
if (hasServerEach) {
out.push(`function __wrnexusEncodeLoopLocals(value: Record<string, any>): string {
out.push(`function __wrnexusEncodeLoopLocals(value: Record<string, unknown>): string {
return __WrnexusBuffer.from(JSON.stringify(value), "utf8").toString("base64");
}`);
}
if (needsScope) {
out.push(`function __wrnexusSerializeScopeValue(value: any): string {
out.push(`function __wrnexusSerializeScopeValue(value: unknown): string {
if (value === undefined) {
return "undefined";
}
@@ -2706,7 +2711,7 @@ function __wireRaw(v: any): string {
}
}
function __wrnexusScopeDecl(obj: Record<string, any>): string {
function __wrnexusScopeDecl(obj: Record<string, unknown>): string {
return Object.keys(obj)
.map(
(key) =>
@@ -2727,7 +2732,7 @@ function __wireRaw(v: any): string {
const serverFunctionSource = serverFunctions ? `${serverFunctions}\n` : "";
out.push(
`export function render(props: ${effectiveProps.length > 0 ? `${ast.name}Props` : "Record<string, any>"} = {} as ${effectiveProps.length > 0 ? `${ast.name}Props` : "Record<string, any>"}): string {\n` +
`export function render(props: ${effectiveProps.length > 0 ? `${ast.name}Props` : "Record<string, unknown>"} = {} as ${effectiveProps.length > 0 ? `${ast.name}Props` : "Record<string, unknown>"}): string {\n` +
` const __p = props || {};\n` +
(decls.length > 0 ? decls.join("\n") + "\n" : "") +
serverFunctionSource +
@@ -2742,7 +2747,7 @@ function __wireRaw(v: any): string {
return out.join("\n\n") + "\n";
}
function __wireRaw(v: any): string {
function __wireRaw(v: unknown): string {
return String(v == null ? "" : v);
}
@@ -2751,19 +2756,19 @@ function wholeAttributeExpression(value: string): string | null {
return match?.[1]?.trim() || null;
}
function __wireHtml(v: any): string {
function __wireHtml(v: unknown): string {
return String(v == null ? "" : v).replace(/[&<>]/g, (c) =>
c === "&" ? "&amp;" : c === "<" ? "&lt;" : "&gt;",
);
}
function __wireAttr(v: any): string {
function __wireAttr(v: unknown): string {
return String(v == null ? "" : v).replace(/[&<>"]/g, (c) =>
c === "&" ? "&amp;" : c === "<" ? "&lt;" : c === ">" ? "&gt;" : "&quot;",
);
}
function __wireProp(v: any): string {
function __wireProp(v: unknown): string {
const value =
v !== null && typeof v === "object" ? JSON.stringify(v) : String(v == null ? "" : v);
@@ -45,7 +45,7 @@ export interface CounterOutputs {
"change"(value: number): void;
}
function __coerce(v: any, def: any, declared: string = "unknown", propName: string = "prop"): any {
function __coerce(v: unknown, def: unknown, declared: string = "unknown", propName: string = "prop"): unknown {
if (v === undefined || v === null) {
return def;
}
@@ -105,7 +105,7 @@ function __coerce(v: any, def: any, declared: string = "unknown", propName: stri
throw new TypeError("Expected an object prop '" + propName + "'");
}
if (declared === "bigint") return BigInt(v);
if (declared === "bigint") return BigInt(v as string | number | bigint | boolean);
if (declared === "function" && typeof v !== "function") {
throw new TypeError("Expected a function prop");
}
@@ -113,15 +113,15 @@ function __coerce(v: any, def: any, declared: string = "unknown", propName: stri
}
function __restProps(
props: Record<string, any>,
props: Record<string, unknown>,
declared: Set<string>,
): Record<string, any> {
): Record<string, unknown> {
return Object.fromEntries(
Object.entries(props).filter(([name]) => !declared.has(name)),
);
}
function __wireHtml(v: any): string {
function __wireHtml(v: unknown): string {
return String(v == null ? "" : v).replace(
/[&<>]/g,
(c) =>
@@ -133,7 +133,7 @@ function __wireHtml(v: any): string {
);
}
function __wireAttr(v: any): string {
function __wireAttr(v: unknown): string {
return String(v == null ? "" : v).replace(
/[&<>"]/g,
(c) =>
@@ -147,7 +147,7 @@ function __wireAttr(v: any): string {
);
}
function __wireBooleanAttr(name: string, value: any): string {
function __wireBooleanAttr(name: string, value: unknown): string {
return value === true ||
value === "true" ||
value === "" ||
@@ -158,7 +158,7 @@ function __wireBooleanAttr(name: string, value: any): string {
: "";
}
function __wireSpreadAttrs(value: any): string {
function __wireSpreadAttrs(value: unknown): string {
if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
const booleanAttributes = new Set(["allowfullscreen","async","autofocus","autoplay","checked","controls","default","defer","disabled","formnovalidate","hidden","inert","ismap","itemscope","loop","multiple","muted","nomodule","novalidate","open","playsinline","readonly","required","reversed","selected"]);
@@ -194,7 +194,7 @@ function __wireSpreadAttrs(value: any): string {
return attributes.join("");
}
function __wireProp(v: any): string {
function __wireProp(v: unknown): string {
const value =
v !== null && typeof v === "object"
? JSON.stringify(v)
@@ -203,11 +203,11 @@ function __wireProp(v: any): string {
return __wireAttr(value);
}
function __wireRaw(v: any): string {
function __wireRaw(v: unknown): string {
return String(v == null ? "" : v);
}
function __wrnexusSerializeScopeValue(value: any): string {
function __wrnexusSerializeScopeValue(value: unknown): string {
if (value === undefined) {
return "undefined";
}
@@ -241,7 +241,7 @@ function __wrnexusSerializeScopeValue(value: any): string {
}
}
function __wrnexusScopeDecl(obj: Record<string, any>): string {
function __wrnexusScopeDecl(obj: Record<string, unknown>): string {
return Object.keys(obj)
.map(
(key) =>
@@ -260,7 +260,7 @@ function __wrnexusSerializeScopeValue(value: any): string {
export function render(props: CounterProps = {} as CounterProps): string {
const __p = props || {};
const label: string = __coerce(__p["label"], ("Count"), "string", "label");
const label: string = __coerce(__p["label"], ("Count"), "string", "label") as string;
const __attrs = __restProps(__p, new Set(["label"]));
let count = (0);
const __scopeState = { "label": label, "count": count };
+2 -1
View File
@@ -31,7 +31,8 @@ export function sqliteSessionStore(path = "sessions.db"): SessionBackend {
if (!row) return undefined;
try {
return { data: JSON.parse(row.data) as Record<string, unknown>, expiresAt: row.expiresAt };
} catch {
} catch (error) {
console.warn(`[wrnexus:db] discarded corrupt session '${id}'`, error);
return undefined;
}
},
+13
View File
@@ -19,6 +19,7 @@ import {
} from "@wrnexus/csr";
import {
renderStyles,
renderActiveThemeCss,
renderThemeCss,
renderThemeRuntime,
type ResolvedTheme,
@@ -122,6 +123,18 @@ export function createDevAssetServer(
? cssResponse(renderThemeCss(theme))
: new Response("Not Found", { status: 404 });
}
const activeThemeMatch = /^\/__wrnexus\/theme\/([^/]+)\/([^/]+)\.css$/.exec(pathname);
if (activeThemeMatch && theme) {
try {
const themeName = decodeURIComponent(activeThemeMatch[1]!);
const accentPart = decodeURIComponent(activeThemeMatch[2]!);
return cssResponse(
renderActiveThemeCss(theme, themeName, accentPart === "_" ? undefined : accentPart),
);
} catch {
return new Response("Not Found", { status: 404 });
}
}
if (pathname === "/__wrnexus/theme.js") {
return theme
? jsResponse(renderThemeRuntime(theme))
+24
View File
@@ -99,6 +99,8 @@ export interface ProdOptions {
clientModulesDir?: string;
/** Absolute path to the pre-built theme stylesheet (`theme.css`). */
themePath?: string;
/** Pre-built active theme/accent stylesheets, loaded on demand. */
themeAssetsDir?: string;
/** Absolute path to the pre-built theme runtime (`theme.js`). */
themeJsPath?: string;
/** Resolved theme config: enables `<html data-theme>` + `theme.css` link. */
@@ -366,6 +368,28 @@ function createProdAssetServer(opts: ProdOptions): AssetServer {
return new Response(opts.schemasJs ?? "window.__wireSchemas={};", { headers: JS_HEADERS });
}
if (pathname === "/__wrnexus/theme.css") return serveFile(opts.themePath, CSS_HEADERS);
const activeThemeMatch = /^\/__wrnexus\/theme\/([^/]+)\/([^/]+)\.css$/.exec(pathname);
if (activeThemeMatch && opts.theme && opts.themeAssetsDir) {
try {
const themeName = decodeURIComponent(activeThemeMatch[1]!);
const accentPart = decodeURIComponent(activeThemeMatch[2]!);
if (!opts.theme.names.includes(themeName))
return new Response("Not Found", { status: 404 });
if (accentPart !== "_" && !opts.theme.accentNames.some((name) => name === accentPart)) {
return new Response("Not Found", { status: 404 });
}
return serveFile(
join(
opts.themeAssetsDir,
encodeURIComponent(themeName),
`${accentPart === "_" ? "_" : encodeURIComponent(accentPart)}.css`,
),
CSS_HEADERS,
);
} catch {
return new Response("Not Found", { status: 404 });
}
}
if (pathname === "/__wrnexus/theme.js") return serveFile(opts.themeJsPath, JS_HEADERS);
if (pathname === "/__wrnexus/ui.css") return serveFile(opts.uiCssPath, CSS_HEADERS);
if (pathname === "/__wrnexus/framework.css")
+12 -73
View File
@@ -66,12 +66,12 @@ 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 { collectScripts, usesMobileRuntime } from "./script-selection.ts";
export { collectScripts, usesMobileRuntime } from "./script-selection.ts";
import {
ACCENT_COOKIE,
THEME_COOKIE,
THEME_CSS_HREF,
THEME_JS_HREF,
activeThemeCssHref,
resolveAccentName,
resolveThemeName,
type MobileConfig,
@@ -81,7 +81,6 @@ import {
type TenancyConfig,
} from "@wrnexus/styles";
import {
I18N_JS_HREF,
renderI18nData,
makeT,
resolveLang,
@@ -945,9 +944,6 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
headParts.push(
`<link rel="stylesheet" href="${versionAssetUrl("/__wrnexus/framework.css", deps.assetVersion)}" />`,
);
} else if (deps.theme && !deps.stylesIncludeFramework) {
const themeHref = versionAssetUrl(THEME_CSS_HREF, deps.assetVersion);
headParts.push(`<link rel="stylesheet" href="${themeHref}" />`);
}
if (deps.hasUi && !deps.hasFrameworkStyles && !deps.stylesIncludeFramework) {
headParts.push(
@@ -1839,6 +1835,7 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
// <html> attributes: no-flash theme (from cookie, validated) + active lang.
const attrs: string[] = [];
let activeThemeHead = "";
if (deps.theme) {
const themeName = resolveThemeName(ctx.cookies.get(THEME_COOKIE), deps.theme);
@@ -1850,6 +1847,13 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
if (accentName) {
attrs.push(`data-accent="${accentName}"`);
}
if (!deps.hasFrameworkStyles && !deps.stylesIncludeFramework) {
const themeHref = versionAssetUrl(
activeThemeCssHref(themeName, accentName),
deps.assetVersion,
);
activeThemeHead = `<link rel="stylesheet" data-wrnexus-theme href="${themeHref}" />`;
}
}
attrs.push(`lang="${safeLanguageTag(language)}"`);
if (deps.i18n) attrs.push(`dir="${deps.i18n.direction[language] ?? "ltr"}"`);
@@ -1873,6 +1877,7 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
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" />` : "",
activeThemeHead,
extraHead,
]
.filter(Boolean)
@@ -2443,72 +2448,6 @@ export function normalizeComponentName(name: string): string {
return name.toLowerCase().replace(/[-_]/g, "");
}
/**
* Decide which framework scripts a rendered page needs. Components are already
* server-rendered into the HTML; the only script is the reactive runtime, and
* only when the page actually contains a scope or a browser-side API fetch.
*/
export function collectScripts(
body: string,
clientRuntimes: readonly ClientRuntimeDefinition[] = [],
navigation: { mode?: "auto" | "client" | "document" } = {},
): RenderScript[] {
const scripts: RenderScript[] = [];
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) ||
/\bdata-wire-accent-(set|clear)\b/.test(body)
) {
scripts.push(THEME_JS_HREF);
}
// Validation: schema descriptors + the generic validator, only for pages with a form.
if (/\bdata-schema="[A-Za-z0-9_-]+"/.test(body)) {
scripts.push("/__wrnexus/schemas.js", "/__wrnexus/validate.js");
}
// Language switcher runtime, only when the page has one.
if (/\bdata-wire-lang-set\b/.test(body) || /\bselect[^>]*\bdata-wire-lang\b/.test(body)) {
scripts.push(I18N_JS_HREF);
}
// Realtime client, only when the page declares a room.
if (/\bdata-room=/.test(body)) {
scripts.push("/__wrnexus/realtime.js");
}
// File-upload runtime (drag-drop + progress), only when a page has an uploader.
if (/\bdata-uploader\b/.test(body)) {
scripts.push("/__wrnexus/uploader.js");
}
// Package runtimes are declarative. Components mark the rendered HTML with
// `data-wrnexus-runtime="id"`; the corresponding package chunk is loaded
// once, without requiring application-authored script tags or public copies.
scripts.push(...runtimeScriptsForMarkup(body, clientRuntimes));
// `auto` is the performance-first default: a fully static page ships no
// framework JavaScript and its links use native document navigation. Routes
// that already need browser behavior also receive progressive navigation.
// `client` preserves the explicit always-on behavior; `document` disables it.
const mode = navigation.mode ?? "auto";
if (mode === "client" || (mode === "auto" && scripts.length > 0)) {
scripts.unshift("/__wrnexus/nav.js");
}
return scripts;
}
/** Whether rendered markup needs the Capacitor/native browser bridge. */
export function usesMobileRuntime(body: string): boolean {
return /\b(?:data-mobile-[\w-]+|data-native-(?:browser|mobile|only|requires|unsupported|options)|data-on-wrnexus-(?:browser|mobile)-[\w-]+)\b/.test(
body,
);
}
function safeLanguageTag(value: string): string {
return /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/.test(value) ? value : "en";
}
@@ -0,0 +1,51 @@
import { I18N_JS_HREF } from "@wrnexus/i18n";
import type { ClientRuntimeDefinition } from "@wrnexus/plugin";
import { THEME_JS_HREF } from "@wrnexus/styles";
import type { RenderScript } from "@wrnexus/ssr";
import { runtimeScriptsForMarkup } from "./plugin-assets.ts";
/** Select browser runtimes from capabilities present in rendered markup. */
export function collectScripts(
body: string,
clientRuntimes: readonly ClientRuntimeDefinition[] = [],
navigation: { mode?: "auto" | "client" | "document" } = {},
): RenderScript[] {
const scripts: RenderScript[] = [];
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");
if (
/\bdata-wire-theme-(toggle|set)\b/.test(body) ||
/\bdata-wire-accent-(set|clear)\b/.test(body)
) {
scripts.push(THEME_JS_HREF);
}
if (/\bdata-schema="[A-Za-z0-9_-]+"/.test(body)) {
scripts.push("/__wrnexus/schemas.js", "/__wrnexus/validate.js");
}
if (/\bdata-wire-lang-set\b/.test(body) || /\bselect[^>]*\bdata-wire-lang\b/.test(body)) {
scripts.push(I18N_JS_HREF);
}
if (/\bdata-room=/.test(body)) scripts.push("/__wrnexus/realtime.js");
if (/\bdata-uploader\b/.test(body)) scripts.push("/__wrnexus/uploader.js");
scripts.push(...runtimeScriptsForMarkup(body, clientRuntimes));
const mode = navigation.mode ?? "auto";
if (mode === "client" || (mode === "auto" && scripts.length > 0)) {
scripts.unshift("/__wrnexus/nav.js");
}
return scripts;
}
/** Whether rendered markup needs the Capacitor/native browser bridge. */
export function usesMobileRuntime(body: string): boolean {
return /\b(?:data-mobile-[\w-]+|data-native-(?:browser|mobile|only|requires|unsupported|options)|data-on-wrnexus-(?:browser|mobile)-[\w-]+)\b/.test(
body,
);
}
+8 -4
View File
@@ -22,7 +22,7 @@ export function webVitalsClient(options: WebVitalsClientOptions = {}): string {
function send(name, value) {
var body = JSON.stringify({ name: name, value: value, rating: rating(name, value), route: location.pathname, navigationType: performance.getEntriesByType("navigation")[0]?.type });
if (navigator.sendBeacon) navigator.sendBeacon(endpoint, new Blob([body], { type: "application/json" }));
else fetch(endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: body, keepalive: true }).catch(function(){});
else fetch(endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: body, keepalive: true }).catch(function(error){ console.warn("[wrnexus:observability] web-vitals delivery failed", error); });
}
function flush() {
if (flushed) return;
@@ -31,9 +31,13 @@ export function webVitalsClient(options: WebVitalsClientOptions = {}): string {
if (inp) send("INP", inp);
if (cls) send("CLS", cls);
}
try { new PerformanceObserver(function(list){ var entries=list.getEntries(); var last=entries[entries.length-1]; if(last) lcp = Math.max(lcp, last.startTime); }).observe({type:"largest-contentful-paint",buffered:true}); } catch(_) {}
try { new PerformanceObserver(function(list){ list.getEntries().forEach(function(e){ if(!e.hadRecentInput) cls += e.value; }); }).observe({type:"layout-shift",buffered:true}); } catch(_) {}
try { new PerformanceObserver(function(list){ list.getEntries().forEach(function(e){ inp=Math.max(inp,e.duration||0); }); }).observe({type:"event",durationThreshold:40,buffered:true}); } catch(_) {}
function observe(type, callback, options) {
try { new PerformanceObserver(callback).observe(options); }
catch(error) { console.warn("[wrnexus:observability] PerformanceObserver unavailable for " + type, error); }
}
observe("largest-contentful-paint", function(list){ var entries=list.getEntries(); var last=entries[entries.length-1]; if(last) lcp = Math.max(lcp, last.startTime); }, {type:"largest-contentful-paint",buffered:true});
observe("layout-shift", function(list){ list.getEntries().forEach(function(e){ if(!e.hadRecentInput) cls += e.value; }); }, {type:"layout-shift",buffered:true});
observe("event", function(list){ list.getEntries().forEach(function(e){ inp=Math.max(inp,e.duration||0); }); }, {type:"event",durationThreshold:40,buffered:true});
addEventListener("visibilitychange", function(){ if(document.visibilityState === "hidden") flush(); });
addEventListener("pagehide", flush, { once: true });
})();`;
+14
View File
@@ -0,0 +1,14 @@
export type ObservabilityDiagnostic = (message: string, error: unknown) => void;
/** Report exporter failures without feeding them back through the exporter. */
export function reportObservabilityFailure(
message: string,
error: unknown,
diagnostic?: ObservabilityDiagnostic,
): void {
if (diagnostic) {
diagnostic(message, error);
return;
}
console.warn(`[wrnexus:observability] ${message}`, error);
}
+5 -1
View File
@@ -2,6 +2,7 @@ import type { MetricPoint } from "./metrics.ts";
import type { LogRecord } from "./logging.ts";
import type { MetricExporter } from "./server.ts";
import type { SpanExporter, SpanRecord, TraceContext } from "./trace.ts";
import { reportObservabilityFailure, type ObservabilityDiagnostic } from "./diagnostics.ts";
export type FrameworkSpanKind =
"database" | "cache" | "queue" | "realtime" | "server-action" | "application";
@@ -26,6 +27,7 @@ export function createOperationTracer(
context?: () => Partial<TraceContext>;
now?: () => number;
onExportError?: (error: unknown) => void;
diagnostic?: ObservabilityDiagnostic;
} = {},
): OperationTracer {
const now = options.now ?? Date.now;
@@ -66,7 +68,9 @@ export function createOperationTracer(
try {
await options.exporter?.export([record]);
} catch (error) {
options.onExportError?.(error);
if (options.onExportError) options.onExportError(error);
else
reportObservabilityFailure("operation span export failed", error, options.diagnostic);
}
}
},
+4 -1
View File
@@ -1,4 +1,5 @@
import { createTracer, type Context, type Middleware } from "@wrnexus/core";
import { reportObservabilityFailure, type ObservabilityDiagnostic } from "./diagnostics.ts";
export interface TraceContext {
version: "00";
@@ -63,6 +64,7 @@ export interface TraceMiddlewareOptions {
routeName?: (ctx: Context) => string;
serverTiming?: boolean;
onExportError?: (error: unknown, span: SpanRecord) => void | Promise<void>;
diagnostic?: ObservabilityDiagnostic;
}
export function traceMiddleware(options: TraceMiddlewareOptions = {}): Middleware {
@@ -131,7 +133,8 @@ export function traceMiddleware(options: TraceMiddlewareOptions = {}): Middlewar
await options.onSpan?.(span);
await options.exporter?.export([span]);
} catch (error) {
await options.onExportError?.(error, span);
if (options.onExportError) await options.onExportError(error, span);
else reportObservabilityFailure("trace export failed", error, options.diagnostic);
}
}
}
@@ -93,4 +93,14 @@ describe("observability integrations", () => {
expect(await profile("render", () => "ok")).toBe("ok");
expect(profiles[0]).toMatchObject({ name: "render", durationMs: 4 });
});
test("operation exporter failures reach diagnostics without recursive telemetry", async () => {
const diagnostics: string[] = [];
const tracer = createOperationTracer({
exporter: { export: () => Promise.reject(new Error("offline")) },
diagnostic: (message) => diagnostics.push(message),
});
await expect(tracer.span("database", "query", async () => 1)).resolves.toBe(1);
expect(diagnostics).toEqual(["operation span export failed"]);
});
});
@@ -8,6 +8,8 @@ describe("@wrnexus/observability", () => {
expect(client).toContain('addEventListener("pagehide", flush');
expect(client).not.toContain('send("INP",max)');
expect(client).not.toContain('send("LCP", last.startTime)');
expect(client).toContain("web-vitals delivery failed");
expect(client).toContain("PerformanceObserver unavailable for");
});
test("records deterministic counters and histograms", () => {
@@ -81,6 +81,22 @@ describe("production observability operations", () => {
expect(failures).toHaveLength(1);
});
test("reports exporter failures through the non-recursive diagnostic hook", async () => {
const diagnostics: Array<{ message: string; error: unknown }> = [];
const ctx = createContext(
new Request("https://example.test/"),
new URL("https://example.test/"),
);
const middleware = traceMiddleware({
random: (target) => target.fill(8),
exporter: { export: () => Promise.reject(new Error("offline")) },
diagnostic: (message, error) => diagnostics.push({ message, error }),
});
expect((await middleware(ctx, () => new Response("ok"))).status).toBe(200);
expect(diagnostics).toHaveLength(1);
expect(diagnostics[0]?.message).toBe("trace export failed");
});
test("exports OTLP JSON traces and metrics", async () => {
const requests: unknown[] = [];
const send = (async (_url: URL | RequestInfo, init?: RequestInit) => {
+5 -2
View File
@@ -1,6 +1,9 @@
import { createStoreContainer } from "./index.ts";
let globalContainer: ReturnType<typeof createStoreContainer> | undefined;
const storeGlobal = globalThis as typeof globalThis & {
__wrnexusStoreContainer?: ReturnType<typeof createStoreContainer>;
};
export function browserStoreContainer(hydration: Record<string, unknown> = {}) {
if (!globalContainer) {
@@ -17,7 +20,7 @@ export function browserStoreContainer(hydration: Record<string, unknown> = {}) {
}
},
});
(globalThis as any).__wrnexusStoreContainer = globalContainer;
storeGlobal.__wrnexusStoreContainer = globalContainer;
}
return globalContainer;
}
@@ -25,5 +28,5 @@ export function browserStoreContainer(hydration: Record<string, unknown> = {}) {
export async function resetBrowserStores() {
await globalContainer?.dispose();
globalContainer = undefined;
delete (globalThis as any).__wrnexusStoreContainer;
delete storeGlobal.__wrnexusStoreContainer;
}
+20 -13
View File
@@ -27,7 +27,7 @@ function readonlySnapshot<S extends object>(state: S): Readonly<S> {
return Object.freeze(clone(state));
}
function storageFor(kind: StorePersistenceConfig<any>["storage"]): Storage | null {
function storageFor(kind: StorePersistenceConfig<object>["storage"]): Storage | null {
if (typeof window === "undefined") return null;
if (kind === "local") return window.localStorage;
if (kind === "session") return window.sessionStorage;
@@ -91,7 +91,10 @@ export class StoreContainer {
readonly runtime: "server" | "client";
readonly request?: unknown;
readonly routeId?: string;
private readonly instances = new Map<string, StoreInstance<any, any, any>>();
private readonly instances = new Map<
string,
StoreInstance<Record<string, unknown>, Record<string, unknown>, Record<string, StoreFunction>>
>();
private readonly hydration: Record<string, unknown>;
private readonly onMutation?: (mutation: StoreMutation) => void;
private readonly lastMutations = new Map<string, StoreMutation>();
@@ -159,7 +162,7 @@ export class StoreContainer {
Object.fromEntries(
Reflect.ownKeys(instance.computed).map((key) => [
String(key),
(instance.computed as any)[key],
Reflect.get(instance.computed, key),
]),
),
),
@@ -314,7 +317,7 @@ export function createStoreInstance<
const publicState = new Proxy({} as State, {
get(_target, property) {
return (mutableState as any)[property];
return Reflect.get(mutableState, property);
},
set(_target, property) {
throw new TypeError(
@@ -334,7 +337,7 @@ export function createStoreInstance<
return {
enumerable: true,
configurable: true,
value: (mutableState as any)[property],
value: Reflect.get(mutableState, property),
writable: false,
};
},
@@ -360,7 +363,7 @@ export function createStoreInstance<
mutate("$reset", () => {
const next = createInitialState();
for (const key of Object.keys(mutableState)) {
if (!(key in next)) delete (mutableState as any)[key];
if (!(key in next)) Reflect.deleteProperty(mutableState, key);
}
Object.assign(mutableState, next);
});
@@ -382,16 +385,20 @@ export function createStoreInstance<
definitions.find((entry) => entry.runtime === "shared") ??
definitions.find((entry) => entry.runtime === "legacy");
if (!selected) continue;
(actions as any)[name] = async (...args: unknown[]) => {
Reflect.set(actions, name, async (...args: unknown[]) => {
currentAction = name;
internalMutation = true;
try {
return await (selected.handler as any)(context, ...args);
const handler = selected.handler as unknown as (
context: StoreActionContext<State>,
...args: unknown[]
) => unknown;
return await handler(context, ...args);
} finally {
currentAction = "direct";
internalMutation = false;
}
};
});
}
const computed = new Proxy({} as C, {
@@ -461,7 +468,7 @@ export function createStoreInstance<
const result: Partial<State> = {};
const serverKeys = new Set(Object.keys(definition.createServerState?.() ?? {}));
for (const [key, value] of Object.entries(mutableState)) {
if (!serverKeys.has(key)) (result as any)[key] = clone(value);
if (!serverKeys.has(key)) Reflect.set(result, key, clone(value));
}
return result;
},
@@ -479,9 +486,9 @@ export function createStoreInstance<
return new Proxy(core as StoreInstance<State, C, A>, {
get(target, property, receiver) {
if (Reflect.has(target, property)) return Reflect.get(target, property, receiver);
if (property in actions) return (actions as any)[property];
if (property in (definition.computed ?? {})) return (computed as any)[property];
if (property in mutableState) return (publicState as any)[property];
if (property in actions) return Reflect.get(actions, property);
if (property in (definition.computed ?? {})) return Reflect.get(computed, property);
if (property in mutableState) return Reflect.get(publicState, property);
return undefined;
},
set(_target, property) {
+2 -1
View File
@@ -1,7 +1,8 @@
export type StoreKind = "global" | "page";
export type StoreRuntime = "shared" | "client" | "server" | "legacy";
export type PersistenceStorage = "memory" | "session" | "local";
export type StoreFunction = (...args: any[]) => any;
/** Broad callable constraint that preserves each action's concrete parameters and return type. */
export type StoreFunction = (...args: any[]) => unknown;
export type StoreCombinedState<
S extends object,
+3
View File
@@ -61,10 +61,13 @@ export {
THEME_PALETTE_NAMES,
THEME_COOKIE,
THEME_CSS_HREF,
THEME_CSS_PREFIX,
THEME_JS_HREF,
activeThemeCssHref,
resolveThemeConfig,
resolveThemeName,
renderThemeCss,
renderActiveThemeCss,
renderThemeRuntime,
defineThemeTokens,
themeVar,
+52 -30
View File
@@ -121,6 +121,7 @@ export interface ResolvedTheme {
export const THEME_COOKIE = "wire-theme";
export const ACCENT_COOKIE = "wire-accent";
export const THEME_CSS_HREF = "/__wrnexus/theme.css";
export const THEME_CSS_PREFIX = "/__wrnexus/theme/";
export const THEME_JS_HREF = "/__wrnexus/theme.js";
/**
@@ -519,6 +520,44 @@ function selectorValue(value: string): string {
return JSON.stringify(value);
}
function globalThemeCss(): string {
return (
'\nhtml,body{font-family:var(--wire-font-sans,"Plus Jakarta Sans",ui-sans-serif,system-ui,sans-serif);}\n' +
":root{--wire-radius-sm:0.55rem;--wire-radius-md:0.9rem;--wire-radius-lg:1.35rem;" +
"--wire-shadow-1:0 1px 2px color-mix(in srgb, black 22%, transparent)," +
"0 10px 30px color-mix(in srgb, black 14%, transparent);}\n" +
"wrn-slot{display:contents;}\n" +
"[data-for]{display:none !important;}\n"
);
}
/** URL for the small stylesheet containing only one active theme/accent pair. */
export function activeThemeCssHref(themeName: string, accentName?: string): string {
return `${THEME_CSS_PREFIX}${encodeURIComponent(themeName)}/${accentName ? encodeURIComponent(accentName) : "_"}.css`;
}
/** Render only the tokens needed for the current SSR-selected theme and accent. */
export function renderActiveThemeCss(
theme: ResolvedTheme,
themeName: string,
accentName?: string,
): string {
const selectedTheme = theme.themes[themeName];
if (!selectedTheme) throw new Error(`Unknown theme '${themeName}'.`);
const blocks = [`:root{${tokensToDeclarations(selectedTheme)}}`];
if (accentName) {
if (!theme.accentNames.includes(accentName as ThemePaletteName)) {
throw new Error(`Unknown theme accent '${accentName}'.`);
}
blocks.push(
`:root{${tokensToDeclarations(
paletteTokens(THEME_PALETTES[accentName as ThemePaletteName], themeScheme(selectedTheme)),
)}}`,
);
}
return blocks.join("\n") + globalThemeCss();
}
/**
* Generate the theme stylesheet.
*
@@ -559,36 +598,7 @@ export function renderThemeCss(theme: ResolvedTheme): string {
}
}
return (
blocks.join("\n") +
'\nhtml,body{font-family:var(--wire-font-sans,"Plus Jakarta Sans",ui-sans-serif,system-ui,sans-serif);}\n' +
// <wrn-slot> wraps content spliced into a component's <slot> so the client
// runtime can tell which scope authored it. It is a pure ownership marker
// and must never introduce a box: an unknown element would otherwise
// default to display:inline and break the component's own flex/grid layout.
/*
* Shape and elevation tokens the Wire UI stylesheet depends on.
*
* ui.css references --wire-radius-sm (51 times) and --wire-radius-md (21)
* but nothing ever defined them, so every component using them fell back
* to square corners in any app that did not declare them itself.
* --wire-shadow-1 had the same problem: the showcase declared it
* privately, so the library looked right there and flat everywhere else.
* Defining them with the rest of the theme keeps a component looking the
* same in every app; an app can still override them.
*/
":root{--wire-radius-sm:0.55rem;--wire-radius-md:0.9rem;--wire-radius-lg:1.35rem;" +
"--wire-shadow-1:0 1px 2px color-mix(in srgb, black 22%, transparent)," +
"0 10px 30px color-mix(in srgb, black 14%, transparent);}\n" +
"wrn-slot{display:contents;}\n" +
// A [data-for] element is the loop TEMPLATE, not a rendered row: the server
// ships it with mustaches unresolved ({item.title}) and hydration replaces
// it with a comment marker. Painted as-is it flashes one blank, literal
// row on every page load before the runtime boots. Hiding it costs
// nothing after hydration -- the runtime strips data-for from the clones
// it renders, so this only ever matches the template itself.
"[data-for]{display:none !important;}\n"
);
return blocks.join("\n") + globalThemeCss();
}
/**
@@ -603,6 +613,7 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
const names = JSON.stringify(theme.names);
const accentNames = JSON.stringify(theme.accentNames);
const defaultAccent = JSON.stringify(theme.defaultAccent ?? null);
const cssPrefix = JSON.stringify(THEME_CSS_PREFIX);
return `(function(){
var THEME_COOKIE=${JSON.stringify(THEME_COOKIE)};
@@ -611,6 +622,7 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
var ACCENTS=${accentNames};
var DEFAULT_THEME=${JSON.stringify(theme.default)};
var DEFAULT_ACCENT=${defaultAccent};
var THEME_CSS_PREFIX=${cssPrefix};
var MAX_AGE=31536000;
var el=document.documentElement;
@@ -637,6 +649,13 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
document.cookie=encodeURIComponent(name)+"=;path=/;max-age=0;samesite=lax"+secure;
}
function syncStylesheet(themeName,accentName){
var link=document.querySelector("link[data-wrnexus-theme]");
if(!link)return;
var accent=accentName?encodeURIComponent(accentName):"_";
link.href=THEME_CSS_PREFIX+encodeURIComponent(themeName)+"/"+accent+".css";
}
function getTheme(){
var current=el.getAttribute("data-theme")||readCookie(THEME_COOKIE)||DEFAULT_THEME;
return THEMES.indexOf(current)>=0?current:DEFAULT_THEME;
@@ -665,6 +684,7 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
if(THEMES.indexOf(name)<0)return;
el.setAttribute("data-theme",name);
writeCookie(THEME_COOKIE,name);
syncStylesheet(name,getAccent());
syncThemeButtons(document);
window.dispatchEvent(new CustomEvent("wrnexus:theme-changed",{detail:{theme:name}}));
}
@@ -678,6 +698,7 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
if(ACCENTS.indexOf(name)<0)return;
el.setAttribute("data-accent",name);
writeCookie(ACCENT_COOKIE,name);
syncStylesheet(getTheme(),name);
syncAccentButtons(document);
window.dispatchEvent(new CustomEvent("wrnexus:accent-changed",{detail:{accent:name}}));
}
@@ -685,6 +706,7 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
function clearAccent(){
el.removeAttribute("data-accent");
deleteCookie(ACCENT_COOKIE);
syncStylesheet(getTheme(),null);
syncAccentButtons(document);
window.dispatchEvent(new CustomEvent("wrnexus:accent-changed",{detail:{accent:null}}));
}
+13
View File
@@ -5,6 +5,8 @@ import {
resolveThemeConfig,
resolveThemeName,
renderThemeCss,
renderActiveThemeCss,
activeThemeCssHref,
renderThemeRuntime,
} from "../src/index.ts";
@@ -85,6 +87,17 @@ test("renderThemeCss emits :root + per-theme blocks and --wire-* vars", () => {
expect(css).toContain("font-family:var(--wire-font-sans");
});
test("active theme CSS contains only the selected theme and accent", () => {
const theme = resolveThemeConfig({ default: "dark", palette: "rose" });
const full = renderThemeCss(theme);
const active = renderActiveThemeCss(theme, "dark", "rose");
expect(active).toContain(":root{");
expect(active).not.toContain('[data-theme="light"]');
expect(active.length).toBeLessThan(full.length / 4);
expect(activeThemeCssHref("dark", "rose")).toBe("/__wrnexus/theme/dark/rose.css");
expect(() => renderActiveThemeCss(theme, "missing")).toThrow("Unknown theme");
});
test("renderThemeRuntime bakes the theme names for cycling", () => {
const t = resolveThemeConfig();
const js = renderThemeRuntime(t);
@@ -0,0 +1,41 @@
import { describe, expect, test } from "bun:test";
import {
createSourceRange,
diagnose,
diagnosticSummary,
sliceSource,
supportsSyntaxFeature,
} from "../src/index.ts";
describe("syntax contract and security diagnostics", () => {
test("validates source ranges and summarizes diagnostic codes", () => {
const range = createSourceRange(2, 6);
expect(sliceSource("0123456789", range)).toBe("2345");
expect(() => createSourceRange(-1, 2)).toThrow(RangeError);
expect(() => createSourceRange(4, 3)).toThrow(RangeError);
expect(supportsSyntaxFeature("runtime-markers")).toBe(true);
expect(supportsSyntaxFeature("dynamic-eval")).toBe(false);
const summary = diagnosticSummary([
{ code: "A", severity: "error", message: "a" },
{ code: "A", severity: "warning", message: "b" },
{ code: "B", severity: "info", message: "c" },
]);
expect(summary).toEqual({ errors: 1, warnings: 1, info: 1, codes: { A: 2, B: 1 } });
});
test("rejects browser secret reads, executable sinks, and sensitive persistence", () => {
const diagnostics = diagnose(`page store Unsafe {
client state { apiToken: string = process.env.API_TOKEN }
persist { storage = "local" include = ["apiToken"] version = 1 }
functions {
client function render(raw: string): void { document.write(raw); setTimeout("run()", 1) }
}
}`);
const codes = diagnostics.map((diagnostic) => diagnostic.code);
expect(codes).toContain("WRN-SEC-SERVER-SECRET-SOURCE");
expect(codes).toContain("WRN-SEC-DOM-SINK");
expect(codes).toContain("WRN-SEC-STRING-TIMER");
expect(codes).toContain("WRN-PERSIST-SENSITIVE");
});
});
+2 -1
View File
@@ -170,7 +170,8 @@ export const UPLOAD_RUNTIME = `
});
xhr.addEventListener("load", function () {
var data = null;
try { data = JSON.parse(xhr.responseText); } catch (e2) {}
try { data = JSON.parse(xhr.responseText); }
catch (error) { console.warn("[wrnexus:uploader] upload response was not valid JSON", error); }
if (xhr.status >= 200 && xhr.status < 300 && data && data.ok) {
done(ui, (data.files && data.files[0]) || null, file);
} else {
+6
View File
@@ -11,6 +11,12 @@ import {
upload,
verifySignedFileToken,
} from "../src/index.ts";
import { UPLOAD_RUNTIME } from "../src/runtime.ts";
test("browser upload response parse failures emit a diagnostic", () => {
expect(UPLOAD_RUNTIME).toContain("upload response was not valid JSON");
expect(UPLOAD_RUNTIME).not.toContain("catch (e2) {}");
});
function configure() {
configureStorage(