Compare commits

..
9 Commits
Author SHA1 Message Date
Clintchiz d22824fa62 release: WRNexusJS 0.2.47 2026-07-18 13:12:16 +05:30
Clintchiz 973d580bc5 release: WRNexusJS 0.2.46 2026-07-15 20:09:47 +05:30
Clintchiz 8a8328a308 release: WRNexusJS 0.2.44 2026-07-15 19:41:42 +05:30
Clintchiz 0f915c3c98 release: WRNexusJS 0.2.44 2026-07-15 18:54:29 +05:30
Clintchiz 9aa13fb4db release: WRNexusJS 0.2.43 2026-07-15 18:41:55 +05:30
Clintchiz 9070a1ef9c release: WRNexusJS 0.2.42 2026-07-15 18:15:22 +05:30
Clintchiz 4230de7065 release: WRNexusJS 0.2.41 2026-07-15 14:52:56 +05:30
Clintchiz 0006aef2c4 release: WRNexusJS 0.2.40 2026-07-15 14:02:27 +05:30
Clintchiz 7dfca4a190 release: WRNexusJS 0.2.40 2026-07-15 13:42:18 +05:30
60 changed files with 2153 additions and 260 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ai", "name": "@wrnexus/ai",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/authz", "name": "@wrnexus/authz",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"description": "@wrnexus/authz — part of the WrNexus framework.", "description": "@wrnexus/authz — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+11 -11
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/cli", "name": "@wrnexus/cli",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"description": "@wrnexus/cli — part of the WrNexus framework.", "description": "@wrnexus/cli — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -28,16 +28,16 @@
"wrnexus": "./dist/index.js" "wrnexus": "./dist/index.js"
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.2.39", "@wrnexus/core": "^0.2.47",
"@wrnexus/router": "^0.2.39", "@wrnexus/router": "^0.2.47",
"@wrnexus/csr": "^0.2.39", "@wrnexus/csr": "^0.2.47",
"@wrnexus/compiler": "^0.2.39", "@wrnexus/compiler": "^0.2.47",
"@wrnexus/styles": "^0.2.39", "@wrnexus/styles": "^0.2.47",
"@wrnexus/dev-server": "^0.2.39", "@wrnexus/dev-server": "^0.2.47",
"@wrnexus/ui": "^0.2.39", "@wrnexus/ui": "^0.2.47",
"@wrnexus/validation": "^0.2.39", "@wrnexus/validation": "^0.2.47",
"@wrnexus/i18n": "^0.2.39", "@wrnexus/i18n": "^0.2.47",
"@wrnexus/db": "^0.2.39" "@wrnexus/db": "^0.2.47"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/compiler", "name": "@wrnexus/compiler",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"description": "@wrnexus/compiler — part of the WrNexus framework.", "description": "@wrnexus/compiler — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/core", "name": "@wrnexus/core",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"description": "@wrnexus/core — part of the WrNexus framework.", "description": "@wrnexus/core — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/csr", "name": "@wrnexus/csr",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"description": "@wrnexus/csr — part of the WrNexus framework.", "description": "@wrnexus/csr — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -21,7 +21,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.2.39" "@wrnexus/core": "^0.2.47"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/db", "name": "@wrnexus/db",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"description": "@wrnexus/db — part of the WrNexus framework.", "description": "@wrnexus/db — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+13 -13
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/dev-server", "name": "@wrnexus/dev-server",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"description": "@wrnexus/dev-server — part of the WrNexus framework.", "description": "@wrnexus/dev-server — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -25,18 +25,18 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.2.39", "@wrnexus/core": "^0.2.47",
"@wrnexus/router": "^0.2.39", "@wrnexus/router": "^0.2.47",
"@wrnexus/ssr": "^0.2.39", "@wrnexus/ssr": "^0.2.47",
"@wrnexus/csr": "^0.2.39", "@wrnexus/csr": "^0.2.47",
"@wrnexus/compiler": "^0.2.39", "@wrnexus/compiler": "^0.2.47",
"@wrnexus/styles": "^0.2.39", "@wrnexus/styles": "^0.2.47",
"@wrnexus/ui": "^0.2.39", "@wrnexus/ui": "^0.2.47",
"@wrnexus/validation": "^0.2.39", "@wrnexus/validation": "^0.2.47",
"@wrnexus/i18n": "^0.2.39", "@wrnexus/i18n": "^0.2.47",
"@wrnexus/db": "^0.2.39", "@wrnexus/db": "^0.2.47",
"@wrnexus/pubsub": "^0.2.39", "@wrnexus/pubsub": "^0.2.47",
"@wrnexus/uploader": "^0.2.39" "@wrnexus/uploader": "^0.2.47"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/encryption", "name": "@wrnexus/encryption",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"description": "@wrnexus/encryption — part of the WrNexus framework.", "description": "@wrnexus/encryption — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/helpers", "name": "@wrnexus/helpers",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
"license": "MIT", "license": "MIT",
@@ -21,7 +21,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.2.39" "@wrnexus/core": "^0.2.47"
}, },
"files": [ "files": [
"dist" "dist"
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/i18n", "name": "@wrnexus/i18n",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"description": "@wrnexus/i18n — part of the WrNexus framework.", "description": "@wrnexus/i18n — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -21,7 +21,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.2.39" "@wrnexus/core": "^0.2.47"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/jwt", "name": "@wrnexus/jwt",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"description": "@wrnexus/jwt — part of the WrNexus framework.", "description": "@wrnexus/jwt — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/mobile", "name": "@wrnexus/mobile",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"description": "@wrnexus/mobile — part of the WrNexus framework.", "description": "@wrnexus/mobile — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -21,7 +21,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/native": "^0.2.39" "@wrnexus/native": "^0.2.47"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/native", "name": "@wrnexus/native",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"description": "@wrnexus/native — part of the WrNexus framework.", "description": "@wrnexus/native — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/oauth", "name": "@wrnexus/oauth",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"description": "@wrnexus/oauth — part of the WrNexus framework.", "description": "@wrnexus/oauth — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/pubsub", "name": "@wrnexus/pubsub",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"description": "@wrnexus/pubsub — part of the WrNexus framework.", "description": "@wrnexus/pubsub — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/queue", "name": "@wrnexus/queue",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"description": "@wrnexus/queue — part of the WrNexus framework.", "description": "@wrnexus/queue — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/reactive", "name": "@wrnexus/reactive",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"description": "@wrnexus/reactive — part of the WrNexus framework.", "description": "@wrnexus/reactive — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/router", "name": "@wrnexus/router",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"description": "@wrnexus/router — part of the WrNexus framework.", "description": "@wrnexus/router — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -21,8 +21,8 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/compiler": "^0.2.39", "@wrnexus/compiler": "^0.2.47",
"@wrnexus/core": "^0.2.39" "@wrnexus/core": "^0.2.47"
}, },
"files": [ "files": [
"dist" "dist"
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ssr", "name": "@wrnexus/ssr",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"description": "@wrnexus/ssr — part of the WrNexus framework.", "description": "@wrnexus/ssr — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -21,7 +21,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.2.39" "@wrnexus/core": "^0.2.47"
}, },
"files": [ "files": [
"dist" "dist"
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/styles", "name": "@wrnexus/styles",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"description": "@wrnexus/styles — part of the WrNexus framework.", "description": "@wrnexus/styles — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -21,7 +21,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/uploader": "^0.2.39" "@wrnexus/uploader": "^0.2.47"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/test", "name": "@wrnexus/test",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"description": "@wrnexus/test — part of the WrNexus framework.", "description": "@wrnexus/test — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/tracking", "name": "@wrnexus/tracking",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"description": "@wrnexus/tracking — part of the WrNexus framework.", "description": "@wrnexus/tracking — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ui", "name": "@wrnexus/ui",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"description": "@wrnexus/ui — part of the WrNexus framework.", "description": "@wrnexus/ui — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -22,7 +22,7 @@
"./ui.css": "./ui.css" "./ui.css": "./ui.css"
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.2.39" "@wrnexus/core": "^0.2.47"
}, },
"files": [ "files": [
"dist", "dist",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/uploader", "name": "@wrnexus/uploader",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"description": "@wrnexus/uploader — part of the WrNexus framework.", "description": "@wrnexus/uploader — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
@@ -21,7 +21,7 @@
} }
}, },
"dependencies": { "dependencies": {
"@wrnexus/core": "^0.2.39" "@wrnexus/core": "^0.2.47"
}, },
"files": [ "files": [
"dist" "dist"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/validation", "name": "@wrnexus/validation",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"description": "@wrnexus/validation — part of the WrNexus framework.", "description": "@wrnexus/validation — part of the WrNexus framework.",
"license": "MIT", "license": "MIT",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ai", "name": "@wrnexus/ai",
"version": "0.2.39", "version": "0.2.47",
"private": true, "private": true,
"type": "module", "type": "module",
"description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/authz", "name": "@wrnexus/authz",
"version": "0.2.39", "version": "0.2.47",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/cli", "name": "@wrnexus/cli",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+72
View File
@@ -429,6 +429,78 @@ const MIGRATIONS: Migration[] = [
// No generated application files require automatic migration. // No generated application files require automatic migration.
}, },
}, },
{
version: "0.2.40",
id: "conditions-bug",
description:
"Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
apply() {
// No generated application files require automatic migration.
},
},
{
version: "0.2.41",
id: "conditions-bug",
description:
"Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
apply() {
// No generated application files require automatic migration.
},
},
{
version: "0.2.42",
id: "conditions-bug",
description:
"Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
apply() {
// No generated application files require automatic migration.
},
},
{
version: "0.2.43",
id: "conditions-bug",
description:
"Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
apply() {
// No generated application files require automatic migration.
},
},
{
version: "0.2.44",
id: "conditions-bug",
description:
"Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
apply() {
// No generated application files require automatic migration.
},
},
{
version: "0.2.45",
id: "conditions-bug",
description:
"Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
apply() {
// No generated application files require automatic migration.
},
},
{
version: "0.2.46",
id: "conditions-bug",
description:
"Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
apply() {
// No generated application files require automatic migration.
},
},
{
version: "0.2.47",
id: "conditions-bug",
description:
"Adds server-rendered component each/if blocks and fixes dynamic component rendering.",
apply() {
// No generated application files require automatic migration.
},
},
]; ];
/** Release tooling uses this to require an explicit migration entry per version. */ /** Release tooling uses this to require an explicit migration entry per version. */
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/compiler", "name": "@wrnexus/compiler",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+150 -28
View File
@@ -524,7 +524,13 @@ function hasClientBehavior(nodes: ViewNode[]): boolean {
// `{t:key}` is i18n sugar resolved server-side — not client reactivity. // `{t:key}` is i18n sugar resolved server-side — not client reactivity.
if (node.type === "text") return /\{(?!t:)[^{}]+\}/.test(node.value); if (node.type === "text") return /\{(?!t:)[^{}]+\}/.test(node.value);
// Server control blocks render on the server; they don't add client reactivity. // Server control blocks render on the server; they don't add client reactivity.
if (node.type === "each" || node.type === "if") return false; if (node.type === "each") {
return hasClientBehavior(node.body) || hasClientBehavior(node.empty);
}
if (node.type === "if") {
return node.branches.some((branch) => hasClientBehavior(branch.body));
}
return ( return (
node.attrs.some((attr) => attr.event || attr.name === "csrGet" || attr.name === "csrText") || node.attrs.some((attr) => attr.event || attr.name === "csrGet" || attr.name === "csrText") ||
hasClientBehavior(node.children) hasClientBehavior(node.children)
@@ -990,6 +996,20 @@ function viewHasEvents(nodes: ViewNode[]): boolean {
}); });
} }
function viewHasServerEach(nodes: ViewNode[]): boolean {
return nodes.some((node) => {
if (node.type === "text") return false;
if (node.type === "each") return true;
if (node.type === "if") {
return node.branches.some((branch) => viewHasServerEach(branch.body));
}
return viewHasServerEach(node.children);
});
}
/** /**
* Compile a text node. Interpolations that reference state stay as client * Compile a text node. Interpolations that reference state stay as client
* mustaches (`{expr}`, hydrated by the reactive runtime); interpolations of * mustaches (`{expr}`, hydrated by the reactive runtime); interpolations of
@@ -1096,6 +1116,18 @@ function renderComponentEachNode(node: EachNode, ctx: CompCtx): string {
); );
} }
function serverLoopLocalsAttribute(ctx: CompCtx): string {
const locals = [...(ctx.serverLocals ?? [])];
if (locals.length === 0) {
return "";
}
const entries = locals.map((name) => `${JSON.stringify(name)}: ${name}`).join(", ");
return ` data-wrn-loop-locals="\${__wrnexusEncodeLoopLocals({ ${entries} })}"`;
}
/** Render a component view node into template-literal-ready source. */ /** Render a component view node into template-literal-ready source. */
function renderComponentNode(node: ViewNode, ctx: CompCtx): string { function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
if (node.type === "text") return compileText(node.value, ctx); if (node.type === "text") return compileText(node.value, ctx);
@@ -1112,6 +1144,16 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
return renderNestedComponentInvocation(node, ctx); return renderNestedComponentInvocation(node, ctx);
} }
const loopVariables = loopVarsOf(node);
const elementContext =
loopVariables.length > 0
? {
...ctx,
loopVars: new Set([...(ctx.loopVars ?? []), ...loopVariables]),
}
: ctx;
let bindIndex = 0; let bindIndex = 0;
const staticClasses: string[] = []; const staticClasses: string[] = [];
const conditionalClasses: Array<{ const conditionalClasses: Array<{
@@ -1143,9 +1185,22 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
return ` ${a.name}`; return ` ${a.name}`;
} }
const rendered = ` ${a.name}="${compileAttrValue(a.value, ctx)}"`; const rendered = ` ${a.name}="${compileAttrValue(a.value, elementContext)}"`;
if (!a.value.includes("{") || !exprRefsState(a.value, ctx.stateNames)) { const referencesState = exprRefsState(a.value, ctx.stateNames);
const referencesLoopVariable = elementContext.loopVars
? exprRefsState(a.value, elementContext.loopVars)
: false;
const referencesServerLocal = ctx.serverLocals
? exprRefsState(a.value, ctx.serverLocals)
: false;
if (
!a.value.includes("{") ||
(!referencesState && !referencesLoopVariable && !referencesServerLocal)
) {
return rendered; return rendered;
} }
@@ -1157,18 +1212,37 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
const initialConditionalClasses = conditionalClasses const initialConditionalClasses = conditionalClasses
.map(({ className, expression }) => { .map(({ className, expression }) => {
const referencesLoopVariable = elementContext.loopVars
? exprRefsState(expression, elementContext.loopVars)
: false;
// data-for variables do not exist during
// initial server rendering.
if (referencesLoopVariable) {
return "";
}
return `\${(${ctx.resolveExpr(expression)}) ? ${JSON.stringify(` ${className}`)} : ""}`; return `\${(${ctx.resolveExpr(expression)}) ? ${JSON.stringify(` ${className}`)} : ""}`;
}) })
.join(""); .join("");
const staticClassValue = staticClasses.join(" "); const staticClassValue = staticClasses.join(" ");
const classReferencesState = exprRefsState(staticClassValue, ctx.stateNames);
const classReferencesLoopVariable = elementContext.loopVars
? exprRefsState(staticClassValue, elementContext.loopVars)
: false;
const classReferencesServerLocal = ctx.serverLocals
? exprRefsState(staticClassValue, ctx.serverLocals)
: false;
const classHasReactiveExpression = const classHasReactiveExpression =
staticClassValue.includes("{") && exprRefsState(staticClassValue, ctx.stateNames); staticClassValue.includes("{") &&
(classReferencesState || classReferencesLoopVariable || classReferencesServerLocal);
const classAttribute = const classAttribute =
staticClasses.length > 0 || conditionalClasses.length > 0 staticClasses.length > 0 || conditionalClasses.length > 0
? ` class="${compileAttrValue(staticClassValue, ctx)}${initialConditionalClasses}"` ? ` class="${compileAttrValue(staticClassValue, elementContext)}${initialConditionalClasses}"`
: ""; : "";
const classReactiveBinding = classHasReactiveExpression const classReactiveBinding = classHasReactiveExpression
@@ -1176,9 +1250,6 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
: ""; : "";
const classBindings = conditionalClasses const classBindings = conditionalClasses
.filter(({ expression }) => {
return !ctx.serverLocals || !exprRefsState(expression, ctx.serverLocals);
})
.map(({ className, expression }, index) => { .map(({ className, expression }, index) => {
const marker = attrEscape(JSON.stringify([className, expression])); const marker = attrEscape(JSON.stringify([className, expression]));
@@ -1186,25 +1257,31 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
}) })
.join(""); .join("");
// A `data-for` element introduces loop variables for its subtree. const loopLocalsAttribute = serverLoopLocalsAttribute(ctx);
const loops = loopVarsOf(node);
const childCtx =
loops.length > 0 ? { ...ctx, loopVars: new Set([...(ctx.loopVars ?? []), ...loops]) } : ctx;
const allAttrs = const allAttrs =
`${classAttribute}` + `${classReactiveBinding}` + `${classBindings}` + `${attrs}`; `${loopLocalsAttribute}` +
`${classAttribute}` +
`${classReactiveBinding}` +
`${classBindings}` +
`${attrs}`;
if (VOID_ELEMENTS.has(node.tag.toLowerCase())) { if (VOID_ELEMENTS.has(node.tag.toLowerCase())) {
return `<${node.tag}${allAttrs}>`; return `<${node.tag}${allAttrs}>`;
} }
const inner = node.children.map((child) => renderComponentNode(child, childCtx)).join(""); const inner = node.children.map((child) => renderComponentNode(child, elementContext)).join("");
return `<${node.tag}${allAttrs}>${inner}</${node.tag}>`; return `<${node.tag}${allAttrs}>${inner}</${node.tag}>`;
} }
function generateComponent(ast: PageAst): string { function generateComponent(ast: PageAst): string {
const out: string[] = []; const out: string[] = [];
const hasServerEach = viewHasServerEach(ast.view);
if (hasServerEach) {
out.push(`import { Buffer } from "node:buffer";`);
}
const effectiveProps = const effectiveProps =
ast.kind === "layout" && !ast.props.some((prop) => prop.name === "content") ast.kind === "layout" && !ast.props.some((prop) => prop.name === "content")
@@ -1276,7 +1353,9 @@ function generateComponent(ast: PageAst): string {
const scopeLine = const scopeLine =
needsScope && scopeKeys.length > 0 needsScope && scopeKeys.length > 0
? ` const __scope = __wrnexusScopeDecl({ ${scopeKeys.map((k) => `${JSON.stringify(k)}: ${nameRefs.get(k)}`).join(", ")} });\n` ? ` const __scope = __wrnexusScopeDecl({ ${scopeKeys
.map((key) => `${JSON.stringify(key)}: ${nameRefs.get(key)}`)
.join(", ")} });\n`
: needsScope : needsScope
? ` const __scope = "";\n` ? ` const __scope = "";\n`
: ""; : "";
@@ -1391,22 +1470,65 @@ function __wireRaw(v: any): string {
return String(v == null ? "" : v); return String(v == null ? "" : v);
}`); }`);
if (needsScope) { if (hasServerEach) {
out.push(`function __wrnexusScopeDecl(obj: Record<string, any>): string { out.push(`function __wrnexusEncodeLoopLocals(value: Record<string, any>): string {
const lit = (v: any) => return Buffer.from(JSON.stringify(value), "utf8").toString("base64");
typeof v === "number" || typeof v === "boolean"
? String(v)
: "'" + String(v).replace(/\\\\/g, "\\\\\\\\").replace(/'/g, "\\\\'").replace(/\\n/g, "\\\\n") + "'";
return Object.keys(obj)
.map((k) => k + ": " + lit(obj[k]))
.join(", ")
.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}`); }`);
} }
if (needsScope) {
out.push(`function __wrnexusSerializeScopeValue(value: any): string {
if (value === undefined) {
return "undefined";
}
if (value === null) {
return "null";
}
if (typeof value === "number") {
return Number.isFinite(value)
? String(value)
: "null";
}
if (typeof value === "boolean") {
return value ? "true" : "false";
}
if (typeof value === "string") {
return JSON.stringify(value);
}
try {
const serialized = JSON.stringify(value);
return serialized === undefined
? "undefined"
: serialized;
} catch {
return "null";
}
}
function __wrnexusScopeDecl(obj: Record<string, any>): string {
return Object.keys(obj)
.map(
(key) =>
key +
": " +
__wrnexusSerializeScopeValue(
obj[key],
),
)
.join(", ")
.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
}`);
}
const serverFunctionSource = serverFunctions ? `${serverFunctions}\n` : ""; const serverFunctionSource = serverFunctions ? `${serverFunctions}\n` : "";
out.push( out.push(
+109 -1
View File
@@ -5,6 +5,7 @@ import { join } from "node:path";
import { pathToFileURL } from "node:url"; import { pathToFileURL } from "node:url";
import { generate, parse } from "../src/index.ts"; import { generate, parse } from "../src/index.ts";
import { compileWireFile } from "../src/index.ts"; import { compileWireFile } from "../src/index.ts";
import { mountHtml } from "@wrnexus/test";
let seq = 0; let seq = 0;
/** Compile a `.wrn` source and import the resulting module. */ /** Compile a `.wrn` source and import the resulting module. */
@@ -97,10 +98,18 @@ test("stateful component: state text baked into a reactive data-text span, prop
); );
const render = mod.render as (p: Record<string, string>) => string; const render = mod.render as (p: Record<string, string>) => string;
const out = render({ start: "10", label: "Score" }); const out = render({ start: "10", label: "Score" });
expect(out).toContain("data-scope=\"start: 10, label: 'Score', count: 10\""); expect(out).toContain('data-scope="start: 10, label: &quot;Score&quot;, count: 10"');
expect(out).toContain('data-on-click="count++"'); expect(out).toContain('data-on-click="count++"');
// label baked as static text; count baked as its initial value AND kept live. // label baked as static text; count baked as its initial value AND kept live.
expect(out).toContain('Score: <span data-text="count">10</span>'); expect(out).toContain('Score: <span data-text="count">10</span>');
expect(out).toContain('data-scope="');
expect(out).toContain("start: 10");
expect(out).toContain("label: &quot;Score&quot;");
expect(out).toContain("count: 10");
}); });
test("prop type coercion follows the default value's type", async () => { test("prop type coercion follows the default value's type", async () => {
@@ -838,3 +847,102 @@ component FAQAccordion {
expect(output).toContain("isOpen(index)"); expect(output).toContain("isOpen(index)");
}); });
test("data-for conditional classes do not evaluate loop variables during SSR", () => {
const output = generate(
parse(`
component Accordion {
props {
items = []
}
state openIndexes = []
functions {
function isOpen(index) {
return openIndexes.includes(index)
}
}
view {
<article
data-for="item, index in items"
class:open="isOpen(index)"
>
{item.title}
</article>
}
}
`),
);
expect(output).toContain('data-for="item, index in items"');
expect(output).toContain("data-wrn-class-0");
expect(output).not.toContain('${(isOpen(index)) ? " open" : ""}');
});
test("server-rendered each locals work in reactive handlers", () => {
const firstLocals = Buffer.from(
JSON.stringify({
item: {
question: "First",
},
index: 0,
}),
"utf8",
).toString("base64");
const secondLocals = Buffer.from(
JSON.stringify({
item: {
question: "Second",
},
index: 1,
}),
"utf8",
).toString("base64");
const dom = mountHtml(`
<div
data-scope="openIndex: -1"
>
<article
data-wrn-loop-locals="${firstLocals}"
>
<button
data-on-click="openIndex = index"
>
First
</button>
<span
data-wrn-class-0='["open","openIndex === index"]'
></span>
</article>
<article
data-wrn-loop-locals="${secondLocals}"
>
<button
data-on-click="openIndex = index"
>
Second
</button>
<span
data-wrn-class-0='["open","openIndex === index"]'
></span>
</article>
</div>
`);
const buttons: any = dom.querySelectorAll("button");
buttons[1].click();
const articles = dom.querySelectorAll("article");
expect(articles[0].querySelector("span")?.classList.contains("open")).toBe(false);
expect(articles[1].querySelector("span")?.classList.contains("open")).toBe(true);
});
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/core", "name": "@wrnexus/core",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/csr", "name": "@wrnexus/csr",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
File diff suppressed because it is too large Load Diff
+46
View File
@@ -1,6 +1,7 @@
import { test, expect, beforeEach } from "bun:test"; import { test, expect, beforeEach } from "bun:test";
import { Window } from "happy-dom"; import { Window } from "happy-dom";
import { REACTIVE_RUNTIME } from "../src/reactive-runtime.ts"; import { REACTIVE_RUNTIME } from "../src/reactive-runtime.ts";
import { mountHtml } from "@wrnexus/test";
// Fresh DOM per test, with the runtime's globals bound. // Fresh DOM per test, with the runtime's globals bound.
function mount(html: string): Window { function mount(html: string): Window {
@@ -164,3 +165,48 @@ test("independent signals in one scope update correctly (dependency tracking)",
click("bb"); click("bb");
expect([ta(), tb()]).toEqual(["2", "101"]); expect([ta(), tb()]).toEqual(["2", "101"]);
}); });
test("data-for preserves arrays of objects from encoded component scope", () => {
const items = [
{
question: "One",
answer: "First",
},
{
question: "Two",
answer: "Second",
},
{
question: "Three",
answer: "Third",
},
{
question: "Four",
answer: "Fourth",
},
];
const encoded = Buffer.from(
JSON.stringify({
items,
}),
"utf8",
).toString("base64");
const dom = mountHtml(`
<div data-wrn-scope="${encoded}">
<article data-for="item, index in items">
<span>{index + 1}</span>
<strong>{item.question}</strong>
</article>
</div>
`);
expect(dom.querySelectorAll("article")).toHaveLength(4);
expect(dom.querySelectorAll("strong").map((element) => element.textContent)).toEqual([
"One",
"Two",
"Three",
"Four",
]);
});
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/db", "name": "@wrnexus/db",
"version": "0.2.39", "version": "0.2.47",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/dev-server", "name": "@wrnexus/dev-server",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+36
View File
@@ -0,0 +1,36 @@
import { existsSync, rmSync } from "node:fs";
import { resolve } from "node:path";
export interface ResetDevCacheOptions {
rootDir?: string;
cacheDir?: string;
enabled?: boolean;
}
export function resetDevCache(options: ResetDevCacheOptions = {}): string {
const rootDir = resolve(options.rootDir ?? process.cwd());
const cacheDir = resolve(rootDir, options.cacheDir ?? ".wrnexus");
if (options.enabled === false) {
return cacheDir;
}
// Safety: never allow deleting the project root.
if (cacheDir === rootDir) {
throw new Error("Refusing to remove the project root as the WRNexus cache directory.");
}
if (existsSync(cacheDir)) {
rmSync(cacheDir, {
recursive: true,
force: true,
maxRetries: 3,
retryDelay: 100,
});
console.log(`[wrnexus] cleared generated cache: ${cacheDir}`);
}
return cacheDir;
}
+7
View File
@@ -29,6 +29,7 @@ import { createDevAssetServer } from "./assets.ts";
import { HmrHub } from "./hmr.ts"; import { HmrHub } from "./hmr.ts";
import { startWatcher } from "./watch.ts"; import { startWatcher } from "./watch.ts";
export { RESTART_EXIT_CODE } from "./restart.ts"; export { RESTART_EXIT_CODE } from "./restart.ts";
import { resetDevCache } from "./cache.ts";
export interface ServeOptions { export interface ServeOptions {
appDir: string; appDir: string;
@@ -122,6 +123,12 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
const router = buildRouter(appDir, { componentDirs: [uiComponentsDir()] }); const router = buildRouter(appDir, { componentDirs: [uiComponentsDir()] });
const styleEntry = opts.styleEntry ?? null; const styleEntry = opts.styleEntry ?? null;
const appRoot = dirname(appDir); const appRoot = dirname(appDir);
resetDevCache({
rootDir: appRoot,
enabled: process.env.WRNEXUS_PRESERVE_CACHE !== "1",
});
// Compile every `.wrn` into ONE cache dir at the project root, instead of a // Compile every `.wrn` into ONE cache dir at the project root, instead of a
// `.wrnexus/` next to each source file (and inside node_modules UI dirs). // `.wrnexus/` next to each source file (and inside node_modules UI dirs).
setCompileCacheDir(join(appRoot, ".wrnexus")); setCompileCacheDir(join(appRoot, ".wrnexus"));
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/encryption", "name": "@wrnexus/encryption",
"version": "0.2.39", "version": "0.2.47",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/helpers", "name": "@wrnexus/helpers",
"version": "0.2.39", "version": "0.2.47",
"private": true, "private": true,
"type": "module", "type": "module",
"description": "Safe convenience helpers for WrNexus request contexts and common application flows.", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/i18n", "name": "@wrnexus/i18n",
"version": "0.2.39", "version": "0.2.47",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/jwt", "name": "@wrnexus/jwt",
"version": "0.2.39", "version": "0.2.47",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/mobile", "name": "@wrnexus/mobile",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/native", "name": "@wrnexus/native",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/oauth", "name": "@wrnexus/oauth",
"version": "0.2.39", "version": "0.2.47",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/pubsub", "name": "@wrnexus/pubsub",
"version": "0.2.39", "version": "0.2.47",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/queue", "name": "@wrnexus/queue",
"version": "0.2.39", "version": "0.2.47",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/reactive", "name": "@wrnexus/reactive",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/router", "name": "@wrnexus/router",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ssr", "name": "@wrnexus/ssr",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/styles", "name": "@wrnexus/styles",
"version": "0.2.39", "version": "0.2.47",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/test", "name": "@wrnexus/test",
"version": "0.2.39", "version": "0.2.47",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/tracking", "name": "@wrnexus/tracking",
"version": "0.2.39", "version": "0.2.47",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ui", "name": "@wrnexus/ui",
"version": "0.2.39", "version": "0.2.47",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/uploader", "name": "@wrnexus/uploader",
"version": "0.2.39", "version": "0.2.47",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/validation", "name": "@wrnexus/validation",
"version": "0.2.39", "version": "0.2.47",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
+1 -1
View File
@@ -1,7 +1,7 @@
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
const version = "0.2.39"; const version = "0.2.47";
const dependencyGroups = [ const dependencyGroups = [
"dependencies", "dependencies",