fix(csr): consume hydration metadata from live DOM
Quality / quality (ubuntu-latest) (push) Failing after 9m49s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-11 18:01:11 +05:30
parent b1086d14e9
commit 794a7b3378
2 changed files with 86 additions and 20 deletions
+58 -18
View File
@@ -823,7 +823,12 @@ export const REACTIVE_RUNTIME = String.raw`
} }
function setupScope(el) { function setupScope(el) {
if (el.__wrnexusScope) return; if (el.__wrnexusScope) {
el.removeAttribute("data-scope");
el.removeAttribute("data-wrn-scope");
el.removeAttribute("data-wrn-behavior");
return;
}
el.__wrnexusScope = true; el.__wrnexusScope = true;
el.__wrnexusHydrated = true; el.__wrnexusHydrated = true;
@@ -1417,20 +1422,44 @@ export const REACTIVE_RUNTIME = String.raw`
// that received the slot and continuing the search from its mount point. // that received the slot and continuing the search from its mount point.
// The loop repeats so slots nested through several components resolve to // The loop repeats so slots nested through several components resolve to
// the scope that actually wrote the markup. // the scope that actually wrote the markup.
function isScopeRoot(node) {
return !!(
node &&
node.nodeType === 1 &&
(node.__wrnexusScope ||
node.hasAttribute("data-scope") ||
node.hasAttribute("data-wrn-scope"))
);
}
function closestScope(node) {
for (var current = node; current; current = current.parentNode) {
if (isScopeRoot(current)) return current;
}
return null;
}
function closestScopeOrSlot(node) {
for (var current = node; current; current = current.parentNode) {
if (
current.nodeType === 1 &&
(current.hasAttribute("data-wrn-slot") || isScopeRoot(current))
) {
return current;
}
}
return null;
}
function ownerScope(node) { function ownerScope(node) {
var cursor = node.nodeType === 1 ? node : node.parentNode; var cursor = node.nodeType === 1 ? node : node.parentNode;
for (var guard = 0; cursor && cursor.closest && guard < 32; guard++) { for (var guard = 0; cursor && guard < 32; guard++) {
var found = cursor.closest( var found = closestScopeOrSlot(cursor);
"[data-scope], [data-wrn-scope], [data-wrn-slot]",
);
if (!found) return null; if (!found) return null;
if (!found.hasAttribute("data-wrn-slot")) return found; if (!found.hasAttribute("data-wrn-slot")) return found;
// Step out of the component whose slot this content filled, then keep // Step out of the component whose slot this content filled, then keep
// looking from just above that component's root. // looking from just above that component's root.
var componentRoot = var componentRoot = closestScope(found.parentNode);
found.parentNode && found.parentNode.closest
? found.parentNode.closest("[data-scope], [data-wrn-scope]")
: null;
cursor = componentRoot ? componentRoot.parentNode : found.parentNode; cursor = componentRoot ? componentRoot.parentNode : found.parentNode;
} }
return null; return null;
@@ -1658,6 +1687,7 @@ export const REACTIVE_RUNTIME = String.raw`
"data-wrn-bind-", "data-wrn-bind-",
) === 0 ) === 0
) { ) {
node.removeAttribute(attribute.name);
var binding; var binding;
try { try {
@@ -2354,6 +2384,7 @@ export const REACTIVE_RUNTIME = String.raw`
return; return;
} }
node.removeAttribute(marker.name);
var binding; var binding;
try { try {
@@ -2538,8 +2569,7 @@ export const REACTIVE_RUNTIME = String.raw`
Array.prototype.slice Array.prototype.slice
.call(el.querySelectorAll("[data-wrn-events]")) .call(el.querySelectorAll("[data-wrn-events]"))
.forEach(function (node) { .forEach(function (node) {
var componentRoot = var componentRoot = closestScope(node);
node.closest("[data-scope], [data-wrn-scope]");
if (!componentRoot || componentRoot === el) return; if (!componentRoot || componentRoot === el) return;
// Only the scope that mounted this component owns its outputs. // Only the scope that mounted this component owns its outputs.
if ( if (
@@ -2615,7 +2645,8 @@ export const REACTIVE_RUNTIME = String.raw`
// server forwards these markers onto the rendered child root; evaluate // server forwards these markers onto the rendered child root; evaluate
// them here and write changes into the child's prop signals. // them here and write changes into the child's prop signals.
Array.prototype.slice Array.prototype.slice
.call(el.querySelectorAll("[data-scope], [data-wrn-scope]")) .call(el.querySelectorAll("*"))
.filter(isScopeRoot)
.forEach(function (node) { .forEach(function (node) {
if (!node.parentNode || ownerScope(node.parentNode) !== el) return; if (!node.parentNode || ownerScope(node.parentNode) !== el) return;
Array.prototype.slice.call(node.attributes).forEach(function (attr) { Array.prototype.slice.call(node.attributes).forEach(function (attr) {
@@ -2742,16 +2773,25 @@ export const REACTIVE_RUNTIME = String.raw`
} }
renderAll(); renderAll();
// Hydration metadata is an SSR transport format, not application DOM.
// All values needed after setup now live in closures, WeakMaps, or private
// element properties, so keep the live DOM limited to actual web markup.
el.removeAttribute("data-scope");
el.removeAttribute("data-wrn-scope");
el.removeAttribute("data-wrn-behavior");
} }
function disposeBehaviors(root) { function disposeBehaviors(root) {
var elements = []; var elements = [];
if (root && root.nodeType === 1 && root.hasAttribute("data-wrn-behavior")) { mountedBehaviorRoots.forEach(function (element) {
elements.push(root); if (
} element === root ||
if (root && root.querySelectorAll) { (root && root.nodeType === 1 && root.contains && root.contains(element))
Array.prototype.push.apply(elements, root.querySelectorAll("[data-wrn-behavior]")); ) {
} elements.push(element);
}
});
elements.reverse().forEach(function (element) { elements.reverse().forEach(function (element) {
var instance = behaviorInstances.get(element); var instance = behaviorInstances.get(element);
if (instance) instance.dispose(); if (instance) instance.dispose();
+28 -2
View File
@@ -522,6 +522,33 @@ test("template literals work inside browser API call arguments", () => {
expect(win.document.querySelector("button")!.textContent).toBe("Saved 3"); expect(win.document.querySelector("button")!.textContent).toBe("Saved 3");
}); });
test("hydration consumes private scope, behavior, and binding metadata", () => {
const behavior = Buffer.from(
JSON.stringify({
functions: "function toggle() { active = !active }",
watches: [],
lifecycle: {},
}),
).toString("base64");
const scope = Buffer.from(JSON.stringify({ active: false })).toString("base64");
const win = mount(
`<div id="scope" data-scope="active: false" data-wrn-scope="${scope}" data-wrn-behavior="${behavior}">` +
`<button id="toggle" data-wrn-bind-0='["aria-expanded","{active}"]' data-on-click="toggle()">Toggle</button>` +
`</div>`,
);
const root = win.document.getElementById("scope")!;
const button = win.document.getElementById("toggle")!;
expect(root.hasAttribute("data-scope")).toBe(false);
expect(root.hasAttribute("data-wrn-scope")).toBe(false);
expect(root.hasAttribute("data-wrn-behavior")).toBe(false);
expect(button.hasAttribute("data-wrn-bind-0")).toBe(false);
expect(button.getAttribute("aria-expanded")).toBe("false");
(button as unknown as { click(): void }).click();
expect(button.getAttribute("aria-expanded")).toBe("true");
});
test("compiled client functions are not overwritten by fallback behavior parsing", () => { test("compiled client functions are not overwritten by fallback behavior parsing", () => {
const behavior = Buffer.from( const behavior = Buffer.from(
JSON.stringify({ JSON.stringify({
@@ -817,8 +844,7 @@ test("development runtime accepts component-local and inline wire variables", ()
win.document.head.innerHTML = win.document.head.innerHTML =
`<style>.component { --wire-component-local: red; color: var(--wire-component-local); ` + `<style>.component { --wire-component-local: red; color: var(--wire-component-local); ` +
`background: var(--wire-inline-local); }</style>`; `background: var(--wire-inline-local); }</style>`;
win.document.body.innerHTML = win.document.body.innerHTML = `<div class="component" style="--wire-inline-local: blue" data-scope=""></div>`;
`<div class="component" style="--wire-inline-local: blue" data-scope=""></div>`;
(globalThis as Record<string, unknown>).window = win; (globalThis as Record<string, unknown>).window = win;
(globalThis as Record<string, unknown>).document = win.document; (globalThis as Record<string, unknown>).document = win.document;
(globalThis as Record<string, unknown>).location = win.location; (globalThis as Record<string, unknown>).location = win.location;