fix(csr): consume hydration metadata from live DOM
This commit is contained in:
@@ -823,7 +823,12 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
}
|
||||
|
||||
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.__wrnexusHydrated = true;
|
||||
@@ -1417,20 +1422,44 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
// that received the slot and continuing the search from its mount point.
|
||||
// The loop repeats so slots nested through several components resolve to
|
||||
// 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) {
|
||||
var cursor = node.nodeType === 1 ? node : node.parentNode;
|
||||
for (var guard = 0; cursor && cursor.closest && guard < 32; guard++) {
|
||||
var found = cursor.closest(
|
||||
"[data-scope], [data-wrn-scope], [data-wrn-slot]",
|
||||
);
|
||||
for (var guard = 0; cursor && guard < 32; guard++) {
|
||||
var found = closestScopeOrSlot(cursor);
|
||||
if (!found) return null;
|
||||
if (!found.hasAttribute("data-wrn-slot")) return found;
|
||||
// Step out of the component whose slot this content filled, then keep
|
||||
// looking from just above that component's root.
|
||||
var componentRoot =
|
||||
found.parentNode && found.parentNode.closest
|
||||
? found.parentNode.closest("[data-scope], [data-wrn-scope]")
|
||||
: null;
|
||||
var componentRoot = closestScope(found.parentNode);
|
||||
cursor = componentRoot ? componentRoot.parentNode : found.parentNode;
|
||||
}
|
||||
return null;
|
||||
@@ -1658,6 +1687,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
"data-wrn-bind-",
|
||||
) === 0
|
||||
) {
|
||||
node.removeAttribute(attribute.name);
|
||||
var binding;
|
||||
|
||||
try {
|
||||
@@ -2354,6 +2384,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
return;
|
||||
}
|
||||
|
||||
node.removeAttribute(marker.name);
|
||||
var binding;
|
||||
|
||||
try {
|
||||
@@ -2538,8 +2569,7 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
Array.prototype.slice
|
||||
.call(el.querySelectorAll("[data-wrn-events]"))
|
||||
.forEach(function (node) {
|
||||
var componentRoot =
|
||||
node.closest("[data-scope], [data-wrn-scope]");
|
||||
var componentRoot = closestScope(node);
|
||||
if (!componentRoot || componentRoot === el) return;
|
||||
// Only the scope that mounted this component owns its outputs.
|
||||
if (
|
||||
@@ -2615,7 +2645,8 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
// server forwards these markers onto the rendered child root; evaluate
|
||||
// them here and write changes into the child's prop signals.
|
||||
Array.prototype.slice
|
||||
.call(el.querySelectorAll("[data-scope], [data-wrn-scope]"))
|
||||
.call(el.querySelectorAll("*"))
|
||||
.filter(isScopeRoot)
|
||||
.forEach(function (node) {
|
||||
if (!node.parentNode || ownerScope(node.parentNode) !== el) return;
|
||||
Array.prototype.slice.call(node.attributes).forEach(function (attr) {
|
||||
@@ -2742,16 +2773,25 @@ export const REACTIVE_RUNTIME = String.raw`
|
||||
}
|
||||
|
||||
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) {
|
||||
var elements = [];
|
||||
if (root && root.nodeType === 1 && root.hasAttribute("data-wrn-behavior")) {
|
||||
elements.push(root);
|
||||
}
|
||||
if (root && root.querySelectorAll) {
|
||||
Array.prototype.push.apply(elements, root.querySelectorAll("[data-wrn-behavior]"));
|
||||
mountedBehaviorRoots.forEach(function (element) {
|
||||
if (
|
||||
element === root ||
|
||||
(root && root.nodeType === 1 && root.contains && root.contains(element))
|
||||
) {
|
||||
elements.push(element);
|
||||
}
|
||||
});
|
||||
elements.reverse().forEach(function (element) {
|
||||
var instance = behaviorInstances.get(element);
|
||||
if (instance) instance.dispose();
|
||||
|
||||
@@ -522,6 +522,33 @@ test("template literals work inside browser API call arguments", () => {
|
||||
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", () => {
|
||||
const behavior = Buffer.from(
|
||||
JSON.stringify({
|
||||
@@ -817,8 +844,7 @@ test("development runtime accepts component-local and inline wire variables", ()
|
||||
win.document.head.innerHTML =
|
||||
`<style>.component { --wire-component-local: red; color: var(--wire-component-local); ` +
|
||||
`background: var(--wire-inline-local); }</style>`;
|
||||
win.document.body.innerHTML =
|
||||
`<div class="component" style="--wire-inline-local: blue" data-scope=""></div>`;
|
||||
win.document.body.innerHTML = `<div class="component" style="--wire-inline-local: blue" data-scope=""></div>`;
|
||||
(globalThis as Record<string, unknown>).window = win;
|
||||
(globalThis as Record<string, unknown>).document = win.document;
|
||||
(globalThis as Record<string, unknown>).location = win.location;
|
||||
|
||||
Reference in New Issue
Block a user