From 94c81a7858863dbee905ee76f66c5df2b26d42f1 Mon Sep 17 00:00:00 2001
From: Luke Chu <37006668+lukechu10@users.noreply.github.com>
Date: Tue, 28 Jun 2022 19:12:25 -0700
Subject: [PATCH] Print hydration key when element tag mismatch

---
 packages/sycamore-web/src/hydrate_node.rs | 624 +++++++++++-----------
 1 file changed, 316 insertions(+), 308 deletions(-)

diff --git a/packages/sycamore-web/src/hydrate_node.rs b/packages/sycamore-web/src/hydrate_node.rs
index 97d04b554..b1dc2d428 100644
--- a/packages/sycamore-web/src/hydrate_node.rs
+++ b/packages/sycamore-web/src/hydrate_node.rs
@@ -1,308 +1,316 @@
-//! Rendering backend for the DOM with hydration support.
-
-use std::fmt;
-use std::hash::{Hash, Hasher};
-
-use sycamore_core::generic_node::{GenericNode, SycamoreElement};
-use sycamore_core::hydrate::{hydration_completed, with_hydration_context};
-use sycamore_core::render::insert;
-use sycamore_core::view::View;
-use sycamore_reactive::*;
-use wasm_bindgen::prelude::*;
-use wasm_bindgen::JsCast;
-use web_sys::Node;
-
-use crate::dom_node::{DomNode, NodeId};
-use crate::hydrate::get_next_element;
-use crate::Html;
-
-/// Rendering backend for the DOM with hydration support.
-///
-/// _This API requires the following crate features to be activated: `hydrate`, `dom`_
-#[derive(Clone)]
-pub struct HydrateNode {
-    node: DomNode,
-}
-
-impl HydrateNode {
-    /// Get the underlying [`web_sys::Node`].
-    pub fn inner_element(&self) -> Node {
-        self.node.inner_element()
-    }
-
-    /// Cast the underlying [`web_sys::Node`] using [`JsCast`].
-    pub fn unchecked_into<T: JsCast>(self) -> T {
-        self.node.unchecked_into()
-    }
-
-    /// Get the [`NodeId`] for the node.
-    pub(super) fn get_node_id(&self) -> NodeId {
-        self.node.get_node_id()
-    }
-
-    /// Create a new [`DomNode`] from a raw [`web_sys::Node`].
-    pub fn from_web_sys(node: Node) -> Self {
-        Self {
-            node: DomNode::from_web_sys(node),
-        }
-    }
-}
-
-impl PartialEq for HydrateNode {
-    fn eq(&self, other: &Self) -> bool {
-        self.node == other.node
-    }
-}
-
-impl Eq for HydrateNode {}
-
-impl Hash for HydrateNode {
-    fn hash<H: Hasher>(&self, state: &mut H) {
-        self.get_node_id().hash(state);
-    }
-}
-
-impl AsRef<JsValue> for HydrateNode {
-    fn as_ref(&self) -> &JsValue {
-        self.node.as_ref()
-    }
-}
-
-impl From<HydrateNode> for JsValue {
-    fn from(node: HydrateNode) -> Self {
-        JsValue::from(node.node)
-    }
-}
-
-impl fmt::Debug for HydrateNode {
-    /// Prints outerHtml of [`Node`].
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        self.node.fmt(f)
-    }
-}
-
-impl GenericNode for HydrateNode {
-    type EventType = web_sys::Event;
-    type PropertyType = JsValue;
-
-    const USE_HYDRATION_CONTEXT: bool = true;
-    const CLIENT_SIDE_HYDRATION: bool = true;
-
-    /// When hydrating, instead of creating a new node, this will attempt to hydrate an existing
-    /// node.
-    fn element<T: SycamoreElement>() -> Self {
-        let el = get_next_element();
-        if let Some(el) = el {
-            // If in debug mode, check that the hydrate element has the same tag as the argument.
-            debug_assert_eq!(
-                el.tag_name().to_ascii_lowercase(),
-                T::TAG_NAME,
-                "hydration error, mismatched element tag"
-            );
-            Self {
-                node: DomNode::from_web_sys(el.into()),
-            }
-        } else {
-            Self {
-                node: DomNode::element::<T>(),
-            }
-        }
-    }
-
-    fn element_from_tag(tag: &str) -> Self {
-        Self {
-            node: DomNode::element_from_tag(tag),
-        }
-    }
-
-    /// When hydrating, instead of creating a new node, this will attempt to hydrate an existing
-    /// node.
-    fn text_node(text: &str) -> Self {
-        // TODO
-        Self {
-            node: DomNode::text_node(text),
-        }
-    }
-
-    fn marker() -> Self {
-        // TODO
-        Self {
-            node: DomNode::marker(),
-        }
-    }
-
-    fn marker_with_text(text: &str) -> Self {
-        // TODO
-        Self {
-            node: DomNode::marker_with_text(text),
-        }
-    }
-
-    #[inline]
-    fn set_attribute(&self, name: &str, value: &str) {
-        self.node.set_attribute(name, value);
-    }
-
-    #[inline]
-    fn remove_attribute(&self, name: &str) {
-        self.node.remove_attribute(name);
-    }
-
-    #[inline]
-    fn set_class_name(&self, value: &str) {
-        self.node.set_class_name(value);
-    }
-
-    #[inline]
-    fn add_class(&self, class: &str) {
-        self.node.add_class(class);
-    }
-
-    #[inline]
-    fn remove_class(&self, class: &str) {
-        self.node.remove_class(class);
-    }
-
-    #[inline]
-    fn set_property(&self, name: &str, value: &JsValue) {
-        self.node.set_property(name, value);
-    }
-
-    #[inline]
-    fn remove_property(&self, name: &str) {
-        self.node.remove_property(name);
-    }
-
-    #[inline]
-    fn append_child(&self, child: &Self) {
-        if hydration_completed() {
-            // Do not append nodes during hydration as that will result in duplicate text nodes.
-            self.node.append_child(&child.node);
-        }
-    }
-
-    #[inline]
-    fn first_child(&self) -> Option<Self> {
-        self.node.first_child().map(|node| Self { node })
-    }
-
-    #[inline]
-    fn insert_child_before(&self, new_node: &Self, reference_node: Option<&Self>) {
-        self.node
-            .insert_child_before(&new_node.node, reference_node.map(|node| &node.node));
-    }
-
-    #[inline]
-    fn remove_child(&self, child: &Self) {
-        self.node.remove_child(&child.node);
-    }
-
-    #[inline]
-    fn replace_child(&self, old: &Self, new: &Self) {
-        self.node.replace_child(&old.node, &new.node);
-    }
-
-    #[inline]
-    fn insert_sibling_before(&self, child: &Self) {
-        self.node.insert_sibling_before(&child.node);
-    }
-
-    #[inline]
-    fn parent_node(&self) -> Option<Self> {
-        self.node.parent_node().map(|node| Self { node })
-    }
-
-    #[inline]
-    fn next_sibling(&self) -> Option<Self> {
-        self.node.next_sibling().map(|node| Self { node })
-    }
-
-    #[inline]
-    fn remove_self(&self) {
-        self.node.remove_self();
-    }
-
-    #[inline]
-    fn event<'a, F: FnMut(Self::EventType) + 'a>(&self, cx: Scope<'a>, name: &str, handler: F) {
-        self.node.event(cx, name, handler);
-    }
-
-    #[inline]
-    fn update_inner_text(&self, text: &str) {
-        self.node.update_inner_text(text);
-    }
-
-    #[inline]
-    fn dangerously_set_inner_html(&self, html: &str) {
-        self.node.dangerously_set_inner_html(html);
-    }
-
-    #[inline]
-    fn clone_node(&self) -> Self {
-        Self {
-            node: self.node.clone_node(),
-        }
-    }
-}
-
-impl Html for HydrateNode {
-    const IS_BROWSER: bool = true;
-}
-
-/// Render a [`View`] under a `parent` node by reusing existing nodes (client side
-/// hydration). Alias for [`hydrate_to`] with `parent` being the `<body>` tag.
-///
-/// For rendering without hydration, use [`render`](super::render) instead.
-///
-/// _This API requires the following crate features to be activated: `hydrate`, `dom`_
-pub fn hydrate(view: impl FnOnce(Scope<'_>) -> View<HydrateNode>) {
-    let window = web_sys::window().unwrap_throw();
-    let document = window.document().unwrap_throw();
-
-    hydrate_to(view, &document.body().unwrap_throw());
-}
-
-/// Render a [`View`] under a `parent` node by reusing existing nodes (client side
-/// hydration). For rendering under the `<body>` tag, use [`hydrate_to`] instead.
-///
-/// For rendering without hydration, use [`render`](super::render) instead.
-///
-/// _This API requires the following crate features to be activated: `hydrate`, `dom`_
-pub fn hydrate_to(view: impl FnOnce(Scope<'_>) -> View<HydrateNode>, parent: &Node) {
-    // Do not call the destructor function, effectively leaking the scope.
-    let _ = hydrate_get_scope(view, parent);
-}
-
-/// Render a [`View`] under a `parent` node, in a way that can be cleaned up.
-/// This function is intended to be used for injecting an ephemeral sycamore view into a
-/// non-sycamore app (for example, a file upload modal where you want to cancel the upload if the
-/// modal is closed).
-///
-/// _This API requires the following crate features to be activated: `hydrate`, `dom`_
-#[must_use = "please hold onto the ReactiveScope until you want to clean things up, or use render_to() instead"]
-pub fn hydrate_get_scope<'a>(
-    view: impl FnOnce(Scope<'_>) -> View<HydrateNode> + 'a,
-    parent: &'a Node,
-) -> ScopeDisposer<'a> {
-    // Get children from parent into a View to set as the initial node value.
-    let mut children = Vec::new();
-    let child_nodes = parent.child_nodes();
-    for i in 0..child_nodes.length() {
-        children.push(child_nodes.get(i).unwrap());
-    }
-    let children = children
-        .into_iter()
-        .map(|x| View::new_node(HydrateNode::from_web_sys(x)))
-        .collect::<Vec<_>>();
-
-    create_scope(|cx| {
-        insert(
-            cx,
-            &HydrateNode::from_web_sys(parent.clone()),
-            with_hydration_context(|| view(cx)),
-            Some(View::new_fragment(children)),
-            None,
-            false,
-        );
-    })
-}
+//! Rendering backend for the DOM with hydration support.
+
+use std::fmt;
+use std::hash::{Hash, Hasher};
+
+use sycamore_core::generic_node::{GenericNode, SycamoreElement};
+use sycamore_core::hydrate::{get_current_id, hydration_completed, with_hydration_context};
+use sycamore_core::render::insert;
+use sycamore_core::view::View;
+use sycamore_reactive::*;
+use wasm_bindgen::prelude::*;
+use wasm_bindgen::JsCast;
+use web_sys::Node;
+
+use crate::dom_node::{DomNode, NodeId};
+use crate::hydrate::get_next_element;
+use crate::Html;
+
+/// Rendering backend for the DOM with hydration support.
+///
+/// _This API requires the following crate features to be activated: `hydrate`, `dom`_
+#[derive(Clone)]
+pub struct HydrateNode {
+    node: DomNode,
+}
+
+impl HydrateNode {
+    /// Get the underlying [`web_sys::Node`].
+    pub fn inner_element(&self) -> Node {
+        self.node.inner_element()
+    }
+
+    /// Cast the underlying [`web_sys::Node`] using [`JsCast`].
+    pub fn unchecked_into<T: JsCast>(self) -> T {
+        self.node.unchecked_into()
+    }
+
+    /// Get the [`NodeId`] for the node.
+    pub(super) fn get_node_id(&self) -> NodeId {
+        self.node.get_node_id()
+    }
+
+    /// Create a new [`DomNode`] from a raw [`web_sys::Node`].
+    pub fn from_web_sys(node: Node) -> Self {
+        Self {
+            node: DomNode::from_web_sys(node),
+        }
+    }
+}
+
+impl PartialEq for HydrateNode {
+    fn eq(&self, other: &Self) -> bool {
+        self.node == other.node
+    }
+}
+
+impl Eq for HydrateNode {}
+
+impl Hash for HydrateNode {
+    fn hash<H: Hasher>(&self, state: &mut H) {
+        self.get_node_id().hash(state);
+    }
+}
+
+impl AsRef<JsValue> for HydrateNode {
+    fn as_ref(&self) -> &JsValue {
+        self.node.as_ref()
+    }
+}
+
+impl From<HydrateNode> for JsValue {
+    fn from(node: HydrateNode) -> Self {
+        JsValue::from(node.node)
+    }
+}
+
+impl fmt::Debug for HydrateNode {
+    /// Prints outerHtml of [`Node`].
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        self.node.fmt(f)
+    }
+}
+
+impl GenericNode for HydrateNode {
+    type EventType = web_sys::Event;
+    type PropertyType = JsValue;
+
+    const USE_HYDRATION_CONTEXT: bool = true;
+    const CLIENT_SIDE_HYDRATION: bool = true;
+
+    /// When hydrating, instead of creating a new node, this will attempt to hydrate an existing
+    /// node.
+    fn element<T: SycamoreElement>() -> Self {
+        let el = get_next_element();
+        if let Some(el) = el {
+            // If in debug mode, check that the hydrate element has the same tag as the argument.
+            #[cfg(debug_assertions)]
+            if T::TAG_NAME != el.tag_name().to_ascii_lowercase() {
+                // Get the hydration key of the expected element.
+                let mut hk = get_current_id().unwrap();
+                hk.1 -= 1; // Decrement the element id because we called get_next_id previously.
+                panic!(
+                        "hydration error, mismatched element tag\nexpected {}, found {}\noccurred at element with hydration key {}.{}",
+                        T::TAG_NAME,
+                        el.tag_name().to_ascii_lowercase(),
+                        hk.0, hk.1
+                    );
+            }
+
+            Self {
+                node: DomNode::from_web_sys(el.into()),
+            }
+        } else {
+            Self {
+                node: DomNode::element::<T>(),
+            }
+        }
+    }
+
+    fn element_from_tag(tag: &str) -> Self {
+        Self {
+            node: DomNode::element_from_tag(tag),
+        }
+    }
+
+    /// When hydrating, instead of creating a new node, this will attempt to hydrate an existing
+    /// node.
+    fn text_node(text: &str) -> Self {
+        // TODO
+        Self {
+            node: DomNode::text_node(text),
+        }
+    }
+
+    fn marker() -> Self {
+        // TODO
+        Self {
+            node: DomNode::marker(),
+        }
+    }
+
+    fn marker_with_text(text: &str) -> Self {
+        // TODO
+        Self {
+            node: DomNode::marker_with_text(text),
+        }
+    }
+
+    #[inline]
+    fn set_attribute(&self, name: &str, value: &str) {
+        self.node.set_attribute(name, value);
+    }
+
+    #[inline]
+    fn remove_attribute(&self, name: &str) {
+        self.node.remove_attribute(name);
+    }
+
+    #[inline]
+    fn set_class_name(&self, value: &str) {
+        self.node.set_class_name(value);
+    }
+
+    #[inline]
+    fn add_class(&self, class: &str) {
+        self.node.add_class(class);
+    }
+
+    #[inline]
+    fn remove_class(&self, class: &str) {
+        self.node.remove_class(class);
+    }
+
+    #[inline]
+    fn set_property(&self, name: &str, value: &JsValue) {
+        self.node.set_property(name, value);
+    }
+
+    #[inline]
+    fn remove_property(&self, name: &str) {
+        self.node.remove_property(name);
+    }
+
+    #[inline]
+    fn append_child(&self, child: &Self) {
+        if hydration_completed() {
+            // Do not append nodes during hydration as that will result in duplicate text nodes.
+            self.node.append_child(&child.node);
+        }
+    }
+
+    #[inline]
+    fn first_child(&self) -> Option<Self> {
+        self.node.first_child().map(|node| Self { node })
+    }
+
+    #[inline]
+    fn insert_child_before(&self, new_node: &Self, reference_node: Option<&Self>) {
+        self.node
+            .insert_child_before(&new_node.node, reference_node.map(|node| &node.node));
+    }
+
+    #[inline]
+    fn remove_child(&self, child: &Self) {
+        self.node.remove_child(&child.node);
+    }
+
+    #[inline]
+    fn replace_child(&self, old: &Self, new: &Self) {
+        self.node.replace_child(&old.node, &new.node);
+    }
+
+    #[inline]
+    fn insert_sibling_before(&self, child: &Self) {
+        self.node.insert_sibling_before(&child.node);
+    }
+
+    #[inline]
+    fn parent_node(&self) -> Option<Self> {
+        self.node.parent_node().map(|node| Self { node })
+    }
+
+    #[inline]
+    fn next_sibling(&self) -> Option<Self> {
+        self.node.next_sibling().map(|node| Self { node })
+    }
+
+    #[inline]
+    fn remove_self(&self) {
+        self.node.remove_self();
+    }
+
+    #[inline]
+    fn event<'a, F: FnMut(Self::EventType) + 'a>(&self, cx: Scope<'a>, name: &str, handler: F) {
+        self.node.event(cx, name, handler);
+    }
+
+    #[inline]
+    fn update_inner_text(&self, text: &str) {
+        self.node.update_inner_text(text);
+    }
+
+    #[inline]
+    fn dangerously_set_inner_html(&self, html: &str) {
+        self.node.dangerously_set_inner_html(html);
+    }
+
+    #[inline]
+    fn clone_node(&self) -> Self {
+        Self {
+            node: self.node.clone_node(),
+        }
+    }
+}
+
+impl Html for HydrateNode {
+    const IS_BROWSER: bool = true;
+}
+
+/// Render a [`View`] under a `parent` node by reusing existing nodes (client side
+/// hydration). Alias for [`hydrate_to`] with `parent` being the `<body>` tag.
+///
+/// For rendering without hydration, use [`render`](super::render) instead.
+///
+/// _This API requires the following crate features to be activated: `hydrate`, `dom`_
+pub fn hydrate(view: impl FnOnce(Scope<'_>) -> View<HydrateNode>) {
+    let window = web_sys::window().unwrap_throw();
+    let document = window.document().unwrap_throw();
+
+    hydrate_to(view, &document.body().unwrap_throw());
+}
+
+/// Render a [`View`] under a `parent` node by reusing existing nodes (client side
+/// hydration). For rendering under the `<body>` tag, use [`hydrate_to`] instead.
+///
+/// For rendering without hydration, use [`render`](super::render) instead.
+///
+/// _This API requires the following crate features to be activated: `hydrate`, `dom`_
+pub fn hydrate_to(view: impl FnOnce(Scope<'_>) -> View<HydrateNode>, parent: &Node) {
+    // Do not call the destructor function, effectively leaking the scope.
+    let _ = hydrate_get_scope(view, parent);
+}
+
+/// Render a [`View`] under a `parent` node, in a way that can be cleaned up.
+/// This function is intended to be used for injecting an ephemeral sycamore view into a
+/// non-sycamore app (for example, a file upload modal where you want to cancel the upload if the
+/// modal is closed).
+///
+/// _This API requires the following crate features to be activated: `hydrate`, `dom`_
+#[must_use = "please hold onto the ReactiveScope until you want to clean things up, or use render_to() instead"]
+pub fn hydrate_get_scope<'a>(
+    view: impl FnOnce(Scope<'_>) -> View<HydrateNode> + 'a,
+    parent: &'a Node,
+) -> ScopeDisposer<'a> {
+    // Get children from parent into a View to set as the initial node value.
+    let mut children = Vec::new();
+    let child_nodes = parent.child_nodes();
+    for i in 0..child_nodes.length() {
+        children.push(child_nodes.get(i).unwrap());
+    }
+    let children = children
+        .into_iter()
+        .map(|x| View::new_node(HydrateNode::from_web_sys(x)))
+        .collect::<Vec<_>>();
+
+    create_scope(|cx| {
+        insert(
+            cx,
+            &HydrateNode::from_web_sys(parent.clone()),
+            with_hydration_context(|| view(cx)),
+            Some(View::new_fragment(children)),
+            None,
+            false,
+        );
+    })
+}