From bbca3bac1fb1c7cb3edce1293f8f90bec74b9f7e Mon Sep 17 00:00:00 2001 From: Vivek Date: Fri, 21 Aug 2026 00:36:37 +0530 Subject: [PATCH 1/3] fix: resolve live() in an attribute hole so a falsy ?bool omits it The SSR renderer resolved live() only in a child hole, so the directive's wrapper object reached the attribute emit sites raw. Each kind failed its own way: the wrapper is truthy, so `?open=${live(false)}` emitted `open=""` whatever it wrapped; `attr=${live(v)}` stringified to `[object Object]`; and `.prop=${live(v)}` serialized the wrapper into the hydration payload. The client has always unwrapped live() once, before its attr/bool/prop dispatch, and the form-action reconcile even simulates SSR by unwrapping while claiming to mirror the server exactly. So the two renderers held contradictory models of the same emit, and the falsy-bool case shipped: webjs.dev's mobile nav menu was served open and hydration then closed it. Unwrap at the same single point on the server, in both the buffered and streaming machines, which makes the SSR bytes match the client by construction rather than through three per-kind rules that can drift. Scoped to live(), the only directive the client accepts in attribute position. render() and streamRender() keep their own isLive branch for a live() nested inside an array child, which a hole-level unwrap never sees. The action-leak guards now fire through live() as well, since a wrapper is not a function: `action="${live(serverAction)}"` previously slipped past them and emitted a form posting to a garbage url. Closes #1443 --- .agents/skills/webjs/references/components.md | 8 + .../src/render-server/template-renderer.js | 15 ++ .../browser/live-attribute-hydration.test.js | 130 +++++++++++++++ .../rendering/live-in-attribute-hole.test.js | 153 ++++++++++++++++++ test/bun/live-attribute-hole.mjs | 90 +++++++++++ test/bun/live-attribute-hole.test.mjs | 13 ++ website/app/docs/directives/page.ts | 1 + 7 files changed, 410 insertions(+) create mode 100644 packages/core/test/rendering/browser/live-attribute-hydration.test.js create mode 100644 packages/core/test/rendering/live-in-attribute-hole.test.js create mode 100644 test/bun/live-attribute-hole.mjs create mode 100644 test/bun/live-attribute-hole.test.mjs diff --git a/.agents/skills/webjs/references/components.md b/.agents/skills/webjs/references/components.md index d5a24dd2d..5ec88276c 100644 --- a/.agents/skills/webjs/references/components.md +++ b/.agents/skills/webjs/references/components.md @@ -382,6 +382,14 @@ Import from `@webjsdev/core/directives`. Everything a `class`/`style`/conditiona | `asyncAppend(iter)` / `asyncReplace(iter)` | Stream from an async iterable, appending each value or replacing with the latest. | | `templateContent(el)` | Render the content of a `