complete performance and reliability follow-ups
This commit is contained in:
@@ -2604,6 +2604,7 @@
|
|||||||
"StylesMode",
|
"StylesMode",
|
||||||
"THEME_COOKIE",
|
"THEME_COOKIE",
|
||||||
"THEME_CSS_HREF",
|
"THEME_CSS_HREF",
|
||||||
|
"THEME_CSS_PREFIX",
|
||||||
"THEME_JS_HREF",
|
"THEME_JS_HREF",
|
||||||
"THEME_PALETTES",
|
"THEME_PALETTES",
|
||||||
"THEME_PALETTE_NAMES",
|
"THEME_PALETTE_NAMES",
|
||||||
@@ -2614,6 +2615,7 @@
|
|||||||
"ThemeSemanticColor",
|
"ThemeSemanticColor",
|
||||||
"ThemeToken",
|
"ThemeToken",
|
||||||
"ThemeTokens",
|
"ThemeTokens",
|
||||||
|
"activeThemeCssHref",
|
||||||
"auditCssPerformance",
|
"auditCssPerformance",
|
||||||
"auditWireTokens",
|
"auditWireTokens",
|
||||||
"bundleCss",
|
"bundleCss",
|
||||||
@@ -2629,6 +2631,7 @@
|
|||||||
"loadEnv",
|
"loadEnv",
|
||||||
"loadRawConfig",
|
"loadRawConfig",
|
||||||
"normalizeStyleSources",
|
"normalizeStyleSources",
|
||||||
|
"renderActiveThemeCss",
|
||||||
"renderFontHead",
|
"renderFontHead",
|
||||||
"renderProductionFontHead",
|
"renderProductionFontHead",
|
||||||
"renderStyles",
|
"renderStyles",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
// Generated by scripts/build-editor-compiler.mjs. Do not edit directly.
|
// Generated by scripts/build-editor-compiler.mjs. Do not edit directly.
|
||||||
// WRN editor compiler source hash: 4f9249b868459eb792fcd6dc7814e440bd2c0ad6d08f1dec947939dc48db92b3
|
// WRN editor compiler source hash: 196a514b18fb6e31fdd8f4ca667a76b0b62cab547af33725ba78564d70bbc01b
|
||||||
// WRN editor compiler generator hash: c71e7fe4258c97b73b384ff14b321f0cf0b30cc2ed0322f5f84b04e757159b18
|
// WRN editor compiler generator hash: c71e7fe4258c97b73b384ff14b321f0cf0b30cc2ed0322f5f84b04e757159b18
|
||||||
// Generated with TypeScript: 5.9.3
|
// Generated with TypeScript: 5.9.3
|
||||||
const __nodeRequire = require;
|
const __nodeRequire = require;
|
||||||
@@ -1454,7 +1454,12 @@ function __wrnexusEscapeHtml(value: unknown): string {
|
|||||||
return String(value).replace(/[&<>"']/g, (ch) => __wrnexusHtmlEscapes[ch] ?? ch);
|
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 = {
|
const adapters = {
|
||||||
cookies: ctx.cookies,
|
cookies: ctx.cookies,
|
||||||
session: ctx.session,
|
session: ctx.session,
|
||||||
@@ -1485,7 +1490,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") {
|
if (typeof ctx.__wrnexusCallApi === "function") {
|
||||||
return await ctx.__wrnexusCallApi(path, method);
|
return await ctx.__wrnexusCallApi(path, method);
|
||||||
}
|
}
|
||||||
@@ -1500,7 +1505,7 @@ async function __wrnexusCallApi(path: string, method: string, ctx: any): Promise
|
|||||||
return type.includes("application/json") ? await res.json() : await res.text();
|
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) {
|
for (const binding of __wrnexusSsrBindings) {
|
||||||
const data = await __wrnexusCallApi(binding.path, binding.method, ctx);
|
const data = await __wrnexusCallApi(binding.path, binding.method, ctx);
|
||||||
const value = __wrnexusEvalData(data, binding.body, binding.helpers, ctx);
|
const value = __wrnexusEvalData(data, binding.body, binding.helpers, ctx);
|
||||||
@@ -1933,14 +1938,14 @@ function generate(ast) {
|
|||||||
out.push(ssrRuntimeSource());
|
out.push(ssrRuntimeSource());
|
||||||
out.push(`const __wrnexusSsrBindings = ${JSON.stringify(ssrBindings, null, 2)};`);
|
out.push(`const __wrnexusSsrBindings = ${JSON.stringify(ssrBindings, null, 2)};`);
|
||||||
const decls = loopConsts.length > 0 ? loopConsts.join("\n") + "\n" : "";
|
const decls = loopConsts.length > 0 ? loopConsts.join("\n") + "\n" : "";
|
||||||
out.push(`export default async function ${ast.name}(ctx: any) {
|
out.push(`export default async function ${ast.name}(ctx: __WrnexusContext) {
|
||||||
${storeDeclarations}
|
${storeDeclarations}
|
||||||
${serverLoadAliases}
|
${serverLoadAliases}
|
||||||
${decls}
|
${decls}
|
||||||
const __state: ${stateType} = ${dynamicStateInitializer};
|
const __state: ${stateType} = ${dynamicStateInitializer};
|
||||||
${ssrStateAliases}
|
${ssrStateAliases}
|
||||||
${ssrComputedAliases}
|
${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)
|
const __scopeValue = Object.entries(__hydrationState)
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
let encoded: string;
|
let encoded: string;
|
||||||
@@ -1966,13 +1971,13 @@ function generate(ast) {
|
|||||||
}`);
|
}`);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
out.push(`export default ${storeBindings.length > 0 ? "async " : ""}function ${ast.name}(ctx: any) {
|
out.push(`export default ${storeBindings.length > 0 ? "async " : ""}function ${ast.name}(ctx: import("@wrnexus/core").Context) {
|
||||||
${storeDeclarations}
|
${storeDeclarations}
|
||||||
${serverLoadAliases}
|
${serverLoadAliases}
|
||||||
const __state: ${stateType} = ${dynamicStateInitializer};
|
const __state: ${stateType} = ${dynamicStateInitializer};
|
||||||
${ssrStateAliases}
|
${ssrStateAliases}
|
||||||
${ssrComputedAliases}
|
${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)
|
const __scopeValue = Object.entries(__hydrationState)
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
let encoded: string;
|
let encoded: string;
|
||||||
@@ -1996,14 +2001,14 @@ function generate(ast) {
|
|||||||
}`);
|
}`);
|
||||||
}
|
}
|
||||||
if (staticShellBody !== undefined) {
|
if (staticShellBody !== undefined) {
|
||||||
out.push(`export async function __wrnexusBuildStaticShell(ctx: any = {}) {
|
out.push(`export async function __wrnexusBuildStaticShell(ctx: import("@wrnexus/core").Context = {} as import("@wrnexus/core").Context) {
|
||||||
${storeDeclarations}
|
${storeDeclarations}
|
||||||
${serverLoadAliases}
|
${serverLoadAliases}
|
||||||
${loopConsts.length > 0 ? loopConsts.join("\n") : ""}
|
${loopConsts.length > 0 ? loopConsts.join("\n") : ""}
|
||||||
const __state: ${stateType} = ${dynamicStateInitializer};
|
const __state: ${stateType} = ${dynamicStateInitializer};
|
||||||
${ssrStateAliases}
|
${ssrStateAliases}
|
||||||
${ssrComputedAliases}
|
${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)
|
const __scopeValue = Object.entries(__hydrationState)
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
let encoded: string;
|
let encoded: string;
|
||||||
@@ -2053,7 +2058,7 @@ function generate(ast) {
|
|||||||
})
|
})
|
||||||
.join("\n");
|
.join("\n");
|
||||||
const visible = exposed.filter((entry) => entry.name);
|
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
|
${exposed
|
||||||
.filter((entry) => !entry.name)
|
.filter((entry) => !entry.name)
|
||||||
.map((entry) => entry.body)
|
.map((entry) => entry.body)
|
||||||
@@ -2076,7 +2081,7 @@ ${visible.length
|
|||||||
out.push(`export async function ${action.name}(${action.args.join(", ")}) {${action.body}}`);
|
out.push(`export async function ${action.name}(${action.args.join(", ")}) {${action.body}}`);
|
||||||
continue;
|
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 invalidate = (...tags: string[]) => {
|
||||||
const bucket = (ctx.locals.__wrnexusInvalidatedTags ??= []);
|
const bucket = (ctx.locals.__wrnexusInvalidatedTags ??= []);
|
||||||
bucket.push(...tags.flat());
|
bucket.push(...tags.flat());
|
||||||
@@ -2098,7 +2103,7 @@ ${ast.actions
|
|||||||
ast.apis.forEach((api, index) => {
|
ast.apis.forEach((api, index) => {
|
||||||
const name = `__wrnexusApi_${api.method}_${index}`;
|
const name = `__wrnexusApi_${api.method}_${index}`;
|
||||||
out.push(`// ${api.method} ${apiRoutePath(api.path)}
|
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((api, index) => ` ${JSON.stringify(`${api.method} ${apiRoutePath(api.path)}`)}: __wrnexusApi_${api.method}_${index},`);
|
const entries = ast.apis.map((api, index) => ` ${JSON.stringify(`${api.method} ${apiRoutePath(api.path)}`)}: __wrnexusApi_${api.method}_${index},`);
|
||||||
out.push(`export const __wrnexusApi = {\n${entries.join("\n")}\n};`);
|
out.push(`export const __wrnexusApi = {\n${entries.join("\n")}\n};`);
|
||||||
@@ -2114,7 +2119,7 @@ const ${name} = async (ctx: any) => {${api.body}};`);
|
|||||||
if (ast.realtimes.length > 0) {
|
if (ast.realtimes.length > 0) {
|
||||||
const handlers = ast.realtimes.flatMap((rt) => rt.handlers.map((h) => {
|
const handlers = ast.realtimes.flatMap((rt) => rt.handlers.map((h) => {
|
||||||
const params = ["ws", ...h.args].join(", ");
|
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};`);
|
out.push(`export const websocket = {\n${handlers.join("\n")}\n};`);
|
||||||
}
|
}
|
||||||
@@ -2697,7 +2702,7 @@ function generateComponent(ast) {
|
|||||||
if (prop.required) {
|
if (prop.required) {
|
||||||
decls.push(` if (__p[${JSON.stringify(prop.name)}] === undefined) throw new TypeError(${JSON.stringify(`${ast.name} requires prop '${prop.name}' (${prop.valueType ?? "unknown"})`)});`);
|
decls.push(` if (__p[${JSON.stringify(prop.name)}] === undefined) throw new TypeError(${JSON.stringify(`${ast.name} requires prop '${prop.name}' (${prop.valueType ?? "unknown"})`)});`);
|
||||||
}
|
}
|
||||||
decls.push(` const ${nameRefs.get(prop.name)}: ${prop.valueType ?? "any"} = __coerce(__p[${JSON.stringify(prop.name)}], (${resolveExpr(prop.default)}), ${JSON.stringify((0, types_ts_1.runtimeTypeOf)(prop.valueType))}, ${JSON.stringify(prop.name)});`);
|
decls.push(` const ${nameRefs.get(prop.name)}: ${prop.valueType ?? "unknown"} = __coerce(__p[${JSON.stringify(prop.name)}], (${resolveExpr(prop.default)}), ${JSON.stringify((0, types_ts_1.runtimeTypeOf)(prop.valueType))}, ${JSON.stringify(prop.name)}) as ${prop.valueType ?? "unknown"};`);
|
||||||
}
|
}
|
||||||
if (!effectiveProps.some((prop) => prop.name === "attrs")) {
|
if (!effectiveProps.some((prop) => prop.name === "attrs")) {
|
||||||
decls.push(` const __attrs = __restProps(__p, new Set(${JSON.stringify(effectiveProps.map((prop) => prop.name))}));`);
|
decls.push(` const __attrs = __restProps(__p, new Set(${JSON.stringify(effectiveProps.map((prop) => prop.name))}));`);
|
||||||
@@ -2762,7 +2767,7 @@ function generateComponent(ast) {
|
|||||||
.map((output) => ` ${JSON.stringify(output.name)}(${output.payload ? `${output.payload.name}${output.payload.optional ? "?" : ""}: ${output.payload.valueType}` : ""}): void;`)
|
.map((output) => ` ${JSON.stringify(output.name)}(${output.payload ? `${output.payload.name}${output.payload.optional ? "?" : ""}: ${output.payload.valueType}` : ""}): void;`)
|
||||||
.join("\n")}\n}`);
|
.join("\n")}\n}`);
|
||||||
}
|
}
|
||||||
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) {
|
if (v === undefined || v === null) {
|
||||||
return def;
|
return def;
|
||||||
}
|
}
|
||||||
@@ -2822,7 +2827,7 @@ function generateComponent(ast) {
|
|||||||
throw new TypeError("Expected an object prop '" + propName + "'");
|
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") {
|
if (declared === "function" && typeof v !== "function") {
|
||||||
throw new TypeError("Expected a function prop");
|
throw new TypeError("Expected a function prop");
|
||||||
}
|
}
|
||||||
@@ -2830,15 +2835,15 @@ function generateComponent(ast) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function __restProps(
|
function __restProps(
|
||||||
props: Record<string, any>,
|
props: Record<string, unknown>,
|
||||||
declared: Set<string>,
|
declared: Set<string>,
|
||||||
): Record<string, any> {
|
): Record<string, unknown> {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
Object.entries(props).filter(([name]) => !declared.has(name)),
|
Object.entries(props).filter(([name]) => !declared.has(name)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wireHtml(v: any): string {
|
function __wireHtml(v: unknown): string {
|
||||||
return String(v == null ? "" : v).replace(
|
return String(v == null ? "" : v).replace(
|
||||||
/[&<>]/g,
|
/[&<>]/g,
|
||||||
(c) =>
|
(c) =>
|
||||||
@@ -2850,7 +2855,7 @@ function __wireHtml(v: any): string {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wireAttr(v: any): string {
|
function __wireAttr(v: unknown): string {
|
||||||
return String(v == null ? "" : v).replace(
|
return String(v == null ? "" : v).replace(
|
||||||
/[&<>"]/g,
|
/[&<>"]/g,
|
||||||
(c) =>
|
(c) =>
|
||||||
@@ -2864,7 +2869,7 @@ function __wireAttr(v: any): string {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wireBooleanAttr(name: string, value: any): string {
|
function __wireBooleanAttr(name: string, value: unknown): string {
|
||||||
return value === true ||
|
return value === true ||
|
||||||
value === "true" ||
|
value === "true" ||
|
||||||
value === "" ||
|
value === "" ||
|
||||||
@@ -2875,7 +2880,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 "";
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
|
||||||
|
|
||||||
const booleanAttributes = new Set(${JSON.stringify([...HTML_BOOLEAN_ATTRIBUTES])});
|
const booleanAttributes = new Set(${JSON.stringify([...HTML_BOOLEAN_ATTRIBUTES])});
|
||||||
@@ -2911,7 +2916,7 @@ function __wireSpreadAttrs(value: any): string {
|
|||||||
return attributes.join("");
|
return attributes.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wireProp(v: any): string {
|
function __wireProp(v: unknown): string {
|
||||||
const value =
|
const value =
|
||||||
v !== null && typeof v === "object"
|
v !== null && typeof v === "object"
|
||||||
? JSON.stringify(v)
|
? JSON.stringify(v)
|
||||||
@@ -2920,16 +2925,16 @@ function __wireProp(v: any): string {
|
|||||||
return __wireAttr(value);
|
return __wireAttr(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wireRaw(v: any): string {
|
function __wireRaw(v: unknown): string {
|
||||||
return String(v == null ? "" : v);
|
return String(v == null ? "" : v);
|
||||||
}`);
|
}`);
|
||||||
if (hasServerEach) {
|
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");
|
return __WrnexusBuffer.from(JSON.stringify(value), "utf8").toString("base64");
|
||||||
}`);
|
}`);
|
||||||
}
|
}
|
||||||
if (needsScope) {
|
if (needsScope) {
|
||||||
out.push(`function __wrnexusSerializeScopeValue(value: any): string {
|
out.push(`function __wrnexusSerializeScopeValue(value: unknown): string {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return "undefined";
|
return "undefined";
|
||||||
}
|
}
|
||||||
@@ -2963,7 +2968,7 @@ function __wireRaw(v: any): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wrnexusScopeDecl(obj: Record<string, any>): string {
|
function __wrnexusScopeDecl(obj: Record<string, unknown>): string {
|
||||||
return Object.keys(obj)
|
return Object.keys(obj)
|
||||||
.map(
|
.map(
|
||||||
(key) =>
|
(key) =>
|
||||||
@@ -2981,7 +2986,7 @@ function __wireRaw(v: any): string {
|
|||||||
}`);
|
}`);
|
||||||
}
|
}
|
||||||
const serverFunctionSource = serverFunctions ? `${serverFunctions}\n` : "";
|
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` +
|
out.push(`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` +
|
` const __p = props || {};\n` +
|
||||||
(decls.length > 0 ? decls.join("\n") + "\n" : "") +
|
(decls.length > 0 ? decls.join("\n") + "\n" : "") +
|
||||||
serverFunctionSource +
|
serverFunctionSource +
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// WRN editor extension source hash: c09cc127dcc7fbae562648b09435fb96134c18e63ba21d7a4b476b916f4a8256
|
// WRN editor extension source hash: 47188196a996fce0b8f1e2a4302e563767451d9b56df885abc60e9c925653b21
|
||||||
// WRN editor extension generator hash: 456d1d614e44e5fb1f19b784176c09cf2ade9b64ef73a17934c2698150b62728
|
// WRN editor extension generator hash: 456d1d614e44e5fb1f19b784176c09cf2ade9b64ef73a17934c2698150b62728
|
||||||
"use strict";
|
"use strict";
|
||||||
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
||||||
@@ -23863,7 +23863,12 @@ function __wrnexusEscapeHtml(value: unknown): string {
|
|||||||
return String(value).replace(/[&<>"']/g, (ch) => __wrnexusHtmlEscapes[ch] ?? ch);
|
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 = {
|
const adapters = {
|
||||||
cookies: ctx.cookies,
|
cookies: ctx.cookies,
|
||||||
session: ctx.session,
|
session: ctx.session,
|
||||||
@@ -23894,7 +23899,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") {
|
if (typeof ctx.__wrnexusCallApi === "function") {
|
||||||
return await ctx.__wrnexusCallApi(path, method);
|
return await ctx.__wrnexusCallApi(path, method);
|
||||||
}
|
}
|
||||||
@@ -23909,7 +23914,7 @@ async function __wrnexusCallApi(path: string, method: string, ctx: any): Promise
|
|||||||
return type.includes("application/json") ? await res.json() : await res.text();
|
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) {
|
for (const binding of __wrnexusSsrBindings) {
|
||||||
const data = await __wrnexusCallApi(binding.path, binding.method, ctx);
|
const data = await __wrnexusCallApi(binding.path, binding.method, ctx);
|
||||||
const value = __wrnexusEvalData(data, binding.body, binding.helpers, ctx);
|
const value = __wrnexusEvalData(data, binding.body, binding.helpers, ctx);
|
||||||
@@ -24300,14 +24305,14 @@ ${helpers}`);
|
|||||||
const decls = loopConsts.length > 0 ? loopConsts.join(`
|
const decls = loopConsts.length > 0 ? loopConsts.join(`
|
||||||
`) + `
|
`) + `
|
||||||
` : "";
|
` : "";
|
||||||
out.push(`export default async function ${ast.name}(ctx: any) {
|
out.push(`export default async function ${ast.name}(ctx: __WrnexusContext) {
|
||||||
${storeDeclarations}
|
${storeDeclarations}
|
||||||
${serverLoadAliases}
|
${serverLoadAliases}
|
||||||
${decls}
|
${decls}
|
||||||
const __state: ${stateType} = ${dynamicStateInitializer};
|
const __state: ${stateType} = ${dynamicStateInitializer};
|
||||||
${ssrStateAliases}
|
${ssrStateAliases}
|
||||||
${ssrComputedAliases}
|
${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)
|
const __scopeValue = Object.entries(__hydrationState)
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
let encoded: string;
|
let encoded: string;
|
||||||
@@ -24332,13 +24337,13 @@ ${helpers}`);
|
|||||||
return await __wrnexusRenderSsrBindings(html, ctx);
|
return await __wrnexusRenderSsrBindings(html, ctx);
|
||||||
}`);
|
}`);
|
||||||
} else {
|
} else {
|
||||||
out.push(`export default ${storeBindings.length > 0 ? "async " : ""}function ${ast.name}(ctx: any) {
|
out.push(`export default ${storeBindings.length > 0 ? "async " : ""}function ${ast.name}(ctx: import("@wrnexus/core").Context) {
|
||||||
${storeDeclarations}
|
${storeDeclarations}
|
||||||
${serverLoadAliases}
|
${serverLoadAliases}
|
||||||
const __state: ${stateType} = ${dynamicStateInitializer};
|
const __state: ${stateType} = ${dynamicStateInitializer};
|
||||||
${ssrStateAliases}
|
${ssrStateAliases}
|
||||||
${ssrComputedAliases}
|
${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)
|
const __scopeValue = Object.entries(__hydrationState)
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
let encoded: string;
|
let encoded: string;
|
||||||
@@ -24362,7 +24367,7 @@ ${helpers}`);
|
|||||||
}`);
|
}`);
|
||||||
}
|
}
|
||||||
if (staticShellBody !== undefined) {
|
if (staticShellBody !== undefined) {
|
||||||
out.push(`export async function __wrnexusBuildStaticShell(ctx: any = {}) {
|
out.push(`export async function __wrnexusBuildStaticShell(ctx: import("@wrnexus/core").Context = {} as import("@wrnexus/core").Context) {
|
||||||
${storeDeclarations}
|
${storeDeclarations}
|
||||||
${serverLoadAliases}
|
${serverLoadAliases}
|
||||||
${loopConsts.length > 0 ? loopConsts.join(`
|
${loopConsts.length > 0 ? loopConsts.join(`
|
||||||
@@ -24370,7 +24375,7 @@ ${helpers}`);
|
|||||||
const __state: ${stateType} = ${dynamicStateInitializer};
|
const __state: ${stateType} = ${dynamicStateInitializer};
|
||||||
${ssrStateAliases}
|
${ssrStateAliases}
|
||||||
${ssrComputedAliases}
|
${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)
|
const __scopeValue = Object.entries(__hydrationState)
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
let encoded: string;
|
let encoded: string;
|
||||||
@@ -24417,7 +24422,7 @@ ${helpers}`);
|
|||||||
}).join(`
|
}).join(`
|
||||||
`);
|
`);
|
||||||
const visible = exposed.filter((entry) => entry.name);
|
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).join(`
|
${exposed.filter((entry) => !entry.name).map((entry) => entry.body).join(`
|
||||||
`)}
|
`)}
|
||||||
${declarations}
|
${declarations}
|
||||||
@@ -24436,7 +24441,7 @@ ${visible.length ? ` const __values = await Promise.all([${visible.map((entry)
|
|||||||
out.push(`export async function ${action.name}(${action.args.join(", ")}) {${action.body}}`);
|
out.push(`export async function ${action.name}(${action.args.join(", ")}) {${action.body}}`);
|
||||||
continue;
|
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 invalidate = (...tags: string[]) => {
|
||||||
const bucket = (ctx.locals.__wrnexusInvalidatedTags ??= []);
|
const bucket = (ctx.locals.__wrnexusInvalidatedTags ??= []);
|
||||||
bucket.push(...tags.flat());
|
bucket.push(...tags.flat());
|
||||||
@@ -24454,7 +24459,7 @@ ${ast.actions.map((action) => ` ${action.name}: createActionClient<${action.sch
|
|||||||
ast.apis.forEach((api, index) => {
|
ast.apis.forEach((api, index) => {
|
||||||
const name = `__wrnexusApi_${api.method}_${index}`;
|
const name = `__wrnexusApi_${api.method}_${index}`;
|
||||||
out.push(`// ${api.method} ${apiRoutePath(api.path)}
|
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((api, index) => ` ${JSON.stringify(`${api.method} ${apiRoutePath(api.path)}`)}: __wrnexusApi_${api.method}_${index},`);
|
const entries = ast.apis.map((api, index) => ` ${JSON.stringify(`${api.method} ${apiRoutePath(api.path)}`)}: __wrnexusApi_${api.method}_${index},`);
|
||||||
out.push(`export const __wrnexusApi = {
|
out.push(`export const __wrnexusApi = {
|
||||||
@@ -24472,7 +24477,7 @@ ${entries.join(`
|
|||||||
if (ast.realtimes.length > 0) {
|
if (ast.realtimes.length > 0) {
|
||||||
const handlers = ast.realtimes.flatMap((rt) => rt.handlers.map((h) => {
|
const handlers = ast.realtimes.flatMap((rt) => rt.handlers.map((h) => {
|
||||||
const params = ["ws", ...h.args].join(", ");
|
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 = {
|
out.push(`export const websocket = {
|
||||||
${handlers.join(`
|
${handlers.join(`
|
||||||
@@ -24929,7 +24934,7 @@ ${handlers.join(`
|
|||||||
if (prop.required) {
|
if (prop.required) {
|
||||||
decls.push(` if (__p[${JSON.stringify(prop.name)}] === undefined) throw new TypeError(${JSON.stringify(`${ast.name} requires prop '${prop.name}' (${prop.valueType ?? "unknown"})`)});`);
|
decls.push(` if (__p[${JSON.stringify(prop.name)}] === undefined) throw new TypeError(${JSON.stringify(`${ast.name} requires prop '${prop.name}' (${prop.valueType ?? "unknown"})`)});`);
|
||||||
}
|
}
|
||||||
decls.push(` const ${nameRefs.get(prop.name)}: ${prop.valueType ?? "any"} = __coerce(__p[${JSON.stringify(prop.name)}], (${resolveExpr(prop.default)}), ${JSON.stringify((0, types_ts_1.runtimeTypeOf)(prop.valueType))}, ${JSON.stringify(prop.name)});`);
|
decls.push(` const ${nameRefs.get(prop.name)}: ${prop.valueType ?? "unknown"} = __coerce(__p[${JSON.stringify(prop.name)}], (${resolveExpr(prop.default)}), ${JSON.stringify((0, types_ts_1.runtimeTypeOf)(prop.valueType))}, ${JSON.stringify(prop.name)}) as ${prop.valueType ?? "unknown"};`);
|
||||||
}
|
}
|
||||||
if (!effectiveProps.some((prop) => prop.name === "attrs")) {
|
if (!effectiveProps.some((prop) => prop.name === "attrs")) {
|
||||||
decls.push(` const __attrs = __restProps(__p, new Set(${JSON.stringify(effectiveProps.map((prop) => prop.name))}));`);
|
decls.push(` const __attrs = __restProps(__p, new Set(${JSON.stringify(effectiveProps.map((prop) => prop.name))}));`);
|
||||||
@@ -24989,7 +24994,7 @@ ${ast.outputs.map((output) => ` ${JSON.stringify(output.name)}(${output.payload
|
|||||||
`)}
|
`)}
|
||||||
}`);
|
}`);
|
||||||
}
|
}
|
||||||
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) {
|
if (v === undefined || v === null) {
|
||||||
return def;
|
return def;
|
||||||
}
|
}
|
||||||
@@ -25049,7 +25054,7 @@ ${ast.outputs.map((output) => ` ${JSON.stringify(output.name)}(${output.payload
|
|||||||
throw new TypeError("Expected an object prop '" + propName + "'");
|
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") {
|
if (declared === "function" && typeof v !== "function") {
|
||||||
throw new TypeError("Expected a function prop");
|
throw new TypeError("Expected a function prop");
|
||||||
}
|
}
|
||||||
@@ -25057,15 +25062,15 @@ ${ast.outputs.map((output) => ` ${JSON.stringify(output.name)}(${output.payload
|
|||||||
}
|
}
|
||||||
|
|
||||||
function __restProps(
|
function __restProps(
|
||||||
props: Record<string, any>,
|
props: Record<string, unknown>,
|
||||||
declared: Set<string>,
|
declared: Set<string>,
|
||||||
): Record<string, any> {
|
): Record<string, unknown> {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
Object.entries(props).filter(([name]) => !declared.has(name)),
|
Object.entries(props).filter(([name]) => !declared.has(name)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wireHtml(v: any): string {
|
function __wireHtml(v: unknown): string {
|
||||||
return String(v == null ? "" : v).replace(
|
return String(v == null ? "" : v).replace(
|
||||||
/[&<>]/g,
|
/[&<>]/g,
|
||||||
(c) =>
|
(c) =>
|
||||||
@@ -25077,7 +25082,7 @@ function __wireHtml(v: any): string {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wireAttr(v: any): string {
|
function __wireAttr(v: unknown): string {
|
||||||
return String(v == null ? "" : v).replace(
|
return String(v == null ? "" : v).replace(
|
||||||
/[&<>"]/g,
|
/[&<>"]/g,
|
||||||
(c) =>
|
(c) =>
|
||||||
@@ -25091,7 +25096,7 @@ function __wireAttr(v: any): string {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wireBooleanAttr(name: string, value: any): string {
|
function __wireBooleanAttr(name: string, value: unknown): string {
|
||||||
return value === true ||
|
return value === true ||
|
||||||
value === "true" ||
|
value === "true" ||
|
||||||
value === "" ||
|
value === "" ||
|
||||||
@@ -25102,7 +25107,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 "";
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
|
||||||
|
|
||||||
const booleanAttributes = new Set(${JSON.stringify([...HTML_BOOLEAN_ATTRIBUTES])});
|
const booleanAttributes = new Set(${JSON.stringify([...HTML_BOOLEAN_ATTRIBUTES])});
|
||||||
@@ -25138,7 +25143,7 @@ function __wireSpreadAttrs(value: any): string {
|
|||||||
return attributes.join("");
|
return attributes.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wireProp(v: any): string {
|
function __wireProp(v: unknown): string {
|
||||||
const value =
|
const value =
|
||||||
v !== null && typeof v === "object"
|
v !== null && typeof v === "object"
|
||||||
? JSON.stringify(v)
|
? JSON.stringify(v)
|
||||||
@@ -25147,16 +25152,16 @@ function __wireProp(v: any): string {
|
|||||||
return __wireAttr(value);
|
return __wireAttr(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wireRaw(v: any): string {
|
function __wireRaw(v: unknown): string {
|
||||||
return String(v == null ? "" : v);
|
return String(v == null ? "" : v);
|
||||||
}`);
|
}`);
|
||||||
if (hasServerEach) {
|
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");
|
return __WrnexusBuffer.from(JSON.stringify(value), "utf8").toString("base64");
|
||||||
}`);
|
}`);
|
||||||
}
|
}
|
||||||
if (needsScope) {
|
if (needsScope) {
|
||||||
out.push(`function __wrnexusSerializeScopeValue(value: any): string {
|
out.push(`function __wrnexusSerializeScopeValue(value: unknown): string {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return "undefined";
|
return "undefined";
|
||||||
}
|
}
|
||||||
@@ -25190,7 +25195,7 @@ function __wireRaw(v: any): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wrnexusScopeDecl(obj: Record<string, any>): string {
|
function __wrnexusScopeDecl(obj: Record<string, unknown>): string {
|
||||||
return Object.keys(obj)
|
return Object.keys(obj)
|
||||||
.map(
|
.map(
|
||||||
(key) =>
|
(key) =>
|
||||||
@@ -25209,7 +25214,7 @@ function __wireRaw(v: any): string {
|
|||||||
}
|
}
|
||||||
const serverFunctionSource = serverFunctions ? `${serverFunctions}
|
const serverFunctionSource = serverFunctions ? `${serverFunctions}
|
||||||
` : "";
|
` : "";
|
||||||
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 {
|
out.push(`export function render(props: ${effectiveProps.length > 0 ? `${ast.name}Props` : "Record<string, unknown>"} = {} as ${effectiveProps.length > 0 ? `${ast.name}Props` : "Record<string, unknown>"}): string {
|
||||||
` + ` const __p = props || {};
|
` + ` const __p = props || {};
|
||||||
` + (decls.length > 0 ? decls.join(`
|
` + (decls.length > 0 ? decls.join(`
|
||||||
`) + `
|
`) + `
|
||||||
|
|||||||
@@ -70,6 +70,19 @@ describe("full app", () => {
|
|||||||
expect(html).not.toContain('<span data-t="nav.home"></span>');
|
expect(html).not.toContain('<span data-t="nav.home"></span>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("home page ships only its active theme stylesheet", async () => {
|
||||||
|
const response = await app.fetch("/");
|
||||||
|
const html = await response.text();
|
||||||
|
const href = html.match(/data-wrnexus-theme href="([^"]+)"/)?.[1];
|
||||||
|
expect(href).toContain("/__wrnexus/theme/dark/");
|
||||||
|
expect(html).not.toContain('href="/__wrnexus/theme.css');
|
||||||
|
const cssResponse = await app.fetch(String(href));
|
||||||
|
expect(cssResponse.status).toBe(200);
|
||||||
|
const css = await cssResponse.text();
|
||||||
|
expect(css.length).toBeLessThan(10_000);
|
||||||
|
expect(css).not.toContain("[data-theme=");
|
||||||
|
});
|
||||||
|
|
||||||
test("GET /api/hello returns a translated greeting", async () => {
|
test("GET /api/hello returns a translated greeting", async () => {
|
||||||
const res = await app.fetch("/api/hello");
|
const res = await app.fetch("/api/hello");
|
||||||
expect(res.status).toBe(200);
|
expect(res.status).toBe(200);
|
||||||
|
|||||||
@@ -38,7 +38,6 @@
|
|||||||
"check": "bun run typecheck && bun run lint && bun run check:component-imports && bun run test && bun run format:check",
|
"check": "bun run typecheck && bun run lint && bun run check:component-imports && bun run test && bun run format:check",
|
||||||
"auth:dev": "bun run --cwd examples/auth-showcase dev",
|
"auth:dev": "bun run --cwd examples/auth-showcase dev",
|
||||||
"auth:check": "bun run --cwd packages/auth check",
|
"auth:check": "bun run --cwd packages/auth check",
|
||||||
"validate:0.6": "node scripts/validate-0.6.mjs",
|
|
||||||
"validate:0.7": "node scripts/validate-0.7.mjs",
|
"validate:0.7": "node scripts/validate-0.7.mjs",
|
||||||
"security:framework": "node scripts/security-performance-audit.mjs",
|
"security:framework": "node scripts/security-performance-audit.mjs",
|
||||||
"security:asvs": "node scripts/check-security-asvs.mjs",
|
"security:asvs": "node scripts/check-security-asvs.mjs",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -44,6 +44,7 @@ import {
|
|||||||
bundleCss,
|
bundleCss,
|
||||||
renderStyles,
|
renderStyles,
|
||||||
resolveThemeConfig,
|
resolveThemeConfig,
|
||||||
|
renderActiveThemeCss,
|
||||||
renderThemeCss,
|
renderThemeCss,
|
||||||
renderThemeRuntime,
|
renderThemeRuntime,
|
||||||
} from "@wrnexus/styles";
|
} from "@wrnexus/styles";
|
||||||
@@ -497,6 +498,19 @@ export async function runBuild(appRoot: string): Promise<void> {
|
|||||||
const themeJs = renderThemeRuntime(theme);
|
const themeJs = renderThemeRuntime(theme);
|
||||||
writeFileSync(join(distDir, "theme.css"), themeCss, "utf8");
|
writeFileSync(join(distDir, "theme.css"), themeCss, "utf8");
|
||||||
writeFileSync(join(distDir, "theme.js"), themeJs, "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(themeCss);
|
||||||
assetHash.update(themeJs);
|
assetHash.update(themeJs);
|
||||||
console.log(`✓ Theme: ${theme.names.length} themes (default: ${theme.default})`);
|
console.log(`✓ Theme: ${theme.names.length} themes (default: ${theme.default})`);
|
||||||
@@ -709,6 +723,7 @@ await createProductionServer(
|
|||||||
controllersPath: join(import.meta.dir, "controllers.js"),
|
controllersPath: join(import.meta.dir, "controllers.js"),
|
||||||
clientModulesDir: join(import.meta.dir, "client"),
|
clientModulesDir: join(import.meta.dir, "client"),
|
||||||
themePath: join(import.meta.dir, "theme.css"),
|
themePath: join(import.meta.dir, "theme.css"),
|
||||||
|
themeAssetsDir: join(import.meta.dir, "theme"),
|
||||||
themeJsPath: join(import.meta.dir, "theme.js"),
|
themeJsPath: join(import.meta.dir, "theme.js"),
|
||||||
theme: ${JSON.stringify(theme)},
|
theme: ${JSON.stringify(theme)},
|
||||||
uiCssPath: join(import.meta.dir, "ui.css"),
|
uiCssPath: join(import.meta.dir, "ui.css"),
|
||||||
|
|||||||
@@ -882,7 +882,12 @@ function __wrnexusEscapeHtml(value: unknown): string {
|
|||||||
return String(value).replace(/[&<>"']/g, (ch) => __wrnexusHtmlEscapes[ch] ?? ch);
|
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 = {
|
const adapters = {
|
||||||
cookies: ctx.cookies,
|
cookies: ctx.cookies,
|
||||||
session: ctx.session,
|
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") {
|
if (typeof ctx.__wrnexusCallApi === "function") {
|
||||||
return await ctx.__wrnexusCallApi(path, method);
|
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();
|
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) {
|
for (const binding of __wrnexusSsrBindings) {
|
||||||
const data = await __wrnexusCallApi(binding.path, binding.method, ctx);
|
const data = await __wrnexusCallApi(binding.path, binding.method, ctx);
|
||||||
const value = __wrnexusEvalData(data, binding.body, binding.helpers, 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)};`);
|
out.push(`const __wrnexusSsrBindings = ${JSON.stringify(ssrBindings, null, 2)};`);
|
||||||
const decls = loopConsts.length > 0 ? loopConsts.join("\n") + "\n" : "";
|
const decls = loopConsts.length > 0 ? loopConsts.join("\n") + "\n" : "";
|
||||||
out.push(
|
out.push(
|
||||||
`export default async function ${ast.name}(ctx: any) {
|
`export default async function ${ast.name}(ctx: __WrnexusContext) {
|
||||||
${storeDeclarations}
|
${storeDeclarations}
|
||||||
${serverLoadAliases}
|
${serverLoadAliases}
|
||||||
${decls}
|
${decls}
|
||||||
const __state: ${stateType} = ${dynamicStateInitializer};
|
const __state: ${stateType} = ${dynamicStateInitializer};
|
||||||
${ssrStateAliases}
|
${ssrStateAliases}
|
||||||
${ssrComputedAliases}
|
${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)
|
const __scopeValue = Object.entries(__hydrationState)
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
let encoded: string;
|
let encoded: string;
|
||||||
@@ -1472,13 +1477,13 @@ export function generate(ast: PageAst): string {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
out.push(
|
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}
|
${storeDeclarations}
|
||||||
${serverLoadAliases}
|
${serverLoadAliases}
|
||||||
const __state: ${stateType} = ${dynamicStateInitializer};
|
const __state: ${stateType} = ${dynamicStateInitializer};
|
||||||
${ssrStateAliases}
|
${ssrStateAliases}
|
||||||
${ssrComputedAliases}
|
${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)
|
const __scopeValue = Object.entries(__hydrationState)
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
let encoded: string;
|
let encoded: string;
|
||||||
@@ -1505,14 +1510,14 @@ export function generate(ast: PageAst): string {
|
|||||||
|
|
||||||
if (staticShellBody !== undefined) {
|
if (staticShellBody !== undefined) {
|
||||||
out.push(
|
out.push(
|
||||||
`export async function __wrnexusBuildStaticShell(ctx: any = {}) {
|
`export async function __wrnexusBuildStaticShell(ctx: import("@wrnexus/core").Context = {} as import("@wrnexus/core").Context) {
|
||||||
${storeDeclarations}
|
${storeDeclarations}
|
||||||
${serverLoadAliases}
|
${serverLoadAliases}
|
||||||
${loopConsts.length > 0 ? loopConsts.join("\n") : ""}
|
${loopConsts.length > 0 ? loopConsts.join("\n") : ""}
|
||||||
const __state: ${stateType} = ${dynamicStateInitializer};
|
const __state: ${stateType} = ${dynamicStateInitializer};
|
||||||
${ssrStateAliases}
|
${ssrStateAliases}
|
||||||
${ssrComputedAliases}
|
${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)
|
const __scopeValue = Object.entries(__hydrationState)
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
let encoded: string;
|
let encoded: string;
|
||||||
@@ -1572,7 +1577,7 @@ export function generate(ast: PageAst): string {
|
|||||||
})
|
})
|
||||||
.join("\n");
|
.join("\n");
|
||||||
const visible = exposed.filter((entry) => entry.name);
|
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
|
${exposed
|
||||||
.filter((entry) => !entry.name)
|
.filter((entry) => !entry.name)
|
||||||
.map((entry) => entry.body)
|
.map((entry) => entry.body)
|
||||||
@@ -1599,7 +1604,7 @@ ${
|
|||||||
);
|
);
|
||||||
continue;
|
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 invalidate = (...tags: string[]) => {
|
||||||
const bucket = (ctx.locals.__wrnexusInvalidatedTags ??= []);
|
const bucket = (ctx.locals.__wrnexusInvalidatedTags ??= []);
|
||||||
bucket.push(...tags.flat());
|
bucket.push(...tags.flat());
|
||||||
@@ -1630,7 +1635,7 @@ ${ast.actions
|
|||||||
ast.apis.forEach((api, index) => {
|
ast.apis.forEach((api, index) => {
|
||||||
const name = `__wrnexusApi_${api.method}_${index}`;
|
const name = `__wrnexusApi_${api.method}_${index}`;
|
||||||
out.push(`// ${api.method} ${apiRoutePath(api.path)}
|
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(
|
const entries = ast.apis.map(
|
||||||
@@ -1652,7 +1657,7 @@ const ${name} = async (ctx: any) => {${api.body}};`);
|
|||||||
const handlers = ast.realtimes.flatMap((rt) =>
|
const handlers = ast.realtimes.flatMap((rt) =>
|
||||||
rt.handlers.map((h) => {
|
rt.handlers.map((h) => {
|
||||||
const params = ["ws", ...h.args].join(", ");
|
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};`);
|
out.push(`export const websocket = {\n${handlers.join("\n")}\n};`);
|
||||||
@@ -2413,7 +2418,7 @@ function generateComponent(ast: PageAst): string {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
decls.push(
|
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")) {
|
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) {
|
if (v === undefined || v === null) {
|
||||||
return def;
|
return def;
|
||||||
}
|
}
|
||||||
@@ -2563,7 +2568,7 @@ function generateComponent(ast: PageAst): string {
|
|||||||
throw new TypeError("Expected an object prop '" + propName + "'");
|
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") {
|
if (declared === "function" && typeof v !== "function") {
|
||||||
throw new TypeError("Expected a function prop");
|
throw new TypeError("Expected a function prop");
|
||||||
}
|
}
|
||||||
@@ -2571,15 +2576,15 @@ function generateComponent(ast: PageAst): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function __restProps(
|
function __restProps(
|
||||||
props: Record<string, any>,
|
props: Record<string, unknown>,
|
||||||
declared: Set<string>,
|
declared: Set<string>,
|
||||||
): Record<string, any> {
|
): Record<string, unknown> {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
Object.entries(props).filter(([name]) => !declared.has(name)),
|
Object.entries(props).filter(([name]) => !declared.has(name)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wireHtml(v: any): string {
|
function __wireHtml(v: unknown): string {
|
||||||
return String(v == null ? "" : v).replace(
|
return String(v == null ? "" : v).replace(
|
||||||
/[&<>]/g,
|
/[&<>]/g,
|
||||||
(c) =>
|
(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(
|
return String(v == null ? "" : v).replace(
|
||||||
/[&<>"]/g,
|
/[&<>"]/g,
|
||||||
(c) =>
|
(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 ||
|
return value === true ||
|
||||||
value === "true" ||
|
value === "true" ||
|
||||||
value === "" ||
|
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 "";
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return "";
|
||||||
|
|
||||||
const booleanAttributes = new Set(${JSON.stringify([...HTML_BOOLEAN_ATTRIBUTES])});
|
const booleanAttributes = new Set(${JSON.stringify([...HTML_BOOLEAN_ATTRIBUTES])});
|
||||||
@@ -2652,7 +2657,7 @@ function __wireSpreadAttrs(value: any): string {
|
|||||||
return attributes.join("");
|
return attributes.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wireProp(v: any): string {
|
function __wireProp(v: unknown): string {
|
||||||
const value =
|
const value =
|
||||||
v !== null && typeof v === "object"
|
v !== null && typeof v === "object"
|
||||||
? JSON.stringify(v)
|
? JSON.stringify(v)
|
||||||
@@ -2661,18 +2666,18 @@ function __wireProp(v: any): string {
|
|||||||
return __wireAttr(value);
|
return __wireAttr(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wireRaw(v: any): string {
|
function __wireRaw(v: unknown): string {
|
||||||
return String(v == null ? "" : v);
|
return String(v == null ? "" : v);
|
||||||
}`);
|
}`);
|
||||||
|
|
||||||
if (hasServerEach) {
|
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");
|
return __WrnexusBuffer.from(JSON.stringify(value), "utf8").toString("base64");
|
||||||
}`);
|
}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needsScope) {
|
if (needsScope) {
|
||||||
out.push(`function __wrnexusSerializeScopeValue(value: any): string {
|
out.push(`function __wrnexusSerializeScopeValue(value: unknown): string {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return "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)
|
return Object.keys(obj)
|
||||||
.map(
|
.map(
|
||||||
(key) =>
|
(key) =>
|
||||||
@@ -2727,7 +2732,7 @@ function __wireRaw(v: any): string {
|
|||||||
const serverFunctionSource = serverFunctions ? `${serverFunctions}\n` : "";
|
const serverFunctionSource = serverFunctions ? `${serverFunctions}\n` : "";
|
||||||
|
|
||||||
out.push(
|
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` +
|
` const __p = props || {};\n` +
|
||||||
(decls.length > 0 ? decls.join("\n") + "\n" : "") +
|
(decls.length > 0 ? decls.join("\n") + "\n" : "") +
|
||||||
serverFunctionSource +
|
serverFunctionSource +
|
||||||
@@ -2742,7 +2747,7 @@ function __wireRaw(v: any): string {
|
|||||||
return out.join("\n\n") + "\n";
|
return out.join("\n\n") + "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wireRaw(v: any): string {
|
function __wireRaw(v: unknown): string {
|
||||||
return String(v == null ? "" : v);
|
return String(v == null ? "" : v);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2751,19 +2756,19 @@ function wholeAttributeExpression(value: string): string | null {
|
|||||||
|
|
||||||
return match?.[1]?.trim() || null;
|
return match?.[1]?.trim() || null;
|
||||||
}
|
}
|
||||||
function __wireHtml(v: any): string {
|
function __wireHtml(v: unknown): string {
|
||||||
return String(v == null ? "" : v).replace(/[&<>]/g, (c) =>
|
return String(v == null ? "" : v).replace(/[&<>]/g, (c) =>
|
||||||
c === "&" ? "&" : c === "<" ? "<" : ">",
|
c === "&" ? "&" : c === "<" ? "<" : ">",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wireAttr(v: any): string {
|
function __wireAttr(v: unknown): string {
|
||||||
return String(v == null ? "" : v).replace(/[&<>"]/g, (c) =>
|
return String(v == null ? "" : v).replace(/[&<>"]/g, (c) =>
|
||||||
c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : """,
|
c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : """,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wireProp(v: any): string {
|
function __wireProp(v: unknown): string {
|
||||||
const value =
|
const value =
|
||||||
v !== null && typeof v === "object" ? JSON.stringify(v) : String(v == null ? "" : v);
|
v !== null && typeof v === "object" ? JSON.stringify(v) : String(v == null ? "" : v);
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export interface CounterOutputs {
|
|||||||
"change"(value: number): void;
|
"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) {
|
if (v === undefined || v === null) {
|
||||||
return def;
|
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 + "'");
|
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") {
|
if (declared === "function" && typeof v !== "function") {
|
||||||
throw new TypeError("Expected a function prop");
|
throw new TypeError("Expected a function prop");
|
||||||
}
|
}
|
||||||
@@ -113,15 +113,15 @@ function __coerce(v: any, def: any, declared: string = "unknown", propName: stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
function __restProps(
|
function __restProps(
|
||||||
props: Record<string, any>,
|
props: Record<string, unknown>,
|
||||||
declared: Set<string>,
|
declared: Set<string>,
|
||||||
): Record<string, any> {
|
): Record<string, unknown> {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
Object.entries(props).filter(([name]) => !declared.has(name)),
|
Object.entries(props).filter(([name]) => !declared.has(name)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wireHtml(v: any): string {
|
function __wireHtml(v: unknown): string {
|
||||||
return String(v == null ? "" : v).replace(
|
return String(v == null ? "" : v).replace(
|
||||||
/[&<>]/g,
|
/[&<>]/g,
|
||||||
(c) =>
|
(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(
|
return String(v == null ? "" : v).replace(
|
||||||
/[&<>"]/g,
|
/[&<>"]/g,
|
||||||
(c) =>
|
(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 ||
|
return value === true ||
|
||||||
value === "true" ||
|
value === "true" ||
|
||||||
value === "" ||
|
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 "";
|
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"]);
|
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("");
|
return attributes.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wireProp(v: any): string {
|
function __wireProp(v: unknown): string {
|
||||||
const value =
|
const value =
|
||||||
v !== null && typeof v === "object"
|
v !== null && typeof v === "object"
|
||||||
? JSON.stringify(v)
|
? JSON.stringify(v)
|
||||||
@@ -203,11 +203,11 @@ function __wireProp(v: any): string {
|
|||||||
return __wireAttr(value);
|
return __wireAttr(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wireRaw(v: any): string {
|
function __wireRaw(v: unknown): string {
|
||||||
return String(v == null ? "" : v);
|
return String(v == null ? "" : v);
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wrnexusSerializeScopeValue(value: any): string {
|
function __wrnexusSerializeScopeValue(value: unknown): string {
|
||||||
if (value === undefined) {
|
if (value === undefined) {
|
||||||
return "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)
|
return Object.keys(obj)
|
||||||
.map(
|
.map(
|
||||||
(key) =>
|
(key) =>
|
||||||
@@ -260,7 +260,7 @@ function __wrnexusSerializeScopeValue(value: any): string {
|
|||||||
|
|
||||||
export function render(props: CounterProps = {} as CounterProps): string {
|
export function render(props: CounterProps = {} as CounterProps): string {
|
||||||
const __p = props || {};
|
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"]));
|
const __attrs = __restProps(__p, new Set(["label"]));
|
||||||
let count = (0);
|
let count = (0);
|
||||||
const __scopeState = { "label": label, "count": count };
|
const __scopeState = { "label": label, "count": count };
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ export function sqliteSessionStore(path = "sessions.db"): SessionBackend {
|
|||||||
if (!row) return undefined;
|
if (!row) return undefined;
|
||||||
try {
|
try {
|
||||||
return { data: JSON.parse(row.data) as Record<string, unknown>, expiresAt: row.expiresAt };
|
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;
|
return undefined;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
} from "@wrnexus/csr";
|
} from "@wrnexus/csr";
|
||||||
import {
|
import {
|
||||||
renderStyles,
|
renderStyles,
|
||||||
|
renderActiveThemeCss,
|
||||||
renderThemeCss,
|
renderThemeCss,
|
||||||
renderThemeRuntime,
|
renderThemeRuntime,
|
||||||
type ResolvedTheme,
|
type ResolvedTheme,
|
||||||
@@ -122,6 +123,18 @@ export function createDevAssetServer(
|
|||||||
? cssResponse(renderThemeCss(theme))
|
? cssResponse(renderThemeCss(theme))
|
||||||
: new Response("Not Found", { status: 404 });
|
: 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") {
|
if (pathname === "/__wrnexus/theme.js") {
|
||||||
return theme
|
return theme
|
||||||
? jsResponse(renderThemeRuntime(theme))
|
? jsResponse(renderThemeRuntime(theme))
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ export interface ProdOptions {
|
|||||||
clientModulesDir?: string;
|
clientModulesDir?: string;
|
||||||
/** Absolute path to the pre-built theme stylesheet (`theme.css`). */
|
/** Absolute path to the pre-built theme stylesheet (`theme.css`). */
|
||||||
themePath?: string;
|
themePath?: string;
|
||||||
|
/** Pre-built active theme/accent stylesheets, loaded on demand. */
|
||||||
|
themeAssetsDir?: string;
|
||||||
/** Absolute path to the pre-built theme runtime (`theme.js`). */
|
/** Absolute path to the pre-built theme runtime (`theme.js`). */
|
||||||
themeJsPath?: string;
|
themeJsPath?: string;
|
||||||
/** Resolved theme config: enables `<html data-theme>` + `theme.css` link. */
|
/** 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 });
|
return new Response(opts.schemasJs ?? "window.__wireSchemas={};", { headers: JS_HEADERS });
|
||||||
}
|
}
|
||||||
if (pathname === "/__wrnexus/theme.css") return serveFile(opts.themePath, CSS_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/theme.js") return serveFile(opts.themeJsPath, JS_HEADERS);
|
||||||
if (pathname === "/__wrnexus/ui.css") return serveFile(opts.uiCssPath, CSS_HEADERS);
|
if (pathname === "/__wrnexus/ui.css") return serveFile(opts.uiCssPath, CSS_HEADERS);
|
||||||
if (pathname === "/__wrnexus/framework.css")
|
if (pathname === "/__wrnexus/framework.css")
|
||||||
|
|||||||
@@ -66,12 +66,12 @@ import type { StoreDefinition } from "@wrnexus/store";
|
|||||||
import type { ClientRuntimeDefinition } from "@wrnexus/plugin";
|
import type { ClientRuntimeDefinition } from "@wrnexus/plugin";
|
||||||
import { CacheCoordinator } from "@wrnexus/cache";
|
import { CacheCoordinator } from "@wrnexus/cache";
|
||||||
import { generateServiceWorker } from "@wrnexus/pwa";
|
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 {
|
import {
|
||||||
ACCENT_COOKIE,
|
ACCENT_COOKIE,
|
||||||
THEME_COOKIE,
|
THEME_COOKIE,
|
||||||
THEME_CSS_HREF,
|
activeThemeCssHref,
|
||||||
THEME_JS_HREF,
|
|
||||||
resolveAccentName,
|
resolveAccentName,
|
||||||
resolveThemeName,
|
resolveThemeName,
|
||||||
type MobileConfig,
|
type MobileConfig,
|
||||||
@@ -81,7 +81,6 @@ import {
|
|||||||
type TenancyConfig,
|
type TenancyConfig,
|
||||||
} from "@wrnexus/styles";
|
} from "@wrnexus/styles";
|
||||||
import {
|
import {
|
||||||
I18N_JS_HREF,
|
|
||||||
renderI18nData,
|
renderI18nData,
|
||||||
makeT,
|
makeT,
|
||||||
resolveLang,
|
resolveLang,
|
||||||
@@ -945,9 +944,6 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
|||||||
headParts.push(
|
headParts.push(
|
||||||
`<link rel="stylesheet" href="${versionAssetUrl("/__wrnexus/framework.css", deps.assetVersion)}" />`,
|
`<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) {
|
if (deps.hasUi && !deps.hasFrameworkStyles && !deps.stylesIncludeFramework) {
|
||||||
headParts.push(
|
headParts.push(
|
||||||
@@ -1839,6 +1835,7 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
|||||||
|
|
||||||
// <html> attributes: no-flash theme (from cookie, validated) + active lang.
|
// <html> attributes: no-flash theme (from cookie, validated) + active lang.
|
||||||
const attrs: string[] = [];
|
const attrs: string[] = [];
|
||||||
|
let activeThemeHead = "";
|
||||||
|
|
||||||
if (deps.theme) {
|
if (deps.theme) {
|
||||||
const themeName = resolveThemeName(ctx.cookies.get(THEME_COOKIE), deps.theme);
|
const themeName = resolveThemeName(ctx.cookies.get(THEME_COOKIE), deps.theme);
|
||||||
@@ -1850,6 +1847,13 @@ export function createHandlers(deps: RuntimeDeps): Handlers {
|
|||||||
if (accentName) {
|
if (accentName) {
|
||||||
attrs.push(`data-accent="${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)}"`);
|
attrs.push(`lang="${safeLanguageTag(language)}"`);
|
||||||
if (deps.i18n) attrs.push(`dir="${deps.i18n.direction[language] ?? "ltr"}"`);
|
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 ? `<link rel="manifest" href="/site.webmanifest" />` : "",
|
||||||
pwaEnabled ? `<meta name="mobile-web-app-capable" content="yes" />` : "",
|
pwaEnabled ? `<meta name="mobile-web-app-capable" content="yes" />` : "",
|
||||||
pwaEnabled ? `<meta name="apple-mobile-web-app-capable" content="yes" />` : "",
|
pwaEnabled ? `<meta name="apple-mobile-web-app-capable" content="yes" />` : "",
|
||||||
|
activeThemeHead,
|
||||||
extraHead,
|
extraHead,
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
@@ -2443,72 +2448,6 @@ export function normalizeComponentName(name: string): string {
|
|||||||
return name.toLowerCase().replace(/[-_]/g, "");
|
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 {
|
function safeLanguageTag(value: string): string {
|
||||||
return /^[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/.test(value) ? value : "en";
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ export function webVitalsClient(options: WebVitalsClientOptions = {}): string {
|
|||||||
function send(name, value) {
|
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 });
|
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" }));
|
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() {
|
function flush() {
|
||||||
if (flushed) return;
|
if (flushed) return;
|
||||||
@@ -31,9 +31,13 @@ export function webVitalsClient(options: WebVitalsClientOptions = {}): string {
|
|||||||
if (inp) send("INP", inp);
|
if (inp) send("INP", inp);
|
||||||
if (cls) send("CLS", cls);
|
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(_) {}
|
function observe(type, callback, options) {
|
||||||
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(callback).observe(options); }
|
||||||
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(_) {}
|
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("visibilitychange", function(){ if(document.visibilityState === "hidden") flush(); });
|
||||||
addEventListener("pagehide", flush, { once: true });
|
addEventListener("pagehide", flush, { once: true });
|
||||||
})();`;
|
})();`;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import type { MetricPoint } from "./metrics.ts";
|
|||||||
import type { LogRecord } from "./logging.ts";
|
import type { LogRecord } from "./logging.ts";
|
||||||
import type { MetricExporter } from "./server.ts";
|
import type { MetricExporter } from "./server.ts";
|
||||||
import type { SpanExporter, SpanRecord, TraceContext } from "./trace.ts";
|
import type { SpanExporter, SpanRecord, TraceContext } from "./trace.ts";
|
||||||
|
import { reportObservabilityFailure, type ObservabilityDiagnostic } from "./diagnostics.ts";
|
||||||
|
|
||||||
export type FrameworkSpanKind =
|
export type FrameworkSpanKind =
|
||||||
"database" | "cache" | "queue" | "realtime" | "server-action" | "application";
|
"database" | "cache" | "queue" | "realtime" | "server-action" | "application";
|
||||||
@@ -26,6 +27,7 @@ export function createOperationTracer(
|
|||||||
context?: () => Partial<TraceContext>;
|
context?: () => Partial<TraceContext>;
|
||||||
now?: () => number;
|
now?: () => number;
|
||||||
onExportError?: (error: unknown) => void;
|
onExportError?: (error: unknown) => void;
|
||||||
|
diagnostic?: ObservabilityDiagnostic;
|
||||||
} = {},
|
} = {},
|
||||||
): OperationTracer {
|
): OperationTracer {
|
||||||
const now = options.now ?? Date.now;
|
const now = options.now ?? Date.now;
|
||||||
@@ -66,7 +68,9 @@ export function createOperationTracer(
|
|||||||
try {
|
try {
|
||||||
await options.exporter?.export([record]);
|
await options.exporter?.export([record]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
options.onExportError?.(error);
|
if (options.onExportError) options.onExportError(error);
|
||||||
|
else
|
||||||
|
reportObservabilityFailure("operation span export failed", error, options.diagnostic);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createTracer, type Context, type Middleware } from "@wrnexus/core";
|
import { createTracer, type Context, type Middleware } from "@wrnexus/core";
|
||||||
|
import { reportObservabilityFailure, type ObservabilityDiagnostic } from "./diagnostics.ts";
|
||||||
|
|
||||||
export interface TraceContext {
|
export interface TraceContext {
|
||||||
version: "00";
|
version: "00";
|
||||||
@@ -63,6 +64,7 @@ export interface TraceMiddlewareOptions {
|
|||||||
routeName?: (ctx: Context) => string;
|
routeName?: (ctx: Context) => string;
|
||||||
serverTiming?: boolean;
|
serverTiming?: boolean;
|
||||||
onExportError?: (error: unknown, span: SpanRecord) => void | Promise<void>;
|
onExportError?: (error: unknown, span: SpanRecord) => void | Promise<void>;
|
||||||
|
diagnostic?: ObservabilityDiagnostic;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function traceMiddleware(options: TraceMiddlewareOptions = {}): Middleware {
|
export function traceMiddleware(options: TraceMiddlewareOptions = {}): Middleware {
|
||||||
@@ -131,7 +133,8 @@ export function traceMiddleware(options: TraceMiddlewareOptions = {}): Middlewar
|
|||||||
await options.onSpan?.(span);
|
await options.onSpan?.(span);
|
||||||
await options.exporter?.export([span]);
|
await options.exporter?.export([span]);
|
||||||
} catch (error) {
|
} 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(await profile("render", () => "ok")).toBe("ok");
|
||||||
expect(profiles[0]).toMatchObject({ name: "render", durationMs: 4 });
|
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).toContain('addEventListener("pagehide", flush');
|
||||||
expect(client).not.toContain('send("INP",max)');
|
expect(client).not.toContain('send("INP",max)');
|
||||||
expect(client).not.toContain('send("LCP", last.startTime)');
|
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", () => {
|
test("records deterministic counters and histograms", () => {
|
||||||
|
|||||||
@@ -81,6 +81,22 @@ describe("production observability operations", () => {
|
|||||||
expect(failures).toHaveLength(1);
|
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 () => {
|
test("exports OTLP JSON traces and metrics", async () => {
|
||||||
const requests: unknown[] = [];
|
const requests: unknown[] = [];
|
||||||
const send = (async (_url: URL | RequestInfo, init?: RequestInit) => {
|
const send = (async (_url: URL | RequestInfo, init?: RequestInit) => {
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { createStoreContainer } from "./index.ts";
|
import { createStoreContainer } from "./index.ts";
|
||||||
|
|
||||||
let globalContainer: ReturnType<typeof createStoreContainer> | undefined;
|
let globalContainer: ReturnType<typeof createStoreContainer> | undefined;
|
||||||
|
const storeGlobal = globalThis as typeof globalThis & {
|
||||||
|
__wrnexusStoreContainer?: ReturnType<typeof createStoreContainer>;
|
||||||
|
};
|
||||||
|
|
||||||
export function browserStoreContainer(hydration: Record<string, unknown> = {}) {
|
export function browserStoreContainer(hydration: Record<string, unknown> = {}) {
|
||||||
if (!globalContainer) {
|
if (!globalContainer) {
|
||||||
@@ -17,7 +20,7 @@ export function browserStoreContainer(hydration: Record<string, unknown> = {}) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
(globalThis as any).__wrnexusStoreContainer = globalContainer;
|
storeGlobal.__wrnexusStoreContainer = globalContainer;
|
||||||
}
|
}
|
||||||
return globalContainer;
|
return globalContainer;
|
||||||
}
|
}
|
||||||
@@ -25,5 +28,5 @@ export function browserStoreContainer(hydration: Record<string, unknown> = {}) {
|
|||||||
export async function resetBrowserStores() {
|
export async function resetBrowserStores() {
|
||||||
await globalContainer?.dispose();
|
await globalContainer?.dispose();
|
||||||
globalContainer = undefined;
|
globalContainer = undefined;
|
||||||
delete (globalThis as any).__wrnexusStoreContainer;
|
delete storeGlobal.__wrnexusStoreContainer;
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-13
@@ -27,7 +27,7 @@ function readonlySnapshot<S extends object>(state: S): Readonly<S> {
|
|||||||
return Object.freeze(clone(state));
|
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 (typeof window === "undefined") return null;
|
||||||
if (kind === "local") return window.localStorage;
|
if (kind === "local") return window.localStorage;
|
||||||
if (kind === "session") return window.sessionStorage;
|
if (kind === "session") return window.sessionStorage;
|
||||||
@@ -91,7 +91,10 @@ export class StoreContainer {
|
|||||||
readonly runtime: "server" | "client";
|
readonly runtime: "server" | "client";
|
||||||
readonly request?: unknown;
|
readonly request?: unknown;
|
||||||
readonly routeId?: string;
|
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 hydration: Record<string, unknown>;
|
||||||
private readonly onMutation?: (mutation: StoreMutation) => void;
|
private readonly onMutation?: (mutation: StoreMutation) => void;
|
||||||
private readonly lastMutations = new Map<string, StoreMutation>();
|
private readonly lastMutations = new Map<string, StoreMutation>();
|
||||||
@@ -159,7 +162,7 @@ export class StoreContainer {
|
|||||||
Object.fromEntries(
|
Object.fromEntries(
|
||||||
Reflect.ownKeys(instance.computed).map((key) => [
|
Reflect.ownKeys(instance.computed).map((key) => [
|
||||||
String(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, {
|
const publicState = new Proxy({} as State, {
|
||||||
get(_target, property) {
|
get(_target, property) {
|
||||||
return (mutableState as any)[property];
|
return Reflect.get(mutableState, property);
|
||||||
},
|
},
|
||||||
set(_target, property) {
|
set(_target, property) {
|
||||||
throw new TypeError(
|
throw new TypeError(
|
||||||
@@ -334,7 +337,7 @@ export function createStoreInstance<
|
|||||||
return {
|
return {
|
||||||
enumerable: true,
|
enumerable: true,
|
||||||
configurable: true,
|
configurable: true,
|
||||||
value: (mutableState as any)[property],
|
value: Reflect.get(mutableState, property),
|
||||||
writable: false,
|
writable: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -360,7 +363,7 @@ export function createStoreInstance<
|
|||||||
mutate("$reset", () => {
|
mutate("$reset", () => {
|
||||||
const next = createInitialState();
|
const next = createInitialState();
|
||||||
for (const key of Object.keys(mutableState)) {
|
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);
|
Object.assign(mutableState, next);
|
||||||
});
|
});
|
||||||
@@ -382,16 +385,20 @@ export function createStoreInstance<
|
|||||||
definitions.find((entry) => entry.runtime === "shared") ??
|
definitions.find((entry) => entry.runtime === "shared") ??
|
||||||
definitions.find((entry) => entry.runtime === "legacy");
|
definitions.find((entry) => entry.runtime === "legacy");
|
||||||
if (!selected) continue;
|
if (!selected) continue;
|
||||||
(actions as any)[name] = async (...args: unknown[]) => {
|
Reflect.set(actions, name, async (...args: unknown[]) => {
|
||||||
currentAction = name;
|
currentAction = name;
|
||||||
internalMutation = true;
|
internalMutation = true;
|
||||||
try {
|
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 {
|
} finally {
|
||||||
currentAction = "direct";
|
currentAction = "direct";
|
||||||
internalMutation = false;
|
internalMutation = false;
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const computed = new Proxy({} as C, {
|
const computed = new Proxy({} as C, {
|
||||||
@@ -461,7 +468,7 @@ export function createStoreInstance<
|
|||||||
const result: Partial<State> = {};
|
const result: Partial<State> = {};
|
||||||
const serverKeys = new Set(Object.keys(definition.createServerState?.() ?? {}));
|
const serverKeys = new Set(Object.keys(definition.createServerState?.() ?? {}));
|
||||||
for (const [key, value] of Object.entries(mutableState)) {
|
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;
|
return result;
|
||||||
},
|
},
|
||||||
@@ -479,9 +486,9 @@ export function createStoreInstance<
|
|||||||
return new Proxy(core as StoreInstance<State, C, A>, {
|
return new Proxy(core as StoreInstance<State, C, A>, {
|
||||||
get(target, property, receiver) {
|
get(target, property, receiver) {
|
||||||
if (Reflect.has(target, property)) return Reflect.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 actions) return Reflect.get(actions, property);
|
||||||
if (property in (definition.computed ?? {})) return (computed as any)[property];
|
if (property in (definition.computed ?? {})) return Reflect.get(computed, property);
|
||||||
if (property in mutableState) return (publicState as any)[property];
|
if (property in mutableState) return Reflect.get(publicState, property);
|
||||||
return undefined;
|
return undefined;
|
||||||
},
|
},
|
||||||
set(_target, property) {
|
set(_target, property) {
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
export type StoreKind = "global" | "page";
|
export type StoreKind = "global" | "page";
|
||||||
export type StoreRuntime = "shared" | "client" | "server" | "legacy";
|
export type StoreRuntime = "shared" | "client" | "server" | "legacy";
|
||||||
export type PersistenceStorage = "memory" | "session" | "local";
|
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<
|
export type StoreCombinedState<
|
||||||
S extends object,
|
S extends object,
|
||||||
|
|||||||
@@ -61,10 +61,13 @@ export {
|
|||||||
THEME_PALETTE_NAMES,
|
THEME_PALETTE_NAMES,
|
||||||
THEME_COOKIE,
|
THEME_COOKIE,
|
||||||
THEME_CSS_HREF,
|
THEME_CSS_HREF,
|
||||||
|
THEME_CSS_PREFIX,
|
||||||
THEME_JS_HREF,
|
THEME_JS_HREF,
|
||||||
|
activeThemeCssHref,
|
||||||
resolveThemeConfig,
|
resolveThemeConfig,
|
||||||
resolveThemeName,
|
resolveThemeName,
|
||||||
renderThemeCss,
|
renderThemeCss,
|
||||||
|
renderActiveThemeCss,
|
||||||
renderThemeRuntime,
|
renderThemeRuntime,
|
||||||
defineThemeTokens,
|
defineThemeTokens,
|
||||||
themeVar,
|
themeVar,
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ export interface ResolvedTheme {
|
|||||||
export const THEME_COOKIE = "wire-theme";
|
export const THEME_COOKIE = "wire-theme";
|
||||||
export const ACCENT_COOKIE = "wire-accent";
|
export const ACCENT_COOKIE = "wire-accent";
|
||||||
export const THEME_CSS_HREF = "/__wrnexus/theme.css";
|
export const THEME_CSS_HREF = "/__wrnexus/theme.css";
|
||||||
|
export const THEME_CSS_PREFIX = "/__wrnexus/theme/";
|
||||||
export const THEME_JS_HREF = "/__wrnexus/theme.js";
|
export const THEME_JS_HREF = "/__wrnexus/theme.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -519,6 +520,44 @@ function selectorValue(value: string): string {
|
|||||||
return JSON.stringify(value);
|
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.
|
* Generate the theme stylesheet.
|
||||||
*
|
*
|
||||||
@@ -559,36 +598,7 @@ export function renderThemeCss(theme: ResolvedTheme): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return blocks.join("\n") + globalThemeCss();
|
||||||
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"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -603,6 +613,7 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
|
|||||||
const names = JSON.stringify(theme.names);
|
const names = JSON.stringify(theme.names);
|
||||||
const accentNames = JSON.stringify(theme.accentNames);
|
const accentNames = JSON.stringify(theme.accentNames);
|
||||||
const defaultAccent = JSON.stringify(theme.defaultAccent ?? null);
|
const defaultAccent = JSON.stringify(theme.defaultAccent ?? null);
|
||||||
|
const cssPrefix = JSON.stringify(THEME_CSS_PREFIX);
|
||||||
|
|
||||||
return `(function(){
|
return `(function(){
|
||||||
var THEME_COOKIE=${JSON.stringify(THEME_COOKIE)};
|
var THEME_COOKIE=${JSON.stringify(THEME_COOKIE)};
|
||||||
@@ -611,6 +622,7 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
|
|||||||
var ACCENTS=${accentNames};
|
var ACCENTS=${accentNames};
|
||||||
var DEFAULT_THEME=${JSON.stringify(theme.default)};
|
var DEFAULT_THEME=${JSON.stringify(theme.default)};
|
||||||
var DEFAULT_ACCENT=${defaultAccent};
|
var DEFAULT_ACCENT=${defaultAccent};
|
||||||
|
var THEME_CSS_PREFIX=${cssPrefix};
|
||||||
var MAX_AGE=31536000;
|
var MAX_AGE=31536000;
|
||||||
var el=document.documentElement;
|
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;
|
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(){
|
function getTheme(){
|
||||||
var current=el.getAttribute("data-theme")||readCookie(THEME_COOKIE)||DEFAULT_THEME;
|
var current=el.getAttribute("data-theme")||readCookie(THEME_COOKIE)||DEFAULT_THEME;
|
||||||
return THEMES.indexOf(current)>=0?current: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;
|
if(THEMES.indexOf(name)<0)return;
|
||||||
el.setAttribute("data-theme",name);
|
el.setAttribute("data-theme",name);
|
||||||
writeCookie(THEME_COOKIE,name);
|
writeCookie(THEME_COOKIE,name);
|
||||||
|
syncStylesheet(name,getAccent());
|
||||||
syncThemeButtons(document);
|
syncThemeButtons(document);
|
||||||
window.dispatchEvent(new CustomEvent("wrnexus:theme-changed",{detail:{theme:name}}));
|
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;
|
if(ACCENTS.indexOf(name)<0)return;
|
||||||
el.setAttribute("data-accent",name);
|
el.setAttribute("data-accent",name);
|
||||||
writeCookie(ACCENT_COOKIE,name);
|
writeCookie(ACCENT_COOKIE,name);
|
||||||
|
syncStylesheet(getTheme(),name);
|
||||||
syncAccentButtons(document);
|
syncAccentButtons(document);
|
||||||
window.dispatchEvent(new CustomEvent("wrnexus:accent-changed",{detail:{accent:name}}));
|
window.dispatchEvent(new CustomEvent("wrnexus:accent-changed",{detail:{accent:name}}));
|
||||||
}
|
}
|
||||||
@@ -685,6 +706,7 @@ export function renderThemeRuntime(theme: ResolvedTheme): string {
|
|||||||
function clearAccent(){
|
function clearAccent(){
|
||||||
el.removeAttribute("data-accent");
|
el.removeAttribute("data-accent");
|
||||||
deleteCookie(ACCENT_COOKIE);
|
deleteCookie(ACCENT_COOKIE);
|
||||||
|
syncStylesheet(getTheme(),null);
|
||||||
syncAccentButtons(document);
|
syncAccentButtons(document);
|
||||||
window.dispatchEvent(new CustomEvent("wrnexus:accent-changed",{detail:{accent:null}}));
|
window.dispatchEvent(new CustomEvent("wrnexus:accent-changed",{detail:{accent:null}}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import {
|
|||||||
resolveThemeConfig,
|
resolveThemeConfig,
|
||||||
resolveThemeName,
|
resolveThemeName,
|
||||||
renderThemeCss,
|
renderThemeCss,
|
||||||
|
renderActiveThemeCss,
|
||||||
|
activeThemeCssHref,
|
||||||
renderThemeRuntime,
|
renderThemeRuntime,
|
||||||
} from "../src/index.ts";
|
} 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");
|
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", () => {
|
test("renderThemeRuntime bakes the theme names for cycling", () => {
|
||||||
const t = resolveThemeConfig();
|
const t = resolveThemeConfig();
|
||||||
const js = renderThemeRuntime(t);
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -170,7 +170,8 @@ export const UPLOAD_RUNTIME = `
|
|||||||
});
|
});
|
||||||
xhr.addEventListener("load", function () {
|
xhr.addEventListener("load", function () {
|
||||||
var data = null;
|
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) {
|
if (xhr.status >= 200 && xhr.status < 300 && data && data.ok) {
|
||||||
done(ui, (data.files && data.files[0]) || null, file);
|
done(ui, (data.files && data.files[0]) || null, file);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ import {
|
|||||||
upload,
|
upload,
|
||||||
verifySignedFileToken,
|
verifySignedFileToken,
|
||||||
} from "../src/index.ts";
|
} 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() {
|
function configure() {
|
||||||
configureStorage(
|
configureStorage(
|
||||||
|
|||||||
@@ -1,495 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
import console from "node:console";
|
|
||||||
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
||||||
import { dirname, join, relative } from "node:path";
|
|
||||||
import process from "node:process";
|
|
||||||
import { spawnSync } from "node:child_process";
|
|
||||||
import { fileURLToPath } from "node:url";
|
|
||||||
import { createRequire } from "node:module";
|
|
||||||
|
|
||||||
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
||||||
const require = createRequire(import.meta.url);
|
|
||||||
const failures = [];
|
|
||||||
const warnings = [];
|
|
||||||
const passes = [];
|
|
||||||
const fail = (message) => failures.push(message);
|
|
||||||
const pass = (message) => passes.push(message);
|
|
||||||
const readJson = (path) => JSON.parse(readFileSync(path, "utf8"));
|
|
||||||
|
|
||||||
function walk(dir, predicate = () => true) {
|
|
||||||
if (!existsSync(dir)) return [];
|
|
||||||
const out = [];
|
|
||||||
for (const name of readdirSync(dir)) {
|
|
||||||
if (["node_modules", ".git", "dist", ".wrnexus"].includes(name)) continue;
|
|
||||||
const path = join(dir, name);
|
|
||||||
const stat = statSync(path);
|
|
||||||
if (stat.isDirectory()) out.push(...walk(path, predicate));
|
|
||||||
else if (predicate(path)) out.push(path);
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
const packageDirs = readdirSync(join(root, "packages"))
|
|
||||||
.filter((name) => existsSync(join(root, "packages", name, "package.json")))
|
|
||||||
.sort();
|
|
||||||
const frameworkVersion = readJson(join(root, "package.json")).version;
|
|
||||||
if (packageDirs.length < 33)
|
|
||||||
fail(`Expected at least 33 framework packages, found ${packageDirs.length}`);
|
|
||||||
else pass(`${packageDirs.length} framework packages are present`);
|
|
||||||
|
|
||||||
for (const name of packageDirs) {
|
|
||||||
const manifest = readJson(join(root, "packages", name, "package.json"));
|
|
||||||
if (manifest.version !== frameworkVersion) {
|
|
||||||
fail(`packages/${name} is ${manifest.version ?? "unversioned"}; expected ${frameworkVersion}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!failures.some((item) => item.startsWith("packages/"))) {
|
|
||||||
pass(`All framework packages are version ${frameworkVersion}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [label, path] of [
|
|
||||||
["root", "package.json"],
|
|
||||||
["VS Code extension", "editors/vscode/package.json"],
|
|
||||||
["managed CAPTCHA service", "services/managed-captcha/package.json"],
|
|
||||||
]) {
|
|
||||||
const version = readJson(join(root, path)).version;
|
|
||||||
if (version !== frameworkVersion)
|
|
||||||
fail(`${label} version is ${version}; expected ${frameworkVersion}`);
|
|
||||||
else pass(`${label} version is ${frameworkVersion}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const path of [
|
|
||||||
"packages/store/src/index.ts",
|
|
||||||
"packages/typecheck/src/index.ts",
|
|
||||||
"packages/syntax/src/v060.ts",
|
|
||||||
"packages/compiler/src/client-codegen.ts",
|
|
||||||
"packages/compiler/src/server-codegen.ts",
|
|
||||||
"packages/compiler/src/type-codegen.ts",
|
|
||||||
"packages/compiler/src/store-codegen.ts",
|
|
||||||
"packages/csr/src/outputs.ts",
|
|
||||||
"packages/csr/src/server-client.ts",
|
|
||||||
"packages/ssr/src/store-context.ts",
|
|
||||||
"packages/ssr/src/rpc.ts",
|
|
||||||
]) {
|
|
||||||
if (!existsSync(join(root, path))) fail(`Missing required v0.6 file: ${path}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const syntaxManifest = readJson(join(root, "packages/syntax/package.json"));
|
|
||||||
const syntaxIndexSource = readFileSync(join(root, "packages/syntax/src/index.ts"), "utf8");
|
|
||||||
const compilerCodegenSource = readFileSync(join(root, "packages/compiler/src/codegen.ts"), "utf8");
|
|
||||||
const publicV060ImportPattern = /(?:\bfrom\s*|\brequire\s*\()\s*["']@wrnexus\/syntax\/v060["']/;
|
|
||||||
const publicV060Imports = walk(root, (path) => /\.(?:ts|tsx|js|mjs|cjs)$/.test(path)).filter(
|
|
||||||
(path) => publicV060ImportPattern.test(readFileSync(path, "utf8")),
|
|
||||||
);
|
|
||||||
if (syntaxManifest.exports?.["./v060"])
|
|
||||||
fail("@wrnexus/syntax still exposes the version-specific ./v060 subpath");
|
|
||||||
if (!syntaxIndexSource.includes("stripRuntimeFunctionModifiers"))
|
|
||||||
fail("@wrnexus/syntax root does not re-export v0.6 helpers");
|
|
||||||
if (!compilerCodegenSource.includes('from "@wrnexus/syntax"'))
|
|
||||||
fail("compiler does not import syntax helpers from @wrnexus/syntax");
|
|
||||||
for (const path of publicV060Imports)
|
|
||||||
fail(`${relative(root, path)} imports the forbidden @wrnexus/syntax/v060 subpath`);
|
|
||||||
if (
|
|
||||||
!failures.some(
|
|
||||||
(item) =>
|
|
||||||
item.includes("syntax/v060") ||
|
|
||||||
item.includes("root does not re-export") ||
|
|
||||||
item.includes("compiler does not import syntax helpers"),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
pass("v0.6 syntax helpers are exposed through @wrnexus/syntax only");
|
|
||||||
}
|
|
||||||
|
|
||||||
const parserSource = readFileSync(join(root, "packages/syntax/src/v060.ts"), "utf8");
|
|
||||||
for (const marker of [
|
|
||||||
"parseOutputs",
|
|
||||||
"parseStructuredImports",
|
|
||||||
"parseStateDeclarations",
|
|
||||||
"parseRuntimeFunctions",
|
|
||||||
"parseStoreLifecycle",
|
|
||||||
]) {
|
|
||||||
if (!parserSource.includes(marker)) fail(`v0.6 parser helper missing: ${marker}`);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
![
|
|
||||||
"parseOutputs",
|
|
||||||
"parseStructuredImports",
|
|
||||||
"parseStateDeclarations",
|
|
||||||
"parseRuntimeFunctions",
|
|
||||||
"parseStoreLifecycle",
|
|
||||||
].some((marker) => !parserSource.includes(marker))
|
|
||||||
) {
|
|
||||||
pass("v0.6 parser helpers are present");
|
|
||||||
}
|
|
||||||
|
|
||||||
const uiFiles = walk(join(root, "packages/ui/components"), (path) => path.endsWith(".wrn"));
|
|
||||||
const forbidden = [
|
|
||||||
["$emit", /\$emit\s*\(/],
|
|
||||||
["legacy @event", /^\s*@event\b/m],
|
|
||||||
["$event", /\$event\b/],
|
|
||||||
["event.detail", /\bevent\.detail\b/],
|
|
||||||
["unclassified function", /^\s*(?:async\s+)?function\s+[A-Za-z_$]/m],
|
|
||||||
];
|
|
||||||
for (const file of uiFiles) {
|
|
||||||
const source = readFileSync(file, "utf8");
|
|
||||||
for (const [label, pattern] of forbidden) {
|
|
||||||
if (pattern.test(source)) fail(`${relative(root, file)} contains ${label}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!failures.some((item) => item.startsWith("packages/ui/components/"))) {
|
|
||||||
pass(`${uiFiles.length} UI components use classified functions and typed output declarations`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function outputBlocks(source) {
|
|
||||||
const blocks = [];
|
|
||||||
for (const match of source.matchAll(/\boutputs\s*\{/g)) {
|
|
||||||
const brace = source.indexOf("{", match.index);
|
|
||||||
let depth = 0;
|
|
||||||
let quote = "";
|
|
||||||
for (let index = brace; index < source.length; index++) {
|
|
||||||
const char = source[index];
|
|
||||||
if (quote) {
|
|
||||||
if (char === "\\") index++;
|
|
||||||
else if (char === quote) quote = "";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (char === '"' || char === "'" || char === "`") quote = char;
|
|
||||||
else if (char === "{") depth++;
|
|
||||||
else if (char === "}" && --depth === 0) {
|
|
||||||
blocks.push(source.slice(brace + 1, index));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return blocks;
|
|
||||||
}
|
|
||||||
for (const file of uiFiles) {
|
|
||||||
const source = readFileSync(file, "utf8");
|
|
||||||
if (outputBlocks(source).some((block) => /\bunknown\b/.test(block))) {
|
|
||||||
fail(`${relative(root, file)} contains an untyped output payload contract`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!failures.some((item) => item.includes("untyped output payload contract"))) {
|
|
||||||
pass("All UI output payload contracts are concrete and contain no unknown types");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [path, markers] of [
|
|
||||||
["packages/compiler/src/client-codegen.ts", ["RESERVED_BINDINGS", "!parameterNames.has"]],
|
|
||||||
[
|
|
||||||
"packages/compiler/src/store-codegen.ts",
|
|
||||||
[
|
|
||||||
"const server = context.server",
|
|
||||||
"persist.migrate",
|
|
||||||
"persist.validate",
|
|
||||||
"__wrnexusApplyStoreHotUpdate",
|
|
||||||
],
|
|
||||||
],
|
|
||||||
["packages/compiler/src/server-codegen.ts", ["remotelyReferencedServerFunctions"]],
|
|
||||||
[
|
|
||||||
"packages/dev-server/src/pipeline.ts",
|
|
||||||
["setCompileImportOptions", 'mode: "compatible"', "WRN-IMPORT-IMPLICIT"],
|
|
||||||
],
|
|
||||||
["packages/dev-server/src/runtime.ts", ["store-update", "__wrnexusApplyStoreHotUpdate"]],
|
|
||||||
]) {
|
|
||||||
const source = readFileSync(join(root, path), "utf8");
|
|
||||||
for (const marker of markers)
|
|
||||||
if (!source.includes(marker)) fail(`${path} is missing focused fix marker: ${marker}`);
|
|
||||||
}
|
|
||||||
if (!failures.some((item) => item.includes("focused fix marker"))) {
|
|
||||||
pass("Focused fixes 1-7 are wired into compiler, store, import, RPC, and HMR sources");
|
|
||||||
}
|
|
||||||
|
|
||||||
const storeTypesSource = readFileSync(join(root, "packages/store/src/types.ts"), "utf8");
|
|
||||||
const storeRuntimeSource = readFileSync(join(root, "packages/store/src/index.ts"), "utf8");
|
|
||||||
const storeCodegenSource = readFileSync(
|
|
||||||
join(root, "packages/compiler/src/store-codegen.ts"),
|
|
||||||
"utf8",
|
|
||||||
);
|
|
||||||
for (const [label, condition] of [
|
|
||||||
[
|
|
||||||
"separate client/server state generics",
|
|
||||||
/StoreCombinedState<[\s\S]*?CS extends object[\s\S]*?SS extends object/.test(
|
|
||||||
storeTypesSource,
|
|
||||||
) &&
|
|
||||||
/createClientState\?: \(\) => CS/.test(storeTypesSource) &&
|
|
||||||
/createServerState\?: \(\) => SS/.test(storeTypesSource),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"callable action default",
|
|
||||||
/StoreFunction = \(\.\.\.args: any\[\]\) => any/.test(storeTypesSource) &&
|
|
||||||
/Record<string, StoreFunction>/.test(storeTypesSource),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"non-colliding initialization promise",
|
|
||||||
/readonly whenReady: Promise<void>/.test(storeTypesSource) &&
|
|
||||||
!/readonly ready: Promise<void>/.test(storeTypesSource) &&
|
|
||||||
/await instance\.whenReady/.test(storeRuntimeSource),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"browser store whenReady code generation",
|
|
||||||
/whenReady: Promise\.resolve\(\)/.test(storeCodegenSource) &&
|
|
||||||
/core\.whenReady = init\(\)/.test(storeCodegenSource),
|
|
||||||
],
|
|
||||||
]) {
|
|
||||||
if (!condition) fail(`Store type regression: missing ${label}`);
|
|
||||||
}
|
|
||||||
if (!failures.some((item) => item.startsWith("Store type regression:"))) {
|
|
||||||
pass(
|
|
||||||
"Store client/server state, callable actions, and ready-state collision regressions are guarded",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const reactiveRuntimeSource = readFileSync(
|
|
||||||
join(root, "packages/csr/src/reactive-runtime.ts"),
|
|
||||||
"utf8",
|
|
||||||
);
|
|
||||||
const clientCodegenSource = readFileSync(
|
|
||||||
join(root, "packages/compiler/src/client-codegen.ts"),
|
|
||||||
"utf8",
|
|
||||||
);
|
|
||||||
const syntaxParserSource = readFileSync(join(root, "packages/syntax/src/parser.ts"), "utf8");
|
|
||||||
const compilerMainSource = readFileSync(join(root, "packages/compiler/src/codegen.ts"), "utf8");
|
|
||||||
for (const [label, condition] of [
|
|
||||||
[
|
|
||||||
"synchronous hydration without a browser module",
|
|
||||||
reactiveRuntimeSource.includes('if (!moduleUrl || moduleUrl === "__WRNEXUS_CLIENT_MODULE__")'),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"runtime binding collision guard",
|
|
||||||
clientCodegenSource.includes("RUNTIME_BINDINGS") &&
|
|
||||||
clientCodegenSource.includes("!RUNTIME_BINDINGS.has(prop.name)"),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"unknown lifecycle hook validation",
|
|
||||||
syntaxParserSource.includes("Unknown lifecycle hook") &&
|
|
||||||
syntaxParserSource.includes("Unknown store lifecycle hook"),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"synchronous pages without imported stores",
|
|
||||||
compilerMainSource.includes('storeBindings.length > 0 ? "async " : ""'),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"mutable store dispose lifecycle",
|
|
||||||
storeRuntimeSource.includes('runLifecycle("$dispose", definition.lifecycle?.dispose)'),
|
|
||||||
],
|
|
||||||
]) {
|
|
||||||
if (!condition) fail(`R7 regression: missing ${label}`);
|
|
||||||
}
|
|
||||||
const rangeSliderReference = readJson(
|
|
||||||
join(root, "packages/ui/component-reference.json"),
|
|
||||||
).components.find((component) => component.name === "RangeSlider");
|
|
||||||
if (
|
|
||||||
!rangeSliderReference ||
|
|
||||||
!["input", "change", "focus", "blur"].every((name) => rangeSliderReference.events.includes(name))
|
|
||||||
) {
|
|
||||||
fail("R7 regression: RangeSlider output reference is incomplete");
|
|
||||||
}
|
|
||||||
if (!failures.some((item) => item.startsWith("R7 regression:"))) {
|
|
||||||
pass(
|
|
||||||
"R7 parser, hydration, codegen, store lifecycle, and component-reference regressions are guarded",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const uiReference = readJson(join(root, "packages/ui/component-reference.json"));
|
|
||||||
const uiCatalog = readJson(join(root, "packages/ui/component-catalog.json"));
|
|
||||||
const declaredUiNames = uiFiles
|
|
||||||
.map((file) => {
|
|
||||||
const declaration = /^\s*component\s+([A-Za-z][A-Za-z0-9_]*)\b/m.exec(
|
|
||||||
readFileSync(file, "utf8"),
|
|
||||||
);
|
|
||||||
return declaration?.[1] ?? null;
|
|
||||||
})
|
|
||||||
.filter(Boolean)
|
|
||||||
.sort();
|
|
||||||
const referenceNames = uiReference.components.map((component) => component.name).sort();
|
|
||||||
const catalogNames = uiCatalog.components.map((component) => component.name).sort();
|
|
||||||
if (JSON.stringify(referenceNames) !== JSON.stringify(declaredUiNames)) {
|
|
||||||
fail("R8 regression: component reference does not match bundled UI declarations");
|
|
||||||
}
|
|
||||||
if (JSON.stringify(catalogNames) !== JSON.stringify(declaredUiNames)) {
|
|
||||||
fail("R8 regression: component catalog does not match bundled UI declarations");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { compileWireFile } = require(join(root, "editors/vscode/src/compiler.cjs"));
|
|
||||||
for (const file of uiFiles) {
|
|
||||||
const output = compileWireFile(readFileSync(file, "utf8"), file);
|
|
||||||
if (!output.includes("${__wireSpreadAttrs(__attrs)}")) {
|
|
||||||
fail(`R8 regression: ${relative(root, file)} does not forward native attributes`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
fail(`R8 regression: bundled UI compilation failed: ${error.message}`);
|
|
||||||
}
|
|
||||||
if (!failures.some((item) => item.startsWith("R8 regression:"))) {
|
|
||||||
pass(
|
|
||||||
"R8 UI compilation, readonly-prop diagnostics, native attributes, catalog, and reference are guarded",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleCountSource = readFileSync(
|
|
||||||
join(root, "packages/ui/components/ToggleCount.wrn"),
|
|
||||||
"utf8",
|
|
||||||
);
|
|
||||||
for (const [label, condition] of [
|
|
||||||
[
|
|
||||||
"peer client-function bindings",
|
|
||||||
clientCodegenSource.includes("context.functions") &&
|
|
||||||
clientCodegenSource.includes("context.functions[name](...args)") &&
|
|
||||||
clientCodegenSource.includes("(...__wrnexusPeerArgs) => context.functions"),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"drift-free ToggleCount animation scheduling",
|
|
||||||
toggleCountSource.includes("duration * frame / totalFrames") &&
|
|
||||||
toggleCountSource.includes("scheduleValueFrame(") &&
|
|
||||||
toggleCountSource.includes("data-animation-token") &&
|
|
||||||
!toggleCountSource.includes("setTimeout(\n animateValueFrame"),
|
|
||||||
],
|
|
||||||
]) {
|
|
||||||
if (!condition) fail(`R9 regression: missing ${label}`);
|
|
||||||
}
|
|
||||||
if (!failures.some((item) => item.startsWith("R9 regression:"))) {
|
|
||||||
pass("R9 peer-function binding and drift-free ToggleCount animation are guarded");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { parse, generateTargets } = require(join(root, "editors/vscode/src/compiler.cjs"));
|
|
||||||
const targets = generateTargets(
|
|
||||||
parse(`component PeerCalls {
|
|
||||||
state { value: number = 0 }
|
|
||||||
functions {
|
|
||||||
client function increment(): void { value += 1 }
|
|
||||||
client function run(): void { increment() }
|
|
||||||
}
|
|
||||||
view { <button @click='run()'>{value}</button> }
|
|
||||||
}`),
|
|
||||||
);
|
|
||||||
const executable = new Function(
|
|
||||||
`${targets.browser.replace(/^export\s+/gm, "")}\nreturn { bindClientScope };`,
|
|
||||||
)();
|
|
||||||
const state = { value: 0 };
|
|
||||||
const functions = executable.bindClientScope({
|
|
||||||
state,
|
|
||||||
props: {},
|
|
||||||
output: {},
|
|
||||||
server: {},
|
|
||||||
refs: {},
|
|
||||||
});
|
|
||||||
functions.run();
|
|
||||||
if (state.value !== 1) {
|
|
||||||
fail(`R10 regression: peer client function state was overwritten (${state.value})`);
|
|
||||||
} else {
|
|
||||||
pass("R10 peer client functions preserve shared scoped state");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
fail(`R10 regression: peer client function execution failed: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const compilerTargetTestSource = readFileSync(
|
|
||||||
join(root, "packages/compiler/test/v060-targets.test.ts"),
|
|
||||||
"utf8",
|
|
||||||
);
|
|
||||||
if (!compilerTargetTestSource.includes('targets.browser.replace(/^export\\s+/gm, "")')) {
|
|
||||||
fail(
|
|
||||||
"R11 regression: peer-function executable test does not strip export keywords with a whitespace regex",
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
pass("R11 peer-function executable regression test uses the correct export-stripping regex");
|
|
||||||
}
|
|
||||||
|
|
||||||
const releaseSource = readFileSync(join(root, "scripts/release.ts"), "utf8");
|
|
||||||
const referenceGeneratorSource = readFileSync(
|
|
||||||
join(root, "scripts/generate-ui-component-reference.mjs"),
|
|
||||||
"utf8",
|
|
||||||
);
|
|
||||||
for (const [label, condition] of [
|
|
||||||
[
|
|
||||||
"canonical generated-reference Git comparison",
|
|
||||||
releaseSource.includes('["diff", "--quiet", "--", ...generatedFiles]') &&
|
|
||||||
releaseSource.includes('["diff", "--name-status", "--", ...generatedFiles]'),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"component catalog included in the release reference gate",
|
|
||||||
releaseSource.includes('"packages/ui/component-catalog.json"'),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"platform-independent component reference ordering",
|
|
||||||
referenceGeneratorSource.includes("function compareText(left, right)") &&
|
|
||||||
referenceGeneratorSource.includes("compareText(left.name, right.name)") &&
|
|
||||||
!referenceGeneratorSource.includes("localeCompare"),
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"line-ending-stable generated reference writes",
|
|
||||||
referenceGeneratorSource.includes("function normalizeNewlines(source)") &&
|
|
||||||
referenceGeneratorSource.includes(
|
|
||||||
"normalizeNewlines(current) === normalizeNewlines(source)",
|
|
||||||
) &&
|
|
||||||
referenceGeneratorSource.includes("return false"),
|
|
||||||
],
|
|
||||||
]) {
|
|
||||||
if (!condition) fail(`R13 regression: missing ${label}`);
|
|
||||||
}
|
|
||||||
if (!failures.some((item) => item.startsWith("R13 regression:"))) {
|
|
||||||
const referenceGenerationTest = spawnSync(
|
|
||||||
process.execPath,
|
|
||||||
[join(root, "scripts/test-ui-reference-generation.mjs")],
|
|
||||||
{ encoding: "utf8" },
|
|
||||||
);
|
|
||||||
if (referenceGenerationTest.status !== 0) {
|
|
||||||
fail(
|
|
||||||
`R13 regression: generated-reference line-ending test failed: ${
|
|
||||||
referenceGenerationTest.stderr.trim() || referenceGenerationTest.stdout.trim()
|
|
||||||
}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
pass(
|
|
||||||
"R13 release reference gate is canonical, complete, platform independent, and line-ending stable",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const showcaseFiles = walk(join(root, "examples/component-showcase/app"), (path) =>
|
|
||||||
path.endsWith(".wrn"),
|
|
||||||
);
|
|
||||||
for (const file of showcaseFiles) {
|
|
||||||
const source = readFileSync(file, "utf8");
|
|
||||||
if (/\b(?:open|defaultOpen)\s*=\s*(?:"true"|'\{true\}'|"\{true\}")/.test(source)) {
|
|
||||||
fail(`${relative(root, file)} forces an overlay open`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!failures.some((item) => item.includes("forces an overlay open"))) {
|
|
||||||
pass(
|
|
||||||
`${showcaseFiles.length} generated showcase pages contain no forced-open overlay regression`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const file of walk(root, (path) => path.endsWith(".json"))) {
|
|
||||||
try {
|
|
||||||
readJson(file);
|
|
||||||
} catch (error) {
|
|
||||||
fail(`Invalid JSON ${relative(root, file)}: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!failures.some((item) => item.startsWith("Invalid JSON"))) pass("All JSON files parse");
|
|
||||||
|
|
||||||
for (const file of walk(root, (path) => /\.(?:mjs|cjs|js)$/.test(path))) {
|
|
||||||
const check = spawnSync(process.execPath, ["--check", file], { encoding: "utf8" });
|
|
||||||
if (check.status !== 0)
|
|
||||||
fail(`JavaScript syntax failed: ${relative(root, file)}\n${check.stderr.trim()}`);
|
|
||||||
}
|
|
||||||
if (!failures.some((item) => item.startsWith("JavaScript syntax failed")))
|
|
||||||
pass("JavaScript/MJS syntax checks pass");
|
|
||||||
|
|
||||||
const bun = spawnSync(process.platform === "win32" ? "bun.exe" : "bun", ["--version"], {
|
|
||||||
encoding: "utf8",
|
|
||||||
});
|
|
||||||
if (bun.status !== 0)
|
|
||||||
warnings.push("Bun is unavailable; run `bun install` and `bun run check` on the target machine.");
|
|
||||||
else pass(`Bun ${bun.stdout.trim()} is available`);
|
|
||||||
|
|
||||||
console.log("WRNexusJS v0.6 validation");
|
|
||||||
for (const message of passes) console.log(`PASS ${message}`);
|
|
||||||
for (const message of warnings) console.warn(`WARN ${message}`);
|
|
||||||
for (const message of failures) console.error(`FAIL ${message}`);
|
|
||||||
console.log(`\n${passes.length} passed, ${warnings.length} warnings, ${failures.length} failed`);
|
|
||||||
process.exitCode = failures.length ? 1 : 0;
|
|
||||||
Reference in New Issue
Block a user