feat: add fragment support for hot reloading and fix some stuff (#659)

This commit is contained in:
Greg Johnston 2023-03-11 07:21:37 -05:00 committed by GitHub
parent 1a3c1e9e52
commit 591212a56a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 226 additions and 78 deletions

View file

@ -1,5 +1,5 @@
/*
! tailwindcss v3.1.8 | MIT License | https://tailwindcss.com
! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com
*/
/*
@ -30,6 +30,7 @@
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
*/
html {
@ -44,6 +45,8 @@ html {
/* 3 */
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
/* 4 */
font-feature-settings: normal;
/* 5 */
}
/*
@ -410,54 +413,13 @@ video {
height: auto;
}
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
/* Make elements with the HTML hidden attribute stay hidden by default */
[hidden] {
display: none;
}
::-webkit-backdrop {
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
@ -569,11 +531,39 @@ video {
border-radius: 0.5rem;
}
.rounded-md {
border-radius: 0.375rem;
}
.rounded-sm {
border-radius: 0.125rem;
}
.bg-amber-600 {
--tw-bg-opacity: 1;
background-color: rgb(217 119 6 / var(--tw-bg-opacity));
}
.bg-sky-600 {
--tw-bg-opacity: 1;
background-color: rgb(2 132 199 / var(--tw-bg-opacity));
}
.bg-sky-300 {
--tw-bg-opacity: 1;
background-color: rgb(125 211 252 / var(--tw-bg-opacity));
}
.bg-sky-500 {
--tw-bg-opacity: 1;
background-color: rgb(14 165 233 / var(--tw-bg-opacity));
}
.bg-amber-500 {
--tw-bg-opacity: 1;
background-color: rgb(245 158 11 / var(--tw-bg-opacity));
}
.p-6 {
padding: 1.5rem;
}
@ -605,6 +595,10 @@ video {
text-align: center;
}
.text-right {
text-align: right;
}
.text-4xl {
font-size: 2.25rem;
line-height: 2.5rem;
@ -615,6 +609,41 @@ video {
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.text-red-500 {
--tw-text-opacity: 1;
color: rgb(239 68 68 / var(--tw-text-opacity));
}
.text-red-200 {
--tw-text-opacity: 1;
color: rgb(254 202 202 / var(--tw-text-opacity));
}
.text-sky-500 {
--tw-text-opacity: 1;
color: rgb(14 165 233 / var(--tw-text-opacity));
}
.text-sky-300 {
--tw-text-opacity: 1;
color: rgb(125 211 252 / var(--tw-text-opacity));
}
.text-sky-700 {
--tw-text-opacity: 1;
color: rgb(3 105 161 / var(--tw-text-opacity));
}
.text-sky-800 {
--tw-text-opacity: 1;
color: rgb(7 89 133 / var(--tw-text-opacity));
}
.text-red-800 {
--tw-text-opacity: 1;
color: rgb(153 27 27 / var(--tw-text-opacity));
}
.hover\:bg-sky-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(3 105 161 / var(--tw-bg-opacity));

View file

@ -63,6 +63,8 @@ pub struct ComponentRepr {
closing: Comment,
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
pub(crate) id: HydrationKey,
#[cfg(debug_assertions)]
pub(crate) view_marker: Option<String>,
}
impl fmt::Debug for ComponentRepr {
@ -205,6 +207,8 @@ impl ComponentRepr {
children: Vec::with_capacity(1),
#[cfg(not(all(target_arch = "wasm32", feature = "web")))]
id,
#[cfg(debug_assertions)]
view_marker: None,
}
}
}

View file

@ -25,6 +25,8 @@ pub struct Fragment {
id: HydrationKey,
/// The nodes contained in the fragment.
pub nodes: Vec<View>,
#[cfg(debug_assertions)]
pub(crate) view_marker: Option<String>,
}
impl FromIterator<View> for Fragment {
@ -52,7 +54,12 @@ impl Fragment {
/// Creates a new [`Fragment`] with the given hydration ID from a [`Vec<Node>`].
pub fn new_with_id(id: HydrationKey, nodes: Vec<View>) -> Self {
Self { id, nodes }
Self {
id,
nodes,
#[cfg(debug_assertions)]
view_marker: None,
}
}
/// Gives access to the [View] children contained within the fragment.
@ -64,6 +71,13 @@ impl Fragment {
pub fn id(&self) -> &HydrationKey {
&self.id
}
#[cfg(debug_assertions)]
/// Adds an optional marker indicating the view macro source.
pub fn with_view_marker(mut self, marker: impl Into<String>) -> Self {
self.view_marker = Some(marker.into());
self
}
}
impl IntoView for Fragment {
@ -71,6 +85,11 @@ impl IntoView for Fragment {
fn into_view(self, cx: leptos_reactive::Scope) -> View {
let mut frag = ComponentRepr::new_with_id("", self.id.clone());
#[cfg(debug_assertions)]
{
frag.view_marker = self.view_marker;
}
frag.children = self.nodes;
frag.into_view(cx)

View file

@ -237,12 +237,17 @@ impl View {
};
cfg_if! {
if #[cfg(debug_assertions)] {
format!(r#"<!--hk={}|leptos-{name}-start-->{}<!--hk={}|leptos-{name}-end-->"#,
let content = format!(r#"<!--hk={}|leptos-{name}-start-->{}<!--hk={}|leptos-{name}-end-->"#,
HydrationCtx::to_string(&node.id, false),
content(),
HydrationCtx::to_string(&node.id, true),
name = to_kebab_case(&node.name)
).into()
);
if let Some(id) = node.view_marker {
format!("<!--leptos-view|{id}|open-->{content}<!--leptos-view|{id}|close-->").into()
} else {
content.into()
}
} else {
format!(
r#"{}<!--hk={}-->"#,

View file

@ -2,7 +2,7 @@ console.log("[HOT RELOADING] Connected to server.");
function patch(json) {
try {
const views = JSON.parse(json);
for ([id, patches] of views) {
for (const [id, patches] of views) {
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_COMMENT),
open = `leptos-view|${id}|open`,
close = `leptos-view|${id}|close`;
@ -16,10 +16,7 @@ function patch(json) {
}
}
// build tree of current actual children
const range = new Range();
range.setStartAfter(start);
range.setEndBefore(end);
const actualChildren = buildActualChildren(start.parentElement, range);
const actualChildren = childrenFromRange(start.parentElement, start, end);
const actions = [];
// build up the set of actions
@ -100,26 +97,49 @@ function patch(json) {
}
})
} else if (action.InsertChild) {
const newChild = fromReplacementNode(action.InsertChild.child, actualChildren),
before = child.children[action.InsertChild.before];
const newChild = fromReplacementNode(action.InsertChild.child, actualChildren);
let children = [];
if(child.children) {
children = child.children;
} else if (child.start && child.end) {
children = childrenFromRange(child.node || child.start.parentElement, start, end);
} else {
console.warn("InsertChildAfter could not build children.");
}
const before = children[action.InsertChild.before];
actions.push(() => {
console.log("[HOT RELOAD] > InsertChild", child, child.node, action.InsertChild, " before ", before);
if (!before) {
if (!before && child.node) {
child.node.appendChild(newChild);
} else {
child.node.insertBefore(newChild, (before.node || before.start));
let node = child.node || child.end.parentElement;
const reference = before ? before.node || before.start : child.end;
node.insertBefore(newChild, reference);
}
})
} else if (action.InsertChildAfter) {
const newChild = fromReplacementNode(action.InsertChildAfter.child, actualChildren),
after = child.children[action.InsertChildAfter.after];
const newChild = fromReplacementNode(action.InsertChildAfter.child, actualChildren);
let children = [];
if(child.children) {
children = child.children;
} else if (child.start && child.end) {
children = childrenFromRange(child.node || child.start.parentElement, start, end);
} else {
console.warn("InsertChildAfter could not build children.");
}
const after = children[action.InsertChildAfter.after];
actions.push(() => {
console.log("[HOT RELOAD] > InsertChildAfter", child, child.node, action.InsertChildAfter, " after ", after);
console.log("newChild is ", newChild);
if (!after || !(after.node || after.start).nextSibling) {
if (child.node && (!after || !(after.node || after.start).nextSibling)) {
child.node.appendChild(newChild);
} else {
child.node.insertBefore(newChild, (after.node || after.start).nextSibling);
} else {
const node = child.node || child.end;
const parent = node.nodeType === Node.COMMENT_NODE ? node.parentNode : node;
if(!after) {
parent.appendChild(newChild);
} else {
parent.insertBefore(newChild, (after.node || after.start).nextSibling);
}
}
})
} else {
@ -138,7 +158,6 @@ function patch(json) {
}
function fromReplacementNode(node, actualChildren) {
console.log("fromReplacementNode", node, actualChildren);
if (node.Html) {
return fromHTML(node.Html);
}
@ -164,7 +183,6 @@ function patch(json) {
actualChildren.length > 1 ? { children: actualChildren } : actualChildren[0],
node.Path
);
console.log("fromReplacementNode", child, "\n", node, actualChildren);
if (child) {
let childNode = child.node;
if (!childNode) {
@ -192,13 +210,13 @@ function patch(json) {
}
}
function buildActualChildren(element, range) {
function buildActualChildren(element, range) {
const walker = document.createTreeWalker(
element,
NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT,
{
acceptNode(node) {
return node.parentNode == element && (!range || range.isPointInRange(node, 0))
return node.parentNode == element && (!range || range.isPointInRange(node, 0));
}
}
);
@ -234,22 +252,62 @@ function patch(json) {
});
} else if (walker.currentNode.textContent.trim() == "<DynChild>") {
let start = walker.currentNode;
while (walker.currentNode.textContent.trim() !== "</DynChild>") {
walker.nextNode();
}
let end = walker.currentNode;
let depth = 1;
while (walker.nextNode()) {
if (walker.currentNode.textContent.trim() == "</DynChild>") {
depth--;
} else if (walker.currentNode.textContent.trim() == "<DynChild>") {
depth++;
}
if(depth == 0) {
break;
}
}
let end = walker.currentNode;
actualChildren.push({
type: "dyn-child",
start, end
});
} else if (walker.currentNode.textContent.trim() == "<>") {
let start = walker.currentNode;
let depth = 1;
while (walker.nextNode()) {
if (walker.currentNode.textContent.trim() == "</>") {
depth--;
} else if (walker.currentNode.textContent.trim() == "<>") {
depth++;
}
if(depth == 0) {
break;
}
}
let end = walker.currentNode;
actualChildren.push({
type: "fragment",
children: childrenFromRange(start.parentElement, start, end),
start, end
});
} else if (walker.currentNode.textContent.trim().startsWith("<")) {
let componentName = walker.currentNode.textContent.trim();
let endMarker = componentName.replace("<", "</");
let depth = 1;
let start = walker.currentNode;
while (walker.currentNode.textContent.trim() !== endMarker) {
walker.nextSibling();
}
let end = walker.currentNode;
while (walker.nextNode()) {
if (walker.currentNode.textContent.trim() == endMarker) {
depth--;
} else if (walker.currentNode.textContent.trim() == componentName) {
depth++;
}
if(depth == 0) {
break;
}
}
let end = walker.currentNode;
actualChildren.push({
type: "component",
start, end
@ -271,12 +329,22 @@ function patch(json) {
return childAtPath(next, rest);
} else if (path == [0]) {
return element;
} else {
} else if (element.start && element.end) {
const actualChildren = childrenFromRange(element.node || element.start.parentElement, element.start, element.end);
return childAtPath({ children: actualChildren }, path);
} else {
console.warn("[HOT RELOADING] Child at ", path, "not found in ", element);
return element;
}
}
function childrenFromRange(parent, start, end) {
const range = new Range();
range.setStartAfter(start);
range.setEndBefore(end);
return buildActualChildren(parent, range);
}
function fromHTML(html) {
const template = document.createElement("template");
template.innerHTML = html;

View file

@ -165,6 +165,7 @@ pub(crate) fn render_view(
Span::call_site(),
nodes,
global_class,
call_site,
),
}
} else {
@ -189,6 +190,7 @@ pub(crate) fn render_view(
true,
TagType::Unknown,
global_class,
call_site,
),
}
}
@ -206,6 +208,7 @@ fn root_node_to_tokens_ssr(
Span::call_site(),
&fragment.children,
global_class,
view_marker,
),
Node::Comment(_) | Node::Doctype(_) | Node::Attribute(_) => quote! {},
Node::Text(node) => {
@ -232,7 +235,13 @@ fn fragment_to_tokens_ssr(
_span: Span,
nodes: &[Node],
global_class: Option<&TokenTree>,
view_marker: Option<String>,
) -> TokenStream {
let view_marker = if let Some(marker) = view_marker {
quote! { .with_view_marker(#marker) }
} else {
quote! {}
};
let nodes = nodes.iter().map(|node| {
let node = root_node_to_tokens_ssr(cx, node, global_class, None);
quote! {
@ -244,6 +253,7 @@ fn fragment_to_tokens_ssr(
leptos::Fragment::lazy(|| vec![
#(#nodes),*
])
#view_marker
}
}
}
@ -638,6 +648,7 @@ fn fragment_to_tokens(
lazy: bool,
parent_type: TagType,
global_class: Option<&TokenTree>,
view_marker: Option<String>,
) -> TokenStream {
let nodes = nodes.iter().map(|node| {
let node = node_to_tokens(cx, node, parent_type, global_class, None);
@ -646,12 +657,20 @@ fn fragment_to_tokens(
#node.into_view(#cx)
}
});
let view_marker = if let Some(marker) = view_marker {
quote! { .with_view_marker(#marker) }
} else {
quote! {}
};
if lazy {
quote! {
{
leptos::Fragment::lazy(|| vec![
#(#nodes),*
])
#view_marker
}
}
} else {
@ -660,6 +679,7 @@ fn fragment_to_tokens(
leptos::Fragment::new(vec![
#(#nodes),*
])
#view_marker
}
}
}
@ -680,6 +700,7 @@ fn node_to_tokens(
true,
parent_type,
global_class,
view_marker,
),
Node::Comment(_) | Node::Doctype(_) => quote! {},
Node::Text(node) => {
@ -772,6 +793,7 @@ fn element_to_tokens(
true,
parent_type,
global_class,
None,
),
Node::Text(node) => {
let value = node.value.as_ref();
@ -1038,6 +1060,7 @@ pub(crate) fn component_to_tokens(
true,
TagType::Unknown,
global_class,
None,
);
let clonables = items_to_clone