Compare commits

...
10 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
Clintchiz 29bbeefe92 release: WRNexusJS 0.2.39 2026-07-15 13:22:20 +05:30
65 changed files with 2549 additions and 307 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ai", "name": "@wrnexus/ai",
"version": "0.2.38", "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.38", "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.38", "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.38", "@wrnexus/core": "^0.2.47",
"@wrnexus/router": "^0.2.38", "@wrnexus/router": "^0.2.47",
"@wrnexus/csr": "^0.2.38", "@wrnexus/csr": "^0.2.47",
"@wrnexus/compiler": "^0.2.38", "@wrnexus/compiler": "^0.2.47",
"@wrnexus/styles": "^0.2.38", "@wrnexus/styles": "^0.2.47",
"@wrnexus/dev-server": "^0.2.38", "@wrnexus/dev-server": "^0.2.47",
"@wrnexus/ui": "^0.2.38", "@wrnexus/ui": "^0.2.47",
"@wrnexus/validation": "^0.2.38", "@wrnexus/validation": "^0.2.47",
"@wrnexus/i18n": "^0.2.38", "@wrnexus/i18n": "^0.2.47",
"@wrnexus/db": "^0.2.38" "@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.38", "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.38", "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.38", "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.38" "@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.38", "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.38", "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.38", "@wrnexus/core": "^0.2.47",
"@wrnexus/router": "^0.2.38", "@wrnexus/router": "^0.2.47",
"@wrnexus/ssr": "^0.2.38", "@wrnexus/ssr": "^0.2.47",
"@wrnexus/csr": "^0.2.38", "@wrnexus/csr": "^0.2.47",
"@wrnexus/compiler": "^0.2.38", "@wrnexus/compiler": "^0.2.47",
"@wrnexus/styles": "^0.2.38", "@wrnexus/styles": "^0.2.47",
"@wrnexus/ui": "^0.2.38", "@wrnexus/ui": "^0.2.47",
"@wrnexus/validation": "^0.2.38", "@wrnexus/validation": "^0.2.47",
"@wrnexus/i18n": "^0.2.38", "@wrnexus/i18n": "^0.2.47",
"@wrnexus/db": "^0.2.38", "@wrnexus/db": "^0.2.47",
"@wrnexus/pubsub": "^0.2.38", "@wrnexus/pubsub": "^0.2.47",
"@wrnexus/uploader": "^0.2.38" "@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.38", "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.38", "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.38" "@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.38", "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.38" "@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.38", "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.38", "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.38" "@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.38", "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.38", "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.38", "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.38", "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.38", "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.38", "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.38", "@wrnexus/compiler": "^0.2.47",
"@wrnexus/core": "^0.2.38" "@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.38", "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.38" "@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.38", "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.38" "@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.38", "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.38", "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.38", "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.38" "@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.38", "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.38" "@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.38", "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
@@ -2,7 +2,7 @@
"name": "wrnexus", "name": "wrnexus",
"displayName": "WRNexus Language Support", "displayName": "WRNexus Language Support",
"description": "Complete language support for WRNexus .wrn files, including highlighting, formatting, diagnostics, snippets, lifecycle hooks, state watchers, component functions, completions, and definition navigation.", "description": "Complete language support for WRNexus .wrn files, including highlighting, formatting, diagnostics, snippets, lifecycle hooks, state watchers, component functions, completions, and definition navigation.",
"version": "0.2.9", "version": "0.2.10",
"publisher": "wrnexus", "publisher": "wrnexus",
"private": true, "private": true,
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
+183 -23
View File
@@ -493,12 +493,6 @@ function parseHtmlView(src, pos) {
let i = pos; let i = pos;
const isNameStart = (c) => /[A-Za-z_]/.test(c); const isNameStart = (c) => /[A-Za-z_]/.test(c);
const isTagNamePart = (c) => /[A-Za-z0-9_$:.-]/.test(c); const isTagNamePart = (c) => /[A-Za-z0-9_$:.-]/.test(c);
const isAttributeNamePart = (c, next) => {
if (c === "/") {
return next !== ">";
}
return /[A-Za-z0-9_$:.[\]%-]/.test(c);
};
const isWs2 = (c) => c === " " || c === "\t" || c === ` const isWs2 = (c) => c === " " || c === "\t" || c === `
` || c === "\r"; ` || c === "\r";
const fail = (msg) => { const fail = (msg) => {
@@ -738,6 +732,7 @@ function parseHtmlView(src, pos) {
} }
// ../../packages/compiler/src/codegen.ts // ../../packages/compiler/src/codegen.ts
var import_node_buffer = require("node:buffer");
function isComponentTag(tag) { function isComponentTag(tag) {
return /^[A-Z][A-Za-z0-9_$]*$/.test(tag); return /^[A-Z][A-Za-z0-9_$]*$/.test(tag);
} }
@@ -976,9 +971,9 @@ function renderNode(node, ssrBindings, csrBindings, apiBindings, loops, reactive
return `<${node.tag}${renderAttrs(node.attrs, csrId, reactive)}>${inner}</${node.tag}>`; return `<${node.tag}${renderAttrs(node.attrs, csrId, reactive)}>${inner}</${node.tag}>`;
} }
function renderPageComponentInvocation(node, ssrBindings, csrBindings, apiBindings, loops, reactive) { function renderPageComponentInvocation(node, ssrBindings, csrBindings, apiBindings, loops, reactive) {
const attrs = node.attrs.filter((attr) => attr.name !== "data-component"); const attrs = node.attrs.filter((attr) => attr.name !== "data-component").map((attr) => renderPageComponentAttr(attr, loops)).join("");
const inner = node.children.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive)).join(""); const inner = node.children.map((child) => renderNode(child, ssrBindings, csrBindings, apiBindings, loops, reactive)).join("");
return `<div data-component="${attrEscape(node.tag)}"${renderAttrs(attrs, undefined, reactive)}>${inner}</div>`; return `<div data-component="${attrEscape(node.tag)}"` + `${attrs}>${inner}</div>`;
} }
function renderNestedComponentInvocation(node, ctx) { function renderNestedComponentInvocation(node, ctx) {
let bindIndex = 0; let bindIndex = 0;
@@ -989,8 +984,10 @@ function renderNestedComponentInvocation(node, ctx) {
if (attr.boolean) { if (attr.boolean) {
return ` ${attr.name}`; return ` ${attr.name}`;
} }
const rendered = ` ${attr.name}="` + `${compileAttrValue(attr.value, ctx)}"`; const wholeExpression = wholeAttributeExpression(attr.value);
if (!attr.value.includes("{") || !exprRefsState(attr.value, ctx.stateNames)) { const compiledValue = wholeExpression ? `\${__wireProp(${ctx.resolveExpr(wholeExpression)})}` : compileAttrValue(attr.value, ctx);
const rendered = ` ${attr.name}="${compiledValue}"`;
if (wholeExpression || !attr.value.includes("{") || !exprRefsState(attr.value, ctx.stateNames)) {
return rendered; return rendered;
} }
const marker = attrEscape(JSON.stringify([attr.name, attr.value])); const marker = attrEscape(JSON.stringify([attr.name, attr.value]));
@@ -1091,6 +1088,28 @@ function __wrnexusEvalData(data: unknown, body: string, helpers = "", ctx: any):
return new Function("$data", "$adapters", "const cookies = $adapters.cookies;\\nconst session = $adapters.session;\\nconst localStorage = $adapters.localStorage;\\nwith ($data ?? {}) {\\n" + helpers + "\\n" + body + "\\n}")(data, adapters); return new Function("$data", "$adapters", "const cookies = $adapters.cookies;\\nconst session = $adapters.session;\\nconst localStorage = $adapters.localStorage;\\nwith ($data ?? {}) {\\n" + helpers + "\\n" + body + "\\n}")(data, adapters);
} }
function __wrnexusPropAttr(
value: unknown,
): string {
const serialized =
value !== null &&
typeof value === "object"
? JSON.stringify(value)
: String(value == null ? "" : value);
return serialized.replace(
/[&<>"]/g,
(character) =>
character === "&"
? "&amp;"
: character === "<"
? "&lt;"
: character === ">"
? "&gt;"
: "&quot;",
);
}
async function __wrnexusCallApi(path: string, method: string, ctx: any): Promise<unknown> { async function __wrnexusCallApi(path: string, method: string, ctx: any): Promise<unknown> {
if (typeof ctx.__wrnexusCallApi === "function") { if (typeof ctx.__wrnexusCallApi === "function") {
return await ctx.__wrnexusCallApi(path, method); return await ctx.__wrnexusCallApi(path, method);
@@ -1348,7 +1367,7 @@ function behaviorAttribute(behavior) {
if (!behavior) { if (!behavior) {
return ""; return "";
} }
const encoded = Buffer.from(JSON.stringify(behavior), "utf8").toString("base64"); const encoded = import_node_buffer.Buffer.from(JSON.stringify(behavior), "utf8").toString("base64");
return ` data-wrn-behavior="${encoded}"`; return ` data-wrn-behavior="${encoded}"`;
} }
var INTERP_RE = /\{([^{}]+)\}/g; var INTERP_RE = /\{([^{}]+)\}/g;
@@ -1360,7 +1379,17 @@ function exprRefsState(expr, stateNames) {
return false; return false;
} }
function viewHasEvents(nodes) { function viewHasEvents(nodes) {
return nodes.some((n) => n.type === "element" && (n.attrs.some((a) => a.event) || viewHasEvents(n.children))); return nodes.some((node) => {
if (node.type === "text")
return false;
if (node.type === "each") {
return viewHasEvents(node.body) || viewHasEvents(node.empty);
}
if (node.type === "if") {
return node.branches.some((branch) => viewHasEvents(branch.body));
}
return node.attrs.some((attr) => attr.event) || viewHasEvents(node.children);
});
} }
function compileText(raw, ctx) { function compileText(raw, ctx) {
let out = ""; let out = "";
@@ -1404,11 +1433,36 @@ function compileAttrValue(raw, ctx) {
} }
return out + escLit(attrEscape(raw.slice(last))); return out + escLit(attrEscape(raw.slice(last)));
} }
function renderComponentIfNode(node, ctx) {
let expression = "``";
for (let index = node.branches.length - 1;index >= 0; index--) {
const branch = node.branches[index];
const body = branch.body.map((child) => renderComponentNode(child, ctx)).join("");
const bodyExpression = "`" + body + "`";
expression = branch.cond === null ? bodyExpression : `(${ctx.resolveExpr(branch.cond)}) ? ${bodyExpression} : ${expression}`;
}
return "${" + expression + "}";
}
function renderComponentEachNode(node, ctx) {
const item = node.item;
const index = node.index ?? "__wi";
const list = ctx.resolveExpr(node.list);
const childCtx = {
...ctx,
serverLocals: new Set([...ctx.serverLocals ?? [], item, index])
};
const body = node.body.map((child) => renderComponentNode(child, childCtx)).join("");
const empty = node.empty.map((child) => renderComponentNode(child, ctx)).join("");
return "${(() => { const __wl = Array.isArray(" + list + ") ? (" + list + ") : []; return __wl.length ? __wl.map((" + item + ", " + index + ") => `" + body + '`).join("") : `' + empty + "`; })()}";
}
function renderComponentNode(node, ctx) { function renderComponentNode(node, ctx) {
if (node.type === "text") if (node.type === "text")
return compileText(node.value, ctx); return compileText(node.value, ctx);
if (node.type === "each" || node.type === "if") { if (node.type === "each") {
throw new Error("Server `{#each}` / `{#if}` blocks are supported in pages, not components. Move them into a page (or use data-for / data-show on the client)."); return renderComponentEachNode(node, ctx);
}
if (node.type === "if") {
return renderComponentIfNode(node, ctx);
} }
if (isComponentTag(node.tag)) { if (isComponentTag(node.tag)) {
return renderNestedComponentInvocation(node, ctx); return renderNestedComponentInvocation(node, ctx);
@@ -1448,7 +1502,9 @@ function renderComponentNode(node, ctx) {
const classHasReactiveExpression = staticClassValue.includes("{") && exprRefsState(staticClassValue, ctx.stateNames); const classHasReactiveExpression = staticClassValue.includes("{") && exprRefsState(staticClassValue, ctx.stateNames);
const classAttribute = staticClasses.length > 0 || conditionalClasses.length > 0 ? ` class="${compileAttrValue(staticClassValue, ctx)}${initialConditionalClasses}"` : ""; const classAttribute = staticClasses.length > 0 || conditionalClasses.length > 0 ? ` class="${compileAttrValue(staticClassValue, ctx)}${initialConditionalClasses}"` : "";
const classReactiveBinding = classHasReactiveExpression ? ` data-wrn-bind-class="${escLit(attrEscape(JSON.stringify(["class", staticClassValue])))}"` : ""; const classReactiveBinding = classHasReactiveExpression ? ` data-wrn-bind-class="${escLit(attrEscape(JSON.stringify(["class", staticClassValue])))}"` : "";
const classBindings = conditionalClasses.map(({ className, expression }, index) => { const classBindings = conditionalClasses.filter(({ expression }) => {
return !ctx.serverLocals || !exprRefsState(expression, ctx.serverLocals);
}).map(({ className, expression }, index) => {
const marker = attrEscape(JSON.stringify([className, expression])); const marker = attrEscape(JSON.stringify([className, expression]));
return ` data-wrn-class-${index}="${escLit(marker)}"`; return ` data-wrn-class-${index}="${escLit(marker)}"`;
}).join(""); }).join("");
@@ -1519,18 +1575,103 @@ ${styles.map(styleEscape).join(`
out.push(`export const __wrnexusBehavior = ${JSON.stringify(behavior, null, 2)};`); out.push(`export const __wrnexusBehavior = ${JSON.stringify(behavior, null, 2)};`);
} }
out.push(`function __coerce(v: any, def: any): any { out.push(`function __coerce(v: any, def: any): any {
if (v === undefined || v === null) return def; if (v === undefined || v === null) {
if (typeof def === "number") return Number(v); return def;
if (typeof def === "boolean") return v === true || v === "" || v === "true"; }
if (typeof def === "number") {
return Number(v);
}
if (typeof def === "boolean") {
return v === true || v === "" || v === "true";
}
if (Array.isArray(def)) {
if (Array.isArray(v)) {
return v;
}
if (typeof v === "string") {
try {
const parsed = JSON.parse(v);
return Array.isArray(parsed) ? parsed : def;
} catch {
return def;
}
}
return def;
}
if (def !== null && typeof def === "object") {
if (
v !== null &&
typeof v === "object" &&
!Array.isArray(v)
) {
return v;
}
if (typeof v === "string") {
try {
const parsed = JSON.parse(v);
return (
parsed !== null &&
typeof parsed === "object" &&
!Array.isArray(parsed)
)
? parsed
: def;
} catch {
return def;
}
}
return def;
}
return String(v); return String(v);
} }
function __wireHtml(v: any): string { function __wireHtml(v: any): string {
return String(v == null ? "" : v).replace(/[&<>]/g, (c) => (c === "&" ? "&amp;" : c === "<" ? "&lt;" : "&gt;")); return String(v == null ? "" : v).replace(
} /[&<>]/g,
function __wireAttr(v: any): string { (c) =>
return String(v == null ? "" : v).replace(/[&<>"]/g, (c) => c === "&"
c === "&" ? "&amp;" : c === "<" ? "&lt;" : c === ">" ? "&gt;" : "&quot;", ? "&amp;"
: c === "<"
? "&lt;"
: "&gt;",
); );
}
function __wireAttr(v: any): string {
return String(v == null ? "" : v).replace(
/[&<>"]/g,
(c) =>
c === "&"
? "&amp;"
: c === "<"
? "&lt;"
: c === ">"
? "&gt;"
: "&quot;",
);
}
function __wireProp(v: any): string {
const value =
v !== null && typeof v === "object"
? JSON.stringify(v)
: String(v == null ? "" : v);
return __wireAttr(value);
}
function __wireRaw(v: any): string {
return String(v == null ? "" : v);
}`); }`);
if (needsScope) { if (needsScope) {
out.push(`function __wrnexusScopeDecl(obj: Record<string, any>): string { out.push(`function __wrnexusScopeDecl(obj: Record<string, any>): string {
@@ -1558,6 +1699,25 @@ function __wireAttr(v: any): string {
`) + ` `) + `
`; `;
} }
function wholeAttributeExpression(value) {
const match = /^\s*\{([\s\S]+)\}\s*$/.exec(value);
return match?.[1]?.trim() || null;
}
function renderPageComponentAttr(attr, dynamicExpressions) {
if (attr.event) {
return ` ${eventAttribute(attr.name)}="${attrEscape(attr.value)}"`;
}
if (attr.boolean) {
return ` ${attr.name}`;
}
const expression = wholeAttributeExpression(attr.value);
if (!expression) {
return ` ${attr.name}="${attrEscape(attr.value)}"`;
}
dynamicExpressions.push(`\${__wrnexusPropAttr(${expression})}`);
const marker = `\x00WRNEACH${dynamicExpressions.length - 1}\x00`;
return ` ${attr.name}="${marker}"`;
}
// ../../packages/compiler/src/native-codegen.ts // ../../packages/compiler/src/native-codegen.ts
class NativeCompileError extends Error { class NativeCompileError extends Error {
+139 -22
View File
@@ -40,41 +40,60 @@ const COMPILER_DIAGNOSTIC_COLLECTION = "wrnexus-compiler";
* @param {vscode.ExtensionContext} context * @param {vscode.ExtensionContext} context
*/ */
function registerFormatter(context) { function registerFormatter(context) {
const provider = vscode.languages.registerDocumentFormattingEditProvider( const selector = {
{ language: WRN_LANGUAGE_ID,
language: WRN_LANGUAGE_ID, };
scheme: "file",
},
{
provideDocumentFormattingEdits(document, options) {
const configuration = vscode.workspace.getConfiguration("wrnexus", document.uri);
const enabled = configuration.get("format.enable", true); const provider = vscode.languages.registerDocumentFormattingEditProvider(selector, {
provideDocumentFormattingEdits(document, options, token) {
if (token.isCancellationRequested) {
return [];
}
if (!enabled) { const configuration = vscode.workspace.getConfiguration("wrnexus", document.uri);
return [];
}
const source = document.getText(); const enabled = configuration.get("format.enable", true);
const printWidth = configuration.get("formatting.printWidth", 100); if (!enabled) {
return [];
}
const source = document.getText();
if (!source.trim()) {
return [];
}
const printWidth = configuration.get("formatting.printWidth", 100);
try {
const formatted = formatWrn(source, { const formatted = formatWrn(source, {
tabSize: options.tabSize, tabSize: options.tabSize || 2,
insertSpaces: options.insertSpaces, insertSpaces: options.insertSpaces !== false,
printWidth, printWidth,
}); });
if (formatted === source) { if (typeof formatted !== "string" || formatted === source) {
return []; return [];
} }
const range = new vscode.Range(document.positionAt(0), document.positionAt(source.length)); const fullRange = new vscode.Range(
document.positionAt(0),
document.positionAt(source.length),
);
return [vscode.TextEdit.replace(range, formatted)]; return [vscode.TextEdit.replace(fullRange, formatted)];
}, } catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error("[wrnexus] formatting failed:", message);
vscode.window.showErrorMessage(`WRNexus formatting failed: ${message}`);
return [];
}
}, },
); });
context.subscriptions.push(provider); context.subscriptions.push(provider);
} }
@@ -259,6 +278,103 @@ function registerCompilerDiagnostics(context) {
); );
} }
/**
* Register semantic highlighting for WRN state declarations and references.
*
* @param {vscode.ExtensionContext} context
*/
function registerSemanticTokens(context) {
const semanticTokenLegend = new vscode.SemanticTokensLegend(
["variable"],
["declaration", "modification"],
);
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const collectStateVariables = (text) => {
const states = new Map();
const pattern = /\bstate\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*(?==|;|\r?$)/gm;
let match;
while ((match = pattern.exec(text)) !== null) {
const name = match[1];
if (!name) {
continue;
}
const offset = match.index + match[0].lastIndexOf(name);
const declarations = states.get(name) ?? new Set();
declarations.add(offset);
states.set(name, declarations);
}
return states;
};
const isModification = (text, offset, length) => {
const after = text.slice(offset + length).match(/^\s*(=|\+=|-=|\*=|\/=|%=|\+\+|--)/);
if (after) {
return true;
}
const before = text.slice(Math.max(0, offset - 8), offset).match(/(\+\+|--)\s*$/);
return Boolean(before);
};
const provider = {
provideDocumentSemanticTokens(document, token) {
const builder = new vscode.SemanticTokensBuilder(semanticTokenLegend);
const text = document.getText();
const states = collectStateVariables(text);
for (const [name, declarationOffsets] of states) {
if (token.isCancellationRequested) {
return builder.build();
}
const pattern = new RegExp(`(?<![A-Za-z0-9_$])${escapeRegExp(name)}(?![A-Za-z0-9_$])`, "g");
let match;
while ((match = pattern.exec(text)) !== null) {
const offset = match.index;
const position = document.positionAt(offset);
let modifiers = [];
if (declarationOffsets.has(offset)) {
modifiers = ["declaration"];
} else if (isModification(text, offset, name.length)) {
modifiers = ["modification"];
}
builder.push(position.line, position.character, name.length, "variable", modifiers);
}
}
return builder.build();
},
};
const registration = vscode.languages.registerDocumentSemanticTokensProvider(
{
language: WRN_LANGUAGE_ID,
},
provider,
semanticTokenLegend,
);
context.subscriptions.push(registration);
}
/** /**
* Activate the WRNexus VS Code extension. * Activate the WRNexus VS Code extension.
* *
@@ -267,10 +383,10 @@ function registerCompilerDiagnostics(context) {
function activate(context) { function activate(context) {
registerDiagnostics(context); registerDiagnostics(context);
registerCompilerDiagnostics(context); registerCompilerDiagnostics(context);
registerCompletionProvider(context); registerCompletionProvider(context);
registerDefinitionProvider(context); registerDefinitionProvider(context);
registerFormatter(context); registerFormatter(context);
registerSemanticTokens(context);
context.subscriptions.push( context.subscriptions.push(
vscode.languages.registerDocumentSemanticTokensProvider( vscode.languages.registerDocumentSemanticTokensProvider(
@@ -497,5 +613,6 @@ module.exports = {
deactivate, deactivate,
registerCompilerDiagnostics, registerCompilerDiagnostics,
registerFormatter, registerFormatter,
registerSemanticTokens,
toCompilerDiagnostic, toCompilerDiagnostic,
}; };
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@wrnexus/ai", "name": "@wrnexus/ai",
"version": "0.2.38", "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.38", "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.38", "version": "0.2.47",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+81
View File
@@ -420,6 +420,87 @@ const MIGRATIONS: Migration[] = [
// No generated application files require automatic migration. // No generated application files require automatic migration.
}, },
}, },
{
version: "0.2.39",
id: "server-function-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.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.38", "version": "0.2.47",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"exports": { "exports": {
+159 -29
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")
@@ -1232,6 +1309,11 @@ function generateComponent(ast: PageAst): string {
}; };
const ctx: CompCtx = { stateNames, resolveExpr }; const ctx: CompCtx = { stateNames, resolveExpr };
const serverFunctions = ast.functions
.map((body) => body.trim())
.filter(Boolean)
.join("\n\n");
const viewCode = ast.view.map((node) => renderComponentNode(node, ctx)).join(""); const viewCode = ast.view.map((node) => renderComponentNode(node, ctx)).join("");
const styles = ast.styles.map((body) => body.trim()).filter(Boolean); const styles = ast.styles.map((body) => body.trim()).filter(Boolean);
const styleTag = const styleTag =
@@ -1262,7 +1344,7 @@ function generateComponent(ast: PageAst): string {
); );
} }
for (const state of ast.states) { for (const state of ast.states) {
decls.push(` const ${nameRefs.get(state.name)} = (${resolveExpr(state.expr)});`); decls.push(` let ${nameRefs.get(state.name)} = (${resolveExpr(state.expr)});`);
} }
const returnExpr = needsScope const returnExpr = needsScope
@@ -1271,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`
: ""; : "";
@@ -1386,26 +1470,72 @@ 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` : "";
out.push( out.push(
`export function render(props: Record<string, any> = {}): string {\n` + `export function render(props: Record<string, any> = {}): 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 +
scopeLine + scopeLine +
` return ${returnExpr};\n` + ` return ${returnExpr};\n` +
`}`, `}`,
+164 -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 () => {
@@ -783,3 +792,157 @@ page Home {
expect(output).toContain("__wrnexusPropAttr(["); expect(output).toContain("__wrnexusPropAttr([");
}); });
test("component functions are available during server rendering", () => {
const output = generate(
parse(`
component FAQAccordion {
props {
allowMultiple = false
items = []
}
state openIndexes = []
functions {
function isOpen(index) {
return openIndexes.includes(index)
}
function toggleItem(index) {
if (isOpen(index)) {
openIndexes = openIndexes.filter(
(itemIndex) =>
itemIndex !== index
)
} else if (allowMultiple) {
openIndexes = [
...openIndexes,
index
]
} else {
openIndexes = [index]
}
}
}
view {
{#each items as item, index}
<button
class:hidden="!isOpen(index)"
@click="toggleItem(index)"
>
{item.title}
</button>
{/each}
}
}
`),
);
expect(output).toContain("function isOpen(index)");
expect(output).toContain("function toggleItem(index)");
expect(output).toContain("let openIndexes =");
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.38", "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.38", "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.38", "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.38", "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.38", "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.38", "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.38", "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.38", "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.38", "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.38", "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.38", "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.38", "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.38", "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.38", "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.38", "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.38", "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.38", "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.38", "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.38", "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.38", "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.38", "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.38", "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.38"; const version = "0.2.47";
const dependencyGroups = [ const dependencyGroups = [
"dependencies", "dependencies",