diff --git a/.publish/ai/package.json b/.publish/ai/package.json index 61869a09..a799255c 100644 --- a/.publish/ai/package.json +++ b/.publish/ai/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ai", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", "license": "MIT", diff --git a/.publish/authz/package.json b/.publish/authz/package.json index 4b6da46d..2f4e483f 100644 --- a/.publish/authz/package.json +++ b/.publish/authz/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/authz", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "description": "@wrnexus/authz — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/cli/package.json b/.publish/cli/package.json index f56a92d6..f8fff2fb 100644 --- a/.publish/cli/package.json +++ b/.publish/cli/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/cli", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "description": "@wrnexus/cli — part of the WrNexus framework.", "license": "MIT", @@ -28,16 +28,16 @@ "wrnexus": "./dist/index.js" }, "dependencies": { - "@wrnexus/core": "^0.2.46", - "@wrnexus/router": "^0.2.46", - "@wrnexus/csr": "^0.2.46", - "@wrnexus/compiler": "^0.2.46", - "@wrnexus/styles": "^0.2.46", - "@wrnexus/dev-server": "^0.2.46", - "@wrnexus/ui": "^0.2.46", - "@wrnexus/validation": "^0.2.46", - "@wrnexus/i18n": "^0.2.46", - "@wrnexus/db": "^0.2.46" + "@wrnexus/core": "^0.2.47", + "@wrnexus/router": "^0.2.47", + "@wrnexus/csr": "^0.2.47", + "@wrnexus/compiler": "^0.2.47", + "@wrnexus/styles": "^0.2.47", + "@wrnexus/dev-server": "^0.2.47", + "@wrnexus/ui": "^0.2.47", + "@wrnexus/validation": "^0.2.47", + "@wrnexus/i18n": "^0.2.47", + "@wrnexus/db": "^0.2.47" }, "files": [ "dist" diff --git a/.publish/compiler/package.json b/.publish/compiler/package.json index 218bc9c9..d27caa1d 100644 --- a/.publish/compiler/package.json +++ b/.publish/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/compiler", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "description": "@wrnexus/compiler — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/core/package.json b/.publish/core/package.json index 34b64c65..96e4caa8 100644 --- a/.publish/core/package.json +++ b/.publish/core/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/core", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "description": "@wrnexus/core — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/csr/package.json b/.publish/csr/package.json index 7b777dca..57825455 100644 --- a/.publish/csr/package.json +++ b/.publish/csr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/csr", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "description": "@wrnexus/csr — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.46" + "@wrnexus/core": "^0.2.47" }, "files": [ "dist" diff --git a/.publish/db/package.json b/.publish/db/package.json index 9dd076ab..9c50a59f 100644 --- a/.publish/db/package.json +++ b/.publish/db/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/db", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "description": "@wrnexus/db — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/dev-server/package.json b/.publish/dev-server/package.json index 971b6e65..c604096d 100644 --- a/.publish/dev-server/package.json +++ b/.publish/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-server", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "description": "@wrnexus/dev-server — part of the WrNexus framework.", "license": "MIT", @@ -25,18 +25,18 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.46", - "@wrnexus/router": "^0.2.46", - "@wrnexus/ssr": "^0.2.46", - "@wrnexus/csr": "^0.2.46", - "@wrnexus/compiler": "^0.2.46", - "@wrnexus/styles": "^0.2.46", - "@wrnexus/ui": "^0.2.46", - "@wrnexus/validation": "^0.2.46", - "@wrnexus/i18n": "^0.2.46", - "@wrnexus/db": "^0.2.46", - "@wrnexus/pubsub": "^0.2.46", - "@wrnexus/uploader": "^0.2.46" + "@wrnexus/core": "^0.2.47", + "@wrnexus/router": "^0.2.47", + "@wrnexus/ssr": "^0.2.47", + "@wrnexus/csr": "^0.2.47", + "@wrnexus/compiler": "^0.2.47", + "@wrnexus/styles": "^0.2.47", + "@wrnexus/ui": "^0.2.47", + "@wrnexus/validation": "^0.2.47", + "@wrnexus/i18n": "^0.2.47", + "@wrnexus/db": "^0.2.47", + "@wrnexus/pubsub": "^0.2.47", + "@wrnexus/uploader": "^0.2.47" }, "files": [ "dist" diff --git a/.publish/encryption/package.json b/.publish/encryption/package.json index 623256ec..9a68502d 100644 --- a/.publish/encryption/package.json +++ b/.publish/encryption/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/encryption", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "description": "@wrnexus/encryption — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/helpers/package.json b/.publish/helpers/package.json index ec698b92..38886de4 100644 --- a/.publish/helpers/package.json +++ b/.publish/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/helpers", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.46" + "@wrnexus/core": "^0.2.47" }, "files": [ "dist" diff --git a/.publish/i18n/package.json b/.publish/i18n/package.json index baeab7df..f86c37af 100644 --- a/.publish/i18n/package.json +++ b/.publish/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/i18n", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "description": "@wrnexus/i18n — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.46" + "@wrnexus/core": "^0.2.47" }, "files": [ "dist" diff --git a/.publish/jwt/package.json b/.publish/jwt/package.json index 8bfb7bd0..4fd6c90f 100644 --- a/.publish/jwt/package.json +++ b/.publish/jwt/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/jwt", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "description": "@wrnexus/jwt — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/mobile/package.json b/.publish/mobile/package.json index acb52552..b2a51e33 100644 --- a/.publish/mobile/package.json +++ b/.publish/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/mobile", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "description": "@wrnexus/mobile — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/native": "^0.2.46" + "@wrnexus/native": "^0.2.47" }, "files": [ "dist" diff --git a/.publish/native/package.json b/.publish/native/package.json index fe43e3f9..bd1224e1 100644 --- a/.publish/native/package.json +++ b/.publish/native/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/native", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "description": "@wrnexus/native — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/oauth/package.json b/.publish/oauth/package.json index 8e3f9138..ae66476b 100644 --- a/.publish/oauth/package.json +++ b/.publish/oauth/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/oauth", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "description": "@wrnexus/oauth — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/pubsub/package.json b/.publish/pubsub/package.json index be2e2da7..647b129c 100644 --- a/.publish/pubsub/package.json +++ b/.publish/pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/pubsub", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "description": "@wrnexus/pubsub — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/queue/package.json b/.publish/queue/package.json index 2be83493..7983a34c 100644 --- a/.publish/queue/package.json +++ b/.publish/queue/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/queue", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "description": "@wrnexus/queue — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/reactive/package.json b/.publish/reactive/package.json index a94ea833..1f438b5b 100644 --- a/.publish/reactive/package.json +++ b/.publish/reactive/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/reactive", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "description": "@wrnexus/reactive — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/router/package.json b/.publish/router/package.json index 18c9b238..5ac06870 100644 --- a/.publish/router/package.json +++ b/.publish/router/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/router", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "description": "@wrnexus/router — part of the WrNexus framework.", "license": "MIT", @@ -21,8 +21,8 @@ } }, "dependencies": { - "@wrnexus/compiler": "^0.2.46", - "@wrnexus/core": "^0.2.46" + "@wrnexus/compiler": "^0.2.47", + "@wrnexus/core": "^0.2.47" }, "files": [ "dist" diff --git a/.publish/ssr/package.json b/.publish/ssr/package.json index e0ef718f..e43b8e8f 100644 --- a/.publish/ssr/package.json +++ b/.publish/ssr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ssr", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "description": "@wrnexus/ssr — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.46" + "@wrnexus/core": "^0.2.47" }, "files": [ "dist" diff --git a/.publish/styles/package.json b/.publish/styles/package.json index 897e838f..b6b6aa53 100644 --- a/.publish/styles/package.json +++ b/.publish/styles/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/styles", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "description": "@wrnexus/styles — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/uploader": "^0.2.46" + "@wrnexus/uploader": "^0.2.47" }, "files": [ "dist" diff --git a/.publish/test/package.json b/.publish/test/package.json index 77e17826..2ae8e470 100644 --- a/.publish/test/package.json +++ b/.publish/test/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/test", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "description": "@wrnexus/test — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/tracking/package.json b/.publish/tracking/package.json index 3279c6a8..3f510755 100644 --- a/.publish/tracking/package.json +++ b/.publish/tracking/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/tracking", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "description": "@wrnexus/tracking — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/ui/package.json b/.publish/ui/package.json index 3c5bd4ba..80b4959a 100644 --- a/.publish/ui/package.json +++ b/.publish/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "description": "@wrnexus/ui — part of the WrNexus framework.", "license": "MIT", @@ -22,7 +22,7 @@ "./ui.css": "./ui.css" }, "dependencies": { - "@wrnexus/core": "^0.2.46" + "@wrnexus/core": "^0.2.47" }, "files": [ "dist", diff --git a/.publish/uploader/package.json b/.publish/uploader/package.json index ac4f47e4..380f7e4e 100644 --- a/.publish/uploader/package.json +++ b/.publish/uploader/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/uploader", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "description": "@wrnexus/uploader — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.46" + "@wrnexus/core": "^0.2.47" }, "files": [ "dist" diff --git a/.publish/validation/package.json b/.publish/validation/package.json index a8822d55..885121ca 100644 --- a/.publish/validation/package.json +++ b/.publish/validation/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/validation", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "description": "@wrnexus/validation — part of the WrNexus framework.", "license": "MIT", diff --git a/packages/ai/package.json b/packages/ai/package.json index 4f2216d1..955e4fb7 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ai", - "version": "0.2.46", + "version": "0.2.47", "private": true, "type": "module", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", diff --git a/packages/authz/package.json b/packages/authz/package.json index 69fe5f64..50289c1d 100644 --- a/packages/authz/package.json +++ b/packages/authz/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/authz", - "version": "0.2.46", + "version": "0.2.47", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/cli/package.json b/packages/cli/package.json index 637b9a14..816da8bc 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/cli", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/cli/src/update.ts b/packages/cli/src/update.ts index 983a0a64..4069763a 100644 --- a/packages/cli/src/update.ts +++ b/packages/cli/src/update.ts @@ -492,6 +492,15 @@ const MIGRATIONS: Migration[] = [ // 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. */ diff --git a/packages/compiler/package.json b/packages/compiler/package.json index 480bfc01..5c8fd930 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/compiler", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/compiler/src/codegen.ts b/packages/compiler/src/codegen.ts index 0a3c18a2..1c420f9f 100644 --- a/packages/compiler/src/codegen.ts +++ b/packages/compiler/src/codegen.ts @@ -996,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 * mustaches (`{expr}`, hydrated by the reactive runtime); interpolations of @@ -1102,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. */ function renderComponentNode(node: ViewNode, ctx: CompCtx): string { if (node.type === "text") return compileText(node.value, ctx); @@ -1167,7 +1193,14 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string { ? exprRefsState(a.value, elementContext.loopVars) : false; - if (!a.value.includes("{") || (!referencesState && !referencesLoopVariable)) { + const referencesServerLocal = ctx.serverLocals + ? exprRefsState(a.value, ctx.serverLocals) + : false; + + if ( + !a.value.includes("{") || + (!referencesState && !referencesLoopVariable && !referencesServerLocal) + ) { return rendered; } @@ -1195,8 +1228,17 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string { 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 = - staticClassValue.includes("{") && exprRefsState(staticClassValue, ctx.stateNames); + staticClassValue.includes("{") && + (classReferencesState || classReferencesLoopVariable || classReferencesServerLocal); const classAttribute = staticClasses.length > 0 || conditionalClasses.length > 0 @@ -1208,9 +1250,6 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string { : ""; const classBindings = conditionalClasses - .filter(({ expression }) => { - return !ctx.serverLocals || !exprRefsState(expression, ctx.serverLocals); - }) .map(({ className, expression }, index) => { const marker = attrEscape(JSON.stringify([className, expression])); @@ -1218,8 +1257,14 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string { }) .join(""); + const loopLocalsAttribute = serverLoopLocalsAttribute(ctx); + const allAttrs = - `${classAttribute}` + `${classReactiveBinding}` + `${classBindings}` + `${attrs}`; + `${loopLocalsAttribute}` + + `${classAttribute}` + + `${classReactiveBinding}` + + `${classBindings}` + + `${attrs}`; if (VOID_ELEMENTS.has(node.tag.toLowerCase())) { return `<${node.tag}${allAttrs}>`; @@ -1232,6 +1277,11 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string { function generateComponent(ast: PageAst): string { const out: string[] = []; + const hasServerEach = viewHasServerEach(ast.view); + + if (hasServerEach) { + out.push(`import { Buffer } from "node:buffer";`); + } const effectiveProps = ast.kind === "layout" && !ast.props.some((prop) => prop.name === "content") @@ -1420,6 +1470,12 @@ function __wireRaw(v: any): string { return String(v == null ? "" : v); }`); + if (hasServerEach) { + out.push(`function __wrnexusEncodeLoopLocals(value: Record): string { + return Buffer.from(JSON.stringify(value), "utf8").toString("base64"); +}`); + } + if (needsScope) { out.push(`function __wrnexusSerializeScopeValue(value: any): string { if (value === undefined) { diff --git a/packages/compiler/test/compiler.test.ts b/packages/compiler/test/compiler.test.ts index 86dcf225..4386672e 100644 --- a/packages/compiler/test/compiler.test.ts +++ b/packages/compiler/test/compiler.test.ts @@ -881,3 +881,68 @@ component Accordion { 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(` +
+
+ + + +
+ +
+ + + +
+
+ `); + + 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); +}); diff --git a/packages/core/package.json b/packages/core/package.json index c438db58..fc5fc0d7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/core", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/csr/package.json b/packages/csr/package.json index 09206d4f..967a7e60 100644 --- a/packages/csr/package.json +++ b/packages/csr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/csr", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/csr/src/reactive-runtime.ts b/packages/csr/src/reactive-runtime.ts index dc5130bf..7a62c819 100644 --- a/packages/csr/src/reactive-runtime.ts +++ b/packages/csr/src/reactive-runtime.ts @@ -9,6 +9,7 @@ * data-scope="count: 0, name: 'x'" declare reactive state on a subtree * data-on-="count++" run a statement in scope on an event * data-text="expr" element textContent follows an expression + * data-wrn-loop-locals="base64-json" preserves SSR {#each} item/index values * data-wrnexus-csr="id" target for generated CSR fetch bindings * {{expr}} or {expr} interpolation inside text nodes * @@ -211,16 +212,96 @@ export const REACTIVE_RUNTIME = String.raw` function signal(initial) { var value = initial; - var subs = new Set(); + var subscribers = new Set(); + var notifying = false; + var pendingNotification = false; + var pendingPrevious; + + function notify(previous) { + if (notifying) { + pendingNotification = true; + + if (pendingPrevious === undefined) { + pendingPrevious = previous; + } + + return; + } + + notifying = true; + + try { + /* + * Iterate a snapshot. Reactive renderers may subscribe again while + * running, but that must not mutate the collection currently being + * iterated. + */ + var snapshot = + Array.from(subscribers); + + snapshot.forEach(function ( + subscriber, + ) { + if ( + subscribers.has( + subscriber, + ) + ) { + subscriber( + value, + previous, + ); + } + }); + } finally { + notifying = false; + } + + if (pendingNotification) { + var nextPrevious = + pendingPrevious; + + pendingNotification = false; + pendingPrevious = undefined; + + notify(nextPrevious); + } + } + return { - get: function () { return value; }, - set: function (v) { - if (Object.is(v, value)) return; - var previous = value; - value = v; - subs.forEach(function (f) { f(value, previous); }); + get: function () { + return value; + }, + + set: function (nextValue) { + if ( + Object.is( + nextValue, + value, + ) + ) { + return; + } + + var previous = value; + value = nextValue; + + notify(previous); + }, + + subscribe: function ( + subscriber, + ) { + subscribers.add( + subscriber, + ); + + return function () { + subscribers.delete( + subscriber, + ); + }; }, - subscribe: function (f) { subs.add(f); return function () { subs.delete(f); }; } }; } function applyReactiveAttribute(node, name, value) { @@ -385,21 +466,52 @@ export const REACTIVE_RUNTIME = String.raw` // change then re-runs only the renderers that actually read it. The Set in // signal.subscribe dedupes, so re-subscribing each run is cheap and bounded. var currentRenderer = null; + var currentRenderer = null; + function reactive(fn) { + var running = false; + function run() { - var prev = currentRenderer; + if (running) { + return; + } + + running = true; + + var previousRenderer = + currentRenderer; + currentRenderer = run; - try { fn(); } finally { currentRenderer = prev; } + + try { + fn(); + } finally { + currentRenderer = + previousRenderer; + + running = false; + } } + renderers.push(run); + return run; } function renderAll() { renderers.forEach(function (f) { f(); }); } function decodeLoopLocals(node) { + if (!node) { + return {}; + } + + var element = + node.nodeType === 1 + ? node + : node.parentElement; + var owner = - node && - node.closest && - node.closest( + element && + element.closest && + element.closest( "[data-wrn-loop-locals]", ); @@ -421,9 +533,7 @@ export const REACTIVE_RUNTIME = String.raw` window.atob(encoded); var bytes = - new Uint8Array( - binary.length, - ); + new Uint8Array(binary.length); for ( var index = 0; @@ -434,16 +544,17 @@ export const REACTIVE_RUNTIME = String.raw` binary.charCodeAt(index); } - var value = - JSON.parse( - new TextDecoder( - "utf-8", - ).decode(bytes), + var decoded = + new TextDecoder("utf-8").decode( + bytes, ); - return value && - typeof value === "object" - ? value + var parsed = + JSON.parse(decoded); + + return parsed && + typeof parsed === "object" + ? parsed : {}; } catch (error) { console.error( @@ -515,17 +626,43 @@ export const REACTIVE_RUNTIME = String.raw` }); } - function writeScope(name, value) { + function writeScope( + name, + value, + ) { if (!signals[name]) { - signals[name] = signal(value); + signals[name] = + signal(value); + + notifyState( + name, + value, + undefined, + ); + renderAll(); - notifyState(name, value, undefined); return; } - var previous = signals[name].get(); - if (Object.is(previous, value)) return; + + var previous = + signals[name].get(); + + if ( + Object.is( + previous, + value, + ) + ) { + return; + } + signals[name].set(value); - notifyState(name, value, previous); + + notifyState( + name, + value, + previous, + ); } function evalExpr(expr, locals) { @@ -1122,22 +1259,46 @@ export const REACTIVE_RUNTIME = String.raw` }); }); - // data-text bindings + // data-text bindings. Server-rendered {#each} nodes recover their item/index + // values from the nearest data-wrn-loop-locals marker. el.querySelectorAll("[data-text]").forEach(function (node) { if (!owns(node)) return; + var expr = node.getAttribute("data-text"); + reactive(function () { - try { node.textContent = String(evalExpr(expr)); } catch (e) { /* ignore */ } + try { + var value = evalExpr( + expr, + decodeLoopLocals(node), + ); + + node.textContent = + value == null ? "" : String(value); + } catch (_) { + // Keep the server-rendered value when evaluation is unavailable. + } }); }); // data-show="expr" — toggle visibility on truthiness. el.querySelectorAll("[data-show]").forEach(function (node) { if (!owns(node)) return; + var showExpr = node.getAttribute("data-show"); + reactive(function () { var visible = true; - try { visible = !!evalExpr(showExpr); } catch (e) { /* keep visible */ } + + try { + visible = !!evalExpr( + showExpr, + decodeLoopLocals(node), + ); + } catch (_) { + // Keep visible when evaluation is unavailable. + } + node.style.display = visible ? "" : "none"; }); }); @@ -1245,7 +1406,7 @@ export const REACTIVE_RUNTIME = String.raw` try { raw = evalExpr( exact[1].trim(), - decodeLoopLocals(node), + locals, ); } catch (error) { return; @@ -1259,10 +1420,10 @@ export const REACTIVE_RUNTIME = String.raw` ) { try { var value = - evalExpr( - expression.trim(), - decodeLoopLocals(node), - ); + evalExpr( + expression.trim(), + locals, + ); return value == null ? "" diff --git a/packages/db/package.json b/packages/db/package.json index 6a6b8c99..9eccb9fc 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/db", - "version": "0.2.46", + "version": "0.2.47", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/dev-server/package.json b/packages/dev-server/package.json index 25454ff7..8229f7a0 100644 --- a/packages/dev-server/package.json +++ b/packages/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-server", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/encryption/package.json b/packages/encryption/package.json index 2aa496dd..e1c8600e 100644 --- a/packages/encryption/package.json +++ b/packages/encryption/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/encryption", - "version": "0.2.46", + "version": "0.2.47", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/helpers/package.json b/packages/helpers/package.json index 788543d6..1c4899e3 100644 --- a/packages/helpers/package.json +++ b/packages/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/helpers", - "version": "0.2.46", + "version": "0.2.47", "private": true, "type": "module", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.", diff --git a/packages/i18n/package.json b/packages/i18n/package.json index 4aa472f8..b3ccc8c5 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/i18n", - "version": "0.2.46", + "version": "0.2.47", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/jwt/package.json b/packages/jwt/package.json index 5586144d..3bb97ffe 100644 --- a/packages/jwt/package.json +++ b/packages/jwt/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/jwt", - "version": "0.2.46", + "version": "0.2.47", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/mobile/package.json b/packages/mobile/package.json index ccd1378c..63811a8e 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/mobile", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/native/package.json b/packages/native/package.json index 9aaffd15..ce401c00 100644 --- a/packages/native/package.json +++ b/packages/native/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/native", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/oauth/package.json b/packages/oauth/package.json index 01e27691..e96d3626 100644 --- a/packages/oauth/package.json +++ b/packages/oauth/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/oauth", - "version": "0.2.46", + "version": "0.2.47", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/pubsub/package.json b/packages/pubsub/package.json index 5f199482..3246c8b1 100644 --- a/packages/pubsub/package.json +++ b/packages/pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/pubsub", - "version": "0.2.46", + "version": "0.2.47", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/queue/package.json b/packages/queue/package.json index f8d02842..5ada9566 100644 --- a/packages/queue/package.json +++ b/packages/queue/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/queue", - "version": "0.2.46", + "version": "0.2.47", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/reactive/package.json b/packages/reactive/package.json index b461579f..73100167 100644 --- a/packages/reactive/package.json +++ b/packages/reactive/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/reactive", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/router/package.json b/packages/router/package.json index 6b4fe64c..4a052c49 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/router", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/ssr/package.json b/packages/ssr/package.json index 31753bc4..b03b47e0 100644 --- a/packages/ssr/package.json +++ b/packages/ssr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ssr", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/styles/package.json b/packages/styles/package.json index 2eaf8b95..eafe51fc 100644 --- a/packages/styles/package.json +++ b/packages/styles/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/styles", - "version": "0.2.46", + "version": "0.2.47", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/test/package.json b/packages/test/package.json index 67fc0bf5..1b2e6c73 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/test", - "version": "0.2.46", + "version": "0.2.47", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/tracking/package.json b/packages/tracking/package.json index 9450f8e9..d23341ad 100644 --- a/packages/tracking/package.json +++ b/packages/tracking/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/tracking", - "version": "0.2.46", + "version": "0.2.47", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/ui/package.json b/packages/ui/package.json index 9b58a874..3d0876e7 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.2.46", + "version": "0.2.47", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/uploader/package.json b/packages/uploader/package.json index 52993abb..c04bf1a3 100644 --- a/packages/uploader/package.json +++ b/packages/uploader/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/uploader", - "version": "0.2.46", + "version": "0.2.47", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/validation/package.json b/packages/validation/package.json index 3248a202..f5984132 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/validation", - "version": "0.2.46", + "version": "0.2.47", "private": true, "type": "module", "main": "src/index.ts", diff --git a/update-package-versions.mjs b/update-package-versions.mjs index 0de494de..9e72e798 100644 --- a/update-package-versions.mjs +++ b/update-package-versions.mjs @@ -1,7 +1,7 @@ import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; import { join } from "node:path"; -const version = "0.2.46"; +const version = "0.2.47"; const dependencyGroups = [ "dependencies",