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) {
|
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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user